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 Strafe Left",
"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": {
"DEFAULT": "Idle",
@@ -51,7 +57,13 @@
"SWORD_BLOCK_MOVE_BACKWARD": "Sword And Shield Block Walk Backwards",
"SWORD_BLOCK_STRAFE_LEFT": "Sword And Shield Strafe Left",
"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",
"animOffsets": {
@@ -111,6 +123,8 @@
"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",
"Sneaking Forward"
]
}

View File

@@ -14,7 +14,11 @@ public enum AnimationAction {
SPRINT,
JUMP,
RUNNING_JUMP,
ROLL,
DUCK,
CROUCH_IDLE,
CROUCH_TO_STAND,
SNEAK,
PICK_UP,
LIE_DOWN,
LIE_UP,
@@ -78,7 +82,11 @@ public enum AnimationAction {
case SPRINT -> TextRegistry.resolve(null, key, "Sprint");
case JUMP -> TextRegistry.resolve(null, key, "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 LIE_DOWN -> TextRegistry.resolve(null, key, "Hinlegen");
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 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 = {
@@ -42,6 +44,8 @@ public class KeyBindings {
{"drawMeleeWeapon", "Nahkampfwaffe ziehen"},
{"drawRangedWeapon", "Fernkampfwaffe ziehen"},
{"sheathWeapon", "Waffe wegstecken"},
{"roll", "Rolle"},
{"sneak", "Schleichen"},
};
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,6 +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_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
@@ -93,10 +102,13 @@ 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 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_DRAW_MELEE, ACT_DRAW_RANGED, ACT_SHEATH, ACT_COMBAT_ATK, ACT_COMBAT_ALT,
ACT_ROLL, ACT_SNEAK
};
private final InputManager inputManager;
@@ -105,6 +117,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;
@@ -194,6 +208,8 @@ public class PlayerInputControl {
private com.jme3.anim.SkinningControl skinningControl = null;
private com.jme3.anim.Armature armature = null;
private com.jme3.anim.Joint hipsJoint = null;
// ── Fußgeräusche ─────────────────────────────────────────────────────────
// 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. */
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()
private final Vector3f scratchCamDir = new Vector3f();
private final Vector3f scratchCamLeft = new Vector3f();
@@ -274,7 +305,7 @@ public class PlayerInputControl {
case "Backward" -> backward = isPressed;
case "Left" -> left = isPressed;
case "Right" -> right = isPressed;
case "Sprint" -> sprint = 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(); }
@@ -283,19 +314,7 @@ public class PlayerInputControl {
case ACT_COMBAT_ATK -> {
if (isPressed) {
if (!weaponDrawn || blockingAnimActive || drawAnimActive || isBlocking()) break;
if (combatAnimActive) {
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;
}
// Immer Hold-Timer starten – auch mitten in einer Kombo
attackHoldPending = true;
attackHoldTimer = 0f;
} else {
@@ -303,16 +322,26 @@ public class PlayerInputControl {
attackHoldPending = false;
if (attackHoldTimer > QUICK_TAP_THRESHOLD) {
handleHeavyAttack();
} else {
// Schneller Klick → schwere Anim abbrechen, normale Kombo starten
combatAnimActive = false;
combatAnimRemaining = 0f;
} else if (combatAnimActive && comboWindowActive) {
// Schneller Klick im Kombo-Fenster → Kombo fortsetzen
combatAnimActive = false;
combatAnimRemaining = 0f;
combatAnimSkipComboWindow = false;
heavyLungeVel = null;
heavyLungeStartRemaining = 0f;
heavyLungeEndRemaining = 0f;
handleCombatAttack();
} else if (!combatAnimActive) {
// Schneller Klick außerhalb Kombo → normaler Angriff
handleCombatAttack();
}
// Schneller Klick während Kombo aber außerhalb Fenster → ignorieren
}
}
}
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) {
armature = skinningControl.getArmature();
logJointNames(armature);
hipsJoint = armature.getJoint("mixamorig:Hips");
jointRightArm = armature.getJoint("mixamorig:RightArm");
jointRightForeArm = armature.getJoint("mixamorig:RightForeArm");
jointLeftArm = armature.getJoint("mixamorig:LeftArm");
@@ -668,12 +698,29 @@ 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.addMapping(ACT_SNEAK, new KeyTrigger(kb.sneak));
inputManager.addListener(actionListener, ACTION_NAMES);
}
@@ -685,7 +732,7 @@ public class PlayerInputControl {
runningClip = animPlayer.getCurrentClip();
}
if (visual != null && animOffsetElapsed < AnimPlayer.BLEND_DURATION) {
if (visual != null && animOffsetElapsed < AnimPlayer.BLEND_DURATION && !rollActive) {
animOffsetElapsed += tpf;
float t = Math.min(1f, animOffsetElapsed / AnimPlayer.BLEND_DURATION);
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)
if (turnActive) {
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
if (lockedInPlace) {
physicsChar.setWalkDirection(Vector3f.ZERO);
@@ -803,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));
@@ -838,9 +952,20 @@ public class PlayerInputControl {
// Hold-Timer muss jede Frame akkumulieren, unabhängig vom Anim-Status
if (attackHoldPending) {
attackHoldTimer += tpf;
// Erst wenn klar ist dass es ein schwerer Angriff wird → Animation starten
if (attackHoldTimer > QUICK_TAP_THRESHOLD && !combatAnimActive) {
startHeavyCharge();
if (attackHoldTimer > QUICK_TAP_THRESHOLD) {
if (combatAnimActive && !combatAnimSkipComboWindow) {
// 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--;
// 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
@@ -1271,6 +1400,62 @@ 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;
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() {
return mainCharacter != null && mainCharacter.getEquippedShield() != null;
}
@@ -1647,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);
}
}

View File

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

View File

@@ -10,6 +10,8 @@ import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import de.blight.game.config.KeyBindings;
import de.blight.game.config.KeyNames;
import de.blight.lang.TextResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,6 +35,10 @@ public class InteractionHudState extends BaseAppState {
private DialogHudState dialogHud;
private BitmapText labelText;
private KeyBindings keyBindings;
public InteractionHudState() {}
public InteractionHudState(KeyBindings keyBindings) { this.keyBindings = keyBindings; }
// ── Lifecycle ─────────────────────────────────────────────────────────────
@@ -134,6 +140,9 @@ public class InteractionHudState extends BaseAppState {
}
String text = TextResolver.get().resolveId(labelKey);
if (keyBindings != null) {
text = text.replace("{interact}", KeyNames.of(keyBindings.interact));
}
labelText.setText(text);
float textW = labelText.getLineWidth();
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.RigidBodyControl;
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.MouseInput;
import com.jme3.input.controls.ActionListener;
@@ -37,6 +35,7 @@ import de.blight.common.PlacedModel;
import de.blight.common.PlacedModelIO;
import de.blight.common.model.CombatDummy;
import de.blight.common.model.CombatDummyIO;
import de.blight.game.animation.AnimationAction;
import de.blight.game.combat.DamageCalculator;
import de.blight.game.combat.MeleeAttackCallback;
import de.blight.game.config.KeyBindings;
@@ -62,11 +61,12 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
// ── Spielwelt-Konstanten ──────────────────────────────────────────────────
private static final float FOCUS_RANGE = 6f;
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 int DEFAULT_MAX_HP = 250;
private static final float FALL_DURATION = 0.35f;
private static final float MODEL_HALF_H = 0.75f;
private static final int DEFAULT_MAX_HP = 10;
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 ────────────────────────────────────────────────
private static final float WB_WIDTH = 0.8f;
@@ -103,8 +103,6 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
// ── Feind-HUD ─────────────────────────────────────────────────────────────
private Node enemyHudNode;
private Geometry enemyHpFill;
private BitmapText enemyNameText;
private BitmapText enemyHpText;
/** x-Koordinate des linken Randes der Feind-HP-Bar (Screen-Pixel). */
private float ehbX;
/** 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);
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);
enemyHudNode.setCullHint(Spatial.CullHint.Always);
}
@@ -375,23 +354,8 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
}
private void updateEnemyHudContent(DummyRuntime dr) {
float sw = cam.getWidth();
float ratio = dr.maxHp > 0 ? (float) dr.currentHp / dr.maxHp : 0f;
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 ───────────────────────────────────────────────────
@@ -437,14 +401,19 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
}
private void knockout(DummyRuntime dr) {
dr.fallen = true;
dr.fallenLerp = 0f;
dr.fallen = true;
dr.fallenLerp = 0f;
dr.knockAngle = 0f;
dr.knockAngVel = KNOCK_INITIAL_VEL;
dr.hpBarNode.setCullHint(Spatial.CullHint.Always);
}
private void standUp(DummyRuntime dr) {
playerInput.requestAnimation(AnimationAction.PICK_UP, 0f, null);
dr.fallen = false;
dr.fallenLerp = 0f;
dr.knockAngle = 0f;
dr.knockAngVel = 0f;
dr.currentHp = dr.maxHp;
dr.restoreTimer = 0f;
dr.dummyNode.setLocalRotation(new Quaternion());
@@ -476,13 +445,18 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
for (int i = 0; i < dummies.size(); i++) {
DummyRuntime dr = dummies.get(i);
// Umfall-Animation
// Umfall-Animation (physikalisch: Schwerkraft-Drehmoment nimmt mit cos(Winkel) ab)
if (dr.fallen && dr.fallenLerp < 1f) {
dr.fallenLerp = Math.min(1f, dr.fallenLerp + tpf / FALL_DURATION);
Quaternion target = new Quaternion().fromAngles(0f, 0f, -FastMath.HALF_PI);
Quaternion cur = new Quaternion();
cur.slerp(Quaternion.IDENTITY, target, dr.fallenLerp);
dr.dummyNode.setLocalRotation(cur);
float gravAccel = KNOCK_GRAVITY * FastMath.cos(dr.knockAngle);
dr.knockAngVel += gravAccel * tpf;
dr.knockAngle += dr.knockAngVel * tpf;
if (dr.knockAngle >= FastMath.HALF_PI) {
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
@@ -588,6 +562,8 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
float hpBarTimer = 0f;
boolean fallen = false;
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,
Node dummyNode, Node hpBarNode, Geometry hpBarFg, int maxHp,

View File

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

View File

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