Compare commits

...

3 Commits

21 changed files with 297 additions and 91 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -24,7 +24,13 @@
"Sword And Shield Block Walk Backwards", "Sword And Shield Block Walk Backwards",
"Sword And Shield Strafe Left", "Sword And Shield Strafe Left",
"Sword And Shield Strafe Right", "Sword And Shield Strafe Right",
"Sword And Shield Heavy Attack" "Sword And Shield Heavy Attack",
"Picking Up",
"Roll",
"Crouched To Standing",
"Crouching Idle",
"Sneaking Forward",
"Standing To Crouched"
], ],
"actionMap": { "actionMap": {
"DEFAULT": "Idle", "DEFAULT": "Idle",
@@ -51,7 +57,13 @@
"SWORD_BLOCK_MOVE_BACKWARD": "Sword And Shield Block Walk Backwards", "SWORD_BLOCK_MOVE_BACKWARD": "Sword And Shield Block Walk Backwards",
"SWORD_BLOCK_STRAFE_LEFT": "Sword And Shield Strafe Left", "SWORD_BLOCK_STRAFE_LEFT": "Sword And Shield Strafe Left",
"SWORD_BLOCK_STRAFE_RIGHT": "Sword And Shield Strafe Right", "SWORD_BLOCK_STRAFE_RIGHT": "Sword And Shield Strafe Right",
"SWORD_HEAVY_ATTACK": "Sword And Shield Heavy Attack" "SWORD_HEAVY_ATTACK": "Sword And Shield Heavy Attack",
"PICK_UP": "Picking Up",
"ROLL": "Roll",
"DUCK": "Standing To Crouched",
"CROUCH_IDLE": "Crouching Idle",
"SNEAK": "Sneaking Forward",
"CROUCH_TO_STAND": "Crouched To Standing"
}, },
"previewModelPath": "Characters/Models/hero.j3o", "previewModelPath": "Characters/Models/hero.j3o",
"animOffsets": { "animOffsets": {
@@ -111,6 +123,8 @@
"Sword And Shield Block Walk Backwards", "Sword And Shield Block Walk Backwards",
"Sword And Shield Strafe Left", "Sword And Shield Strafe Left",
"Sword And Shield Strafe Right", "Sword And Shield Strafe Right",
"Sword And Shield Heavy Attack" "Sword And Shield Heavy Attack",
"Roll",
"Sneaking Forward"
] ]
} }

View File

@@ -14,7 +14,11 @@ public enum AnimationAction {
SPRINT, SPRINT,
JUMP, JUMP,
RUNNING_JUMP, RUNNING_JUMP,
ROLL,
DUCK, DUCK,
CROUCH_IDLE,
CROUCH_TO_STAND,
SNEAK,
PICK_UP, PICK_UP,
LIE_DOWN, LIE_DOWN,
LIE_UP, LIE_UP,
@@ -78,7 +82,11 @@ public enum AnimationAction {
case SPRINT -> TextRegistry.resolve(null, key, "Sprint"); case SPRINT -> TextRegistry.resolve(null, key, "Sprint");
case JUMP -> TextRegistry.resolve(null, key, "Jump"); case JUMP -> TextRegistry.resolve(null, key, "Jump");
case RUNNING_JUMP -> TextRegistry.resolve(null, key, "Running Jump"); case RUNNING_JUMP -> TextRegistry.resolve(null, key, "Running Jump");
case DUCK -> TextRegistry.resolve(null, key, "Duck"); case ROLL -> TextRegistry.resolve(null, key, "Rolle");
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 PICK_UP -> TextRegistry.resolve(null, key, "Pick up");
case LIE_DOWN -> TextRegistry.resolve(null, key, "Hinlegen"); case LIE_DOWN -> TextRegistry.resolve(null, key, "Hinlegen");
case LIE_UP -> TextRegistry.resolve(null, key, "Aufstehen (Bett)"); case LIE_UP -> TextRegistry.resolve(null, key, "Aufstehen (Bett)");

View File

@@ -22,6 +22,8 @@ public class KeyBindings {
public int drawMeleeWeapon = KeyInput.KEY_1; public int drawMeleeWeapon = KeyInput.KEY_1;
public int drawRangedWeapon = KeyInput.KEY_2; public int drawRangedWeapon = KeyInput.KEY_2;
public int sheathWeapon = KeyInput.KEY_X; 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. */ /** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
public static final String[][] ENTRIES = { public static final String[][] ENTRIES = {
@@ -42,6 +44,8 @@ public class KeyBindings {
{"drawMeleeWeapon", "Nahkampfwaffe ziehen"}, {"drawMeleeWeapon", "Nahkampfwaffe ziehen"},
{"drawRangedWeapon", "Fernkampfwaffe ziehen"}, {"drawRangedWeapon", "Fernkampfwaffe ziehen"},
{"sheathWeapon", "Waffe wegstecken"}, {"sheathWeapon", "Waffe wegstecken"},
{"roll", "Rolle"},
{"sneak", "Schleichen"},
}; };
public int get(String fieldName) { public int get(String fieldName) {

View File

@@ -7,9 +7,16 @@ import com.jme3.anim.tween.action.BaseAction;
import com.jme3.bullet.control.CharacterControl; import com.jme3.bullet.control.CharacterControl;
import com.jme3.input.InputManager; import com.jme3.input.InputManager;
import com.jme3.input.MouseInput; import com.jme3.input.MouseInput;
import com.jme3.input.RawInputListener;
import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger; import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger; 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.ItemSubCategory;
import de.blight.common.model.MainCharacter; import de.blight.common.model.MainCharacter;
import com.jme3.math.Quaternion; import com.jme3.math.Quaternion;
@@ -64,6 +71,8 @@ public class PlayerInputControl {
private static final int STAMINA_ATTACK5_COST = 20; private static final int STAMINA_ATTACK5_COST = 20;
private static final int STAMINA_HEAVY_COST = 30; private static final int STAMINA_HEAVY_COST = 30;
private static final int STAMINA_BLOCK_HIT_COST = 30; private static final int STAMINA_BLOCK_HIT_COST = 30;
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. */ /** Dauer der Ziehen/Wegstecken-Animation in Sekunden. */
private static final float DRAW_DURATION = 0.85f; 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 // Ziel-Quaternions für die Ziehen-Animation: aus Heavy-Attack-Wind-Up bei t=0.933s gelesen
@@ -93,10 +102,13 @@ public class PlayerInputControl {
private static final String ACT_SHEATH = "SheathWeapon"; private static final String ACT_SHEATH = "SheathWeapon";
private static final String ACT_COMBAT_ATK = "CombatAttack"; private static final String ACT_COMBAT_ATK = "CombatAttack";
private static final String ACT_COMBAT_ALT = "CombatAltAction"; 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 = { private static final String[] ACTION_NAMES = {
"Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk", "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, ACT_SNEAK
}; };
private final InputManager inputManager; private final InputManager inputManager;
@@ -105,6 +117,8 @@ public class PlayerInputControl {
private CharacterControl physicsChar; private CharacterControl physicsChar;
private Spatial visual; private Spatial visual;
private int sprintKey = -1;
private RawInputListener sprintRawListener = null;
private boolean forward, backward, left, right, sprint, walk; private boolean forward, backward, left, right, sprint, walk;
private boolean paused = false; private boolean paused = false;
@@ -194,6 +208,8 @@ public class PlayerInputControl {
private com.jme3.anim.SkinningControl skinningControl = null; private com.jme3.anim.SkinningControl skinningControl = null;
private com.jme3.anim.Armature armature = null; private com.jme3.anim.Armature armature = null;
private com.jme3.anim.Joint hipsJoint = null;
// ── Fußgeräusche ───────────────────────────────────────────────────────── // ── Fußgeräusche ─────────────────────────────────────────────────────────
// Bone-Namen nach Phase-0-Bone-Log eintragen: // Bone-Namen nach Phase-0-Bone-Log eintragen:
@@ -258,6 +274,21 @@ public class PlayerInputControl {
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */ /** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
private Vector3f autopilotDir = null; 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;
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() // Pre-allokierte Scratch-Objekte — keine Heap-Allokation pro Frame in update()
private final Vector3f scratchCamDir = new Vector3f(); private final Vector3f scratchCamDir = new Vector3f();
private final Vector3f scratchCamLeft = new Vector3f(); private final Vector3f scratchCamLeft = new Vector3f();
@@ -274,7 +305,7 @@ public class PlayerInputControl {
case "Backward" -> backward = isPressed; case "Backward" -> backward = isPressed;
case "Left" -> left = isPressed; case "Left" -> left = isPressed;
case "Right" -> right = isPressed; case "Right" -> right = isPressed;
case "Sprint" -> sprint = isPressed; case "Sprint" -> sprint = isPressed;
case "Walk" -> walk = isPressed; case "Walk" -> walk = isPressed;
case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } } case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } }
case ACT_DRAW_MELEE -> { if (isPressed) handleDrawMelee(); } case ACT_DRAW_MELEE -> { if (isPressed) handleDrawMelee(); }
@@ -283,19 +314,7 @@ public class PlayerInputControl {
case ACT_COMBAT_ATK -> { case ACT_COMBAT_ATK -> {
if (isPressed) { if (isPressed) {
if (!weaponDrawn || blockingAnimActive || drawAnimActive || isBlocking()) break; if (!weaponDrawn || blockingAnimActive || drawAnimActive || isBlocking()) break;
if (combatAnimActive) { // Immer Hold-Timer starten – auch mitten in einer Kombo
if (comboWindowActive) {
// Kombo-Follow während zweiter Hälfte: sofort fluid weiter
combatAnimActive = false;
combatAnimRemaining = 0f;
combatAnimSkipComboWindow = false;
heavyLungeVel = null;
heavyLungeStartRemaining = 0f;
heavyLungeEndRemaining = 0f;
handleCombatAttack();
}
break;
}
attackHoldPending = true; attackHoldPending = true;
attackHoldTimer = 0f; attackHoldTimer = 0f;
} else { } else {
@@ -303,16 +322,26 @@ public class PlayerInputControl {
attackHoldPending = false; attackHoldPending = false;
if (attackHoldTimer > QUICK_TAP_THRESHOLD) { if (attackHoldTimer > QUICK_TAP_THRESHOLD) {
handleHeavyAttack(); handleHeavyAttack();
} else { } else if (combatAnimActive && comboWindowActive) {
// Schneller Klick → schwere Anim abbrechen, normale Kombo starten // Schneller Klick im Kombo-Fenster → Kombo fortsetzen
combatAnimActive = false; combatAnimActive = false;
combatAnimRemaining = 0f; combatAnimRemaining = 0f;
combatAnimSkipComboWindow = false;
heavyLungeVel = null;
heavyLungeStartRemaining = 0f;
heavyLungeEndRemaining = 0f;
handleCombatAttack();
} else if (!combatAnimActive) {
// Schneller Klick außerhalb Kombo → normaler Angriff
handleCombatAttack(); handleCombatAttack();
} }
// Schneller Klick während Kombo aber außerhalb Fenster → ignorieren
} }
} }
} }
case ACT_COMBAT_ALT -> handleCombatAlt(isPressed); case ACT_COMBAT_ALT -> handleCombatAlt(isPressed);
case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); }
case ACT_SNEAK -> { if (!weaponDrawn) handleSneakKey(isPressed); }
} }
}; };
@@ -355,6 +384,7 @@ public class PlayerInputControl {
if (skinningControl != null) { if (skinningControl != null) {
armature = skinningControl.getArmature(); armature = skinningControl.getArmature();
logJointNames(armature); logJointNames(armature);
hipsJoint = armature.getJoint("mixamorig:Hips");
jointRightArm = armature.getJoint("mixamorig:RightArm"); jointRightArm = armature.getJoint("mixamorig:RightArm");
jointRightForeArm = armature.getJoint("mixamorig:RightForeArm"); jointRightForeArm = armature.getJoint("mixamorig:RightForeArm");
jointLeftArm = armature.getJoint("mixamorig:LeftArm"); jointLeftArm = armature.getJoint("mixamorig:LeftArm");
@@ -668,12 +698,29 @@ public class PlayerInputControl {
inputManager.addMapping("Right", new KeyTrigger(kb.right)); inputManager.addMapping("Right", new KeyTrigger(kb.right));
inputManager.addMapping("Jump", new KeyTrigger(kb.jump)); inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint)); 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("Walk", new KeyTrigger(kb.walk));
inputManager.addMapping(ACT_DRAW_MELEE, new KeyTrigger(kb.drawMeleeWeapon)); inputManager.addMapping(ACT_DRAW_MELEE, new KeyTrigger(kb.drawMeleeWeapon));
inputManager.addMapping(ACT_DRAW_RANGED, new KeyTrigger(kb.drawRangedWeapon)); inputManager.addMapping(ACT_DRAW_RANGED, new KeyTrigger(kb.drawRangedWeapon));
inputManager.addMapping(ACT_SHEATH, new KeyTrigger(kb.sheathWeapon)); inputManager.addMapping(ACT_SHEATH, new KeyTrigger(kb.sheathWeapon));
inputManager.addMapping(ACT_COMBAT_ATK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); inputManager.addMapping(ACT_COMBAT_ATK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
inputManager.addMapping(ACT_COMBAT_ALT, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); 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); inputManager.addListener(actionListener, ACTION_NAMES);
} }
@@ -685,7 +732,7 @@ public class PlayerInputControl {
runningClip = animPlayer.getCurrentClip(); runningClip = animPlayer.getCurrentClip();
} }
if (visual != null && animOffsetElapsed < AnimPlayer.BLEND_DURATION) { if (visual != null && animOffsetElapsed < AnimPlayer.BLEND_DURATION && !rollActive) {
animOffsetElapsed += tpf; animOffsetElapsed += tpf;
float t = Math.min(1f, animOffsetElapsed / AnimPlayer.BLEND_DURATION); float t = Math.min(1f, animOffsetElapsed / AnimPlayer.BLEND_DURATION);
animOffsetCurrent.set(animOffsetFrom); animOffsetCurrent.set(animOffsetFrom);
@@ -722,6 +769,51 @@ public class PlayerInputControl {
} }
} }
if (rollActive) {
rollRemaining -= tpf;
if (rollRemaining <= 0f) {
rollActive = false;
rollDir = null;
currentAnim = null;
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
} else {
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);
}
if (!rollEndedEarly) return;
}
}
// Drehung auf der Stelle (kein Vorwärtsbewegen) // Drehung auf der Stelle (kein Vorwärtsbewegen)
if (turnActive) { if (turnActive) {
turnRemaining -= tpf; turnRemaining -= tpf;
@@ -761,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 // Ruhezustand (liegen / sitzen) – blockiert alle Bewegungseingaben
if (lockedInPlace) { if (lockedInPlace) {
physicsChar.setWalkDirection(Vector3f.ZERO); physicsChar.setWalkDirection(Vector3f.ZERO);
@@ -803,16 +914,19 @@ public class PlayerInputControl {
// Schon ab WAITING Gehgeschwindigkeit erzwingen, damit beim Blockstart kein Sprint-Frame entsteht // Schon ab WAITING Gehgeschwindigkeit erzwingen, damit beim Blockstart kein Sprint-Frame entsteht
boolean inBlockStance = isBlocking() || blockPhase == BlockPhase.WAITING; boolean inBlockStance = isBlocking() || blockPhase == BlockPhase.WAITING;
boolean sneaking = sneakPhase != SneakPhase.NONE;
// Ausdauer: Regen + Sprint-Verbrauch // Ausdauer: Regen + Sprint-Verbrauch
boolean isSprinting = sprint && !weaponDrawn && moving && !staminaExhausted; boolean isSprinting = sprint && !weaponDrawn && moving && !staminaExhausted && !sneaking;
tickStamina(tpf, isSprinting); tickStamina(tpf, isSprinting);
if (moving) { if (moving) {
scratchMoveDir.normalizeLocal(); scratchMoveDir.normalizeLocal();
boolean canSprint = sprint && !weaponDrawn && !staminaExhausted boolean canSprint = !sneaking && sprint && !weaponDrawn && !staminaExhausted
&& (mainCharacter == null || mainCharacter.getMaxStamina() <= 0 && (mainCharacter == null || mainCharacter.getMaxStamina() <= 0
|| mainCharacter.getCurrentStamina() > 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 : canSprint ? MOVE_SPEED * SPRINT_MULT
: MOVE_SPEED; : MOVE_SPEED;
physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed)); physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed));
@@ -838,9 +952,20 @@ public class PlayerInputControl {
// Hold-Timer muss jede Frame akkumulieren, unabhängig vom Anim-Status // Hold-Timer muss jede Frame akkumulieren, unabhängig vom Anim-Status
if (attackHoldPending) { if (attackHoldPending) {
attackHoldTimer += tpf; attackHoldTimer += tpf;
// Erst wenn klar ist dass es ein schwerer Angriff wird → Animation starten if (attackHoldTimer > QUICK_TAP_THRESHOLD) {
if (attackHoldTimer > QUICK_TAP_THRESHOLD && !combatAnimActive) { if (combatAnimActive && !combatAnimSkipComboWindow) {
startHeavyCharge(); // Normale Kombo unterbrechen und schweren Angriff einleiten
combatAnimActive = false;
combatAnimRemaining = 0f;
comboWindowActive = false;
comboStep = 0;
heavyLungeVel = null;
heavyLungeStartRemaining = 0f;
heavyLungeEndRemaining = 0f;
startHeavyCharge(); // setzt combatAnimSkipComboWindow = true → verhindert Loop
} else if (!combatAnimActive) {
startHeavyCharge();
}
} }
} }
@@ -944,10 +1069,14 @@ public class PlayerInputControl {
if (groundGraceFrames > 0) groundGraceFrames--; if (groundGraceFrames > 0) groundGraceFrames--;
// Block-Phase verwaltet ihre eigenen Animationen. // 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; AnimationAction target;
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) { if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP; target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
} else if (sneaking) {
target = moving ? AnimationAction.SNEAK : AnimationAction.CROUCH_IDLE;
} else if (moving) { } else if (moving) {
target = (inDeepWater || walk) ? AnimationAction.WALK target = (inDeepWater || walk) ? AnimationAction.WALK
: sprint ? AnimationAction.SPRINT : sprint ? AnimationAction.SPRINT
@@ -1271,6 +1400,62 @@ public class PlayerInputControl {
return true; 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;
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);
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);
visual.setLocalRotation(scratchRot);
}
playAction(AnimationAction.ROLL);
currentAnim = AnimationAction.ROLL;
}
private boolean hasShieldEquipped() { private boolean hasShieldEquipped() {
return mainCharacter != null && mainCharacter.getEquippedShield() != null; return mainCharacter != null && mainCharacter.getEquippedShield() != null;
} }
@@ -1647,22 +1832,32 @@ public class PlayerInputControl {
} }
private String currentAnimToGait(AnimationAction anim) { private String currentAnimToGait(AnimationAction anim) {
if (anim == AnimationAction.WALK) return "walking"; if (anim == null) return null;
if (anim == AnimationAction.RUN) return "running"; return switch (anim) {
if (anim == AnimationAction.SPRINT) return "sprinting"; case WALK,
return null; 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) { private void triggerStep(String gait) {
float vol = (sneakPhase == SneakPhase.ACTIVE) ? 0.25f : 1f;
if (currentWaterDepth > 0f) { if (currentWaterDepth > 0f) {
// Im Wasser: Wassergeräusche aus footsteps/water/walking bzw. /running
float[] weights = new float[de.blight.game.audio.SurfaceType.values().length]; float[] weights = new float[de.blight.game.audio.SurfaceType.values().length];
weights[de.blight.game.audio.SurfaceType.WATER.ordinal()] = 1f; weights[de.blight.game.audio.SurfaceType.WATER.ordinal()] = 1f;
footstepSystem.play(weights, gait); footstepSystem.play(weights, gait, vol);
} else { } else {
Vector3f pos = physicsChar.getPhysicsLocation(); Vector3f pos = physicsChar.getPhysicsLocation();
float[] weights = surfaceQuery.apply(pos.x, pos.z); float[] weights = surfaceQuery.apply(pos.x, pos.z);
footstepSystem.play(weights, gait); footstepSystem.play(weights, gait, vol);
} }
} }

View File

@@ -409,7 +409,7 @@ public class WorldScene extends BaseAppState {
WorldNpcsState npcsState = new WorldNpcsState(keyBindings, physicsChar, playerInput, mc); WorldNpcsState npcsState = new WorldNpcsState(keyBindings, physicsChar, playerInput, mc);
npcsState.setThirdPersonCamera(thirdPersonCam); npcsState.setThirdPersonCamera(thirdPersonCam);
app.getStateManager().attach(npcsState); app.getStateManager().attach(npcsState);
app.getStateManager().attach(new InteractionHudState()); app.getStateManager().attach(new InteractionHudState(keyBindings));
inventoryState = new InventoryState(mc, keyBindings); inventoryState = new InventoryState(mc, keyBindings);
inventoryState.setEnabled(false); inventoryState.setEnabled(false);
app.getStateManager().attach(inventoryState); app.getStateManager().attach(inventoryState);

View File

@@ -10,6 +10,8 @@ import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import de.blight.game.config.KeyBindings;
import de.blight.game.config.KeyNames;
import de.blight.lang.TextResolver; import de.blight.lang.TextResolver;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -33,6 +35,10 @@ public class InteractionHudState extends BaseAppState {
private DialogHudState dialogHud; private DialogHudState dialogHud;
private BitmapText labelText; private BitmapText labelText;
private KeyBindings keyBindings;
public InteractionHudState() {}
public InteractionHudState(KeyBindings keyBindings) { this.keyBindings = keyBindings; }
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
@@ -134,6 +140,9 @@ public class InteractionHudState extends BaseAppState {
} }
String text = TextResolver.get().resolveId(labelKey); String text = TextResolver.get().resolveId(labelKey);
if (keyBindings != null) {
text = text.replace("{interact}", KeyNames.of(keyBindings.interact));
}
labelText.setText(text); labelText.setText(text);
float textW = labelText.getLineWidth(); float textW = labelText.getLineWidth();
labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f); labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f);

View File

@@ -8,8 +8,6 @@ import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.control.CharacterControl; import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory; import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.InputManager; import com.jme3.input.InputManager;
import com.jme3.input.MouseInput; import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.ActionListener;
@@ -37,6 +35,7 @@ import de.blight.common.PlacedModel;
import de.blight.common.PlacedModelIO; import de.blight.common.PlacedModelIO;
import de.blight.common.model.CombatDummy; import de.blight.common.model.CombatDummy;
import de.blight.common.model.CombatDummyIO; import de.blight.common.model.CombatDummyIO;
import de.blight.game.animation.AnimationAction;
import de.blight.game.combat.DamageCalculator; import de.blight.game.combat.DamageCalculator;
import de.blight.game.combat.MeleeAttackCallback; import de.blight.game.combat.MeleeAttackCallback;
import de.blight.game.config.KeyBindings; import de.blight.game.config.KeyBindings;
@@ -62,11 +61,12 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
// ── Spielwelt-Konstanten ────────────────────────────────────────────────── // ── Spielwelt-Konstanten ──────────────────────────────────────────────────
private static final float FOCUS_RANGE = 6f; private static final float FOCUS_RANGE = 6f;
private static final float RESTORE_DELAY = 60f; private static final float RESTORE_DELAY = 60f;
private static final float HP_BAR_SHOW_TIME = 5f; //private static final float HP_BAR_SHOW_TIME = 5f;
private static final float STAND_UP_RANGE = 3f; private static final float STAND_UP_RANGE = 3f;
private static final int DEFAULT_MAX_HP = 250; private static final int DEFAULT_MAX_HP = 10;
private static final float FALL_DURATION = 0.35f; private static final float MODEL_HALF_H = 0.75f;
private static final float MODEL_HALF_H = 0.75f; private static final float KNOCK_INITIAL_VEL = 0.4f; // rad/s initial push
private static final float KNOCK_GRAVITY = 8f; // angular accel at upright (rad/s²)
// ── Welt-HP-Bar-Konstanten ──────────────────────────────────────────────── // ── Welt-HP-Bar-Konstanten ────────────────────────────────────────────────
private static final float WB_WIDTH = 0.8f; private static final float WB_WIDTH = 0.8f;
@@ -103,8 +103,6 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
// ── Feind-HUD ───────────────────────────────────────────────────────────── // ── Feind-HUD ─────────────────────────────────────────────────────────────
private Node enemyHudNode; private Node enemyHudNode;
private Geometry enemyHpFill; private Geometry enemyHpFill;
private BitmapText enemyNameText;
private BitmapText enemyHpText;
/** x-Koordinate des linken Randes der Feind-HP-Bar (Screen-Pixel). */ /** x-Koordinate des linken Randes der Feind-HP-Bar (Screen-Pixel). */
private float ehbX; private float ehbX;
/** y-Koordinate des unteren Randes der Feind-HP-Bar (Screen-Pixel). */ /** y-Koordinate des unteren Randes der Feind-HP-Bar (Screen-Pixel). */
@@ -314,25 +312,6 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
enemyHpFill.setLocalTranslation(ehbX + EHB_BORDER, ehbY + EHB_BORDER, 2f); enemyHpFill.setLocalTranslation(ehbX + EHB_BORDER, ehbY + EHB_BORDER, 2f);
enemyHudNode.attachChild(enemyHpFill); enemyHudNode.attachChild(enemyHpFill);
BitmapFont font = assetManager.loadFont("Interface/Fonts/Default.fnt");
float renderedSize = font.getCharSet().getRenderedSize();
// Gegner-Name (über Bar)
enemyNameText = new BitmapText(font);
enemyNameText.setSize(renderedSize * 2f);
enemyNameText.setLocalScale(0.5f);
enemyNameText.setColor(new ColorRGBA(1f, 0.95f, 0.6f, 1f));
enemyNameText.setQueueBucket(RenderQueue.Bucket.Gui);
enemyHudNode.attachChild(enemyNameText);
// HP-Zahlen (in Bar)
enemyHpText = new BitmapText(font);
enemyHpText.setSize(renderedSize * 2f);
enemyHpText.setLocalScale(0.5f);
enemyHpText.setColor(ColorRGBA.White);
enemyHpText.setQueueBucket(RenderQueue.Bucket.Gui);
enemyHudNode.attachChild(enemyHpText);
guiNode.attachChild(enemyHudNode); guiNode.attachChild(enemyHudNode);
enemyHudNode.setCullHint(Spatial.CullHint.Always); enemyHudNode.setCullHint(Spatial.CullHint.Always);
} }
@@ -375,23 +354,8 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
} }
private void updateEnemyHudContent(DummyRuntime dr) { private void updateEnemyHudContent(DummyRuntime dr) {
float sw = cam.getWidth();
float ratio = dr.maxHp > 0 ? (float) dr.currentHp / dr.maxHp : 0f; float ratio = dr.maxHp > 0 ? (float) dr.currentHp / dr.maxHp : 0f;
applyEnemyHpRatio(ratio); applyEnemyHpRatio(ratio);
String name = "Kampfpuppe";
enemyNameText.setText(name);
float nameW = enemyNameText.getLineWidth() * 0.5f;
enemyNameText.setLocalTranslation(
sw * 0.5f - nameW,
ehbY + EHB_H + EHB_BORDER * 2 + 6f, 3f);
String hpStr = dr.currentHp + " / " + dr.maxHp;
enemyHpText.setText(hpStr);
float hpW = enemyHpText.getLineWidth() * 0.5f;
enemyHpText.setLocalTranslation(
sw * 0.5f - hpW,
ehbY + EHB_BORDER + 1f, 3f);
} }
// ── MeleeAttackCallback ─────────────────────────────────────────────────── // ── MeleeAttackCallback ───────────────────────────────────────────────────
@@ -437,14 +401,19 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
} }
private void knockout(DummyRuntime dr) { private void knockout(DummyRuntime dr) {
dr.fallen = true; dr.fallen = true;
dr.fallenLerp = 0f; dr.fallenLerp = 0f;
dr.knockAngle = 0f;
dr.knockAngVel = KNOCK_INITIAL_VEL;
dr.hpBarNode.setCullHint(Spatial.CullHint.Always); dr.hpBarNode.setCullHint(Spatial.CullHint.Always);
} }
private void standUp(DummyRuntime dr) { private void standUp(DummyRuntime dr) {
playerInput.requestAnimation(AnimationAction.PICK_UP, 0f, null);
dr.fallen = false; dr.fallen = false;
dr.fallenLerp = 0f; dr.fallenLerp = 0f;
dr.knockAngle = 0f;
dr.knockAngVel = 0f;
dr.currentHp = dr.maxHp; dr.currentHp = dr.maxHp;
dr.restoreTimer = 0f; dr.restoreTimer = 0f;
dr.dummyNode.setLocalRotation(new Quaternion()); dr.dummyNode.setLocalRotation(new Quaternion());
@@ -476,13 +445,18 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
for (int i = 0; i < dummies.size(); i++) { for (int i = 0; i < dummies.size(); i++) {
DummyRuntime dr = dummies.get(i); DummyRuntime dr = dummies.get(i);
// Umfall-Animation // Umfall-Animation (physikalisch: Schwerkraft-Drehmoment nimmt mit cos(Winkel) ab)
if (dr.fallen && dr.fallenLerp < 1f) { if (dr.fallen && dr.fallenLerp < 1f) {
dr.fallenLerp = Math.min(1f, dr.fallenLerp + tpf / FALL_DURATION); float gravAccel = KNOCK_GRAVITY * FastMath.cos(dr.knockAngle);
Quaternion target = new Quaternion().fromAngles(0f, 0f, -FastMath.HALF_PI); dr.knockAngVel += gravAccel * tpf;
Quaternion cur = new Quaternion(); dr.knockAngle += dr.knockAngVel * tpf;
cur.slerp(Quaternion.IDENTITY, target, dr.fallenLerp); if (dr.knockAngle >= FastMath.HALF_PI) {
dr.dummyNode.setLocalRotation(cur); dr.knockAngle = FastMath.HALF_PI;
dr.knockAngVel = 0f;
dr.fallenLerp = 1f;
}
dr.dummyNode.setLocalRotation(
new Quaternion().fromAngles(-dr.knockAngle, 0f, 0f));
} }
// Welt-HP-Balken-Timer // Welt-HP-Balken-Timer
@@ -588,6 +562,8 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
float hpBarTimer = 0f; float hpBarTimer = 0f;
boolean fallen = false; boolean fallen = false;
float fallenLerp = 0f; float fallenLerp = 0f;
float knockAngle = 0f; // aktueller Kippwinkel (0..PI/2)
float knockAngVel = 0f; // Winkelgeschwindigkeit (rad/s)
DummyRuntime(String id, float wx, float wy, float wz, 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,

View File

@@ -7,7 +7,7 @@ dialog.speaker.player=Spieler
# ── Interactables ───────────────────────────────────────────────────────────── # ── Interactables ─────────────────────────────────────────────────────────────
interactable.bench.name=Bank interactable.bench.name=Bank
interactable.bed.name=Bett interactable.bed.name=Bett
dummy.stand_up=Aufstellen [F] dummy.stand_up=Aufstellen [{interact}]
dummy.name=Kampfpuppe dummy.name=Kampfpuppe
# ── Items ──────────────────────────────────────────────────────────────────── # ── Items ────────────────────────────────────────────────────────────────────

View File

@@ -7,7 +7,7 @@ dialog.speaker.player=Player
# ── Interactables ───────────────────────────────────────────────────────────── # ── Interactables ─────────────────────────────────────────────────────────────
interactable.bench.name=Bench interactable.bench.name=Bench
interactable.bed.name=Bed interactable.bed.name=Bed
dummy.stand_up=Stand Up [F] dummy.stand_up=Stand Up [{interact}]
dummy.name=Training Dummy dummy.name=Training Dummy
# ── Items ──────────────────────────────────────────────────────────────────── # ── Items ────────────────────────────────────────────────────────────────────