Weiter am Kampfsystem gearbeitet - hier Kampfpuppen und ausweichrolle

This commit is contained in:
2026-09-24 12:05:37 +02:00
parent 0a1c7cd835
commit 470c743848
12 changed files with 166 additions and 80 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -24,7 +24,9 @@
"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"
], ],
"actionMap": { "actionMap": {
"DEFAULT": "Idle", "DEFAULT": "Idle",
@@ -51,7 +53,9 @@
"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"
}, },
"previewModelPath": "Characters/Models/hero.j3o", "previewModelPath": "Characters/Models/hero.j3o",
"animOffsets": { "animOffsets": {

View File

@@ -14,6 +14,7 @@ public enum AnimationAction {
SPRINT, SPRINT,
JUMP, JUMP,
RUNNING_JUMP, RUNNING_JUMP,
ROLL,
DUCK, DUCK,
PICK_UP, PICK_UP,
LIE_DOWN, LIE_DOWN,
@@ -78,6 +79,7 @@ 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 ROLL -> TextRegistry.resolve(null, key, "Rolle");
case DUCK -> TextRegistry.resolve(null, key, "Duck"); case DUCK -> TextRegistry.resolve(null, key, "Duck");
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");

View File

@@ -64,6 +64,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_DOUBLE_TAP_WINDOW = 0.3f;
/** 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
@@ -194,6 +196,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 +262,17 @@ 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;
// ── 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;
// 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();
@@ -270,10 +285,10 @@ public class PlayerInputControl {
private final ActionListener actionListener = (name, isPressed, tpf) -> { private final ActionListener actionListener = (name, isPressed, tpf) -> {
if (paused) return; if (paused) return;
switch (name) { switch (name) {
case "Forward" -> forward = isPressed; case "Forward" -> { if (isPressed) detectRollTap(1); forward = isPressed; }
case "Backward" -> backward = isPressed; case "Backward" -> { if (isPressed) detectRollTap(2); backward = isPressed; }
case "Left" -> left = isPressed; case "Left" -> { if (isPressed) detectRollTap(3); left = isPressed; }
case "Right" -> right = isPressed; case "Right" -> { if (isPressed) detectRollTap(4); 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; } }
@@ -283,19 +298,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,12 +306,20 @@ 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
} }
} }
} }
@@ -355,6 +366,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");
@@ -685,7 +697,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);
@@ -704,6 +716,29 @@ public class PlayerInputControl {
return; 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 // Navigator: hat Vorrang vor allem außer Pause
if (navigator != null && navigator.isActive()) { if (navigator != null && navigator.isActive()) {
navigator.update(tpf); navigator.update(tpf);
@@ -722,6 +757,20 @@ public class PlayerInputControl {
} }
} }
if (rollActive) {
rollRemaining -= tpf;
if (rollRemaining <= 0f) {
rollActive = false;
rollDir = null;
currentAnim = null;
} else {
if (physicsChar != null) {
physicsChar.setWalkDirection(Vector3f.ZERO);
}
return;
}
}
// Drehung auf der Stelle (kein Vorwärtsbewegen) // Drehung auf der Stelle (kein Vorwärtsbewegen)
if (turnActive) { if (turnActive) {
turnRemaining -= tpf; turnRemaining -= tpf;
@@ -838,11 +887,22 @@ 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) {
// 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(); startHeavyCharge();
} }
} }
}
// Draw/Sheath-Animation Timer (Oberkörper-Overlay, Beine laufen weiter) // Draw/Sheath-Animation Timer (Oberkörper-Overlay, Beine laufen weiter)
if (drawAnimActive) { if (drawAnimActive) {
@@ -1271,6 +1331,41 @@ public class PlayerInputControl {
return true; 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 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;
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;
} }

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 ───────────────────────────────────────────────────
@@ -439,12 +403,17 @@ 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 ────────────────────────────────────────────────────────────────────