Kampfsystem: NPC verhalten weiter angepasst, einige Bugs gefixed
This commit is contained in:
@@ -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)");
|
||||
|
||||
@@ -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<String, TransformTrack> 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<String, String> 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,6 +111,18 @@ public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback
|
||||
/** Aktuell in der Welt gespawnte NPCs. */
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user