Character und Dialog System weiter gebaut

This commit is contained in:
2026-07-06 21:06:27 +02:00
parent b05eb1055d
commit 8bcee4491e
40 changed files with 2006 additions and 250 deletions

View File

@@ -16,8 +16,10 @@ import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
import de.blight.game.console.JmeConsole;
import de.blight.game.scene.WorldScene;
import de.blight.lang.TextResolver;
import javax.imageio.ImageIO;
import java.util.Locale;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
@@ -132,6 +134,7 @@ public class BlightGame extends SimpleApplication {
@Override
public void simpleInitApp() {
TextResolver.init(Locale.GERMAN);
status("Lade Tastenbelegung...");
flyCam.setEnabled(false);
inputManager.deleteMapping(INPUT_MAPPING_EXIT);

View File

@@ -22,7 +22,7 @@ public class ThirdPersonCamera {
private static final float MAX_DISTANCE = BASE_DISTANCE + 3f;
private static final float MIN_VERTICAL_ANGLE = 0.08f; // Kamera immer leicht über Schulter
private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f;
private static final float TARGET_HEIGHT = 1.6f;
private static final float TARGET_HEIGHT = 1.0f;
private final Camera cam;
private final InputManager inputManager;

View File

@@ -306,8 +306,9 @@ public class WorldScene extends BaseAppState {
app.getStateManager().attach(
new WorldInteractableState(keyBindings, physicsChar, playerInput));
app.getStateManager().attach(new DialogHudState());
app.getStateManager().attach(
new WorldNpcsState(keyBindings, physicsChar, playerInput, mc));
WorldNpcsState npcsState = new WorldNpcsState(keyBindings, physicsChar, playerInput, mc);
npcsState.setThirdPersonCamera(thirdPersonCam);
app.getStateManager().attach(npcsState);
app.getStateManager().attach(new InteractionHudState());
inventoryState = new InventoryState(mc, keyBindings);
inventoryState.setEnabled(false);
@@ -472,6 +473,23 @@ public class WorldScene extends BaseAppState {
// Das Modell hat den Ursprung an den Füßen → wir brauchen einen -0.9m-Versatz im wrapper-Node.
private static final float CAPSULE_VISUAL_OFFSET_Y = -(0.5f + 0.4f); // -(halfCylHeight + radius)
/**
* Setzt Ambient=(1,1,1,1) auf allen Lighting.j3md-Materialien eines Modells,
* damit es den globalen AmbientLight korrekt empfängt (Blender-Export setzt Ambient oft auf schwarz).
*/
public static void applyFullAmbient(Spatial s) {
if (s instanceof Geometry g) {
com.jme3.material.Material m = g.getMaterial();
if (m != null && "Lighting".equals(m.getMaterialDef().getName())) {
m.setColor("Ambient", ColorRGBA.White.clone());
}
} else if (s instanceof Node n) {
for (Spatial child : n.getChildren()) {
applyFullAmbient(child);
}
}
}
/** Lädt das Hauptcharakter-Modell, falls im character/-Verzeichnis definiert; sonst Platzhalter. */
private Node loadOrBuildCharacter() {
MainCharacter mc = findMainCharacter();
@@ -480,6 +498,7 @@ public class WorldScene extends BaseAppState {
Spatial loaded = assetManager.loadModel(mc.getModelPath());
stripEmbeddedClips(loaded, mc.getModelPath());
loaded.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
applyFullAmbient(loaded);
// Auf 1.8 m skalieren Höhe aus Vertex-Daten (zuverlässiger als BoundingBox
// bei Skinned-Meshes, die vor der ersten SkinningControl-Runde falsche Bounds liefern)

View File

@@ -257,7 +257,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
// ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) ──────────────
float dawnFactor = 1f - FastMath.clamp(elev * 5f, 0f, 1f);
ColorRGBA sunColor = SUN_DAWN.clone().interpolateLocal(SUN_DAY, 1f - dawnFactor);
sunBaseColor = sunColor.mult(elevC * 0.85f);
sunBaseColor = sunColor.mult(elevC * 0.68f);
sun.setColor(sunBaseColor.mult(1f - caveFactor));
// ── Sonnen-Sphere ausblenden wenn unter Horizont ────────────────────

View File

@@ -23,6 +23,9 @@ import de.blight.common.model.DialogOption;
import de.blight.common.model.MainCharacter;
import de.blight.common.model.NPC;
import de.blight.common.model.TextReference;
import de.blight.common.model.trigger.ChangeRoutineTrigger;
import de.blight.common.model.trigger.NpcStatusTrigger;
import de.blight.common.model.trigger.Trigger;
import de.blight.game.config.MenuCanvas;
import de.blight.game.config.NinePatch;
import de.blight.lang.TextResolver;
@@ -97,6 +100,8 @@ public class DialogHudState extends BaseAppState {
private NPC currentNpc;
private MainCharacter mainChar;
private Runnable onClose;
private Runnable onOptionsShown;
private boolean optionsShownFired;
/** Alle Seiten des aktuellen Textes. */
private List<List<String>> textPages = new ArrayList<>();
@@ -149,14 +154,17 @@ public class DialogHudState extends BaseAppState {
/**
* Startet den Dialog mit dem gegebenen NPC.
*
* @param npc der angesprochene NPC
* @param mc der Hauptcharakter
* @param onCloseCallback wird aufgerufen wenn der Dialog endet
* @param npc der angesprochene NPC
* @param mc der Hauptcharakter
* @param onOptionsShown wird einmalig aufgerufen wenn Optionen angezeigt werden (darf null sein)
* @param onCloseCallback wird aufgerufen wenn der Dialog endet
*/
public void startDialog(NPC npc, MainCharacter mc, Runnable onCloseCallback) {
this.currentNpc = npc;
this.mainChar = mc;
this.onClose = onCloseCallback;
public void startDialog(NPC npc, MainCharacter mc, Runnable onOptionsShown, Runnable onCloseCallback) {
this.currentNpc = npc;
this.mainChar = mc;
this.onClose = onCloseCallback;
this.onOptionsShown = onOptionsShown;
this.optionsShownFired = false;
buildPanel();
registerInput();
@@ -170,6 +178,7 @@ public class DialogHudState extends BaseAppState {
if (greetText != null && !greetText.isBlank()) {
showText(Phase.TEXT_NPC, resolveNpcName(npc), greetText, () -> {
if (available.isEmpty()) {
log.info("[DialogHud] NPC '{}' hat keine Optionen nach Begrüßung.", npc.getCharacterId());
closeDialog();
} else {
showOptions(available);
@@ -178,7 +187,7 @@ public class DialogHudState extends BaseAppState {
} else if (!available.isEmpty()) {
showOptions(available);
} else {
// Keine Nachricht, keine Optionen → sofort schließen
log.info("[DialogHud] NPC '{}' hat keine Optionen und keine Begrüßung Dialog übersprungen.", npc.getCharacterId());
closeDialog();
}
}
@@ -225,10 +234,18 @@ public class DialogHudState extends BaseAppState {
displayOptions.clear();
selectedOpt = 0;
if (!optionsShownFired && onOptionsShown != null) {
optionsShownFired = true;
onOptionsShown.run();
}
for (DialogOption opt : options) {
String label = TextResolver.get().resolveId(opt.getLabel() != null ? opt.getLabel() : "");
if (label.isBlank()) label = opt.getId() != null
? opt.getId().substring(0, Math.min(opt.getId().length(), 20)) : "?";
String label = opt.getLabel() != null
? TextResolver.get().resolveId(opt.getLabel().id()) : "";
if (label.isBlank()) {
label = opt.getId() != null
? opt.getId().substring(0, Math.min(opt.getId().length(), 20)) : "?";
}
displayOptions.add(new DisplayOption(label, opt));
}
displayOptions.add(new DisplayOption(t(EXIT_KEY), null));
@@ -401,6 +418,30 @@ public class DialogHudState extends BaseAppState {
currentNpc.getCurrentOptions().addAll(opt.getNextOptions());
if (opt.isEnablesTrade()) currentNpc.setTrader(true);
if (mainChar != null) mainChar.handleDialogOption(opt);
// Trigger feuern
if (mainChar != null && opt.getOnChosenTriggers() != null) {
WorldNpcsState npcs = getStateManager().getState(WorldNpcsState.class);
for (Trigger t : opt.getOnChosenTriggers()) {
if (!t.isTriggarable(mainChar)) continue;
if (t instanceof NpcStatusTrigger n && npcs != null) {
npcs.applyNpcStatusTrigger(n);
} else if (t instanceof ChangeRoutineTrigger r && npcs != null) {
npcs.applyChangeRoutineTrigger(r);
} else {
t.trigger(mainChar);
}
}
}
// Dialog-Zustand im Spielstand sichern
String npcId = currentNpc.getCharacterId();
if (npcId != null) {
SaveGameState saveState = getStateManager().getState(SaveGameState.class);
if (saveState != null) {
saveState.reportDialogState(npcId, currentNpc.currentOptionIds());
}
}
}
// ── Dialog beenden ────────────────────────────────────────────────────────

View File

@@ -73,6 +73,17 @@ public class SaveGameState extends BaseAppState {
persist();
}
/** Gibt den gespeicherten Dialog-Zustand eines NPCs zurück, oder {@code null} wenn keiner vorhanden. */
public java.util.List<String> getDialogState(String npcId) {
return save.world.npcDialogState.get(npcId);
}
/** Speichert die aktuell verfügbaren Dialog-Optionen eines NPCs im Spielstand. */
public void reportDialogState(String npcId, java.util.List<String> activeOptionIds) {
save.world.npcDialogState.put(npcId, new java.util.ArrayList<>(activeOptionIds));
persist();
}
/** Meldet einen besiegten Gegner (für zukünftige Implementierung). */
public void reportEnemyDefeated(String enemyId) {
save.world.defeatedEnemies.add(enemyId);

View File

@@ -176,6 +176,7 @@ public class WorldItemsState extends BaseAppState {
}
inputManager.addMapping(INTERACT_ACTION,
new MouseButtonTrigger(MouseInput.BUTTON_LEFT),
new KeyTrigger(keyBindings.interact));
inputManager.addMapping(SECONDARY_ATTACK_ACTION,
new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));

View File

@@ -6,8 +6,12 @@ import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.*;
import com.jme3.renderer.Camera;
@@ -16,9 +20,13 @@ import com.jme3.scene.shape.Box;
import de.blight.common.PlacedModel;
import de.blight.common.PlacedModelIO;
import de.blight.common.model.*;
import de.blight.common.model.trigger.ChangeRoutineTrigger;
import de.blight.common.model.trigger.NpcStatusTrigger;
import de.blight.game.animation.AnimationAction;
import de.blight.game.animation.AnimationLibrary;
import de.blight.game.config.KeyBindings;
import de.blight.game.control.PlayerInputControl;
import de.blight.game.control.ThirdPersonCamera;
import de.blight.game.state.TerrainChunkState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,6 +64,7 @@ public class WorldNpcsState extends BaseAppState {
private Node npcsRoot;
private DayNightState dayNight;
private TerrainChunkState terrainChunks;
private AnimationLibrary animLib;
// ── NPC-Daten ─────────────────────────────────────────────────────────────
@@ -71,7 +80,22 @@ public class WorldNpcsState extends BaseAppState {
/** Aktuell in der Welt gespawnte NPCs. */
private final List<SpawnedNpc> spawned = new ArrayList<>();
private record SpawnedNpc(NPC npc, Spatial visual, float worldX, float worldZ) {}
private static class SpawnedNpc {
final NPC npc;
final Spatial visual;
float worldX, worldZ;
AnimationAction currentAction = null;
SpawnedNpc(NPC npc, Spatial visual, float x, float z) {
this.npc = npc;
this.visual = visual;
this.worldX = x;
this.worldZ = z;
}
NPC npc() { return npc; }
Spatial visual() { return visual; }
}
// ── Periodischer Check ────────────────────────────────────────────────────
@@ -80,8 +104,29 @@ public class WorldNpcsState extends BaseAppState {
// ── Hover + Dialog ────────────────────────────────────────────────────────
private int hoveredIdx = -1;
private boolean dialogActive = false;
private int hoveredIdx = -1;
private boolean dialogActive = false;
private ThirdPersonCamera thirdPersonCam;
// ── NPC-Rotations-Animation ───────────────────────────────────────────────
private static final float ROT_DURATION = 0.5f;
private static final float REVERT_DURATION = 0.6f;
private static final float REVERT_DELAY_NO_OPTIONS = 3.0f;
private static final float REVERT_DELAY_AFTER_DIALOG = 0.3f;
/** NPC der gerade smooth rotiert wird. */
private SpawnedNpc rotNpc = null;
private Quaternion rotFrom = new Quaternion();
private Quaternion rotTo = new Quaternion();
private float rotElapsed = 0f;
/** NPC der nach einer Verzögerung zurück rotiert wird. */
private SpawnedNpc revertNpc = null;
private Quaternion revertFrom = new Quaternion();
private Quaternion revertTo = new Quaternion();
private float revertTimer = 0f;
private float revertDelay = 0f;
// ── Konstruktor ───────────────────────────────────────────────────────────
@@ -93,6 +138,10 @@ public class WorldNpcsState extends BaseAppState {
this.mainCharacter = mainCharacter;
}
public void setThirdPersonCamera(ThirdPersonCamera cam) {
this.thirdPersonCam = cam;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
@@ -114,13 +163,16 @@ public class WorldNpcsState extends BaseAppState {
protected void onEnable() {
dayNight = getStateManager().getState(DayNightState.class);
terrainChunks = getStateManager().getState(TerrainChunkState.class);
animLib = getStateManager().getState(AnimationLibrary.class);
loadAllNpcs();
buildInteractablePositions();
rootNode.attachChild(npcsRoot);
app.getInputManager().addMapping(INTERACT_ACTION, new KeyTrigger(keyBindings.interact));
app.getInputManager().addMapping(INTERACT_ACTION,
new MouseButtonTrigger(MouseInput.BUTTON_LEFT),
new KeyTrigger(keyBindings.interact));
app.getInputManager().addListener(interactListener, INTERACT_ACTION);
checkTimer = CHECK_INTERVAL; // sofortiger erster Check im nächsten Frame
@@ -160,6 +212,8 @@ public class WorldNpcsState extends BaseAppState {
if (!dialogActive) {
updateHover();
}
updateNpcRotation(tpf);
updateNpcRevert(tpf);
}
// ── Spawn-Logik ───────────────────────────────────────────────────────────
@@ -176,9 +230,12 @@ public class WorldNpcsState extends BaseAppState {
if (pos == null || dist > UNLOAD_RADIUS) {
s.visual().removeFromParent();
toRemove.add(s);
if (rotNpc == s) rotNpc = null;
if (revertNpc == s) revertNpc = null;
} else {
// Position aktualisieren wenn sich die Stunde geändert hat
s.visual().setLocalTranslation(pos);
playNpcAction(s, activityActionFor(s.npc(), currentHour));
}
}
spawned.removeAll(toRemove);
@@ -199,7 +256,9 @@ public class WorldNpcsState extends BaseAppState {
Spatial vis = buildVisual(npc);
vis.setLocalTranslation(pos);
npcsRoot.attachChild(vis);
spawned.add(new SpawnedNpc(npc, vis, pos.x, pos.z));
SpawnedNpc s = new SpawnedNpc(npc, vis, pos.x, pos.z);
spawned.add(s);
playNpcAction(s, activityActionFor(npc, currentHour));
log.debug("[WorldNpcs] NPC '{}' gespawnt bei ({}, {})", npc.getCharacterId(), pos.x, pos.z);
}
}
@@ -221,6 +280,7 @@ public class WorldNpcsState extends BaseAppState {
if (block == null || block.getActivity() == null) return null;
RoutineActivity act = block.getActivity();
if (act.getType() == null) return null;
return switch (act.getType()) {
case STAND, TALK -> worldPointToVec(act.getPosition());
case SIT -> {
@@ -292,14 +352,152 @@ public class WorldNpcsState extends BaseAppState {
if (dialog == null) return;
dialogActive = true;
playerInput.lockInPlace();
dialog.startDialog(entry.npc(), mainCharacter, () -> {
// Spieler und NPC drehen sich smooth zueinander
Vector3f playerPos = physicsChar.getPhysicsLocation();
Vector3f npcPos = entry.visual().getLocalTranslation().clone();
Vector3f dirToNpc = npcPos.subtract(playerPos);
dirToNpc.y = 0f;
if (dirToNpc.lengthSquared() > 0.001f) {
dirToNpc.normalizeLocal();
playerInput.lockInPlace();
playerInput.requestTurn(dirToNpc, 0.4f, null);
} else {
playerInput.lockInPlace();
}
Quaternion origRot = entry.visual().getLocalRotation().clone();
Quaternion facingRot = computeFacingQuat(npcPos, playerPos);
beginSmoothRotation(entry, origRot, facingRot);
// Kamera wird erst gesetzt wenn Optionen erscheinen
boolean[] optionsWereShown = {false};
Runnable onOptions = () -> {
optionsWereShown[0] = true;
enterDialogCamera(npcPos, playerPos);
};
dialog.startDialog(entry.npc(), mainCharacter, onOptions, () -> {
exitDialogCamera();
dialogActive = false;
playerInput.unlockFromPlace();
float delay = optionsWereShown[0] ? REVERT_DELAY_AFTER_DIALOG : REVERT_DELAY_NO_OPTIONS;
scheduleRevert(entry, facingRot, origRot, delay);
});
}
private Quaternion computeFacingQuat(Vector3f npcPos, Vector3f playerPos) {
float dx = playerPos.x - npcPos.x;
float dz = playerPos.z - npcPos.z;
float angle = FastMath.atan2(dx, dz);
return new Quaternion().fromAngleAxis(angle, Vector3f.UNIT_Y);
}
private void beginSmoothRotation(SpawnedNpc npc, Quaternion from, Quaternion to) {
if (revertNpc == npc) revertNpc = null;
rotNpc = npc;
rotFrom = from.clone();
rotTo = to.clone();
rotElapsed = 0f;
}
private void scheduleRevert(SpawnedNpc npc, Quaternion from, Quaternion to, float delay) {
revertNpc = npc;
revertFrom = from.clone();
revertTo = to.clone();
revertTimer = 0f;
revertDelay = delay;
}
private void updateNpcRotation(float tpf) {
if (rotNpc == null) return;
rotElapsed += tpf;
float p = FastMath.clamp(rotElapsed / ROT_DURATION, 0f, 1f);
Quaternion q = new Quaternion().slerp(rotFrom, rotTo, p);
rotNpc.visual().setLocalRotation(q);
if (p >= 1f) rotNpc = null;
}
private void updateNpcRevert(float tpf) {
if (revertNpc == null) return;
revertTimer += tpf;
if (revertTimer < revertDelay) return;
float p = FastMath.clamp((revertTimer - revertDelay) / REVERT_DURATION, 0f, 1f);
Quaternion q = new Quaternion().slerp(revertFrom, revertTo, p);
revertNpc.visual().setLocalRotation(q);
if (p >= 1f) revertNpc = null;
}
private void enterDialogCamera(Vector3f npcPos, Vector3f playerPos) {
// Mittelpunkt auf Hüfthöhe
Vector3f mid = npcPos.add(playerPos).multLocal(0.5f);
mid.y += 0.9f;
// Zwei mögliche Seiten (je 90° zur Blicklinie)
Vector3f axis = npcPos.subtract(playerPos).normalizeLocal();
Vector3f sideA = axis.cross(Vector3f.UNIT_Y).normalizeLocal();
Vector3f sideB = sideA.negate();
float camSide = 3.5f;
float camUp = 1.2f;
Vector3f posA = mid.add(sideA.mult(camSide)).add(0, camUp, 0);
Vector3f posB = mid.add(sideB.mult(camSide)).add(0, camUp, 0);
posA = avoidClipping(mid, posA);
posB = avoidClipping(mid, posB);
// Seite mit mehr Abstand ist weniger verdeckt
Vector3f camPos = posA.distance(mid) >= posB.distance(mid) ? posA : posB;
if (thirdPersonCam != null) {
thirdPersonCam.setPaused(true);
}
cam.setLocation(camPos);
cam.lookAt(mid, Vector3f.UNIT_Y);
}
private void exitDialogCamera() {
if (thirdPersonCam != null) {
thirdPersonCam.setPaused(false);
}
}
private Vector3f avoidClipping(Vector3f from, Vector3f to) {
Vector3f dir = to.subtract(from);
float maxDist = dir.length();
dir.normalizeLocal();
CollisionResults results = new CollisionResults();
rootNode.collideWith(new Ray(from, dir), results);
if (results.size() == 0) return to;
CollisionResult nearest = results.getClosestCollision();
if (nearest.getDistance() >= maxDist) return to;
float safeDist = Math.max(1.2f, nearest.getDistance() - 0.3f);
return from.add(dir.mult(safeDist));
}
// ── Trigger-Ausführung (aus DialogHudState) ───────────────────────────────
public void applyNpcStatusTrigger(NpcStatusTrigger t) {
allNpcs.stream()
.filter(n -> t.getNpcId().equals(n.getCharacterId()))
.findFirst()
.ifPresent(n -> {
n.setStatus(t.getTargetStatus());
log.info("[WorldNpcs] Trigger: NPC '{}' Status → {}", n.getCharacterId(), t.getTargetStatus());
});
}
public void applyChangeRoutineTrigger(ChangeRoutineTrigger t) {
allNpcs.stream()
.filter(n -> t.getNpcId().equals(n.getCharacterId()))
.findFirst()
.ifPresent(n -> {
n.setCurrentRoutine(t.getRoutineName());
log.info("[WorldNpcs] Trigger: NPC '{}' Routine → {}", n.getCharacterId(), t.getRoutineName());
});
}
// ── Accessor für InteractionHudState ──────────────────────────────────────
public String getHoveredLabelKey() {
@@ -317,14 +515,18 @@ public class WorldNpcsState extends BaseAppState {
private void loadAllNpcs() {
allNpcs.clear();
SaveGameState saveState = getStateManager().getState(SaveGameState.class);
try {
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
if (gc instanceof NPC npc) {
java.util.List<String> savedState = saveState != null
? saveState.getDialogState(npc.getCharacterId()) : null;
npc.initRuntimeTree(savedState);
allNpcs.add(npc);
}
}
log.info("[WorldNpcs] {} NPCs mit Routinen geladen.", allNpcs.size());
log.info("[WorldNpcs] {} NPCs geladen.", allNpcs.size());
} catch (Exception e) {
log.warn("[WorldNpcs] Fehler beim Laden der NPCs: {}", e.getMessage());
}
@@ -356,6 +558,37 @@ public class WorldNpcsState extends BaseAppState {
return new Vector3f(p.x, y, p.z);
}
private AnimationAction activityActionFor(NPC npc, int hour) {
NpcRoutine routine = npc.getActiveRoutine();
if (routine == null || routine.getBlocks() == null) return AnimationAction.IDLE;
for (RoutineBlock b : routine.getBlocks()) {
if (!b.covers(hour)) continue;
RoutineActivity act = b.getActivity();
if (act == null || act.getType() == null) return AnimationAction.IDLE;
return switch (act.getType()) {
case PATROL -> AnimationAction.WALK;
case SIT -> AnimationAction.SITTING;
case SLEEP -> AnimationAction.LYING;
case STAND, TALK, WORK -> AnimationAction.IDLE;
};
}
return AnimationAction.IDLE;
}
private void playNpcAction(SpawnedNpc s, AnimationAction action) {
if (action == s.currentAction || animLib == null) return;
String setName = s.npc().getAnimSetPath();
if (setName == null || setName.isBlank()) return;
String clipName = AnimationLibrary.getClipForAction(AnimationLibrary.findAssetRoot(), setName, action);
if (clipName == null && action != AnimationAction.IDLE) {
clipName = AnimationLibrary.getClipForAction(AnimationLibrary.findAssetRoot(), setName, AnimationAction.IDLE);
}
if (clipName == null) return;
if (animLib.playOn(clipName, s.visual())) {
s.currentAction = action;
}
}
private static float dist2d(Vector3f a, Vector3f b) {
float dx = a.x - b.x;
float dz = a.z - b.z;
@@ -368,6 +601,10 @@ public class WorldNpcsState extends BaseAppState {
try {
Spatial model = assets.loadModel(modelPath);
model.setName("npc_" + npc.getCharacterId());
de.blight.game.scene.WorldScene.applyFullAmbient(model);
if (animLib != null) {
animLib.applyAllTo(model);
}
return model;
} catch (Exception e) {
log.warn("[WorldNpcs] Modell '{}' nicht ladbar Platzhalter.", modelPath);