diff --git a/assets/imported/animations/Crouched To Standing.fbx b/assets/imported/animations/Crouched To Standing.fbx new file mode 100644 index 0000000..a2533c3 Binary files /dev/null and b/assets/imported/animations/Crouched To Standing.fbx differ diff --git a/assets/imported/animations/Crouching Idle.fbx b/assets/imported/animations/Crouching Idle.fbx new file mode 100644 index 0000000..9738ed3 Binary files /dev/null and b/assets/imported/animations/Crouching Idle.fbx differ diff --git a/assets/imported/animations/Sneaking Forward.fbx b/assets/imported/animations/Sneaking Forward.fbx index 98633cd..5721162 100644 Binary files a/assets/imported/animations/Sneaking Forward.fbx and b/assets/imported/animations/Sneaking Forward.fbx differ diff --git a/assets/imported/animations/Standing To Crouched.fbx b/assets/imported/animations/Standing To Crouched.fbx new file mode 100644 index 0000000..9b04050 Binary files /dev/null and b/assets/imported/animations/Standing To Crouched.fbx differ diff --git a/blight-assets/src/main/resources/Characters/clips/Crouched To Standing.j3o b/blight-assets/src/main/resources/Characters/clips/Crouched To Standing.j3o new file mode 100644 index 0000000..3c43c6a Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Crouched To Standing.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Crouching Idle.j3o b/blight-assets/src/main/resources/Characters/clips/Crouching Idle.j3o new file mode 100644 index 0000000..1451b04 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Crouching Idle.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sneaking Forward.j3o b/blight-assets/src/main/resources/Characters/clips/Sneaking Forward.j3o new file mode 100644 index 0000000..8f48c98 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sneaking Forward.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Standing To Crouched.j3o b/blight-assets/src/main/resources/Characters/clips/Standing To Crouched.j3o new file mode 100644 index 0000000..2161269 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Standing To Crouched.j3o differ 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 ab4958e..7b0d787 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -26,7 +26,11 @@ "Sword And Shield Strafe Right", "Sword And Shield Heavy Attack", "Picking Up", - "Roll" + "Roll", + "Crouched To Standing", + "Crouching Idle", + "Sneaking Forward", + "Standing To Crouched" ], "actionMap": { "DEFAULT": "Idle", @@ -55,7 +59,11 @@ "SWORD_BLOCK_STRAFE_RIGHT": "Sword And Shield Strafe Right", "SWORD_HEAVY_ATTACK": "Sword And Shield Heavy Attack", "PICK_UP": "Picking Up", - "ROLL": "Roll" + "ROLL": "Roll", + "DUCK": "Standing To Crouched", + "CROUCH_IDLE": "Crouching Idle", + "SNEAK": "Sneaking Forward", + "CROUCH_TO_STAND": "Crouched To Standing" }, "previewModelPath": "Characters/Models/hero.j3o", "animOffsets": { @@ -116,6 +124,7 @@ "Sword And Shield Strafe Left", "Sword And Shield Strafe Right", "Sword And Shield Heavy Attack", - "Roll" + "Roll", + "Sneaking Forward" ] } \ No newline at end of file diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java index 84023ad..5070fa8 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java @@ -16,6 +16,9 @@ public enum AnimationAction { RUNNING_JUMP, ROLL, DUCK, + CROUCH_IDLE, + CROUCH_TO_STAND, + SNEAK, PICK_UP, LIE_DOWN, LIE_UP, @@ -80,7 +83,10 @@ public enum AnimationAction { case JUMP -> TextRegistry.resolve(null, key, "Jump"); case RUNNING_JUMP -> TextRegistry.resolve(null, key, "Running Jump"); case ROLL -> TextRegistry.resolve(null, key, "Rolle"); - case DUCK -> TextRegistry.resolve(null, key, "Duck"); + case DUCK -> TextRegistry.resolve(null, key, "Hinducken"); + case CROUCH_IDLE -> TextRegistry.resolve(null, key, "Ducken (Idle)"); + case CROUCH_TO_STAND -> TextRegistry.resolve(null, key, "Aufstehen (Ducken)"); + case SNEAK -> TextRegistry.resolve(null, key, "Schleichen"); case PICK_UP -> TextRegistry.resolve(null, key, "Pick up"); case LIE_DOWN -> TextRegistry.resolve(null, key, "Hinlegen"); case LIE_UP -> TextRegistry.resolve(null, key, "Aufstehen (Bett)"); 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 96499eb..9e8517d 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 @@ -23,6 +23,7 @@ public class KeyBindings { public int drawRangedWeapon = KeyInput.KEY_2; public int sheathWeapon = KeyInput.KEY_X; public int roll = KeyInput.KEY_LCONTROL; + public int sneak = KeyInput.KEY_X; /** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */ public static final String[][] ENTRIES = { @@ -44,6 +45,7 @@ public class KeyBindings { {"drawRangedWeapon", "Fernkampfwaffe ziehen"}, {"sheathWeapon", "Waffe wegstecken"}, {"roll", "Rolle"}, + {"sneak", "Schleichen"}, }; 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 eb26c8a..213ba32 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 @@ -103,11 +103,12 @@ public class PlayerInputControl { 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 ACT_SNEAK = "Sneak"; 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_ROLL + ACT_ROLL, ACT_SNEAK }; private final InputManager inputManager; @@ -273,6 +274,12 @@ public class PlayerInputControl { /** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */ private Vector3f autopilotDir = null; + // ── Schleichen / Ducken ─────────────────────────────────────────────────── + private enum SneakPhase { NONE, DOWN, ACTIVE, UP } + private SneakPhase sneakPhase = SneakPhase.NONE; + private float sneakTransitionRemaining = 0f; + private boolean sneakKeyHeld = false; + // ── Dodge Roll ──────────────────────────────────────────────────────────── private boolean rollActive = false; private float rollRemaining = 0f; @@ -333,7 +340,8 @@ public class PlayerInputControl { } } case ACT_COMBAT_ALT -> handleCombatAlt(isPressed); - case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); } + case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); } + case ACT_SNEAK -> { if (!weaponDrawn) handleSneakKey(isPressed); } } }; @@ -712,6 +720,7 @@ public class PlayerInputControl { 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.addMapping(ACT_SNEAK, new KeyTrigger(kb.sneak)); inputManager.addListener(actionListener, ACTION_NAMES); } @@ -844,6 +853,25 @@ public class PlayerInputControl { } } + // ── Sneak-Übergangs-Phasen: Timer ticken, Bewegung läuft weiter (walk speed) ── + if (sneakPhase == SneakPhase.DOWN) { + sneakTransitionRemaining -= tpf; + if (sneakTransitionRemaining <= 0f) { + if (sneakKeyHeld) { + sneakPhase = SneakPhase.ACTIVE; + currentAnim = null; + } else { + startSneakUp(); + } + } + } else if (sneakPhase == SneakPhase.UP) { + sneakTransitionRemaining -= tpf; + if (sneakTransitionRemaining <= 0f) { + sneakPhase = SneakPhase.NONE; + currentAnim = null; + } + } + // Ruhezustand (liegen / sitzen) – blockiert alle Bewegungseingaben if (lockedInPlace) { physicsChar.setWalkDirection(Vector3f.ZERO); @@ -886,16 +914,19 @@ public class PlayerInputControl { // Schon ab WAITING Gehgeschwindigkeit erzwingen, damit beim Blockstart kein Sprint-Frame entsteht boolean inBlockStance = isBlocking() || blockPhase == BlockPhase.WAITING; + boolean sneaking = sneakPhase != SneakPhase.NONE; + // Ausdauer: Regen + Sprint-Verbrauch - boolean isSprinting = sprint && !weaponDrawn && moving && !staminaExhausted; + boolean isSprinting = sprint && !weaponDrawn && moving && !staminaExhausted && !sneaking; tickStamina(tpf, isSprinting); if (moving) { scratchMoveDir.normalizeLocal(); - boolean canSprint = sprint && !weaponDrawn && !staminaExhausted + boolean canSprint = !sneaking && sprint && !weaponDrawn && !staminaExhausted && (mainCharacter == null || mainCharacter.getMaxStamina() <= 0 || mainCharacter.getCurrentStamina() > 0); - float speed = (inDeepWater || walk || inBlockStance) ? MOVE_SPEED * WALK_MULT + float speed = sneaking ? MOVE_SPEED * WALK_MULT + : (inDeepWater || walk || inBlockStance) ? MOVE_SPEED * WALK_MULT : canSprint ? MOVE_SPEED * SPRINT_MULT : MOVE_SPEED; physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed)); @@ -1038,10 +1069,14 @@ public class PlayerInputControl { if (groundGraceFrames > 0) groundGraceFrames--; // Block-Phase verwaltet ihre eigenen Animationen. - if (!combatAnimActive && !isBlocking()) { + // Während Sneak-Übergang (DOWN/UP) läuft die Transitions-Animation durch – nicht überschreiben. + if (!combatAnimActive && !isBlocking() + && sneakPhase != SneakPhase.DOWN && sneakPhase != SneakPhase.UP) { AnimationAction target; if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) { target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP; + } else if (sneaking) { + target = moving ? AnimationAction.SNEAK : AnimationAction.CROUCH_IDLE; } else if (moving) { target = (inDeepWater || walk) ? AnimationAction.WALK : sprint ? AnimationAction.SPRINT @@ -1365,6 +1400,31 @@ public class PlayerInputControl { return true; } + private void handleSneakKey(boolean pressed) { + sneakKeyHeld = pressed; + if (pressed && sneakPhase == SneakPhase.NONE + && !rollActive && !pickupActive && !blockingAnimActive + && !turnActive && !lockedInPlace && !combatAnimActive) { + sneakPhase = SneakPhase.DOWN; + boolean moving = forward || backward || left || right; + sneakTransitionRemaining = resolveClipLength(AnimationAction.DUCK, 0.5f) * (moving ? 0.5f : 1f); + nextTransitionLength = 0.1f; + playAction(AnimationAction.DUCK); + currentAnim = AnimationAction.DUCK; + } else if (!pressed && sneakPhase == SneakPhase.ACTIVE) { + startSneakUp(); + } + } + + private void startSneakUp() { + sneakPhase = SneakPhase.UP; + boolean moving = forward || backward || left || right; + sneakTransitionRemaining = resolveClipLength(AnimationAction.CROUCH_TO_STAND, 0.5f) * (moving ? 0.5f : 1f); + nextTransitionLength = 0.1f; + playAction(AnimationAction.CROUCH_TO_STAND); + currentAnim = AnimationAction.CROUCH_TO_STAND; + } + private void triggerRollFromInput() { if (rollActive || pickupActive || blockingAnimActive || turnActive || lockedInPlace || combatAnimActive || isBlocking()) return; @@ -1772,22 +1832,32 @@ public class PlayerInputControl { } private String currentAnimToGait(AnimationAction anim) { - if (anim == AnimationAction.WALK) return "walking"; - if (anim == AnimationAction.RUN) return "running"; - if (anim == AnimationAction.SPRINT) return "sprinting"; - return null; + if (anim == null) return null; + return switch (anim) { + case WALK, + SWORD_WALK, AXE_WALK, TWO_HAND_WALK, + BOW_WALK, CROSSBOW_WALK, CRYSTAL_WALK, + SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, + SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT -> "walking"; + case RUN, + SWORD_RUN, AXE_RUN, TWO_HAND_RUN, + BOW_RUN, CROSSBOW_RUN, CRYSTAL_RUN -> "running"; + case SPRINT -> "sprinting"; + case SNEAK -> "walking"; + default -> null; + }; } private void triggerStep(String gait) { + float vol = (sneakPhase == SneakPhase.ACTIVE) ? 0.25f : 1f; if (currentWaterDepth > 0f) { - // Im Wasser: Wassergeräusche aus footsteps/water/walking bzw. /running float[] weights = new float[de.blight.game.audio.SurfaceType.values().length]; weights[de.blight.game.audio.SurfaceType.WATER.ordinal()] = 1f; - footstepSystem.play(weights, gait); + footstepSystem.play(weights, gait, vol); } else { Vector3f pos = physicsChar.getPhysicsLocation(); float[] weights = surfaceQuery.apply(pos.x, pos.z); - footstepSystem.play(weights, gait); + footstepSystem.play(weights, gait, vol); } }