Character und Dialog System weiter gebaut
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ────────────────────
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user