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 Strafe Left",
"Sword And Shield Strafe Right",
"Sword And Shield Heavy Attack"
"Sword And Shield Heavy Attack",
"Picking Up",
"Roll"
],
"actionMap": {
"DEFAULT": "Idle",
@@ -51,7 +53,9 @@
"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"
},
"previewModelPath": "Characters/Models/hero.j3o",
"animOffsets": {

View File

@@ -14,6 +14,7 @@ public enum AnimationAction {
SPRINT,
JUMP,
RUNNING_JUMP,
ROLL,
DUCK,
PICK_UP,
LIE_DOWN,
@@ -78,6 +79,7 @@ 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 ROLL -> TextRegistry.resolve(null, key, "Rolle");
case DUCK -> TextRegistry.resolve(null, key, "Duck");
case PICK_UP -> TextRegistry.resolve(null, key, "Pick up");
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_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;
/** 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
@@ -194,6 +196,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 +262,17 @@ public class PlayerInputControl {
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
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()
private final Vector3f scratchCamDir = new Vector3f();
private final Vector3f scratchCamLeft = new Vector3f();
@@ -270,10 +285,10 @@ public class PlayerInputControl {
private final ActionListener actionListener = (name, isPressed, tpf) -> {
if (paused) return;
switch (name) {
case "Forward" -> forward = isPressed;
case "Backward" -> backward = isPressed;
case "Left" -> left = isPressed;
case "Right" -> right = isPressed;
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 "Walk" -> walk = isPressed;
case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } }
@@ -283,19 +298,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,12 +306,20 @@ 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
}
}
}
@@ -355,6 +366,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");
@@ -685,7 +697,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);
@@ -704,6 +716,29 @@ 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);
@@ -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)
if (turnActive) {
turnRemaining -= tpf;
@@ -838,9 +887,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();
}
}
}
@@ -1271,6 +1331,41 @@ 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 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() {
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);
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 ────────────────────────────────────────────────────────────────────