Kampfsystem: NPC verhalten weiter angepasst, einige Bugs gefixed
This commit is contained in:
BIN
assets/imported/animations/Sword And Shield Death 2.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Death 2.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Death.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Death.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Idle bored.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Idle bored.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Idle boring 2.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Idle boring 2.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Idle boring 3.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Idle boring 3.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Idle boring.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Idle boring.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Jump Running.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Jump Running.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Jump(1).fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Jump(1).fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Jump.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Jump.fbx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,7 +9,6 @@
|
|||||||
"T-Pose",
|
"T-Pose",
|
||||||
"Walking",
|
"Walking",
|
||||||
"Jumping",
|
"Jumping",
|
||||||
"Sword And Shield Slash",
|
|
||||||
"Sword And Shield Idle",
|
"Sword And Shield Idle",
|
||||||
"Sword And Shield Run",
|
"Sword And Shield Run",
|
||||||
"Sword And Shield Walk",
|
"Sword And Shield Walk",
|
||||||
@@ -30,7 +29,15 @@
|
|||||||
"Crouched To Standing",
|
"Crouched To Standing",
|
||||||
"Crouching Idle",
|
"Crouching Idle",
|
||||||
"Sneaking Forward",
|
"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": {
|
"actionMap": {
|
||||||
"DEFAULT": "Idle",
|
"DEFAULT": "Idle",
|
||||||
@@ -63,7 +70,18 @@
|
|||||||
"DUCK": "Standing To Crouched",
|
"DUCK": "Standing To Crouched",
|
||||||
"CROUCH_IDLE": "Crouching Idle",
|
"CROUCH_IDLE": "Crouching Idle",
|
||||||
"SNEAK": "Sneaking Forward",
|
"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",
|
"previewModelPath": "Characters/Models/hero.j3o",
|
||||||
"animOffsets": {
|
"animOffsets": {
|
||||||
@@ -125,6 +143,8 @@
|
|||||||
"Sword And Shield Strafe Right",
|
"Sword And Shield Strafe Right",
|
||||||
"Sword And Shield Heavy Attack",
|
"Sword And Shield Heavy Attack",
|
||||||
"Roll",
|
"Roll",
|
||||||
"Sneaking Forward"
|
"Sneaking Forward",
|
||||||
|
"Sword And Shield Jump Running",
|
||||||
|
"Sword And Shield Jump"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -663,6 +663,18 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
// j3o-Quelle: Clip bereits gespeichert – nur Preview, kein erneutes Speichern.
|
// j3o-Quelle: Clip bereits gespeichert – nur Preview, kein erneutes Speichern.
|
||||||
// snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary.
|
// snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary.
|
||||||
boolean isJ3oSource = animAssetPath.matches("(?i).*\\.j3o$");
|
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;
|
com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm;
|
||||||
|
|
||||||
int saved = 0;
|
int saved = 0;
|
||||||
@@ -1248,6 +1260,32 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
if (armature != null) holder.addControl(new SkinningControl(armature));
|
if (armature != null) holder.addControl(new SkinningControl(armature));
|
||||||
java.nio.file.Files.createDirectories(outFile.getParent());
|
java.nio.file.Files.createDirectories(outFile.getParent());
|
||||||
BinaryExporter.getInstance().save(holder, outFile.toFile());
|
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 ───────────────────────────────────────────────────────────
|
// ── FBX-Import ───────────────────────────────────────────────────────────
|
||||||
@@ -1924,12 +1962,27 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
translations = retargeted;
|
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(),
|
newTracks.add(new com.jme3.anim.TransformTrack(dstJoint, tt.getTimes(),
|
||||||
translations, tt.getRotations(), tt.getScales()));
|
translations, tt.getRotations(), null));
|
||||||
}
|
}
|
||||||
if (newTracks.isEmpty()) return null;
|
if (newTracks.isEmpty()) return null;
|
||||||
AnimClip result = new AnimClip(src.getName());
|
AnimClip result = new AnimClip(src.getName());
|
||||||
result.setTracks(newTracks.toArray(new com.jme3.anim.AnimTrack[0]));
|
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());
|
LOG.debug("[AnimPreview] normalizeAndRetarget '{}': {} Tracks, length={}", src.getName(), newTracks.size(), result.getLength());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,15 @@ public enum AnimationAction {
|
|||||||
SECRET,
|
SECRET,
|
||||||
TALK_SITTING,
|
TALK_SITTING,
|
||||||
|
|
||||||
|
// ── Schwert + Schild ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ── Schwert ───────────────────────────────────────────────────────────────
|
// ── Schwert ───────────────────────────────────────────────────────────────
|
||||||
SWORD_ATTACK_1,
|
SWORD_IDLE_BORED, SWORD_IDLE_BORED_2, SWORD_IDLE_BORED_3, SWORD_DEATH, SWORD_DEATH_2,
|
||||||
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK,
|
SWORD_ATTACK_1, SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,SWORD_COUNTERATTACK, SWORD_IDLE_IMPACT,
|
||||||
SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,
|
SWORD_IDLE, SWORD_WALK, SWORD_RUN, SWORD_JUMP, SWORD_RUNNING_JUMP,
|
||||||
SWORD_IDLE_TO_BLOCK, SWORD_COUNTERATTACK, SWORD_BLOCK_IMPACT, SWORD_IDLE_IMPACT,
|
SWORD_BLOCK, SWORD_IDLE_TO_BLOCK, SWORD_BLOCK_IMPACT, SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT,
|
||||||
SWORD_IDLE, SWORD_WALK, SWORD_RUN,
|
|
||||||
SWORD_BLOCK_MOVE, SWORD_BLOCK_MOVE_BACKWARD, SWORD_BLOCK_STRAFE_LEFT, SWORD_BLOCK_STRAFE_RIGHT,
|
|
||||||
|
|
||||||
// ── Axt ───────────────────────────────────────────────────────────────────
|
// ── Axt ───────────────────────────────────────────────────────────────────
|
||||||
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
|
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
|
||||||
@@ -104,9 +106,14 @@ public enum AnimationAction {
|
|||||||
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
|
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
|
||||||
case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)");
|
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_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_BLOCK -> TextRegistry.resolve(null, key, "Block (Schwert)");
|
||||||
case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)");
|
case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)");
|
||||||
case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
|
case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
|
||||||
|
|||||||
@@ -71,9 +71,12 @@ public final class RetargetingSystem {
|
|||||||
// Mixamo-Clips, deren Knochen in Blender nur umbenannt (nicht retargeted) wurden,
|
// Mixamo-Clips, deren Knochen in Blender nur umbenannt (nicht retargeted) wurden,
|
||||||
// haben denselben Knochennamen aber Mixamo-Bind-Pose → benötigen die volle Formel.
|
// haben denselben Knochennamen aber Mixamo-Bind-Pose → benötigen die volle Formel.
|
||||||
if (isSameRig(nameMap, sourceArmature, targetArmature)) {
|
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);
|
return redirectTracks(sourceClip, targetArmature);
|
||||||
}
|
}
|
||||||
|
log.info("[Retarget] '{}' same-rig=FALSE → full retarget (srcJoints={} dstJoints={})",
|
||||||
|
sourceClip.getName(), sourceArmature.getJointCount(), targetArmature.getJointCount());
|
||||||
|
|
||||||
// ── Source track lookup ───────────────────────────────────────────────
|
// ── Source track lookup ───────────────────────────────────────────────
|
||||||
Map<String, TransformTrack> srcTrackMap = new HashMap<>();
|
Map<String, TransformTrack> srcTrackMap = new HashMap<>();
|
||||||
@@ -402,10 +405,27 @@ public final class RetargetingSystem {
|
|||||||
|
|
||||||
// ── Same-rig detection & redirect ────────────────────────────────────────
|
// ── 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<String, String> nameMap, Armature src, Armature dst) {
|
private static boolean isSameRig(Map<String, String> nameMap, Armature src, Armature dst) {
|
||||||
for (var e : nameMap.entrySet()) {
|
for (var e : nameMap.entrySet()) {
|
||||||
if (!e.getKey().equals(e.getValue())) return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -417,13 +437,31 @@ public final class RetargetingSystem {
|
|||||||
if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint srcJoint) {
|
if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint srcJoint) {
|
||||||
Joint dstJoint = targetArmature.getJoint(srcJoint.getName());
|
Joint dstJoint = targetArmature.getJoint(srcJoint.getName());
|
||||||
if (dstJoint == null) continue;
|
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(),
|
newTracks.add(new TransformTrack(dstJoint, tt.getTimes(),
|
||||||
tt.getTranslations(), tt.getRotations(), tt.getScales()));
|
tt.getTranslations(), tt.getRotations(), null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newTracks.isEmpty()) return null;
|
if (newTracks.isEmpty()) return null;
|
||||||
AnimClip result = new AnimClip(sourceClip.getName());
|
AnimClip result = new AnimClip(sourceClip.getName());
|
||||||
result.setTracks(newTracks.toArray(new AnimTrack[0]));
|
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());
|
log.debug("[Retarget] '{}' redirected: {} Tracks", sourceClip.getName(), newTracks.size());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,6 +285,11 @@ public class PlayerInputControl {
|
|||||||
// ── Dodge Roll ────────────────────────────────────────────────────────────
|
// ── Dodge Roll ────────────────────────────────────────────────────────────
|
||||||
private boolean rollActive = false;
|
private boolean rollActive = false;
|
||||||
private float rollRemaining = 0f;
|
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 float rollInitRemaining = 0f;
|
||||||
private boolean rollBaselineCaptured = false;
|
private boolean rollBaselineCaptured = false;
|
||||||
private float rollHipsBaseY = 0f;
|
private float rollHipsBaseY = 0f;
|
||||||
@@ -969,20 +974,8 @@ 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;
|
||||||
if (attackHoldTimer > QUICK_TAP_THRESHOLD) {
|
if (attackHoldTimer > QUICK_TAP_THRESHOLD && !combatAnimActive) {
|
||||||
if (combatAnimActive && !combatAnimSkipComboWindow) {
|
startHeavyCharge();
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1091,15 +1084,41 @@ public class PlayerInputControl {
|
|||||||
&& sneakPhase != SneakPhase.DOWN && sneakPhase != SneakPhase.UP) {
|
&& sneakPhase != SneakPhase.DOWN && sneakPhase != SneakPhase.UP) {
|
||||||
AnimationAction target;
|
AnimationAction target;
|
||||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||||
|
boredIdleTimer = 0f; boredAnimRemaining = 0f;
|
||||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||||
} else if (sneaking) {
|
} else if (sneaking) {
|
||||||
|
boredIdleTimer = 0f; boredAnimRemaining = 0f;
|
||||||
target = moving ? AnimationAction.SNEAK : AnimationAction.CROUCH_IDLE;
|
target = moving ? AnimationAction.SNEAK : AnimationAction.CROUCH_IDLE;
|
||||||
} else if (moving) {
|
} else if (moving) {
|
||||||
|
boredIdleTimer = 0f; boredAnimRemaining = 0f;
|
||||||
target = (inDeepWater || walk) ? AnimationAction.WALK
|
target = (inDeepWater || walk) ? AnimationAction.WALK
|
||||||
: sprint ? AnimationAction.SPRINT
|
: sprint ? AnimationAction.SPRINT
|
||||||
: AnimationAction.RUN;
|
: AnimationAction.RUN;
|
||||||
} else {
|
} 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);
|
target = resolveWeaponAction(target);
|
||||||
@@ -1490,6 +1509,12 @@ public class PlayerInputControl {
|
|||||||
case IDLE -> weaponIdleAction();
|
case IDLE -> weaponIdleAction();
|
||||||
case WALK -> weaponWalkAction();
|
case WALK -> weaponWalkAction();
|
||||||
case RUN, SPRINT -> weaponRunAction();
|
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;
|
default -> base;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1558,7 +1583,7 @@ public class PlayerInputControl {
|
|||||||
return switch (s) {
|
return switch (s) {
|
||||||
case AXE -> AnimationAction.AXE_DRAW;
|
case AXE -> AnimationAction.AXE_DRAW;
|
||||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_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) {
|
return switch (s) {
|
||||||
case AXE -> AnimationAction.AXE_SHEATHE;
|
case AXE -> AnimationAction.AXE_SHEATHE;
|
||||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_SHEATHE;
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_SHEATHE;
|
||||||
default -> AnimationAction.SWORD_SHEATHE;
|
default -> AnimationAction.AXE_DRAW;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -196,6 +196,13 @@ public class NpcAggroBehavior {
|
|||||||
/** Returns a random phrase index in [0, 4]. */
|
/** Returns a random phrase index in [0, 4]. */
|
||||||
public int randomPhraseIdx() { return rng.nextInt(5); }
|
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 ────────────────────────────────────────────────────
|
// ── Static sight check ────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -111,6 +111,18 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
/** Aktuell in der Welt gespawnte NPCs. */
|
/** Aktuell in der Welt gespawnte NPCs. */
|
||||||
private final List<SpawnedNpc> spawned = new ArrayList<>();
|
private final List<SpawnedNpc> 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 {
|
private static class SpawnedNpc {
|
||||||
final NPC npc;
|
final NPC npc;
|
||||||
final Spatial visual;
|
final Spatial visual;
|
||||||
@@ -151,6 +163,23 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
// ── Physik-Kapsel (Kollision) ─────────────────────────────────────────
|
// ── Physik-Kapsel (Kollision) ─────────────────────────────────────────
|
||||||
RigidBodyControl rbc = null;
|
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) {
|
SpawnedNpc(NPC npc, Spatial visual, float x, float z) {
|
||||||
this.npc = npc;
|
this.npc = npc;
|
||||||
this.visual = visual;
|
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_DIST = 3.0f;
|
||||||
private static final float NPC_CLOSE_IN_SPEED = CharacterNavigator.WALK_SPEED_MPS;
|
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 ────────────────────────────────────────────────────────
|
// ── NPC HP / Kampf ────────────────────────────────────────────────────────
|
||||||
private static final int DEFAULT_NPC_MAX_HP = 100;
|
private static final int DEFAULT_NPC_MAX_HP = 100;
|
||||||
private static final int DEFAULT_NPC_MAX_MANA = 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();
|
de.blight.common.model.ItemSubCategory sub = w.getSubCategory();
|
||||||
if (sub == null) { return AnimationAction.IDLE; }
|
if (sub == null) { return AnimationAction.IDLE; }
|
||||||
return switch (sub) {
|
return switch (sub) {
|
||||||
case SWORD -> AnimationAction.SWORD_SHEATHE;
|
|
||||||
case AXE -> AnimationAction.AXE_SHEATHE;
|
case AXE -> AnimationAction.AXE_SHEATHE;
|
||||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_SHEATHE;
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_SHEATHE;
|
||||||
case BOW -> AnimationAction.BOW_SHEATHE;
|
case BOW -> AnimationAction.BOW_SHEATHE;
|
||||||
@@ -1412,6 +1467,8 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
|
|
||||||
private void updateNpcCombat(float tpf) {
|
private void updateNpcCombat(float tpf) {
|
||||||
if (dialogActive) { return; }
|
if (dialogActive) { return; }
|
||||||
|
if (playerInput.isBlocking()) { playerBlockTimer += tpf; }
|
||||||
|
else { playerBlockTimer = 0f; }
|
||||||
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||||
for (SpawnedNpc s : spawned) {
|
for (SpawnedNpc s : spawned) {
|
||||||
|
|
||||||
@@ -1441,6 +1498,10 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
s.combatRunAnim = null;
|
s.combatRunAnim = null;
|
||||||
playNpcAction(s, AnimationAction.IDLE);
|
playNpcAction(s, AnimationAction.IDLE);
|
||||||
} else {
|
} 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); }
|
if (s.combatRunAnim != null) { playNpcAction(s, s.combatRunAnim); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1486,16 +1547,27 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
|
|
||||||
if (!s.aggroBehavior.isAttacking()) { continue; }
|
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();
|
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||||
float dist = dist2d(npcPos, playerPos);
|
float dist = dist2d(npcPos, playerPos);
|
||||||
|
|
||||||
if (dist <= NPC_ATTACK_RANGE) {
|
if (dist <= NPC_ATTACK_RANGE) {
|
||||||
// Kampfdistanz erreicht: stehen, drehen, combat-idle
|
|
||||||
faceToward(s, playerPos);
|
faceToward(s, playerPos);
|
||||||
if (s.weaponDrawn) { playNpcAction(s, AnimationAction.IDLE); }
|
if (s.weaponDrawn) { updateNpcCombatPhase(s, tpf, playerPos); }
|
||||||
continue;
|
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
|
// Close-in: Spieler ≤ 3m und flieht nicht → NPC geht langsam ran
|
||||||
if (dist <= NPC_CLOSE_IN_DIST && !playerInput.isRunningOrSprinting()) {
|
if (dist <= NPC_CLOSE_IN_DIST && !playerInput.isRunningOrSprinting()) {
|
||||||
Vector3f moveDir = steerTowardPlayer(npcPos, playerPos);
|
Vector3f moveDir = steerTowardPlayer(npcPos, playerPos);
|
||||||
@@ -1662,6 +1734,7 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
lastAttackId = attackId;
|
lastAttackId = attackId;
|
||||||
hitThisAttack.clear();
|
hitThisAttack.clear();
|
||||||
}
|
}
|
||||||
|
boolean isHeavy = critMultiplier >= 3.0f;
|
||||||
for (SpawnedNpc s : spawned) {
|
for (SpawnedNpc s : spawned) {
|
||||||
if (hitThisAttack.contains(s.npc().getCharacterId())) { continue; }
|
if (hitThisAttack.contains(s.npc().getCharacterId())) { continue; }
|
||||||
Vector3f npcPos = s.visual().getWorldTranslation();
|
Vector3f npcPos = s.visual().getWorldTranslation();
|
||||||
@@ -1670,16 +1743,53 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
float dz = npcPos.z - gripPos.z;
|
float dz = npcPos.z - gripPos.z;
|
||||||
if (dx * dx + dy * dy + dz * dz <= bladeLength * bladeLength) {
|
if (dx * dx + dy * dy + dz * dz <= bladeLength * bladeLength) {
|
||||||
hitThisAttack.add(s.npc().getCharacterId());
|
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);
|
s.currentHp = Math.max(0, s.currentHp - damage);
|
||||||
refreshNpcHpBar(s);
|
refreshNpcHpBar(s);
|
||||||
log.info("[NPC {}] -{} HP → {}/{}{}", s.npc().getCharacterId(), damage,
|
log.info("[NPC {}] -{} HP → {}/{}{}", s.npc().getCharacterId(), damage,
|
||||||
s.currentHp, s.maxHp, s.currentHp <= 0 ? " K.O.!" : "");
|
s.currentHp, s.maxHp, s.currentHp <= 0 ? " K.O.!" : "");
|
||||||
|
if (s.currentHp <= 0) {
|
||||||
|
knockoutNpc(s);
|
||||||
|
} else {
|
||||||
|
staggerNpc(s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Feind-HUD ─────────────────────────────────────────────────────────────
|
// ── Feind-HUD ─────────────────────────────────────────────────────────────
|
||||||
@@ -1779,4 +1889,384 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
|||||||
mesh.getBuffer(com.jme3.scene.VertexBuffer.Type.TexCoord).setUpdateNeeded();
|
mesh.getBuffer(com.jme3.scene.VertexBuffer.Type.TexCoord).setUpdateNeeded();
|
||||||
mesh.updateBound();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user