diff --git a/blight-assets/src/main/resources/Characters/sets/human.animset.json b/blight-assets/src/main/resources/Characters/sets/human.animset.json index b42ac66..ab4958e 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -115,6 +115,7 @@ "Sword And Shield Block Walk Backwards", "Sword And Shield Strafe Left", "Sword And Shield Strafe Right", - "Sword And Shield Heavy Attack" + "Sword And Shield Heavy Attack", + "Roll" ] } \ No newline at end of file diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java index ec68382..96499eb 100644 --- a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java +++ b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java @@ -22,6 +22,7 @@ public class KeyBindings { public int drawMeleeWeapon = KeyInput.KEY_1; public int drawRangedWeapon = KeyInput.KEY_2; public int sheathWeapon = KeyInput.KEY_X; + public int roll = KeyInput.KEY_LCONTROL; /** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */ public static final String[][] ENTRIES = { @@ -42,6 +43,7 @@ public class KeyBindings { {"drawMeleeWeapon", "Nahkampfwaffe ziehen"}, {"drawRangedWeapon", "Fernkampfwaffe ziehen"}, {"sheathWeapon", "Waffe wegstecken"}, + {"roll", "Rolle"}, }; public int get(String fieldName) { 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 66663ef..eb26c8a 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 @@ -7,9 +7,16 @@ import com.jme3.anim.tween.action.BaseAction; import com.jme3.bullet.control.CharacterControl; import com.jme3.input.InputManager; import com.jme3.input.MouseInput; +import com.jme3.input.RawInputListener; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.input.event.JoyAxisEvent; +import com.jme3.input.event.JoyButtonEvent; +import com.jme3.input.event.KeyInputEvent; +import com.jme3.input.event.MouseButtonEvent; +import com.jme3.input.event.MouseMotionEvent; +import com.jme3.input.event.TouchEvent; import de.blight.common.model.ItemSubCategory; import de.blight.common.model.MainCharacter; import com.jme3.math.Quaternion; @@ -64,8 +71,8 @@ public class PlayerInputControl { private static final int STAMINA_ATTACK5_COST = 20; private static final int STAMINA_HEAVY_COST = 30; private static final int STAMINA_BLOCK_HIT_COST = 30; - private static final int STAMINA_ROLL_COST = 20; - private static final float ROLL_DOUBLE_TAP_WINDOW = 0.3f; + private static final int STAMINA_ROLL_COST = 20; + private static final float ROLL_AIRBORNE_THRESHOLD = 0.90f; // Hips-Y unter 90% Baseline → Luftphase /** Dauer der Ziehen/Wegstecken-Animation in Sekunden. */ private static final float DRAW_DURATION = 0.85f; // Ziel-Quaternions für die Ziehen-Animation: aus Heavy-Attack-Wind-Up bei t=0.933s gelesen @@ -95,10 +102,12 @@ public class PlayerInputControl { private static final String ACT_SHEATH = "SheathWeapon"; private static final String ACT_COMBAT_ATK = "CombatAttack"; private static final String ACT_COMBAT_ALT = "CombatAltAction"; + private static final String ACT_ROLL = "Roll"; private static final String[] ACTION_NAMES = { "Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk", - ACT_DRAW_MELEE, ACT_DRAW_RANGED, ACT_SHEATH, ACT_COMBAT_ATK, ACT_COMBAT_ALT + ACT_DRAW_MELEE, ACT_DRAW_RANGED, ACT_SHEATH, ACT_COMBAT_ATK, ACT_COMBAT_ALT, + ACT_ROLL }; private final InputManager inputManager; @@ -107,6 +116,8 @@ public class PlayerInputControl { private CharacterControl physicsChar; private Spatial visual; + private int sprintKey = -1; + private RawInputListener sprintRawListener = null; private boolean forward, backward, left, right, sprint, walk; private boolean paused = false; @@ -263,15 +274,13 @@ public class PlayerInputControl { private Vector3f autopilotDir = null; // ── Dodge Roll ──────────────────────────────────────────────────────────── - // Double-tap-Timer pro Richtung: -1 = kein erster Tap ausstehend - private float rollTapFwd = -1f; - private float rollTapBwd = -1f; - private float rollTapLeft = -1f; - private float rollTapRight = -1f; - private int pendingRollDir = 0; // 1=vor, 2=zurück, 3=links, 4=rechts - private boolean rollActive = false; - private float rollRemaining = 0f; - private Vector3f rollDir = null; + private boolean rollActive = false; + private float rollRemaining = 0f; + private float rollInitRemaining = 0f; + private boolean rollBaselineCaptured = false; + private float rollHipsBaseY = 0f; + private boolean rollAirborne = false; + private Vector3f rollDir = null; // Pre-allokierte Scratch-Objekte — keine Heap-Allokation pro Frame in update() private final Vector3f scratchCamDir = new Vector3f(); @@ -285,11 +294,11 @@ public class PlayerInputControl { private final ActionListener actionListener = (name, isPressed, tpf) -> { if (paused) return; switch (name) { - case "Forward" -> { if (isPressed) detectRollTap(1); forward = isPressed; } - case "Backward" -> { if (isPressed) detectRollTap(2); backward = isPressed; } - case "Left" -> { if (isPressed) detectRollTap(3); left = isPressed; } - case "Right" -> { if (isPressed) detectRollTap(4); right = isPressed; } - case "Sprint" -> sprint = isPressed; + case "Forward" -> forward = isPressed; + case "Backward" -> backward = isPressed; + case "Left" -> left = isPressed; + case "Right" -> right = isPressed; + case "Sprint" -> sprint = isPressed; case "Walk" -> walk = isPressed; case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } } case ACT_DRAW_MELEE -> { if (isPressed) handleDrawMelee(); } @@ -324,6 +333,7 @@ public class PlayerInputControl { } } case ACT_COMBAT_ALT -> handleCombatAlt(isPressed); + case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); } } }; @@ -680,12 +690,28 @@ public class PlayerInputControl { inputManager.addMapping("Right", new KeyTrigger(kb.right)); inputManager.addMapping("Jump", new KeyTrigger(kb.jump)); inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint)); + if (sprintRawListener != null) inputManager.removeRawInputListener(sprintRawListener); + sprintKey = kb.sprint; + sprintRawListener = new RawInputListener() { + public void onKeyEvent(KeyInputEvent e) { + if (e.getKeyCode() == sprintKey) sprint = !e.isReleased(); + } + public void beginInput() {} + public void endInput() {} + public void onJoyAxisEvent(JoyAxisEvent e) {} + public void onJoyButtonEvent(JoyButtonEvent e) {} + public void onMouseMotionEvent(MouseMotionEvent e) {} + public void onMouseButtonEvent(MouseButtonEvent e) {} + public void onTouchEvent(TouchEvent e) {} + }; + inputManager.addRawInputListener(sprintRawListener); inputManager.addMapping("Walk", new KeyTrigger(kb.walk)); inputManager.addMapping(ACT_DRAW_MELEE, new KeyTrigger(kb.drawMeleeWeapon)); inputManager.addMapping(ACT_DRAW_RANGED, new KeyTrigger(kb.drawRangedWeapon)); inputManager.addMapping(ACT_SHEATH, new KeyTrigger(kb.sheathWeapon)); inputManager.addMapping(ACT_COMBAT_ATK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); inputManager.addMapping(ACT_COMBAT_ALT, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); + inputManager.addMapping(ACT_ROLL, new KeyTrigger(kb.roll)); inputManager.addListener(actionListener, ACTION_NAMES); } @@ -716,29 +742,6 @@ public class PlayerInputControl { return; } - // Double-tap-Timer ablaufen lassen - if (rollTapFwd > 0f) { rollTapFwd -= tpf; if (rollTapFwd < 0f) rollTapFwd = -1f; } - if (rollTapBwd > 0f) { rollTapBwd -= tpf; if (rollTapBwd < 0f) rollTapBwd = -1f; } - if (rollTapLeft > 0f) { rollTapLeft -= tpf; if (rollTapLeft < 0f) rollTapLeft = -1f; } - if (rollTapRight > 0f) { rollTapRight -= tpf; if (rollTapRight < 0f) rollTapRight = -1f; } - - // Doppeltippen → Rolle einleiten (nicht während Kampf, Block, Pickup usw.) - if (pendingRollDir != 0) { - if (!rollActive && !pickupActive && !blockingAnimActive && !turnActive - && !lockedInPlace && !combatAnimActive && !isBlocking()) { - cam.getDirection(scratchCamDir).setY(0).normalizeLocal(); - cam.getLeft(scratchCamLeft).setY(0).normalizeLocal(); - Vector3f dir = switch (pendingRollDir) { - case 1 -> scratchCamDir.clone(); - case 2 -> scratchCamDir.clone().negateLocal(); - case 3 -> scratchCamLeft.clone(); - default -> scratchCamLeft.clone().negateLocal(); - }; - startRoll(dir); - } - pendingRollDir = 0; - } - // Navigator: hat Vorrang vor allem außer Pause if (navigator != null && navigator.isActive()) { navigator.update(tpf); @@ -763,11 +766,42 @@ public class PlayerInputControl { rollActive = false; rollDir = null; currentAnim = null; + if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); } else { - if (physicsChar != null) { - physicsChar.setWalkDirection(Vector3f.ZERO); + boolean rollEndedEarly = false; + if (rollInitRemaining > 0f) { + rollInitRemaining -= tpf; + if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); + } else if (!rollBaselineCaptured && hipsJoint != null) { + rollBaselineCaptured = true; + rollHipsBaseY = hipsJoint.getModelTransform().getTranslation().y; + rollAirborne = false; + log.info("[ROLL] Baseline hipsY={}", rollHipsBaseY); + if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); + } else if (rollBaselineCaptured && hipsJoint != null && physicsChar != null) { + float hipsY = hipsJoint.getModelTransform().getTranslation().y; + boolean wasAirborne = rollAirborne; + rollAirborne = hipsY < rollHipsBaseY * ROLL_AIRBORNE_THRESHOLD; + if (rollAirborne != wasAirborne) { + log.info("[ROLL] hipsY={} baseY={} → {}", hipsY, rollHipsBaseY, + rollAirborne ? "AIRBORNE" : "GROUNDED"); + } + // Gelandet + Bewegungstaste → Rolle beenden, direkt in Lauf-Logik fallen + if (wasAirborne && !rollAirborne && (forward || backward || left || right)) { + rollActive = false; + rollDir = null; + currentAnim = null; + rollEndedEarly = true; + physicsChar.setWalkDirection(Vector3f.ZERO); + } else if (rollAirborne && rollDir != null) { + physicsChar.setWalkDirection(scratchWalk.set(rollDir).multLocal(MOVE_SPEED)); + } else { + physicsChar.setWalkDirection(Vector3f.ZERO); + } + } else { + if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); } - return; + if (!rollEndedEarly) return; } } @@ -1331,32 +1365,28 @@ public class PlayerInputControl { return true; } - private void detectRollTap(int dir) { - float cur = switch (dir) { - case 1 -> rollTapFwd; - case 2 -> rollTapBwd; - case 3 -> rollTapLeft; - default -> rollTapRight; - }; - if (cur > 0f) { - pendingRollDir = dir; - } else { - switch (dir) { - case 1 -> rollTapFwd = ROLL_DOUBLE_TAP_WINDOW; - case 2 -> rollTapBwd = ROLL_DOUBLE_TAP_WINDOW; - case 3 -> rollTapLeft = ROLL_DOUBLE_TAP_WINDOW; - default -> rollTapRight = ROLL_DOUBLE_TAP_WINDOW; - } - } + private void triggerRollFromInput() { + if (rollActive || pickupActive || blockingAnimActive || turnActive + || lockedInPlace || combatAnimActive || isBlocking()) return; + cam.getDirection(scratchCamDir).setY(0).normalizeLocal(); + cam.getLeft(scratchCamLeft).setY(0).normalizeLocal(); + Vector3f rollDirVec; + if (backward && !forward) rollDirVec = scratchCamDir.clone().negateLocal(); + else if (left && !right) rollDirVec = scratchCamLeft.clone(); + else if (right && !left) rollDirVec = scratchCamLeft.clone().negateLocal(); + else rollDirVec = scratchCamDir.clone(); + startRoll(rollDirVec); } private void startRoll(Vector3f dir) { if (!spendStamina(STAMINA_ROLL_COST)) return; nextTransitionLength = 0.1f; - rollActive = true; - rollRemaining = resolveClipLength(AnimationAction.ROLL, 0.7f); - rollDir = dir; - forward = backward = left = right = false; + rollActive = true; + rollRemaining = resolveClipLength(AnimationAction.ROLL, 0.7f); + rollInitRemaining = 0.12f; + rollBaselineCaptured = false; + rollAirborne = false; + rollDir = dir; if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); if (visual != null) { scratchRot.lookAt(dir, Vector3f.UNIT_Y);