diff --git a/assets/imported/models/wooden straw man 3d model.glb b/assets/imported/models/wooden straw man 3d model.glb new file mode 100644 index 0000000..d26791d Binary files /dev/null and b/assets/imported/models/wooden straw man 3d model.glb differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/imported/kampfdummy.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/imported/kampfdummy.j3o.thumb.png new file mode 100644 index 0000000..070094b Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/imported/kampfdummy.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/imported/wooden straw man 3d model.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/imported/wooden straw man 3d model.j3o.thumb.png new file mode 100644 index 0000000..d6868eb Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/imported/wooden straw man 3d model.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/imported/kampfdummy.j3o b/blight-assets/src/main/resources/Models/imported/kampfdummy.j3o new file mode 100644 index 0000000..07e7af2 Binary files /dev/null and b/blight-assets/src/main/resources/Models/imported/kampfdummy.j3o differ diff --git a/blight-assets/src/main/resources/Models/imported/kampfdummy.j3o.meta b/blight-assets/src/main/resources/Models/imported/kampfdummy.j3o.meta new file mode 100644 index 0000000..228a6b4 --- /dev/null +++ b/blight-assets/src/main/resources/Models/imported/kampfdummy.j3o.meta @@ -0,0 +1,30 @@ +#Tue Sep 22 20:54:03 CEST 2026 +attachedEmitters.count=0 +attachedLights.count=0 +attachedSounds.count=0 +castShadow=true +category= +cullDistance=120.0 +footstepSurface= +interactableOffsetX=0.0 +interactableOffsetY=0.5 +interactableOffsetZ=0.0 +interactableRotY=0.0 +interactableType=NONE +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +modelCategory=DUMMY +name=kampfdummy +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=true +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/imported/wooden straw man 3d model.j3o.meta b/blight-assets/src/main/resources/Models/imported/wooden straw man 3d model.j3o.meta new file mode 100644 index 0000000..5c54bcd --- /dev/null +++ b/blight-assets/src/main/resources/Models/imported/wooden straw man 3d model.j3o.meta @@ -0,0 +1,30 @@ +#Tue Sep 22 20:52:39 CEST 2026 +attachedEmitters.count=0 +attachedLights.count=0 +attachedSounds.count=0 +castShadow=true +category= +cullDistance=120.0 +footstepSurface= +interactableOffsetX=0.0 +interactableOffsetY=0.5 +interactableOffsetZ=0.0 +interactableRotY=0.0 +interactableType=NONE +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +modelCategory=NONE +name=wooden straw man 3d model +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=true +tags= +uniformScale=true diff --git a/blight-game/src/main/java/de/blight/game/combat/MeleeAttackCallback.java b/blight-game/src/main/java/de/blight/game/combat/MeleeAttackCallback.java index 658c72b..5515fba 100644 --- a/blight-game/src/main/java/de/blight/game/combat/MeleeAttackCallback.java +++ b/blight-game/src/main/java/de/blight/game/combat/MeleeAttackCallback.java @@ -3,16 +3,19 @@ package de.blight.game.combat; import com.jme3.math.Vector3f; /** - * Wird aufgerufen wenn der Spieler einen Nahkampfangriff auslöst. - * Implementierende Klassen können z.B. Treffererkennung auf Zielobjekten durchführen. + * Wird pro Frame während des Treffer-Fensters einer Nahkampf-Animation aufgerufen. + * Jeder Schwung hat eine eindeutige attackId; hitThisAttack muss pro ID zurückgesetzt werden. */ public interface MeleeAttackCallback { /** - * @param charPosition Weltposition des Charakters beim Angriff - * @param charForward Blickrichtung des Charakters (normiert, Y=0) - * @param baseDamage Basisschaden der Waffe (0 = unbewaffnet) - * @param critMultiplier Schadens-Multiplikator (1.0 = normal, 2.0 = Kombo-5, 1.5–3.0 = Schwerer Angriff) + * @param attackId Eindeutige ID pro Schwung (inkrementiert bei jedem Angriff) + * @param gripPos Weltposition des Griffes (Hand-Bone) + * @param bladeDir Normierter Klingen-Richtungsvektor in Weltkoordinaten + * @param bladeLength Länge der Klinge in Metern (Ray-Reichweite) + * @param baseDamage Basisschaden der Waffe (0 = unbewaffnet → Fallback auf 5) + * @param critMultiplier Schadens-Multiplikator (1.0 = normal, 2.0 = Kombo-5, 3.0 = Schwerer Angriff) */ - void onMeleeAttack(Vector3f charPosition, Vector3f charForward, int baseDamage, float critMultiplier); + void onMeleeAttack(int attackId, Vector3f gripPos, Vector3f bladeDir, + float bladeLength, int baseDamage, float critMultiplier); } diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index b6335e8..7924ac0 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -131,7 +131,15 @@ public class PlayerInputControl { private float heavyLungeEndRemaining = 0f; /** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */ private float combatAnimDuration = 0f; - private MeleeAttackCallback meleeAttackCallback = null; + private MeleeAttackCallback meleeAttackCallback = null; + private WeaponAttachmentControl weaponAttach = null; + private static final float HIT_WINDOW_START = 0.20f; + private static final float HIT_WINDOW_END = 0.75f; + private static final float WEAPON_BLADE_LENGTH = 1.0f; + private int attackIdCounter = 0; + private int currentAttackId = -1; + private boolean hitWindowActive = false; + private float hitWindowCritMult = 1.0f; // Draw/Sheath Animation System private boolean drawAnimActive = false; private float drawAnimTimer = 0f; @@ -372,7 +380,8 @@ public class PlayerInputControl { public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; } public boolean isLastAttackCritical() { return lastAttackCritical; } public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; } - public void setMeleeAttackCallback(MeleeAttackCallback cb) { this.meleeAttackCallback = cb; } + public void setMeleeAttackCallback(MeleeAttackCallback cb) { this.meleeAttackCallback = cb; } + public void setWeaponAttachmentControl(WeaponAttachmentControl w) { this.weaponAttach = w; } public boolean isPaused() { return paused; } @@ -833,6 +842,7 @@ public class PlayerInputControl { physicsChar.setWalkDirection(inLungeWindow ? heavyLungeVel : Vector3f.ZERO); if (combatAnimRemaining <= 0f) { combatAnimActive = false; + hitWindowActive = false; heavyLungeVel = null; heavyLungeStartRemaining = 0f; heavyLungeEndRemaining = 0f; @@ -856,6 +866,15 @@ public class PlayerInputControl { comboWindowActive = true; log.info("[Combat] Kombo-Fenster geöffnet (step={}, remaining={}s)", comboStep, combatAnimRemaining); } + // Per-Frame Waffen-Treffercheck während Hit-Window + if (hitWindowActive && isMeleeType(drawnWeaponType) && combatAnimDuration > 0f) { + float t = (combatAnimDuration - combatAnimRemaining) / combatAnimDuration; + if (t >= HIT_WINDOW_START && t <= HIT_WINDOW_END) { + fireWeaponCallback(hitWindowCritMult); + } else if (t > HIT_WINDOW_END) { + hitWindowActive = false; + } + } pollFootsteps(tpf); return; } @@ -989,8 +1008,10 @@ public class PlayerInputControl { int step = comboStep; boolean isComboFollow = comboWindowActive; // war Kombo-Fenster offen? AnimationAction atk = getMeleeComboAction(drawnWeaponType, step); - lastAttackCritical = (step == 4); - fireMeleeCallback(lastAttackCritical ? 2.0f : 1.0f); + lastAttackCritical = (step == 4); + currentAttackId = ++attackIdCounter; + hitWindowActive = true; + hitWindowCritMult = lastAttackCritical ? 2.0f : 1.0f; comboStep = (step + 1) % 5; comboWindowActive = false; float dur = resolveClipLength(atk, 0.6f); @@ -1302,21 +1323,20 @@ public class PlayerInputControl { }; } - private void fireMeleeCallback(float critMultiplier) { - if (meleeAttackCallback == null || physicsChar == null) { + private void fireWeaponCallback(float critMultiplier) { + if (meleeAttackCallback == null || weaponAttach == null) { + return; + } + com.jme3.scene.Spatial ws = weaponAttach.getWeaponSpatial(); + com.jme3.scene.Node hn = weaponAttach.getHandNode(); + if (ws == null || hn == null) { return; } int base = (mainCharacter != null && mainCharacter.getEquippedMeleeWeapon() != null) ? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0; - Vector3f pos = physicsChar.getPhysicsLocation(); - Vector3f fwd = (visual != null) - ? visual.getLocalRotation().mult(new Vector3f(0f, 0f, -1f)) - : new Vector3f(0f, 0f, -1f); - fwd.y = 0f; - if (fwd.lengthSquared() > 0.001f) { - fwd.normalizeLocal(); - } - meleeAttackCallback.onMeleeAttack(pos, fwd, base, critMultiplier); + Vector3f grip = hn.getWorldTranslation().clone(); + Vector3f bladeDir = ws.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Y).normalizeLocal(); + meleeAttackCallback.onMeleeAttack(currentAttackId, grip, bladeDir, WEAPON_BLADE_LENGTH, base, critMultiplier); } private float computeHeavyMultiplier() { @@ -1368,8 +1388,9 @@ public class PlayerInputControl { if (!weaponDrawn || drawnWeaponType == null || !isMeleeType(drawnWeaponType)) { return; } + currentAttackId = ++attackIdCounter; float mult = computeHeavyMultiplier(); - fireMeleeCallback(mult); + fireWeaponCallback(mult); log.info("[Combat] Schwerer Angriff ausgelöst | Haltezeit={}s Multiplikator={}x", String.format("%.2f", attackHoldTimer), String.format("%.2f", mult)); } diff --git a/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java b/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java index 129d182..56c13ba 100644 --- a/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java +++ b/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java @@ -84,6 +84,9 @@ public class WeaponAttachmentControl extends AbstractControl { this.mc = mc; } + public Spatial getWeaponSpatial() { return weaponSpatial; } + public Node getHandNode() { return handNode; } + @Override protected void controlUpdate(float tpf) { if (skinCtrl == null) { diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 74bc384..89bf2c7 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -362,7 +362,9 @@ public class WorldScene extends BaseAppState { MainCharacter mc = findMainCharacter(); if (mc != null) { playerInput.setMainCharacter(mc); - character.addControl(new de.blight.game.control.WeaponAttachmentControl(assetManager, playerInput, mc)); + var wac = new de.blight.game.control.WeaponAttachmentControl(assetManager, playerInput, mc); + character.addControl(wac); + playerInput.setWeaponAttachmentControl(wac); // SaveGameState mit Charakter und Positions-Lieferant verknüpfen de.blight.game.state.SaveGameState saveState = app.getStateManager().getState(de.blight.game.state.SaveGameState.class); diff --git a/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java b/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java index 66445ed..e413cc3 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java @@ -4,7 +4,10 @@ import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; +import com.jme3.bullet.BulletAppState; import com.jme3.bullet.control.CharacterControl; +import com.jme3.bullet.control.RigidBodyControl; +import com.jme3.bullet.util.CollisionShapeFactory; import com.jme3.font.BitmapFont; import com.jme3.font.BitmapText; import com.jme3.input.InputManager; @@ -44,7 +47,9 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.FloatBuffer; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Verwaltet Kampfpuppen auf der Karte: HP-Tracking, Treffer-Erkennung, @@ -55,8 +60,6 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback private static final Logger log = LoggerFactory.getLogger(WorldDummyState.class); // ── Spielwelt-Konstanten ────────────────────────────────────────────────── - private static final float ATTACK_RANGE = 2.5f; - private static final float ATTACK_CONE_DOT = 0.2f; private static final float FOCUS_RANGE = 6f; private static final float RESTORE_DELAY = 60f; private static final float HP_BAR_SHOW_TIME = 5f; @@ -82,14 +85,17 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback private final PlayerInputControl playerInput; private final KeyBindings keyBindings; - private AssetManager assetManager; - private InputManager inputManager; - private Node rootNode; - private Node guiNode; - private Camera cam; + private AssetManager assetManager; + private InputManager inputManager; + private Node rootNode; + private Node guiNode; + private Camera cam; + private BulletAppState bulletAppState; // ── Laufzeit-Zustand ───────────────────────────────────────────────────── - private final List dummies = new ArrayList<>(); + private final List dummies = new ArrayList<>(); + private int lastAttackId = -1; + private final Set hitThisAttack = new HashSet<>(); private int hoveredDummyIdx = -1; // gefallene Puppe in Aufstell-Reichweite private int focusedDummyIdx = -1; // nächste Puppe in Fokus-Reichweite (Waffe gezückt) @@ -116,11 +122,12 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback @Override protected void initialize(Application app) { SimpleApplication sapp = (SimpleApplication) app; - this.assetManager = app.getAssetManager(); - this.inputManager = app.getInputManager(); - this.rootNode = sapp.getRootNode(); - this.guiNode = sapp.getGuiNode(); - this.cam = app.getCamera(); + this.assetManager = app.getAssetManager(); + this.inputManager = app.getInputManager(); + this.rootNode = sapp.getRootNode(); + this.guiNode = sapp.getGuiNode(); + this.cam = app.getCamera(); + this.bulletAppState = app.getStateManager().getState(BulletAppState.class); loadDummies(); buildEnemyHud(); @@ -149,6 +156,9 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback @Override protected void cleanup(Application app) { for (DummyRuntime dr : dummies) { + if (dr.rbc != null && bulletAppState != null) { + bulletAppState.getPhysicsSpace().remove(dr.rbc); + } rootNode.detachChild(dr.dummyNode); } dummies.clear(); @@ -203,10 +213,22 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback dummyNode.setLocalScale(m.scale()); rootNode.attachChild(dummyNode); + RigidBodyControl rbc = null; + if (m.solid() && bulletAppState != null) { + try { + rbc = new RigidBodyControl(CollisionShapeFactory.createMeshShape(model), 0f); + model.addControl(rbc); + bulletAppState.getPhysicsSpace().add(rbc); + } catch (Exception pe) { + log.warn("[WorldDummy] Kollisions-Shape für '{}' fehlgeschlagen: {}", m.modelPath(), pe.getMessage()); + rbc = null; + } + } + Geometry hpFg = (Geometry) ((Node) hpBarNode.getChild(0)).getChild(1); return new DummyRuntime( m.interactableId() != null ? m.interactableId() : "?", - m.x(), m.y(), m.z(), dummyNode, hpBarNode, hpFg, maxHp); + m.x(), m.y(), m.z(), dummyNode, hpBarNode, hpFg, maxHp, rbc); } private Spatial loadModel(PlacedModel m) { @@ -374,25 +396,24 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback // ── MeleeAttackCallback ─────────────────────────────────────────────────── @Override - public void onMeleeAttack(Vector3f charPos, Vector3f charForward, int baseDamage, float critMultiplier) { + public void onMeleeAttack(int attackId, Vector3f gripPos, Vector3f bladeDir, + float bladeLength, int baseDamage, float critMultiplier) { + if (attackId != lastAttackId) { + lastAttackId = attackId; + hitThisAttack.clear(); + } for (DummyRuntime dr : dummies) { - if (dr.fallen) { + if (dr.fallen || hitThisAttack.contains(dr.id)) { continue; } - float dx = dr.worldX - charPos.x; - float dz = dr.worldZ - charPos.z; - float distSq = dx * dx + dz * dz; - if (distSq > ATTACK_RANGE * ATTACK_RANGE) { - continue; + // Distanz vom Griff zum Dummy-Zentrum (Torso-Höhe ~0.9m) + float dx = dr.worldX - gripPos.x; + float dy = (dr.worldY + 0.9f) - gripPos.y; + float dz = dr.worldZ - gripPos.z; + if (dx * dx + dy * dy + dz * dz <= bladeLength * bladeLength) { + hitThisAttack.add(dr.id); + applyDamage(dr, DamageCalculator.calculate(baseDamage, critMultiplier)); } - float len = (float) Math.sqrt(distSq); - if (len > 0.05f) { - float dot = (dx / len) * charForward.x + (dz / len) * charForward.z; - if (dot < ATTACK_CONE_DOT) { - continue; - } - } - applyDamage(dr, DamageCalculator.calculate(baseDamage, critMultiplier)); } } @@ -401,8 +422,6 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback private void applyDamage(DummyRuntime dr, int damage) { dr.currentHp = Math.max(0, dr.currentHp - damage); dr.restoreTimer = 0f; - dr.hpBarTimer = HP_BAR_SHOW_TIME; - dr.hpBarNode.setCullHint(Spatial.CullHint.Inherit); refreshWorldHpBar(dr); log.info("[Dummy {}] -{} HP → {}/{}{}", dr.id, damage, dr.currentHp, dr.maxHp, dr.currentHp <= 0 ? " K.O.!" : ""); @@ -551,20 +570,22 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback // ── Innere Klasse ───────────────────────────────────────────────────────── private static class DummyRuntime { - final String id; - final float worldX, worldY, worldZ; - final Node dummyNode; - final Node hpBarNode; - final Geometry hpBarFg; + final String id; + final float worldX, worldY, worldZ; + final Node dummyNode; + final Node hpBarNode; + final Geometry hpBarFg; + final RigidBodyControl rbc; int currentHp; - final int maxHp; + final int maxHp; float restoreTimer = 0f; float hpBarTimer = 0f; boolean fallen = false; float fallenLerp = 0f; DummyRuntime(String id, float wx, float wy, float wz, - Node dummyNode, Node hpBarNode, Geometry hpBarFg, int maxHp) { + Node dummyNode, Node hpBarNode, Geometry hpBarFg, int maxHp, + RigidBodyControl rbc) { this.id = id; this.worldX = wx; this.worldY = wy; @@ -574,6 +595,7 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback this.hpBarFg = hpBarFg; this.maxHp = maxHp; this.currentHp = maxHp; + this.rbc = rbc; } } } diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index 17677d0..541ad5b 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -14,3 +14,4 @@ Models/northcoast/alte_fischerhytte.j3o 322.73001 2.00000 -856.09003 -1.83155 1. Models/imported/feuerstelle1.j3o 1.46137 2.00000 -1.08840 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 NONE Models/trees/palm/palm_20260914_195434.j3o 226.76038 1.25643 -893.77020 -1.59663 1.00000 -0.00000 0.00000 true true true 50.00000 150.00000 300.00000 PALM Models/trees/willow/willow_20260914_210538.j3o 256.04678 5.84959 -887.66675 0.00000 1.00000 0.00000 0.00000 true true true 50.00000 150.00000 300.00000 TREE +Models/imported/kampfdummy.j3o -5.95449 2.00000 -11.12527 0.00000 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 DUMMY