Laufen und Rollen verbessert

This commit is contained in:
2026-09-24 16:01:47 +02:00
parent 470c743848
commit f457b897bd
3 changed files with 98 additions and 65 deletions

View File

@@ -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) {

View File

@@ -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);