diff --git a/assets/imported/animations/Sword And Shield Death 2.fbx b/assets/imported/animations/Sword And Shield Death 2.fbx new file mode 100644 index 0000000..a3ff419 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Death 2.fbx differ diff --git a/assets/imported/animations/Sword And Shield Death.fbx b/assets/imported/animations/Sword And Shield Death.fbx new file mode 100644 index 0000000..ec2f69e Binary files /dev/null and b/assets/imported/animations/Sword And Shield Death.fbx differ diff --git a/assets/imported/animations/Sword And Shield Idle bored.fbx b/assets/imported/animations/Sword And Shield Idle bored.fbx new file mode 100644 index 0000000..8bb7e63 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Idle bored.fbx differ diff --git a/assets/imported/animations/Sword And Shield Idle boring 2.fbx b/assets/imported/animations/Sword And Shield Idle boring 2.fbx new file mode 100644 index 0000000..cf61f7d Binary files /dev/null and b/assets/imported/animations/Sword And Shield Idle boring 2.fbx differ diff --git a/assets/imported/animations/Sword And Shield Idle boring 3.fbx b/assets/imported/animations/Sword And Shield Idle boring 3.fbx new file mode 100644 index 0000000..5b1d173 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Idle boring 3.fbx differ diff --git a/assets/imported/animations/Sword And Shield Idle boring.fbx b/assets/imported/animations/Sword And Shield Idle boring.fbx new file mode 100644 index 0000000..d6712d3 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Idle boring.fbx differ diff --git a/assets/imported/animations/Sword And Shield Jump Running.fbx b/assets/imported/animations/Sword And Shield Jump Running.fbx new file mode 100644 index 0000000..5e9b4b4 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Jump Running.fbx differ diff --git a/assets/imported/animations/Sword And Shield Jump(1).fbx b/assets/imported/animations/Sword And Shield Jump(1).fbx new file mode 100644 index 0000000..e706378 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Jump(1).fbx differ diff --git a/assets/imported/animations/Sword And Shield Jump.fbx b/assets/imported/animations/Sword And Shield Jump.fbx new file mode 100644 index 0000000..92e4970 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Jump.fbx differ diff --git a/assets/imported/animations/Sword And Shield Slash.fbx b/assets/imported/animations/Sword And Shield Slash.fbx index d6793db..935715b 100644 Binary files a/assets/imported/animations/Sword And Shield Slash.fbx and b/assets/imported/animations/Sword And Shield Slash.fbx differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Death 2.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Death 2.j3o new file mode 100644 index 0000000..9f7964c Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Death 2.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Death.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Death.j3o new file mode 100644 index 0000000..7e5d99b Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Death.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Idle boring 2.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Idle boring 2.j3o new file mode 100644 index 0000000..5963252 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Idle boring 2.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Idle boring 3.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Idle boring 3.j3o new file mode 100644 index 0000000..d11eb41 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Idle boring 3.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Idle boring.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Idle boring.j3o new file mode 100644 index 0000000..1836aa0 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Idle boring.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Jump Running.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Jump Running.j3o new file mode 100644 index 0000000..9f70c7e Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Jump Running.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Jump.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Jump.j3o new file mode 100644 index 0000000..b5d76b4 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Jump.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Slash.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Slash.j3o index ff95fd4..d7c6c5b 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Slash.j3o and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Slash.j3o differ diff --git a/blight-assets/src/main/resources/Characters/sets/human.animset.json b/blight-assets/src/main/resources/Characters/sets/human.animset.json index 7b0d787..d87a59f 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -9,7 +9,6 @@ "T-Pose", "Walking", "Jumping", - "Sword And Shield Slash", "Sword And Shield Idle", "Sword And Shield Run", "Sword And Shield Walk", @@ -30,7 +29,15 @@ "Crouched To Standing", "Crouching Idle", "Sneaking Forward", - "Standing To Crouched" + "Standing To Crouched", + "Sword And Shield Death", + "Sword And Shield Death 2", + "Sword And Shield Idle boring", + "Sword And Shield Idle boring 2", + "Sword And Shield Idle boring 3", + "Sword And Shield Jump Running", + "Sword And Shield Jump", + "Sword And Shield Slash" ], "actionMap": { "DEFAULT": "Idle", @@ -63,7 +70,18 @@ "DUCK": "Standing To Crouched", "CROUCH_IDLE": "Crouching Idle", "SNEAK": "Sneaking Forward", - "CROUCH_TO_STAND": "Crouched To Standing" + "CROUCH_TO_STAND": "Crouched To Standing", + "DEATH": "Sword And Shield Death", + "SWORD_SHIELD_IDLE_BORED": "Sword And Shield Idle bored", + "SWORD_SHIELD_DEATH_2": "Sword And Shield Death 2", + "SWORD_SHIELD_JUMP": "Sword And Shield Jump", + "SWORD_JUMP": "Sword And Shield Jump", + "SWORD_RUNNING_JUMP": "Sword And Shield Jump Running", + "SWORD_DEATH_2": "Sword And Shield Death 2", + "SWORD_DEATH": "Sword And Shield Death", + "SWORD_IDLE_BORED": "Sword And Shield Idle boring", + "SWORD_IDLE_BORED_2": "Sword And Shield Idle boring 2", + "SWORD_IDLE_BORED_3": "Sword And Shield Idle boring 3" }, "previewModelPath": "Characters/Models/hero.j3o", "animOffsets": { @@ -125,6 +143,8 @@ "Sword And Shield Strafe Right", "Sword And Shield Heavy Attack", "Roll", - "Sneaking Forward" + "Sneaking Forward", + "Sword And Shield Jump Running", + "Sword And Shield Jump" ] } \ No newline at end of file diff --git a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java index 0b6aeb1..f7bfb9b 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java @@ -663,6 +663,18 @@ public class AnimPreviewState extends BaseAppState { // j3o-Quelle: Clip bereits gespeichert – nur Preview, kein erneutes Speichern. // snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary. boolean isJ3oSource = animAssetPath.matches("(?i).*\\.j3o$"); + + // FBX-Import erfordert ein geladenes Charakter-Modell. + // Ohne Ziel-Armatur kann normalizeAndRetargetClip die $AssimpFbx$_Translation-Orphan-Joints + // nicht in den Hips-Track falten → Hips-Translation geht verloren → Charakter schrumpft. + if (!isJ3oSource && srcArm != null && dstArm == null) { + input.animPreviewStatus = + "Fehler: FBX-Import erfordert ein geladenes Charakter-Modell. " + + "Bitte zuerst das Modell laden, dann die Animation importieren."; + LOG.warn("[AnimPreview] FBX-Import ohne Charakter-Modell abgebrochen: {}", animAssetPath); + return; + } + com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm; int saved = 0; @@ -1248,6 +1260,32 @@ public class AnimPreviewState extends BaseAppState { if (armature != null) holder.addControl(new SkinningControl(armature)); java.nio.file.Files.createDirectories(outFile.getParent()); BinaryExporter.getInstance().save(holder, outFile.toFile()); + syncClipToMirrors(outFile); + } + + private void syncClipToMirrors(java.nio.file.Path savedFile) { + java.nio.file.Path rel; + try { + rel = ASSET_ROOT.relativize(savedFile); + } catch (Exception e) { + return; + } + java.nio.file.Path projectRoot = de.blight.editor.ProjectRoot.PATH; + java.nio.file.Path[] mirrors = { + projectRoot.resolve("blight-assets").resolve("bin").resolve("main").resolve(rel), + projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main").resolve(rel), + }; + for (java.nio.file.Path mirror : mirrors) { + try { + if (java.nio.file.Files.exists(mirror.getParent())) { + java.nio.file.Files.copy(savedFile, mirror, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + LOG.info("[AnimPreview] Spiegel aktualisiert: {}", mirror); + } + } catch (Exception e) { + LOG.warn("[AnimPreview] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage()); + } + } } // ── FBX-Import ─────────────────────────────────────────────────────────── @@ -1924,12 +1962,27 @@ public class AnimPreviewState extends BaseAppState { } translations = retargeted; } + // Scale-Tracks entfernen – FBX-Skalierung (Mixamo cm, Rig-Scale) schrumpft sonst den Charakter. newTracks.add(new com.jme3.anim.TransformTrack(dstJoint, tt.getTimes(), - translations, tt.getRotations(), tt.getScales())); + translations, tt.getRotations(), null)); } if (newTracks.isEmpty()) return null; AnimClip result = new AnimClip(src.getName()); result.setTracks(newTracks.toArray(new com.jme3.anim.AnimTrack[0])); + // Diagnose: Hips-Translation im ersten Frame ausgeben + for (com.jme3.anim.AnimTrack t : newTracks) { + if (t instanceof com.jme3.anim.TransformTrack tt + && tt.getTarget() instanceof com.jme3.anim.Joint j + && j.getName().contains("Hips") && !j.getName().contains("$AssimpFbx$") + && tt.getTranslations() != null && tt.getTranslations().length > 0) { + com.jme3.math.Vector3f tr0 = tt.getTranslations()[0]; + LOG.info("[AnimPreview] normalizeAndRetarget '{}': Hips[0]=({},{},{}) tracks={} orphansFolded={}", + src.getName(), + String.format("%.3f", tr0.x), String.format("%.3f", tr0.y), String.format("%.3f", tr0.z), + newTracks.size(), orphanByParent.size()); + break; + } + } LOG.debug("[AnimPreview] normalizeAndRetarget '{}': {} Tracks, length={}", src.getName(), newTracks.size(), result.getLength()); return result; } diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java index 5070fa8..c2190ac 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java @@ -36,13 +36,15 @@ public enum AnimationAction { SECRET, TALK_SITTING, + // ── Schwert + Schild ────────────────────────────────────────────────────── + + + // ── Schwert ─────────────────────────────────────────────────────────────── - SWORD_ATTACK_1, - SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, - SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK, - SWORD_IDLE_TO_BLOCK, SWORD_COUNTERATTACK, SWORD_BLOCK_IMPACT, SWORD_IDLE_IMPACT, - SWORD_IDLE, SWORD_WALK, SWORD_RUN, - SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT, + SWORD_IDLE_BORED, SWORD_IDLE_BORED_2, SWORD_IDLE_BORED_3, SWORD_DEATH, SWORD_DEATH_2, + SWORD_ATTACK_1, SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,SWORD_COUNTERATTACK, SWORD_IDLE_IMPACT, + SWORD_IDLE, SWORD_WALK, SWORD_RUN, SWORD_JUMP, SWORD_RUNNING_JUMP, + SWORD_BLOCK, SWORD_IDLE_TO_BLOCK, SWORD_BLOCK_IMPACT, SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT, // ── Axt ─────────────────────────────────────────────────────────────────── AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, @@ -104,9 +106,14 @@ public enum AnimationAction { case SECRET -> TextRegistry.resolve(null, key, "Geheimnis"); case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)"); + case SWORD_JUMP -> TextRegistry.resolve(null, key, "Sprung (Schwert)"); + case SWORD_RUNNING_JUMP -> TextRegistry.resolve(null, key, "Sprung Laufend (Schwert)"); + case SWORD_IDLE_BORED -> TextRegistry.resolve(null, key, "Idle Gelangweilt (Schwert)"); + case SWORD_IDLE_BORED_2 -> TextRegistry.resolve(null, key, "Idle Gelangweilt 2 (Schwert)"); + case SWORD_IDLE_BORED_3 -> TextRegistry.resolve(null, key, "Idle Gelangweilt 3 (Schwert)"); + case SWORD_DEATH -> TextRegistry.resolve(null, key, "Tod (Schwert)"); + case SWORD_DEATH_2 -> TextRegistry.resolve(null, key, "Tod 2 (Schwert)"); case SWORD_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Schwert)"); - case SWORD_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Schwert)"); - case SWORD_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Schwert)"); case SWORD_BLOCK -> TextRegistry.resolve(null, key, "Block (Schwert)"); case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)"); case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)"); diff --git a/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java b/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java index cacebbc..9988f71 100644 --- a/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java +++ b/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java @@ -71,9 +71,12 @@ public final class RetargetingSystem { // Mixamo-Clips, deren Knochen in Blender nur umbenannt (nicht retargeted) wurden, // haben denselben Knochennamen aber Mixamo-Bind-Pose → benötigen die volle Formel. if (isSameRig(nameMap, sourceArmature, targetArmature)) { - log.debug("[Retarget] '{}' same-rig detected – fast path (redirect only)", sourceClip.getName()); + log.info("[Retarget] '{}' same-rig=TRUE → redirectTracks (srcJoints={} dstJoints={})", + sourceClip.getName(), sourceArmature.getJointCount(), targetArmature.getJointCount()); return redirectTracks(sourceClip, targetArmature); } + log.info("[Retarget] '{}' same-rig=FALSE → full retarget (srcJoints={} dstJoints={})", + sourceClip.getName(), sourceArmature.getJointCount(), targetArmature.getJointCount()); // ── Source track lookup ─────────────────────────────────────────────── Map srcTrackMap = new HashMap<>(); @@ -402,10 +405,27 @@ public final class RetargetingSystem { // ── Same-rig detection & redirect ──────────────────────────────────────── - /** True wenn alle Namen exakt selbst-gemappt sind (gleiche Knochen-Namen → gleicher Rig-Typ). */ + /** True nur wenn alle Namen exakt selbst-gemappt UND alle Bind-Posen übereinstimmen. */ private static boolean isSameRig(Map nameMap, Armature src, Armature dst) { for (var e : nameMap.entrySet()) { if (!e.getKey().equals(e.getValue())) return false; + Joint srcJ = src.getJoint(e.getKey()); + Joint dstJ = dst.getJoint(e.getValue()); + if (srcJ == null || dstJ == null) continue; + Quaternion sb = srcJ.getInitialTransform() != null + ? srcJ.getInitialTransform().getRotation() : new Quaternion(); + Quaternion db = dstJ.getInitialTransform() != null + ? dstJ.getInitialTransform().getRotation() : new Quaternion(); + if (Math.abs(sb.dot(db)) < 0.999f) { + log.info("[isSameRig] MISMATCH '{}': src=({},{},{},{}) dst=({},{},{},{}) dot={}", + e.getKey(), + String.format("%.4f", sb.getX()), String.format("%.4f", sb.getY()), + String.format("%.4f", sb.getZ()), String.format("%.4f", sb.getW()), + String.format("%.4f", db.getX()), String.format("%.4f", db.getY()), + String.format("%.4f", db.getZ()), String.format("%.4f", db.getW()), + String.format("%.6f", Math.abs(sb.dot(db)))); + return false; + } } return true; } @@ -417,13 +437,31 @@ public final class RetargetingSystem { if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint srcJoint) { Joint dstJoint = targetArmature.getJoint(srcJoint.getName()); if (dstJoint == null) continue; + // Scale-Tracks werden nie übernommen: Mixamo/Blender FBX enthält oft + // nicht-triviale Scales (cm-Einheiten, Rig-Skalierung), die den Charakter + // auf Homunkuli-Größe schrumpfen würden. Joint-Scaling passiert über die + // Charakter-Transform, nicht über Animations-Tracks. newTracks.add(new TransformTrack(dstJoint, tt.getTimes(), - tt.getTranslations(), tt.getRotations(), tt.getScales())); + tt.getTranslations(), tt.getRotations(), null)); } } if (newTracks.isEmpty()) return null; AnimClip result = new AnimClip(sourceClip.getName()); result.setTracks(newTracks.toArray(new AnimTrack[0])); + // Log first Hips translation to diagnose shrinking + for (AnimTrack t : newTracks) { + if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint j + && j.getName().contains("Hips") && !j.getName().contains("$AssimpFbx$") + && tt.getTranslations() != null && tt.getTranslations().length > 0) { + var tr = tt.getTranslations()[0]; + log.info("[Retarget] '{}' Hips[0]: t=({},{},{}) scales={} tracks={}", + sourceClip.getName(), + String.format("%.3f", tr.x), String.format("%.3f", tr.y), String.format("%.3f", tr.z), + tt.getScales() != null ? "yes" : "null", + newTracks.size()); + break; + } + } log.debug("[Retarget] '{}' redirected: {} Tracks", sourceClip.getName(), newTracks.size()); return result; } diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index 2b67b81..64de766 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -285,6 +285,11 @@ public class PlayerInputControl { // ── Dodge Roll ──────────────────────────────────────────────────────────── private boolean rollActive = false; private float rollRemaining = 0f; + private static final float BORED_IDLE_DELAY = 30f; + private float boredIdleTimer = 0f; + private float boredAnimRemaining = 0f; + private AnimationAction boredVariant = AnimationAction.SWORD_IDLE_BORED; + private final java.util.Random playerRng = new java.util.Random(); private float rollInitRemaining = 0f; private boolean rollBaselineCaptured = false; private float rollHipsBaseY = 0f; @@ -969,20 +974,8 @@ public class PlayerInputControl { // Hold-Timer muss jede Frame akkumulieren, unabhängig vom Anim-Status if (attackHoldPending) { attackHoldTimer += tpf; - 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(); - } + if (attackHoldTimer > QUICK_TAP_THRESHOLD && !combatAnimActive) { + startHeavyCharge(); } } @@ -1091,15 +1084,41 @@ public class PlayerInputControl { && sneakPhase != SneakPhase.DOWN && sneakPhase != SneakPhase.UP) { AnimationAction target; if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) { + boredIdleTimer = 0f; boredAnimRemaining = 0f; target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP; } else if (sneaking) { + boredIdleTimer = 0f; boredAnimRemaining = 0f; target = moving ? AnimationAction.SNEAK : AnimationAction.CROUCH_IDLE; } else if (moving) { + boredIdleTimer = 0f; boredAnimRemaining = 0f; target = (inDeepWater || walk) ? AnimationAction.WALK : sprint ? AnimationAction.SPRINT : AnimationAction.RUN; } else { - target = AnimationAction.IDLE; + boolean swordShield = drawnWeaponType == de.blight.common.model.ItemSubCategory.SWORD + && hasShieldEquipped(); + if (swordShield) { + if (boredAnimRemaining > 0f) { + boredAnimRemaining -= tpf; + target = boredVariant; + } else { + boredIdleTimer += tpf; + if (boredIdleTimer >= BORED_IDLE_DELAY) { + boredIdleTimer = 0f; + int pick = playerRng.nextInt(3); + boredVariant = pick == 0 ? AnimationAction.SWORD_IDLE_BORED + : pick == 1 ? AnimationAction.SWORD_IDLE_BORED_2 + : AnimationAction.SWORD_IDLE_BORED_3; + boredAnimRemaining = resolveClipLength(boredVariant, 3.0f); + target = boredVariant; + } else { + target = AnimationAction.IDLE; + } + } + } else { + boredIdleTimer = 0f; boredAnimRemaining = 0f; + target = AnimationAction.IDLE; + } } target = resolveWeaponAction(target); @@ -1490,6 +1509,12 @@ public class PlayerInputControl { case IDLE -> weaponIdleAction(); case WALK -> weaponWalkAction(); case RUN, SPRINT -> weaponRunAction(); + case JUMP -> (drawnWeaponType == de.blight.common.model.ItemSubCategory.SWORD + && hasShieldEquipped()) + ? AnimationAction.SWORD_JUMP : base; + case RUNNING_JUMP -> (drawnWeaponType == de.blight.common.model.ItemSubCategory.SWORD + && hasShieldEquipped()) + ? AnimationAction.SWORD_RUNNING_JUMP : base; default -> base; }; } @@ -1558,7 +1583,7 @@ public class PlayerInputControl { return switch (s) { case AXE -> AnimationAction.AXE_DRAW; case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_DRAW; - default -> AnimationAction.SWORD_DRAW; + default -> AnimationAction.AXE_DRAW; }; } @@ -1566,7 +1591,7 @@ public class PlayerInputControl { return switch (s) { case AXE -> AnimationAction.AXE_SHEATHE; case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_SHEATHE; - default -> AnimationAction.SWORD_SHEATHE; + default -> AnimationAction.AXE_DRAW; }; } diff --git a/blight-game/src/main/java/de/blight/game/npc/NpcAggroBehavior.java b/blight-game/src/main/java/de/blight/game/npc/NpcAggroBehavior.java index 3acf8dc..e19bf5f 100644 --- a/blight-game/src/main/java/de/blight/game/npc/NpcAggroBehavior.java +++ b/blight-game/src/main/java/de/blight/game/npc/NpcAggroBehavior.java @@ -196,6 +196,13 @@ public class NpcAggroBehavior { /** Returns a random phrase index in [0, 4]. */ public int randomPhraseIdx() { return rng.nextInt(5); } + /** Setzt den Aggro-Zustand sofort auf CALM zurück (z. B. bei K.O.). */ + public void forceCalm() { + phase = AggroPhase.CALM; + scheduleAnchor = null; + reAggroGuard = false; + } + // ── Static sight check ──────────────────────────────────────────────────── /** diff --git a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java index 66661ad..1f0b002 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java @@ -111,6 +111,18 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback /** Aktuell in der Welt gespawnte NPCs. */ private final List spawned = new ArrayList<>(); + private enum NpcCombatPhase { + IDLE_COMBAT, // in Reichweite, Think-Timer läuft + CHARGING, // außer Reichweite, läuft ran + ATTACKING, // Angriffsanimation + BLOCKING, // Block-Pose, wartet auf Spieler-Treffer + COUNTER_WINDOW, // nach erfolgreichem Block → immer Konter + COUNTERING, // Konterangriff-Animation + DODGING, // Ausweichrolle + STAGGERED, // Trefferreaktion + KNOCKED_OUT // 0 HP, Umfall-Physik + } + private static class SpawnedNpc { final NPC npc; final Spatial visual; @@ -151,6 +163,23 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback // ── Physik-Kapsel (Kollision) ───────────────────────────────────────── RigidBodyControl rbc = null; + // ── Kampf-Zustand ────────────────────────────────────────────────────── + NpcCombatPhase combatPhase = NpcCombatPhase.IDLE_COMBAT; + float combatPhaseTimer = 0f; + float thinkTimer = 0f; + int comboHitsRemaining = 0; + int comboStep = 0; + float attackHitWinTimer = 0f; + float attackClipDuration = 0f; + boolean attackHitFired = false; + boolean knockedOut = false; + float knockAngle = 0f; + float knockAngVel = 0f; + float dodgeDirX = 0f; + float dodgeDirZ = 0f; + boolean npcHeavyAttack = false; + de.blight.common.model.ItemSubCategory combatWeaponType = null; + SpawnedNpc(NPC npc, Spatial visual, float x, float z) { this.npc = npc; this.visual = visual; @@ -247,6 +276,33 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback private static final float NPC_CLOSE_IN_DIST = 3.0f; private static final float NPC_CLOSE_IN_SPEED = CharacterNavigator.WALK_SPEED_MPS; + // ── NPC Kampf-KI ────────────────────────────────────────────────────────── + private static final float NPC_THINK_MIN_NORMAL = 0.8f; + private static final float NPC_THINK_MAX_NORMAL = 2.0f; + private static final float NPC_THINK_MIN_LOW_HP = 0.4f; + private static final float NPC_THINK_MAX_LOW_HP = 1.0f; + private static final float NPC_LOW_HP_THRESHOLD = 0.5f; + private static final float NPC_BLOCK_CHANCE_NORMAL = 0.35f; + private static final float NPC_BLOCK_CHANCE_LOW_HP = 0.15f; + private static final float NPC_DODGE_CHANCE = 0.40f; + private static final float NPC_COMBO_EXTEND_P1 = 0.70f; + private static final float NPC_COMBO_EXTEND_P2 = 0.30f; + private static final float NPC_HIT_WIN_START = 0.35f; + private static final float NPC_HIT_WIN_END = 0.65f; + private static final int NPC_UNARMED_DAMAGE = 8; + private static final int NPC_COMBO3_DAMAGE_MULT = 2; + private static final int NPC_BLOCK_STAMINA_BASE = 10; + private static final float NPC_KNOCK_INITIAL_VEL = 0.4f; + private static final float NPC_KNOCK_GRAVITY = 8f; + private static final float NPC_STAGGER_FALLBACK = 0.6f; + private static final float NPC_CLIP_FALLBACK_ATK = 0.7f; + private static final float NPC_CLIP_FALLBACK_BLOCK = 0.4f; + private static final float NPC_CLIP_FALLBACK_CTR = 0.5f; + private static final float NPC_CLIP_FALLBACK_ROLL = 0.8f; + + private final java.util.Random npcRng = new java.util.Random(); + private float playerBlockTimer = 0f; + // ── NPC HP / Kampf ──────────────────────────────────────────────────────── private static final int DEFAULT_NPC_MAX_HP = 100; private static final int DEFAULT_NPC_MAX_MANA = 100; @@ -1384,7 +1440,6 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback de.blight.common.model.ItemSubCategory sub = w.getSubCategory(); if (sub == null) { return AnimationAction.IDLE; } return switch (sub) { - case SWORD -> AnimationAction.SWORD_SHEATHE; case AXE -> AnimationAction.AXE_SHEATHE; case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_SHEATHE; case BOW -> AnimationAction.BOW_SHEATHE; @@ -1412,6 +1467,8 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback private void updateNpcCombat(float tpf) { if (dialogActive) { return; } + if (playerInput.isBlocking()) { playerBlockTimer += tpf; } + else { playerBlockTimer = 0f; } Vector3f playerPos = physicsChar.getPhysicsLocation(); for (SpawnedNpc s : spawned) { @@ -1441,6 +1498,10 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback s.combatRunAnim = null; playNpcAction(s, AnimationAction.IDLE); } else { + // Waffe gezückt: Kampf-KI initialisieren + s.combatWeaponType = resolveNpcCombatWeaponType(s); + s.combatPhase = NpcCombatPhase.IDLE_COMBAT; + resetNpcThinkTimer(s); if (s.combatRunAnim != null) { playNpcAction(s, s.combatRunAnim); } } } @@ -1486,16 +1547,27 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback if (!s.aggroBehavior.isAttacking()) { continue; } + // K.O.: nur Umfall-Physik, keine sonstige Kampflogik + if (s.knockedOut) { + updateNpcCombatPhase(s, tpf, playerPos); + continue; + } + Vector3f npcPos = s.visual().getWorldTranslation(); float dist = dist2d(npcPos, playerPos); if (dist <= NPC_ATTACK_RANGE) { - // Kampfdistanz erreicht: stehen, drehen, combat-idle faceToward(s, playerPos); - if (s.weaponDrawn) { playNpcAction(s, AnimationAction.IDLE); } + if (s.weaponDrawn) { updateNpcCombatPhase(s, tpf, playerPos); } continue; } + // Außerhalb Angriffsreichweite → CHARGING-Phase + if (s.combatPhase != NpcCombatPhase.CHARGING + && s.combatPhase != NpcCombatPhase.DODGING) { + s.combatPhase = NpcCombatPhase.CHARGING; + } + // Close-in: Spieler ≤ 3m und flieht nicht → NPC geht langsam ran if (dist <= NPC_CLOSE_IN_DIST && !playerInput.isRunningOrSprinting()) { Vector3f moveDir = steerTowardPlayer(npcPos, playerPos); @@ -1662,6 +1734,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback lastAttackId = attackId; hitThisAttack.clear(); } + boolean isHeavy = critMultiplier >= 3.0f; for (SpawnedNpc s : spawned) { if (hitThisAttack.contains(s.npc().getCharacterId())) { continue; } Vector3f npcPos = s.visual().getWorldTranslation(); @@ -1670,16 +1743,53 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback float dz = npcPos.z - gripPos.z; if (dx * dx + dy * dy + dz * dz <= bladeLength * bladeLength) { hitThisAttack.add(s.npc().getCharacterId()); - applyNpcDamage(s, DamageCalculator.calculate(baseDamage, critMultiplier), critMultiplier > 1.0f); + applyNpcDamage(s, DamageCalculator.calculate(baseDamage, critMultiplier), + critMultiplier > 1.0f, isHeavy, critMultiplier); } } } - private void applyNpcDamage(SpawnedNpc s, int damage, boolean isCrit) { + private void applyNpcDamage(SpawnedNpc s, int damage, boolean isCrit, boolean isHeavy, float critMultiplier) { + if (s.knockedOut) { return; } + + if (s.combatPhase == NpcCombatPhase.BLOCKING) { + if (isHeavy) { + // Schwerer Angriff: Block bricht, kein Schaden — NPC weicht ggf. aus + if (npcRng.nextFloat() < NPC_DODGE_CHANCE) { + startNpcDodge(s); + } else { + staggerNpc(s); + } + } else { + // Normaler Treffer absorbiert: Stamina-Drain + int cost = Math.round(NPC_BLOCK_STAMINA_BASE * critMultiplier); + s.currentStamina = Math.max(0, s.currentStamina - cost); + if (s.currentStamina <= 0) { + // Stamina leer: Block bricht + staggerNpc(s); + } else { + // Block hält: Impact-Animation + Konter + AnimationAction impact = resolveNpcBlockImpactAction(s); + float dur = resolveNpcClipLength(s, impact, NPC_CLIP_FALLBACK_BLOCK); + playNpcAction(s, impact); + playerInput.triggerBlockImpact(); + s.combatPhase = NpcCombatPhase.COUNTER_WINDOW; + s.combatPhaseTimer = dur; + } + } + return; // kein HP-Verlust bei geblockt + } + + // Nicht blockend: Schaden anwenden s.currentHp = Math.max(0, s.currentHp - damage); refreshNpcHpBar(s); log.info("[NPC {}] -{} HP → {}/{}{}", s.npc().getCharacterId(), damage, s.currentHp, s.maxHp, s.currentHp <= 0 ? " K.O.!" : ""); + if (s.currentHp <= 0) { + knockoutNpc(s); + } else { + staggerNpc(s); + } } // ── Feind-HUD ───────────────────────────────────────────────────────────── @@ -1779,4 +1889,384 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback mesh.getBuffer(com.jme3.scene.VertexBuffer.Type.TexCoord).setUpdateNeeded(); mesh.updateBound(); } + + // ── NPC Kampf-KI ───────────────────────────────────────────────────────── + + private void updateNpcCombatPhase(SpawnedNpc s, float tpf, Vector3f playerPos) { + switch (s.combatPhase) { + + case KNOCKED_OUT -> { + // DEATH-Animation läuft einmalig (DontLoop) und friert auf dem letzten Frame ein. + } + + case IDLE_COMBAT -> { + playNpcAction(s, resolveNpcIdleAction(s)); + s.thinkTimer -= tpf; + if (s.thinkTimer > 0f) { return; } + // Think-Timer abgelaufen → Entscheidung + Vector3f npcPos = s.visual().getWorldTranslation(); + float dist = dist2d(npcPos, playerPos); + if (dist > NPC_ATTACK_RANGE) { + s.combatPhase = NpcCombatPhase.CHARGING; + return; + } + boolean lowHp = (float) s.currentHp / s.maxHp < NPC_LOW_HP_THRESHOLD; + float blockChance = lowHp ? NPC_BLOCK_CHANCE_LOW_HP : NPC_BLOCK_CHANCE_NORMAL; + float heavyChance = heavyAttackChance(); + if (heavyChance > 0f && npcRng.nextFloat() < heavyChance) { + startNpcHeavyAttack(s); + } else if (npcRng.nextFloat() < blockChance && s.currentStamina >= NPC_BLOCK_STAMINA_BASE) { + startNpcBlock(s); + } else { + startNpcAttackSequence(s); + } + } + + case CHARGING -> { + // Wird durch äußere Bewegungslogik in updateNpcCombat verwaltet. + // Wenn in Reichweite, wechselt updateNpcCombat zu IDLE_COMBAT + resetTimer. + } + + case ATTACKING, COUNTERING -> { + s.combatPhaseTimer -= tpf; + s.attackHitWinTimer += tpf; + float progress = s.attackClipDuration > 0f + ? s.attackHitWinTimer / s.attackClipDuration : 1f; + if (!s.attackHitFired + && progress >= NPC_HIT_WIN_START + && progress <= NPC_HIT_WIN_END) { + s.attackHitFired = true; + fireNpcAttack(s, playerPos); + } + if (s.combatPhaseTimer <= 0f) { + if (s.combatPhase == NpcCombatPhase.ATTACKING + && s.comboHitsRemaining > 0) { + Vector3f npcPos = s.visual().getWorldTranslation(); + if (dist2d(npcPos, playerPos) <= NPC_ATTACK_RANGE + 0.5f) { + s.comboHitsRemaining--; + s.comboStep++; + startNpcAttackStep(s); + return; + } + } + s.combatPhase = NpcCombatPhase.IDLE_COMBAT; + resetNpcThinkTimer(s); + } + } + + case BLOCKING -> { + playNpcAction(s, resolveNpcBlockHoldAction(s)); + s.combatPhaseTimer -= tpf; + if (s.combatPhaseTimer <= 0f || s.currentStamina <= 0) { + s.combatPhase = NpcCombatPhase.IDLE_COMBAT; + resetNpcThinkTimer(s); + } + } + + case COUNTER_WINDOW -> { + // Immer sofort kontern + s.combatPhaseTimer -= tpf; + if (s.combatPhaseTimer <= 0f) { + startNpcCounter(s); + } + } + + case STAGGERED -> { + s.combatPhaseTimer -= tpf; + if (s.combatPhaseTimer <= 0f) { + s.combatPhase = NpcCombatPhase.IDLE_COMBAT; + resetNpcThinkTimer(s); + } + } + + case DODGING -> { + s.combatPhaseTimer -= tpf; + // Seitliche Ausweich-Bewegung (bereits in startNpcDodge gesetzt) + if (s.dodgeDirX != 0f || s.dodgeDirZ != 0f) { + Vector3f npcPos = s.visual().getWorldTranslation(); + float nx = npcPos.x + s.dodgeDirX * NPC_MOVE_SPEED * tpf; + float nz = npcPos.z + s.dodgeDirZ * NPC_MOVE_SPEED * tpf; + float ny = terrainChunks != null ? terrainChunks.getHeightAt(nx, nz) : npcPos.y; + s.visual().setLocalTranslation(nx, ny, nz); + } + if (s.combatPhaseTimer <= 0f) { + s.dodgeDirX = 0f; + s.dodgeDirZ = 0f; + s.combatPhase = NpcCombatPhase.IDLE_COMBAT; + resetNpcThinkTimer(s); + } + } + } + } + + /** Steigt nach 2 s dauerhaftem Block: +10 %/s, max. 60 %. */ + private float heavyAttackChance() { + float t = Math.max(0f, playerBlockTimer - 2f); + return Math.min(0.60f, t * 0.10f); + } + + private void startNpcHeavyAttack(SpawnedNpc s) { + s.comboHitsRemaining = 0; + s.comboStep = 2; // ATTACK_3 animation + s.npcHeavyAttack = true; + startNpcAttackStep(s); + } + + private void startNpcAttackSequence(SpawnedNpc s) { + s.comboStep = 0; + int hits = 1; + if (npcRng.nextFloat() < NPC_COMBO_EXTEND_P1) { hits++; } + if (hits == 2 && npcRng.nextFloat() < NPC_COMBO_EXTEND_P2) { hits++; } + s.comboHitsRemaining = hits - 1; + startNpcAttackStep(s); + } + + private void startNpcAttackStep(SpawnedNpc s) { + AnimationAction atk = resolveNpcAttackAction(s, s.comboStep); + float dur = resolveNpcClipLength(s, atk, NPC_CLIP_FALLBACK_ATK); + playNpcAction(s, atk); + s.combatPhase = NpcCombatPhase.ATTACKING; + s.combatPhaseTimer = dur; + s.attackClipDuration = dur; + s.attackHitWinTimer = 0f; + s.attackHitFired = false; + } + + private void startNpcBlock(SpawnedNpc s) { + AnimationAction blockEntry = resolveNpcBlockAction(s); + float entryDur = resolveNpcClipLength(s, blockEntry, NPC_CLIP_FALLBACK_BLOCK); + float holdTime = 0.5f + npcRng.nextFloat() * 1.5f; + playNpcAction(s, blockEntry); + s.combatPhase = NpcCombatPhase.BLOCKING; + s.combatPhaseTimer = entryDur + holdTime; + } + + private void startNpcCounter(SpawnedNpc s) { + AnimationAction ctr = resolveNpcCounterAction(s); + float dur = resolveNpcClipLength(s, ctr, NPC_CLIP_FALLBACK_CTR); + playNpcAction(s, ctr); + s.combatPhase = NpcCombatPhase.COUNTERING; + s.combatPhaseTimer = dur; + s.attackClipDuration = dur; + s.attackHitWinTimer = 0f; + s.attackHitFired = false; + } + + private void startNpcDodge(SpawnedNpc s) { + playNpcAction(s, AnimationAction.ROLL); + float dur = resolveNpcClipLength(s, AnimationAction.ROLL, NPC_CLIP_FALLBACK_ROLL); + // Seitlich vom Spieler wegbewegen + Vector3f npcPos = s.visual().getWorldTranslation(); + Vector3f playerPos = physicsChar.getPhysicsLocation(); + float dx = npcPos.x - playerPos.x; + float dz = npcPos.z - playerPos.z; + float len = (float) Math.sqrt(dx * dx + dz * dz); + // Perpendicular links oder rechts (zufällig) + float sign = npcRng.nextBoolean() ? 1f : -1f; + s.dodgeDirX = (len > 0.001f) ? sign * (dz / len) : 1f; + s.dodgeDirZ = (len > 0.001f) ? sign * (-dx / len) : 0f; + s.combatPhase = NpcCombatPhase.DODGING; + s.combatPhaseTimer = dur; + } + + private void staggerNpc(SpawnedNpc s) { + s.attackHitFired = false; + AnimationAction stagger = resolveNpcStaggerAction(s); + float dur = resolveNpcClipLength(s, stagger, NPC_STAGGER_FALLBACK); + playNpcAction(s, stagger); + s.combatPhase = NpcCombatPhase.STAGGERED; + s.combatPhaseTimer = dur; + } + + private void knockoutNpc(SpawnedNpc s) { + s.knockedOut = true; + s.combatPhase = NpcCombatPhase.KNOCKED_OUT; + if (s.hpBarNode != null) { s.hpBarNode.setCullHint(Spatial.CullHint.Always); } + s.aggroBehavior.forceCalm(); + playNpcDeathAnim(s); + log.info("[NPC {}] K.O.!", s.npc().getCharacterId()); + } + + private void playNpcDeathAnim(SpawnedNpc s) { + if (animLib == null) { return; } + String setName = s.npc().getAnimSetPath(); + if (setName == null || setName.isBlank()) { return; } + AnimationAction deathAction = npcRng.nextBoolean() + ? AnimationAction.SWORD_DEATH : AnimationAction.SWORD_DEATH_2; + String clip = AnimationLibrary.getClipForAction( + AnimationLibrary.findAssetRoot(), setName, deathAction); + if (clip == null) { + clip = AnimationLibrary.getClipForAction( + AnimationLibrary.findAssetRoot(), setName, AnimationAction.SWORD_DEATH); + } + if (clip == null) { return; } + if (!animLib.applyTo(clip, s.visual())) { return; } + AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(s.visual()); + if (ac == null) { return; } + ac.setCurrentAction(clip, com.jme3.anim.AnimComposer.DEFAULT_LAYER, false); + s.currentAction = deathAction; + } + + private void fireNpcAttack(SpawnedNpc s, Vector3f playerPos) { + Vector3f npcPos = s.visual().getWorldTranslation(); + if (dist2d(npcPos, playerPos) > NPC_ATTACK_RANGE + 0.3f) { return; } + Item weapon = findItemById(s.npc().getEquippedWeaponId()); + int base = (weapon != null && weapon.getDamage() > 0) ? weapon.getDamage() : NPC_UNARMED_DAMAGE; + boolean isHeavy = s.npcHeavyAttack; + s.npcHeavyAttack = false; // verbraucht + + if (isHeavy) { + if (playerInput.isBlocking()) { + // Schwerer Angriff bricht Block — kein HP-Verlust, aber Stagger + playerInput.triggerBlockImpact(); + AnimationAction impact = resolvePlayerImpactAction(); + if (impact != null) { playerInput.requestAnimation(impact, NPC_STAGGER_FALLBACK, null); } + } else { + applyPlayerDamage(Math.max(1, base * 3)); + AnimationAction impact = resolvePlayerImpactAction(); + if (impact != null) { playerInput.requestAnimation(impact, NPC_STAGGER_FALLBACK, null); } + } + return; + } + + int damage = Math.max(1, base * (s.comboStep == 2 ? NPC_COMBO3_DAMAGE_MULT : 1)); + if (playerInput.isBlocking()) { + playerInput.triggerBlockImpact(); + // Konter überspringt Block nicht — normaler Schlag absorbiert + if (s.combatPhase != NpcCombatPhase.COUNTERING) { return; } + } + applyPlayerDamage(damage); + AnimationAction impact = resolvePlayerImpactAction(); + if (impact != null) { playerInput.requestAnimation(impact, NPC_STAGGER_FALLBACK, null); } + } + + private void applyPlayerDamage(int damage) { + if (mainCharacter == null) { return; } + int hp = Math.max(0, mainCharacter.getCurrentHp() - damage); + mainCharacter.setCurrentHp(hp); + log.info("[NPC→Player] -{} HP → {}/{}", damage, hp, mainCharacter.getMaxHp()); + } + + private void resetNpcThinkTimer(SpawnedNpc s) { + boolean lowHp = (float) s.currentHp / s.maxHp < NPC_LOW_HP_THRESHOLD; + float min = lowHp ? NPC_THINK_MIN_LOW_HP : NPC_THINK_MIN_NORMAL; + float max = lowHp ? NPC_THINK_MAX_LOW_HP : NPC_THINK_MAX_NORMAL; + s.thinkTimer = min + npcRng.nextFloat() * (max - min); + } + + // ── NPC Animations-Resolver ─────────────────────────────────────────────── + + private de.blight.common.model.ItemSubCategory resolveNpcCombatWeaponType(SpawnedNpc s) { + Item w = findItemById(s.npc().getEquippedWeaponId()); + if (w == null || w.getSubCategory() == null) { + return de.blight.common.model.ItemSubCategory.SWORD; + } + return w.getSubCategory(); + } + + private AnimationAction resolveNpcIdleAction(SpawnedNpc s) { + if (s.combatWeaponType == null) { return AnimationAction.IDLE; } + return switch (s.combatWeaponType) { + case SWORD -> AnimationAction.SWORD_IDLE; + case AXE -> AnimationAction.AXE_IDLE; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE; + default -> AnimationAction.IDLE; + }; + } + + private AnimationAction resolveNpcAttackAction(SpawnedNpc s, int step) { + de.blight.common.model.ItemSubCategory wt = + s.combatWeaponType != null ? s.combatWeaponType : de.blight.common.model.ItemSubCategory.SWORD; + return switch (wt) { + case AXE -> switch (step) { + case 1 -> AnimationAction.AXE_ATTACK_2; + case 2 -> AnimationAction.AXE_ATTACK_3; + default -> AnimationAction.AXE_ATTACK_1; + }; + case TWO_HANDED_SWORD -> switch (step) { + case 1 -> AnimationAction.TWO_HAND_ATTACK_2; + case 2 -> AnimationAction.TWO_HAND_ATTACK_3; + default -> AnimationAction.TWO_HAND_ATTACK_1; + }; + default -> switch (step) { + case 1 -> AnimationAction.SWORD_ATTACK_2; + case 2 -> AnimationAction.SWORD_ATTACK_3; + default -> AnimationAction.SWORD_ATTACK_1; + }; + }; + } + + private AnimationAction resolveNpcBlockAction(SpawnedNpc s) { + if (s.combatWeaponType == null) { return AnimationAction.SWORD_IDLE_TO_BLOCK; } + return switch (s.combatWeaponType) { + case AXE -> AnimationAction.AXE_IDLE_TO_BLOCK; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_TO_BLOCK; + default -> AnimationAction.SWORD_IDLE_TO_BLOCK; + }; + } + + private AnimationAction resolveNpcBlockHoldAction(SpawnedNpc s) { + if (s.combatWeaponType == null) { return AnimationAction.SWORD_BLOCK; } + return switch (s.combatWeaponType) { + case AXE -> AnimationAction.AXE_BLOCK; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK; + default -> AnimationAction.SWORD_BLOCK; + }; + } + + private AnimationAction resolveNpcBlockImpactAction(SpawnedNpc s) { + if (s.combatWeaponType == null) { return AnimationAction.SWORD_BLOCK_IMPACT; } + return switch (s.combatWeaponType) { + case AXE -> AnimationAction.AXE_BLOCK_IMPACT; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT; + default -> AnimationAction.SWORD_BLOCK_IMPACT; + }; + } + + private AnimationAction resolveNpcCounterAction(SpawnedNpc s) { + if (s.combatWeaponType == null) { return AnimationAction.SWORD_COUNTERATTACK; } + return switch (s.combatWeaponType) { + case AXE -> AnimationAction.AXE_COUNTERATTACK; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_COUNTERATTACK; + default -> AnimationAction.SWORD_COUNTERATTACK; + }; + } + + private AnimationAction resolveNpcStaggerAction(SpawnedNpc s) { + if (s.combatWeaponType == null) { return AnimationAction.SWORD_IDLE_IMPACT; } + return switch (s.combatWeaponType) { + case AXE -> AnimationAction.AXE_IDLE_IMPACT; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT; + default -> AnimationAction.SWORD_IDLE_IMPACT; + }; + } + + private AnimationAction resolvePlayerImpactAction() { + de.blight.common.model.ItemSubCategory wt = playerInput.getDrawnWeaponType(); + if (wt == null) { return AnimationAction.SWORD_IDLE_IMPACT; } + return switch (wt) { + case AXE -> AnimationAction.AXE_IDLE_IMPACT; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT; + default -> AnimationAction.SWORD_IDLE_IMPACT; + }; + } + + private float resolveNpcClipLength(SpawnedNpc s, AnimationAction action, float fallback) { + if (animLib == null) { return fallback; } + String setName = s.npc().getAnimSetPath(); + if (setName == null || setName.isBlank()) { return fallback; } + try { + String clip = AnimationLibrary.getClipForAction( + AnimationLibrary.findAssetRoot(), setName, action); + if (clip == null) { return fallback; } + com.jme3.anim.AnimComposer ac = + de.blight.game.animation.RetargetingSystem.findAnimComposer(s.visual()); + if (ac == null) { return fallback; } + com.jme3.anim.AnimClip animClip = ac.getAnimClip(clip); + float len = animClip != null ? (float) animClip.getLength() : 0f; + return len > 0f ? len : fallback; + } catch (Exception e) { + return fallback; + } + } } diff --git a/doc/Freibeuter.odt b/doc/Freibeuter.odt index 9c0f824..f26d2a9 100644 Binary files a/doc/Freibeuter.odt and b/doc/Freibeuter.odt differ