Navigation, Bank-Setzen und AnimSet-Editor Keyframes
- CharacterNavigator: universelle Pfad-Navigation für Spieler und NPCs (PathFinder + Terrain-Slope-Check + Stuck-Erkennung, Walk/Run) - PlayerInputControl: navigateTo/stopNavigation-API, Navigator hat Vorrang vor WASD; setNavigationSources für PathFinder + TerrainChunkState - WorldInteractableState: Bank-Setzen komplett neu (< 5m, E-Taste), Navigator läuft zum Sitzpunkt, dreht Rücken zur Bank, spielt sit_down_bench / sitting / get_up_sitting; Bett weiterhin mit Rücklauf - AnimSet-Editor: Kamera startet mit -45° Pitch; AnimKeyframe-Offset-Editor - WorldScene: PathFinder + ObstacleRoot an PlayerInputControl übergeben Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,18 +23,29 @@
|
|||||||
"RUNNING_JUMP": "running_jump",
|
"RUNNING_JUMP": "running_jump",
|
||||||
"JUMP": "idle_jump",
|
"JUMP": "idle_jump",
|
||||||
"PICK_UP": "pickup",
|
"PICK_UP": "pickup",
|
||||||
"SIT_UP": "stand_up",
|
|
||||||
"SITTING": "sitting",
|
"SITTING": "sitting",
|
||||||
"SIT_DOWN": "sit_down_bench"
|
"SIT_DOWN": "sit_down_bench",
|
||||||
|
"SIT_UP": "get_up_sitting"
|
||||||
},
|
},
|
||||||
"previewModelPath": "Models/Chars/mainchar.j3o",
|
"previewModelPath": "Models/Chars/mainchar.j3o",
|
||||||
"motionKeyframes": {
|
"motionKeyframes": {
|
||||||
"sit_down_bench": [
|
"get_up_sitting": [
|
||||||
{
|
{
|
||||||
"time": 0.0,
|
"time": 0.0,
|
||||||
"tx": 0.0,
|
"tx": 0.0,
|
||||||
"ty": 0.0,
|
"ty": 0.0,
|
||||||
"tz": 0.25,
|
"tz": -0.25,
|
||||||
|
"rx": 0.0,
|
||||||
|
"ry": 0.0,
|
||||||
|
"rz": 0.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sitting": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"tx": 0.0,
|
||||||
|
"ty": 0.0,
|
||||||
|
"tz": -0.25,
|
||||||
"rx": 0.0,
|
"rx": 0.0,
|
||||||
"ry": 0.0,
|
"ry": 0.0,
|
||||||
"rz": 0.0
|
"rz": 0.0
|
||||||
|
|||||||
@@ -538,7 +538,7 @@ public class SharedInput {
|
|||||||
|
|
||||||
// ── Animations-Vorschau ──────────────────────────────────────────────────
|
// ── Animations-Vorschau ──────────────────────────────────────────────────
|
||||||
public volatile float animPreviewRotY = 0f;
|
public volatile float animPreviewRotY = 0f;
|
||||||
public volatile float animPreviewRotX = 25f;
|
public volatile float animPreviewRotX = -45f;
|
||||||
public volatile float animPreviewZoom = 1.0f;
|
public volatile float animPreviewZoom = 1.0f;
|
||||||
public volatile float animPreviewSpeed = 1.0f;
|
public volatile float animPreviewSpeed = 1.0f;
|
||||||
public volatile int animPreviewW = 512;
|
public volatile int animPreviewW = 512;
|
||||||
|
|||||||
@@ -319,6 +319,8 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
previewTarget.set(0, 1, 0);
|
previewTarget.set(0, 1, 0);
|
||||||
input.animPreviewZoom = 1.0f;
|
input.animPreviewZoom = 1.0f;
|
||||||
|
input.animPreviewRotX = -45f;
|
||||||
|
input.animPreviewRotY = 0f;
|
||||||
|
|
||||||
// Clips sammeln und melden
|
// Clips sammeln und melden
|
||||||
List<String> clips = new ArrayList<>();
|
List<String> clips = new ArrayList<>();
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import com.jme3.bullet.control.CharacterControl;
|
|||||||
import com.jme3.input.InputManager;
|
import com.jme3.input.InputManager;
|
||||||
import com.jme3.input.controls.ActionListener;
|
import com.jme3.input.controls.ActionListener;
|
||||||
import com.jme3.input.controls.KeyTrigger;
|
import com.jme3.input.controls.KeyTrigger;
|
||||||
import com.jme3.math.FastMath;
|
|
||||||
import com.jme3.math.Quaternion;
|
import com.jme3.math.Quaternion;
|
||||||
import com.jme3.math.Vector3f;
|
import com.jme3.math.Vector3f;
|
||||||
import com.jme3.renderer.Camera;
|
import com.jme3.renderer.Camera;
|
||||||
@@ -14,6 +13,8 @@ import de.blight.game.animation.AnimationAction;
|
|||||||
import de.blight.game.animation.AnimationLibrary;
|
import de.blight.game.animation.AnimationLibrary;
|
||||||
import de.blight.game.animation.RetargetingSystem;
|
import de.blight.game.animation.RetargetingSystem;
|
||||||
import de.blight.game.config.KeyBindings;
|
import de.blight.game.config.KeyBindings;
|
||||||
|
import de.blight.game.navigation.CharacterNavigator;
|
||||||
|
import de.blight.game.state.TerrainChunkState;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -51,6 +52,11 @@ public class PlayerInputControl {
|
|||||||
private String runningClip;
|
private String runningClip;
|
||||||
|
|
||||||
private com.jme3.anim.SkinningControl skinningControl = null;
|
private com.jme3.anim.SkinningControl skinningControl = null;
|
||||||
|
|
||||||
|
// ── Navigation ────────────────────────────────────────────────────────────
|
||||||
|
private CharacterNavigator navigator = null;
|
||||||
|
private de.blight.game.navigation.PathFinder navPathFinder = null;
|
||||||
|
private TerrainChunkState navTerrain = null;
|
||||||
private int jumpFrames = 0;
|
private int jumpFrames = 0;
|
||||||
private boolean pickupActive = false;
|
private boolean pickupActive = false;
|
||||||
private float pickupRemaining = 0f;
|
private float pickupRemaining = 0f;
|
||||||
@@ -61,14 +67,6 @@ public class PlayerInputControl {
|
|||||||
private float blockingAnimTotal = 0f;
|
private float blockingAnimTotal = 0f;
|
||||||
private Runnable blockingAnimCallback = null;
|
private Runnable blockingAnimCallback = null;
|
||||||
|
|
||||||
/** Manueller Motion-Override: pro Aktion konfigurierbare Keyframe-Liste. */
|
|
||||||
private java.util.Map<String, java.util.List<de.blight.game.animation.AnimKeyframe>>
|
|
||||||
motionKeyframesMap = java.util.Map.of();
|
|
||||||
private java.util.List<de.blight.game.animation.AnimKeyframe>
|
|
||||||
currentMotionKfs = null;
|
|
||||||
private Vector3f preMotionTranslation = null;
|
|
||||||
private Quaternion preMotionRotation = null;
|
|
||||||
|
|
||||||
/** Drehung auf der Stelle (kein Vorwärtsbewegen, nur Rotation). */
|
/** Drehung auf der Stelle (kein Vorwärtsbewegen, nur Rotation). */
|
||||||
private boolean turnActive = false;
|
private boolean turnActive = false;
|
||||||
private float turnRemaining = 0f;
|
private float turnRemaining = 0f;
|
||||||
@@ -108,6 +106,17 @@ public class PlayerInputControl {
|
|||||||
this.visual = visual;
|
this.visual = visual;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt die Navigationsquellen (PathFinder + Terrain). Muss vor oder beim
|
||||||
|
* ersten setAnimationContext gesetzt sein, damit der CharacterNavigator
|
||||||
|
* korrekt initialisiert wird.
|
||||||
|
*/
|
||||||
|
public void setNavigationSources(de.blight.game.navigation.PathFinder pathFinder,
|
||||||
|
TerrainChunkState terrain) {
|
||||||
|
this.navPathFinder = pathFinder;
|
||||||
|
this.navTerrain = terrain;
|
||||||
|
}
|
||||||
|
|
||||||
public void setAnimationContext(AnimationLibrary animLib, String animSetName, Path assetRoot) {
|
public void setAnimationContext(AnimationLibrary animLib, String animSetName, Path assetRoot) {
|
||||||
this.animLib = animLib;
|
this.animLib = animLib;
|
||||||
this.animSetName = animSetName;
|
this.animSetName = animSetName;
|
||||||
@@ -118,16 +127,11 @@ public class PlayerInputControl {
|
|||||||
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
||||||
skinningControl = findSkinningControl(visual);
|
skinningControl = findSkinningControl(visual);
|
||||||
log.info("[AnimCtx] SkinningControl gefunden: {}", skinningControl != null);
|
log.info("[AnimCtx] SkinningControl gefunden: {}", skinningControl != null);
|
||||||
if (animSetName != null && assetRoot != null) {
|
// Navigator (neu) aufbauen sobald alle Abhängigkeiten bereit sind
|
||||||
try {
|
if (navPathFinder != null && navTerrain != null && physicsChar != null && visual != null) {
|
||||||
java.nio.file.Path setDir = assetRoot.resolve("animations").resolve("sets");
|
navigator = new CharacterNavigator(physicsChar, visual, navPathFinder, navTerrain);
|
||||||
de.blight.game.animation.AnimSet set = de.blight.game.animation.AnimSet.load(setDir, animSetName);
|
navigator.setAnimationContext(animLib, animSetName, assetRoot);
|
||||||
motionKeyframesMap = set.getMotionKeyframes();
|
log.info("[AnimCtx] CharacterNavigator initialisiert.");
|
||||||
log.info("[AnimCtx] MotionKeyframes geladen: {} Aktionen: {}",
|
|
||||||
motionKeyframesMap.size(), motionKeyframesMap.keySet());
|
|
||||||
} catch (Exception e) {
|
|
||||||
motionKeyframesMap = java.util.Map.of();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (animSetName != null) {
|
if (animSetName != null) {
|
||||||
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.IDLE);
|
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.IDLE);
|
||||||
@@ -189,21 +193,6 @@ public class PlayerInputControl {
|
|||||||
if (duration <= 0f) {
|
if (duration <= 0f) {
|
||||||
duration = resolveClipLength(action, 1.5f);
|
duration = resolveClipLength(action, 1.5f);
|
||||||
}
|
}
|
||||||
String kfClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
|
||||||
currentMotionKfs = kfClip != null ? motionKeyframesMap.get(kfClip) : null;
|
|
||||||
if (currentMotionKfs != null && !currentMotionKfs.isEmpty() && visual != null) {
|
|
||||||
// KF-Werte sind absolute Positionen im Charakter-Lokalraum (nicht additiv)
|
|
||||||
preMotionTranslation = Vector3f.ZERO;
|
|
||||||
preMotionRotation = visual.getLocalRotation().clone();
|
|
||||||
} else {
|
|
||||||
// Keine Keyframes: visuelle Verschiebung aus vorheriger Keyframe-Aktion zurücksetzen
|
|
||||||
if (visual != null) {
|
|
||||||
visual.setLocalTranslation(Vector3f.ZERO);
|
|
||||||
}
|
|
||||||
currentMotionKfs = null;
|
|
||||||
preMotionTranslation = null;
|
|
||||||
preMotionRotation = null;
|
|
||||||
}
|
|
||||||
blockingAnimActive = true;
|
blockingAnimActive = true;
|
||||||
blockingAnimRemaining = duration;
|
blockingAnimRemaining = duration;
|
||||||
blockingAnimTotal = duration;
|
blockingAnimTotal = duration;
|
||||||
@@ -267,6 +256,38 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
public boolean isLockedInPlace() { return lockedInPlace; }
|
public boolean isLockedInPlace() { return lockedInPlace; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet die Navigation zum angegebenen Welt-Punkt.
|
||||||
|
* Während der Navigation werden WASD-Eingaben ignoriert.
|
||||||
|
* Der CharacterNavigator übernimmt Bewegung und Animation.
|
||||||
|
*
|
||||||
|
* @param target Zielposition (Y wird auf Terrain gesampled)
|
||||||
|
* @param speed {@link CharacterNavigator.Speed#WALK} oder {@link CharacterNavigator.Speed#RUN}
|
||||||
|
* @param onArrival Callback nach Ankunft (null erlaubt)
|
||||||
|
* @param onFailed Callback bei Abbruch (null erlaubt)
|
||||||
|
*/
|
||||||
|
public void navigateTo(Vector3f target, CharacterNavigator.Speed speed,
|
||||||
|
Runnable onArrival, Runnable onFailed) {
|
||||||
|
if (navigator == null) {
|
||||||
|
log.warn("[Nav] navigateTo: kein CharacterNavigator – setNavigationSources() vorher aufrufen.");
|
||||||
|
if (onFailed != null) onFailed.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
forward = backward = left = right = false;
|
||||||
|
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
|
autopilotDir = null;
|
||||||
|
navigator.navigateTo(target, speed, onArrival, onFailed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bricht eine laufende Navigation ab (kein Callback). */
|
||||||
|
public void stopNavigation() {
|
||||||
|
if (navigator != null) navigator.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNavigating() {
|
||||||
|
return navigator != null && navigator.isActive();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spielt eine Animations-Aktion als Dauer-Loop während des Ruhezustands.
|
* Spielt eine Animations-Aktion als Dauer-Loop während des Ruhezustands.
|
||||||
* Nur sinnvoll nach {@link #lockInPlace()}.
|
* Nur sinnvoll nach {@link #lockInPlace()}.
|
||||||
@@ -276,21 +297,6 @@ public class PlayerInputControl {
|
|||||||
public void playLockedAnimation(AnimationAction action) {
|
public void playLockedAnimation(AnimationAction action) {
|
||||||
playAction(action);
|
playAction(action);
|
||||||
currentAnim = action;
|
currentAnim = action;
|
||||||
String lockedClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
|
||||||
java.util.List<de.blight.game.animation.AnimKeyframe> kfs =
|
|
||||||
lockedClip != null ? motionKeyframesMap.get(lockedClip) : null;
|
|
||||||
log.info("[AnimKF] playLockedAnimation({}) clip='{}' → KFs gefunden: {}",
|
|
||||||
action, lockedClip, kfs != null ? kfs.size() : "null");
|
|
||||||
if (kfs != null && !kfs.isEmpty() && visual != null) {
|
|
||||||
currentMotionKfs = kfs;
|
|
||||||
preMotionTranslation = Vector3f.ZERO;
|
|
||||||
preMotionRotation = visual.getLocalRotation().clone();
|
|
||||||
applyMotionKeyframes(0f);
|
|
||||||
log.info("[AnimKF] Offset angewendet: visual.localTranslation={}", visual.getLocalTranslation());
|
|
||||||
currentMotionKfs = null;
|
|
||||||
preMotionTranslation = null;
|
|
||||||
preMotionRotation = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerMappings(KeyBindings kb) {
|
private void registerMappings(KeyBindings kb) {
|
||||||
@@ -308,7 +314,6 @@ public class PlayerInputControl {
|
|||||||
if (physicsChar == null) return;
|
if (physicsChar == null) return;
|
||||||
|
|
||||||
if (paused) {
|
if (paused) {
|
||||||
// Autopilot bei Pause sofort beenden
|
|
||||||
if (autopilotDir != null) {
|
if (autopilotDir != null) {
|
||||||
autopilotDir = null;
|
autopilotDir = null;
|
||||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
@@ -316,6 +321,12 @@ public class PlayerInputControl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigator: hat Vorrang vor allem außer Pause
|
||||||
|
if (navigator != null && navigator.isActive()) {
|
||||||
|
navigator.update(tpf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Pickup-Animation hat höchste Priorität
|
// Pickup-Animation hat höchste Priorität
|
||||||
if (pickupActive) {
|
if (pickupActive) {
|
||||||
pickupRemaining -= tpf;
|
pickupRemaining -= tpf;
|
||||||
@@ -354,26 +365,16 @@ public class PlayerInputControl {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blockierende Einmal-Animation (lie_down, sit_down, lie_up, sit_up …)
|
// Blockierende Einmal-Animation (lie_down, lie_up …)
|
||||||
if (blockingAnimActive) {
|
if (blockingAnimActive) {
|
||||||
blockingAnimRemaining -= tpf;
|
blockingAnimRemaining -= tpf;
|
||||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
float elapsed = blockingAnimTotal - blockingAnimRemaining;
|
|
||||||
applyMotionKeyframes(elapsed);
|
|
||||||
|
|
||||||
if (blockingAnimRemaining <= 0f) {
|
if (blockingAnimRemaining <= 0f) {
|
||||||
blockingAnimActive = false;
|
blockingAnimActive = false;
|
||||||
applyMotionKeyframes(blockingAnimTotal); // Endwert einrasten
|
|
||||||
currentMotionKfs = null;
|
|
||||||
preMotionTranslation = null;
|
|
||||||
preMotionRotation = null;
|
|
||||||
Runnable cb = blockingAnimCallback;
|
Runnable cb = blockingAnimCallback;
|
||||||
blockingAnimCallback = null;
|
blockingAnimCallback = null;
|
||||||
if (cb != null) cb.run();
|
if (cb != null) cb.run();
|
||||||
// Kein Ruhezustand nach der Animation → visuellen Versatz zurücksetzen
|
|
||||||
if (!lockedInPlace && visual != null) {
|
|
||||||
visual.setLocalTranslation(Vector3f.ZERO);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -489,59 +490,6 @@ public class PlayerInputControl {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Interpoliert die Motion-Keyframes der laufenden Aktion für den Zeitpunkt {@code time}
|
|
||||||
* und setzt Translation + Rotation des Visual-Nodes.
|
|
||||||
* TX/TZ werden im Charakter-lokalen Raum (Rotation zu Animations-Start) angewendet.
|
|
||||||
* Dabei gilt: positive TX = rechts vom Charakter, positives TZ = vor dem Charakter
|
|
||||||
* (= weg von der Bank), negatives TZ = hinter den Charakter (= zur Bank hin).
|
|
||||||
* TY ist Welt-Y. RX/RY/RZ sind additiv zur Startrotation.
|
|
||||||
*/
|
|
||||||
private void applyMotionKeyframes(float time) {
|
|
||||||
if (currentMotionKfs == null || currentMotionKfs.isEmpty()) return;
|
|
||||||
if (preMotionRotation == null || visual == null) return;
|
|
||||||
|
|
||||||
de.blight.game.animation.AnimKeyframe before = null, after = null;
|
|
||||||
for (de.blight.game.animation.AnimKeyframe kf : currentMotionKfs) {
|
|
||||||
if (kf.time <= time) { before = kf; }
|
|
||||||
else if (after == null) { after = kf; break; }
|
|
||||||
}
|
|
||||||
|
|
||||||
float tx, ty, tz, rx, ry, rz;
|
|
||||||
if (before == null && after == null) { return; }
|
|
||||||
else if (before == null) {
|
|
||||||
tx = after.tx; ty = after.ty; tz = after.tz;
|
|
||||||
rx = after.rx; ry = after.ry; rz = after.rz;
|
|
||||||
} else if (after == null) {
|
|
||||||
tx = before.tx; ty = before.ty; tz = before.tz;
|
|
||||||
rx = before.rx; ry = before.ry; rz = before.rz;
|
|
||||||
} else {
|
|
||||||
float t = Math.max(0f, Math.min(1f,
|
|
||||||
(time - before.time) / (after.time - before.time)));
|
|
||||||
tx = lerp(before.tx, after.tx, t);
|
|
||||||
ty = lerp(before.ty, after.ty, t);
|
|
||||||
tz = lerp(before.tz, after.tz, t);
|
|
||||||
rx = lerp(before.rx, after.rx, t);
|
|
||||||
ry = lerp(before.ry, after.ry, t);
|
|
||||||
rz = lerp(before.rz, after.rz, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TX/TZ im Charakter-lokalen Raum: preMotionRotation dreht den Offset in Welt-Raum.
|
|
||||||
// Konvention: local -Z = forward (lookAt-Konvention), also tz negativ = hinter Charakter.
|
|
||||||
Vector3f localXZ = preMotionRotation.mult(new Vector3f(tx, 0f, tz));
|
|
||||||
visual.setLocalTranslation(localXZ.x, ty, localXZ.z);
|
|
||||||
|
|
||||||
// Rotation: additiv zur Startrotation via SLERP der Euler-Offsets
|
|
||||||
Quaternion rotOffset = new Quaternion();
|
|
||||||
rotOffset.fromAngles(
|
|
||||||
rx * FastMath.DEG_TO_RAD,
|
|
||||||
ry * FastMath.DEG_TO_RAD,
|
|
||||||
rz * FastMath.DEG_TO_RAD);
|
|
||||||
visual.setLocalRotation(preMotionRotation.mult(rotOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
|
||||||
|
|
||||||
private boolean tryPlay(String clip) {
|
private boolean tryPlay(String clip) {
|
||||||
if (animComposer == null || !animLib.applyTo(clip, visual)) {
|
if (animComposer == null || !animLib.applyTo(clip, visual)) {
|
||||||
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
package de.blight.game.navigation;
|
||||||
|
|
||||||
|
import com.jme3.anim.AnimComposer;
|
||||||
|
import com.jme3.anim.SkinningControl;
|
||||||
|
import com.jme3.bullet.control.CharacterControl;
|
||||||
|
import com.jme3.math.Quaternion;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.scene.Spatial;
|
||||||
|
import de.blight.common.model.WorldPoint;
|
||||||
|
import de.blight.game.animation.AnimationAction;
|
||||||
|
import de.blight.game.animation.AnimationLibrary;
|
||||||
|
import de.blight.game.animation.RetargetingSystem;
|
||||||
|
import de.blight.game.state.TerrainChunkState;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Universelle Punkt-zu-Punkt-Navigation für beliebige Charaktere mit dem animset "human"
|
||||||
|
* (Spieler und NPCs gleichermaßen).
|
||||||
|
*
|
||||||
|
* <p>Verwendung:
|
||||||
|
* <pre>
|
||||||
|
* CharacterNavigator nav = new CharacterNavigator(physicsChar, visual, pathFinder, terrain);
|
||||||
|
* nav.setAnimationContext(animLib, "human", assetRoot);
|
||||||
|
*
|
||||||
|
* nav.navigateTo(ziel, Speed.RUN, this::onAngekommen, this::onAbbruch);
|
||||||
|
*
|
||||||
|
* // jeden Frame:
|
||||||
|
* nav.update(tpf);
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>Hindernisse werden vom PathFinder (Wegnetz + Raycasting) und dem SteeringHelper
|
||||||
|
* (Bug-Algorithmus) umgangen. Steile Terrain-Segmente werden durch seitliche Umwege
|
||||||
|
* ersetzt; ist kein Umweg begehbar, wird das Segment direkt passiert (Physik sorgt
|
||||||
|
* dann für Abbremsung).
|
||||||
|
*/
|
||||||
|
public class CharacterNavigator {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CharacterNavigator.class);
|
||||||
|
|
||||||
|
// ── Tuneable ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Ankunftsradius zum finalen Ziel (m). */
|
||||||
|
private static final float ARRIVE_RADIUS = 0.45f;
|
||||||
|
/** Radius zum Weiterspringen auf den nächsten Wegpunkt (m). */
|
||||||
|
private static final float WAYPOINT_RADIUS = 0.9f;
|
||||||
|
/** Rotationsgeschwindigkeit (rad/s). */
|
||||||
|
private static final float ROTATE_SPEED = 8f;
|
||||||
|
/** Geh-Geschwindigkeit (m/frame bei 60fps). */
|
||||||
|
public static final float WALK_SPEED = 0.035f;
|
||||||
|
/** Renn-Geschwindigkeit (m/frame bei 60fps). */
|
||||||
|
public static final float RUN_SPEED = 0.07f;
|
||||||
|
/** Minimale Bewegung (m/s) unter der ein Charakter als steckengeblieben gilt. */
|
||||||
|
private static final float STUCK_MIN_SPEED = 0.2f;
|
||||||
|
/** Zeit (s) mit zu langsamer Bewegung bis zum Abbruch. */
|
||||||
|
private static final float STUCK_TIMEOUT = 3.5f;
|
||||||
|
/** Maximale Steigung als tan(α) — 36° entspricht ≈ 0.73. */
|
||||||
|
private static final float MAX_SLOPE_TAN = (float) Math.tan(Math.toRadians(36));
|
||||||
|
/** Abtastabstand für Steigungsprüfung entlang eines Pfadsegments (m). */
|
||||||
|
private static final float SLOPE_STEP = 1.5f;
|
||||||
|
|
||||||
|
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public enum Speed { WALK, RUN }
|
||||||
|
|
||||||
|
private final CharacterControl physicsChar;
|
||||||
|
private final Spatial visual;
|
||||||
|
private final PathFinder pathFinder;
|
||||||
|
private final TerrainChunkState terrain;
|
||||||
|
|
||||||
|
// Animation (optional — ohne Context wird nur bewegt, keine Animation gespielt)
|
||||||
|
private AnimationLibrary animLib;
|
||||||
|
private String animSetName;
|
||||||
|
private Path assetRoot;
|
||||||
|
private AnimComposer animComposer;
|
||||||
|
private SkinningControl skinningControl;
|
||||||
|
private AnimationAction currentAnim = null;
|
||||||
|
|
||||||
|
// ── Laufzustand ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private boolean active = false;
|
||||||
|
private List<WorldPoint> path = new ArrayList<>();
|
||||||
|
private int pathStep = 0;
|
||||||
|
private Speed speed = Speed.WALK;
|
||||||
|
private Runnable onArrival = null;
|
||||||
|
private Runnable onFailed = null;
|
||||||
|
|
||||||
|
/** Für Stuck-Erkennung: Position beim letzten Frame. */
|
||||||
|
private Vector3f lastPos = null;
|
||||||
|
private float stuckTimer = 0f;
|
||||||
|
|
||||||
|
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public CharacterNavigator(CharacterControl physicsChar,
|
||||||
|
Spatial visual,
|
||||||
|
PathFinder pathFinder,
|
||||||
|
TerrainChunkState terrain) {
|
||||||
|
this.physicsChar = physicsChar;
|
||||||
|
this.visual = visual;
|
||||||
|
this.pathFinder = pathFinder;
|
||||||
|
this.terrain = terrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Animations-Kontext ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt den Animations-Kontext. Muss vor dem ersten {@link #navigateTo} aufgerufen
|
||||||
|
* werden wenn Animationen abgespielt werden sollen; optional für reine Bewegung.
|
||||||
|
*/
|
||||||
|
public void setAnimationContext(AnimationLibrary animLib, String animSetName, Path assetRoot) {
|
||||||
|
this.animLib = animLib;
|
||||||
|
this.animSetName = animSetName;
|
||||||
|
this.assetRoot = assetRoot;
|
||||||
|
this.animComposer = (visual != null) ? RetargetingSystem.findAnimComposer(visual) : null;
|
||||||
|
this.skinningControl = (visual != null) ? RetargetingSystem.findSkinningControl(visual) : null;
|
||||||
|
this.currentAnim = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet die Navigation zu {@code target}. Der Y-Wert des Ziels wird ignoriert –
|
||||||
|
* alle Wegpunkte werden auf die Terrain-Höhe gesampled.
|
||||||
|
*
|
||||||
|
* @param target Zielposition (Y wird durch Terrain ersetzt)
|
||||||
|
* @param speed {@link Speed#WALK} oder {@link Speed#RUN}
|
||||||
|
* @param onArrival Callback nach Ankunft (darf null sein)
|
||||||
|
* @param onFailed Callback bei Abbruch durch Steckenbleiben (darf null sein)
|
||||||
|
*/
|
||||||
|
public void navigateTo(Vector3f target, Speed speed,
|
||||||
|
Runnable onArrival, Runnable onFailed) {
|
||||||
|
this.speed = speed;
|
||||||
|
this.onArrival = onArrival;
|
||||||
|
this.onFailed = onFailed;
|
||||||
|
this.active = true;
|
||||||
|
this.stuckTimer = 0f;
|
||||||
|
this.lastPos = physicsChar.getPhysicsLocation().clone();
|
||||||
|
this.currentAnim = null;
|
||||||
|
|
||||||
|
Vector3f from3 = physicsChar.getPhysicsLocation();
|
||||||
|
WorldPoint from = new WorldPoint(from3.x, from3.y, from3.z);
|
||||||
|
WorldPoint to = new WorldPoint(target.x, target.y, target.z);
|
||||||
|
|
||||||
|
List<WorldPoint> raw = (pathFinder != null)
|
||||||
|
? pathFinder.findPath(from, to)
|
||||||
|
: List.of(to);
|
||||||
|
|
||||||
|
path = buildWalkablePath(raw);
|
||||||
|
pathStep = 0;
|
||||||
|
|
||||||
|
log.info("[Navigator] navigateTo ({:.1f},{:.1f}) speed={} waypoints={}",
|
||||||
|
target.x, target.z, speed, path.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wie {@link #navigateTo} ohne Fehler-Callback. */
|
||||||
|
public void navigateTo(Vector3f target, Speed speed, Runnable onArrival) {
|
||||||
|
navigateTo(target, speed, onArrival, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bricht die Navigation sofort ab, kein Callback wird ausgeführt. */
|
||||||
|
public void stop() {
|
||||||
|
if (!active) return;
|
||||||
|
active = false;
|
||||||
|
path.clear();
|
||||||
|
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
|
playAnim(AnimationAction.IDLE);
|
||||||
|
onArrival = null;
|
||||||
|
onFailed = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() { return active; }
|
||||||
|
|
||||||
|
// ── Update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Muss jeden Frame aufgerufen werden, solange Navigation aktiv ist.
|
||||||
|
* Bewegt den Charakter, dreht ihn zum nächsten Wegpunkt und spielt
|
||||||
|
* die passende Animation.
|
||||||
|
*/
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (!active || physicsChar == null) return;
|
||||||
|
|
||||||
|
Vector3f pos = physicsChar.getPhysicsLocation();
|
||||||
|
|
||||||
|
// ── Finales Ziel erreicht? ────────────────────────────────────────────
|
||||||
|
if (path.isEmpty()) { arrive(); return; }
|
||||||
|
WorldPoint goal = path.get(path.size() - 1);
|
||||||
|
float gdx = goal.x - pos.x, gdz = goal.z - pos.z;
|
||||||
|
if (gdx * gdx + gdz * gdz <= ARRIVE_RADIUS * ARRIVE_RADIUS) { arrive(); return; }
|
||||||
|
|
||||||
|
// ── Wegpunkte überspringen die bereits passiert sind ──────────────────
|
||||||
|
while (pathStep < path.size() - 1) {
|
||||||
|
WorldPoint wp = path.get(pathStep);
|
||||||
|
float dx = wp.x - pos.x, dz = wp.z - pos.z;
|
||||||
|
if (dx * dx + dz * dz > WAYPOINT_RADIUS * WAYPOINT_RADIUS) break;
|
||||||
|
pathStep++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Zum aktuellen Wegpunkt bewegen ────────────────────────────────────
|
||||||
|
WorldPoint target = path.get(pathStep);
|
||||||
|
float dx = target.x - pos.x, dz = target.z - pos.z;
|
||||||
|
float len = (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
if (len < 0.001f) { pathStep++; return; }
|
||||||
|
|
||||||
|
Vector3f dir = new Vector3f(dx / len, 0f, dz / len);
|
||||||
|
float moveSpeed = (speed == Speed.RUN) ? RUN_SPEED : WALK_SPEED;
|
||||||
|
physicsChar.setWalkDirection(dir.mult(moveSpeed));
|
||||||
|
rotateVisual(dir, tpf);
|
||||||
|
playAnim(speed == Speed.RUN ? AnimationAction.RUN : AnimationAction.WALK);
|
||||||
|
|
||||||
|
// ── Stuck-Erkennung ───────────────────────────────────────────────────
|
||||||
|
float movedPerSec = pos.distance(lastPos) / Math.max(tpf, 0.001f);
|
||||||
|
if (movedPerSec < STUCK_MIN_SPEED) {
|
||||||
|
stuckTimer += tpf;
|
||||||
|
if (stuckTimer > STUCK_TIMEOUT) {
|
||||||
|
log.warn("[Navigator] Stecken erkannt – Navigation abgebrochen.");
|
||||||
|
fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stuckTimer = 0f;
|
||||||
|
}
|
||||||
|
lastPos = pos.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Interna ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void arrive() {
|
||||||
|
active = false;
|
||||||
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
|
playAnim(AnimationAction.IDLE);
|
||||||
|
Runnable cb = onArrival;
|
||||||
|
onArrival = null;
|
||||||
|
if (cb != null) cb.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fail() {
|
||||||
|
active = false;
|
||||||
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
|
playAnim(AnimationAction.IDLE);
|
||||||
|
Runnable cb = onFailed;
|
||||||
|
onFailed = null;
|
||||||
|
if (cb != null) cb.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rotateVisual(Vector3f dir, float tpf) {
|
||||||
|
if (visual == null) return;
|
||||||
|
Quaternion target = new Quaternion();
|
||||||
|
target.lookAt(dir, Vector3f.UNIT_Y);
|
||||||
|
Quaternion cur = visual.getLocalRotation().clone();
|
||||||
|
cur.slerp(target, ROTATE_SPEED * tpf);
|
||||||
|
visual.setLocalRotation(cur);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playAnim(AnimationAction action) {
|
||||||
|
if (action == currentAnim) return;
|
||||||
|
if (animLib == null || animSetName == null || visual == null || animComposer == null) return;
|
||||||
|
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
||||||
|
if (clip == null) {
|
||||||
|
clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT);
|
||||||
|
}
|
||||||
|
if (clip == null) return;
|
||||||
|
if (!animLib.applyTo(clip, visual)) return;
|
||||||
|
com.jme3.anim.tween.action.Action act = animComposer.setCurrentAction(clip);
|
||||||
|
if (act == null) return;
|
||||||
|
if (skinningControl != null && !skinningControl.isEnabled()) {
|
||||||
|
skinningControl.setEnabled(true);
|
||||||
|
}
|
||||||
|
currentAnim = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pfad-Aufbereitung ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bereitet den rohen PathFinder-Pfad auf:
|
||||||
|
* <ol>
|
||||||
|
* <li>Y-Koordinaten aller Wegpunkte werden auf Terrain-Höhe gesampled.</li>
|
||||||
|
* <li>Zu steile Segmente (> {@value #MAX_SLOPE_TAN} tan) werden durch
|
||||||
|
* seitliche Umwegpunkte ersetzt; ist kein begehbarer Umweg vorhanden,
|
||||||
|
* bleibt das Segment im Pfad (Physik bremst den Charakter natürlich ab).</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
private List<WorldPoint> buildWalkablePath(List<WorldPoint> raw) {
|
||||||
|
List<WorldPoint> snapped = new ArrayList<>(raw.size());
|
||||||
|
for (WorldPoint wp : raw) {
|
||||||
|
float y = (terrain != null) ? terrain.getHeightAt(wp.x, wp.z) : wp.y;
|
||||||
|
snapped.add(new WorldPoint(wp.x, y, wp.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terrain == null || snapped.size() < 2) return snapped;
|
||||||
|
|
||||||
|
List<WorldPoint> result = new ArrayList<>();
|
||||||
|
result.add(snapped.get(0));
|
||||||
|
for (int i = 0; i < snapped.size() - 1; i++) {
|
||||||
|
WorldPoint a = snapped.get(i);
|
||||||
|
WorldPoint b = snapped.get(i + 1);
|
||||||
|
if (!isSegmentWalkable(a.x, a.z, b.x, b.z)) {
|
||||||
|
WorldPoint detour = findFlatDetour(a, b);
|
||||||
|
if (detour != null) {
|
||||||
|
result.add(detour);
|
||||||
|
log.debug("[Navigator] Steiles Segment, Umweg: ({:.1f},{:.1f})",
|
||||||
|
detour.x, detour.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.add(b);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ob ein Pfadsegment von (x1,z1) nach (x2,z2) begehbar ist,
|
||||||
|
* indem die Terrain-Höhe alle {@value #SLOPE_STEP}m abgetastet wird.
|
||||||
|
*/
|
||||||
|
private boolean isSegmentWalkable(float x1, float z1, float x2, float z2) {
|
||||||
|
if (terrain == null) return true;
|
||||||
|
float dx = x2 - x1, dz = z2 - z1;
|
||||||
|
float dist = (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
if (dist < 0.1f) return true;
|
||||||
|
float nx = dx / dist, nz = dz / dist;
|
||||||
|
float prevH = terrain.getHeightAt(x1, z1);
|
||||||
|
for (float t = SLOPE_STEP; t < dist; t += SLOPE_STEP) {
|
||||||
|
float h = terrain.getHeightAt(x1 + nx * t, z1 + nz * t);
|
||||||
|
float dh = Math.abs(h - prevH);
|
||||||
|
if (dh / SLOPE_STEP > MAX_SLOPE_TAN) return false;
|
||||||
|
prevH = h;
|
||||||
|
}
|
||||||
|
// letztes Teilstück bis b
|
||||||
|
float h = terrain.getHeightAt(x2, z2);
|
||||||
|
float remaining = dist - (float) Math.floor(dist / SLOPE_STEP) * SLOPE_STEP;
|
||||||
|
if (remaining > 0.01f && Math.abs(h - prevH) / remaining > MAX_SLOPE_TAN) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sucht einen begehbaren Umweg um ein zu steiles Segment von {@code a} nach {@code b}.
|
||||||
|
* Versucht senkrechte Versätze (links und rechts) in 3m-Schritten bis 12m.
|
||||||
|
*
|
||||||
|
* @return Umweg-Wegpunkt oder {@code null} wenn keiner gefunden.
|
||||||
|
*/
|
||||||
|
private WorldPoint findFlatDetour(WorldPoint a, WorldPoint b) {
|
||||||
|
float dx = b.x - a.x, dz = b.z - a.z;
|
||||||
|
float len = (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
if (len < 0.001f) return null;
|
||||||
|
float mx = (a.x + b.x) * 0.5f, mz = (a.z + b.z) * 0.5f;
|
||||||
|
// Senkrechter Einheitsvektor
|
||||||
|
float perpX = -dz / len, perpZ = dx / len;
|
||||||
|
for (float side : new float[]{1f, -1f}) {
|
||||||
|
for (float offset = 3f; offset <= 12f; offset += 3f) {
|
||||||
|
float cx = mx + perpX * side * offset;
|
||||||
|
float cz = mz + perpZ * side * offset;
|
||||||
|
if (isSegmentWalkable(a.x, a.z, cx, cz)
|
||||||
|
&& isSegmentWalkable(cx, cz, b.x, b.z)) {
|
||||||
|
float cy = terrain.getHeightAt(cx, cz);
|
||||||
|
return new WorldPoint(cx, cy, cz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -173,6 +173,15 @@ public class WorldScene extends BaseAppState {
|
|||||||
playerInput.setPhysicsCharacter(physicsChar);
|
playerInput.setPhysicsCharacter(physicsChar);
|
||||||
playerInput.setVisual(characterVisual != null ? characterVisual : character);
|
playerInput.setVisual(characterVisual != null ? characterVisual : character);
|
||||||
|
|
||||||
|
// Navigation: PathFinder + Terrain bereitstellen (Navigator wird in setAnimationContext erstellt)
|
||||||
|
try {
|
||||||
|
de.blight.game.navigation.PathFinder pf = de.blight.game.navigation.PathFinder.load();
|
||||||
|
pf.setObstacleRoot(app.getRootNode());
|
||||||
|
playerInput.setNavigationSources(pf, terrainChunkState);
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
log.warn("[WorldScene] PathFinder nicht ladbar – Navigation deaktiviert: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager());
|
thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager());
|
||||||
thirdPersonCam.setTarget(character);
|
thirdPersonCam.setTarget(character);
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,10 @@ import de.blight.common.model.BedIO;
|
|||||||
import de.blight.common.model.Bench;
|
import de.blight.common.model.Bench;
|
||||||
import de.blight.common.model.BenchIO;
|
import de.blight.common.model.BenchIO;
|
||||||
import de.blight.common.model.InteractableType;
|
import de.blight.common.model.InteractableType;
|
||||||
import de.blight.common.model.WorldPoint;
|
|
||||||
import de.blight.game.animation.AnimationAction;
|
import de.blight.game.animation.AnimationAction;
|
||||||
import de.blight.game.config.KeyBindings;
|
import de.blight.game.config.KeyBindings;
|
||||||
import de.blight.game.control.PlayerInputControl;
|
import de.blight.game.control.PlayerInputControl;
|
||||||
import de.blight.game.navigation.PathFinder;
|
import de.blight.game.navigation.CharacterNavigator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -31,24 +30,31 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* Steuert die Interaktion des Hauptcharakters mit Betten und Bänken.
|
* Steuert die Interaktion des Hauptcharakters mit Betten und Bänken.
|
||||||
*
|
*
|
||||||
* <h2>Ablauf Bett</h2>
|
* <h2>Ablauf Bank</h2>
|
||||||
* <pre>
|
* <pre>
|
||||||
* Interact-Taste (E)
|
* E (< 5m)
|
||||||
* → WALKING : walk zu Punkt neben dem Bett (via PathFinder)
|
* → WALKING : CharacterNavigator → Sitzpunkt (umlaufen wenn nötig)
|
||||||
* → LIE_ANIM : lie_down Animation (Charakter gleitet in Liegeposition)
|
* → PLAY_ANIM : Drehen (Rücken zur Bank) → sit_down_bench
|
||||||
* → RESTING : Charakter liegt; alle Eingaben gesperrt
|
* → RESTING : sitting-Loop; Eingaben gesperrt
|
||||||
* → GET_UP : Rechtsklick startet lie_up Animation
|
* → GET_UP : Rechtsklick → get_up_sitting → IDLE
|
||||||
* → WALKING_BACK : Charakter kehrt zur Ausgangsposition zurück
|
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* Bank läuft analog mit sit_down / sit_up und einem 0,5m-Pfeil.
|
* <h2>Ablauf Bett</h2>
|
||||||
|
* <pre>
|
||||||
|
* E (< 6m)
|
||||||
|
* → WALKING : CharacterNavigator → Anfahrtspunkt
|
||||||
|
* → PLAY_ANIM : Drehen → lie_down
|
||||||
|
* → RESTING : lying-Loop; Eingaben gesperrt
|
||||||
|
* → GET_UP : Rechtsklick → lie_up
|
||||||
|
* → WALKING_BACK : Rückkehr zur Ausgangsposition
|
||||||
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public class WorldInteractableState extends BaseAppState {
|
public class WorldInteractableState extends BaseAppState {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(WorldInteractableState.class);
|
private static final Logger log = LoggerFactory.getLogger(WorldInteractableState.class);
|
||||||
|
|
||||||
private static final float INTERACT_RANGE = 6f;
|
private static final float BENCH_RANGE = 5f;
|
||||||
private static final float REACH_DIST = 0.35f;
|
private static final float BED_RANGE = 6f;
|
||||||
private static final float WALK_TIMEOUT = 12f;
|
private static final float WALK_TIMEOUT = 12f;
|
||||||
|
|
||||||
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
||||||
@@ -59,9 +65,8 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
|
|
||||||
private InputManager inputManager;
|
private InputManager inputManager;
|
||||||
|
|
||||||
// ── Interactable-Daten aus der Karte ─────────────────────────────────────
|
// ── Interactable-Einträge ────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Beschreibt ein platziertes Interactable-Objekt. */
|
|
||||||
private record InteractableEntry(
|
private record InteractableEntry(
|
||||||
float worldX, float worldY, float worldZ,
|
float worldX, float worldY, float worldZ,
|
||||||
InteractableType type,
|
InteractableType type,
|
||||||
@@ -70,33 +75,22 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
|
|
||||||
private final List<InteractableEntry> entries = new ArrayList<>();
|
private final List<InteractableEntry> entries = new ArrayList<>();
|
||||||
|
|
||||||
// ── Zustandsmaschine ──────────────────────────────────────────────────────
|
// ── Zustandsmaschine ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
private enum Phase {
|
private enum Phase {
|
||||||
IDLE,
|
IDLE,
|
||||||
WALKING, // Annäherung an Interactable
|
WALKING,
|
||||||
PLAY_ANIM, // Einlege-/Sitz-Animation läuft
|
PLAY_ANIM,
|
||||||
RESTING, // Charakter liegt/sitzt; nur Rechtsklick erlaubt
|
RESTING,
|
||||||
GET_UP_ANIM, // Aufsteh-Animation läuft
|
GET_UP_ANIM,
|
||||||
WALKING_BACK // Rückkehr zur Ausgangsposition
|
WALKING_BACK
|
||||||
}
|
}
|
||||||
|
|
||||||
private Phase phase = Phase.IDLE;
|
private Phase phase = Phase.IDLE;
|
||||||
private int targetIdx = -1;
|
private int targetIdx = -1;
|
||||||
private float walkTimer = 0f;
|
private float walkTimer = 0f;
|
||||||
|
|
||||||
/** Ziel-Weltpunkt, zu dem der Charakter laufen soll (neben dem Objekt). */
|
|
||||||
private Vector3f approachTarget = null;
|
|
||||||
/** Position des Charakters vor der Interaktion (für Rückkehr). */
|
|
||||||
private Vector3f originPos = null;
|
private Vector3f originPos = null;
|
||||||
/** Aktive Pfadliste für Annäherung / Rückkehr. */
|
|
||||||
private List<WorldPoint> currentPath = new ArrayList<>();
|
|
||||||
private int pathStep = 0;
|
|
||||||
|
|
||||||
private PathFinder pathFinder = null;
|
|
||||||
|
|
||||||
/** Sitz-/Liegeposition des aktuell angesteuerten Interactables (für Bypass-Berechnung). */
|
|
||||||
private Vector3f interactableSitPt = null;
|
|
||||||
|
|
||||||
// ── Eingabe-Mapping ───────────────────────────────────────────────────────
|
// ── Eingabe-Mapping ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -116,10 +110,6 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
@Override
|
@Override
|
||||||
protected void initialize(Application app) {
|
protected void initialize(Application app) {
|
||||||
this.inputManager = app.getInputManager();
|
this.inputManager = app.getInputManager();
|
||||||
|
|
||||||
try { pathFinder = PathFinder.load(); }
|
|
||||||
catch (IOException e) { log.warn("[WorldInteractable] Wegnetz nicht ladbar: {}", e.getMessage()); }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<PlacedModel> models = PlacedModelIO.load();
|
List<PlacedModel> models = PlacedModelIO.load();
|
||||||
for (PlacedModel m : models) {
|
for (PlacedModel m : models) {
|
||||||
@@ -154,95 +144,14 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
|
|
||||||
@Override protected void cleanup(Application app) {}
|
@Override protected void cleanup(Application app) {}
|
||||||
|
|
||||||
// ── Update ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void update(float tpf) {
|
public void update(float tpf) {
|
||||||
switch (phase) {
|
// Navigation läuft im CharacterNavigator (PlayerInputControl.update).
|
||||||
case WALKING -> updateWalking(tpf);
|
// Nur walkTimer für Bett-Rückkehr brauchen wir noch.
|
||||||
case WALKING_BACK -> updateWalkingBack(tpf);
|
if (phase == Phase.WALKING_BACK) walkTimer += tpf;
|
||||||
default -> {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateWalking(float tpf) {
|
// ── Listener ─────────────────────────────────────────────────────────────
|
||||||
if (playerInput.isPaused()) { cancelInteraction(); return; }
|
|
||||||
|
|
||||||
walkTimer += tpf;
|
|
||||||
if (walkTimer > WALK_TIMEOUT) {
|
|
||||||
log.info("[WorldInteractable] Annäherung abgebrochen (Timeout).");
|
|
||||||
cancelInteraction();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (targetIdx < 0 || targetIdx >= entries.size()) { cancelInteraction(); return; }
|
|
||||||
|
|
||||||
Vector3f pos = physicsChar.getPhysicsLocation();
|
|
||||||
Vector3f dest = approachTarget;
|
|
||||||
|
|
||||||
float dx = dest.x - pos.x;
|
|
||||||
float dz = dest.z - pos.z;
|
|
||||||
float distSq = dx * dx + dz * dz;
|
|
||||||
|
|
||||||
if (distSq <= REACH_DIST * REACH_DIST) {
|
|
||||||
startRestAnim();
|
|
||||||
} else {
|
|
||||||
// Nächsten Wegpunkt aus dem Pfad verwenden
|
|
||||||
advancePath(pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void advancePath(Vector3f pos) {
|
|
||||||
if (currentPath.isEmpty()) {
|
|
||||||
// Direkt zum Ziel
|
|
||||||
float dx = approachTarget.x - pos.x;
|
|
||||||
float dz = approachTarget.z - pos.z;
|
|
||||||
float len = (float) Math.sqrt(dx * dx + dz * dz);
|
|
||||||
if (len > 0.001f) playerInput.setAutopilotDirection(new Vector3f(dx / len, 0f, dz / len));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Zum aktuellen Wegpunkt steuern; wenn nah genug → weiter
|
|
||||||
while (pathStep < currentPath.size()) {
|
|
||||||
WorldPoint wp = currentPath.get(pathStep);
|
|
||||||
float dx = wp.x - pos.x;
|
|
||||||
float dz = wp.z - pos.z;
|
|
||||||
float d2 = dx * dx + dz * dz;
|
|
||||||
if (d2 < 0.8f * 0.8f) { pathStep++; continue; }
|
|
||||||
float len = (float) Math.sqrt(d2);
|
|
||||||
playerInput.setAutopilotDirection(new Vector3f(dx / len, 0f, dz / len));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Pfad fertig → direkt zum Annäherungs-Ziel
|
|
||||||
float dx = approachTarget.x - pos.x;
|
|
||||||
float dz = approachTarget.z - pos.z;
|
|
||||||
float len = (float) Math.sqrt(dx * dx + dz * dz);
|
|
||||||
if (len > 0.001f) playerInput.setAutopilotDirection(new Vector3f(dx / len, 0f, dz / len));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateWalkingBack(float tpf) {
|
|
||||||
if (originPos == null) { phase = Phase.IDLE; return; }
|
|
||||||
|
|
||||||
walkTimer += tpf;
|
|
||||||
if (walkTimer > WALK_TIMEOUT) {
|
|
||||||
playerInput.setAutopilotDirection(null);
|
|
||||||
phase = Phase.IDLE;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3f pos = physicsChar.getPhysicsLocation();
|
|
||||||
float dx = originPos.x - pos.x;
|
|
||||||
float dz = originPos.z - pos.z;
|
|
||||||
float distSq = dx * dx + dz * dz;
|
|
||||||
|
|
||||||
if (distSq <= REACH_DIST * REACH_DIST) {
|
|
||||||
playerInput.setAutopilotDirection(null);
|
|
||||||
phase = Phase.IDLE;
|
|
||||||
} else {
|
|
||||||
float len = (float) Math.sqrt(distSq);
|
|
||||||
playerInput.setAutopilotDirection(new Vector3f(dx / len, 0f, dz / len));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Listener ──────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private final ActionListener interactListener = (name, isPressed, tpf) -> {
|
private final ActionListener interactListener = (name, isPressed, tpf) -> {
|
||||||
if (!isPressed || phase != Phase.IDLE) return;
|
if (!isPressed || phase != Phase.IDLE) return;
|
||||||
@@ -255,215 +164,191 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
startGetUp();
|
startGetUp();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Logik ─────────────────────────────────────────────────────────────────
|
// ── Suche nächstes Interactable ───────────────────────────────────────────
|
||||||
|
|
||||||
private int findNearestInRange() {
|
private int findNearestInRange() {
|
||||||
Vector3f pos = physicsChar.getPhysicsLocation();
|
Vector3f pos = physicsChar.getPhysicsLocation();
|
||||||
int bestIdx = -1;
|
int bestIdx = -1;
|
||||||
float bestDist = INTERACT_RANGE;
|
float bestDist = Float.MAX_VALUE;
|
||||||
for (int i = 0; i < entries.size(); i++) {
|
for (int i = 0; i < entries.size(); i++) {
|
||||||
InteractableEntry e = entries.get(i);
|
InteractableEntry e = entries.get(i);
|
||||||
float dx = e.worldX() - pos.x;
|
float dx = e.worldX() - pos.x;
|
||||||
float dz = e.worldZ() - pos.z;
|
float dz = e.worldZ() - pos.z;
|
||||||
float d = (float) Math.sqrt(dx * dx + dz * dz);
|
float d = (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
if (d < bestDist) { bestDist = d; bestIdx = i; }
|
float range = (e.type() == InteractableType.BENCH) ? BENCH_RANGE : BED_RANGE;
|
||||||
|
if (d < range && d < bestDist) { bestDist = d; bestIdx = i; }
|
||||||
}
|
}
|
||||||
return bestIdx;
|
return bestIdx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Annäherung ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void startApproach(int idx) {
|
private void startApproach(int idx) {
|
||||||
targetIdx = idx;
|
targetIdx = idx;
|
||||||
walkTimer = 0f;
|
walkTimer = 0f;
|
||||||
originPos = physicsChar.getPhysicsLocation().clone();
|
originPos = physicsChar.getPhysicsLocation().clone();
|
||||||
|
|
||||||
InteractableEntry entry = entries.get(idx);
|
InteractableEntry entry = entries.get(idx);
|
||||||
|
|
||||||
// Kollision des Zielobjekts deaktivieren, damit der Charakter hindurchgehen kann
|
|
||||||
setTargetPhysicsEnabled(entry, false);
|
setTargetPhysicsEnabled(entry, false);
|
||||||
|
|
||||||
approachTarget = computeApproachTarget(entry);
|
Vector3f target = computeApproachTarget(entry);
|
||||||
|
if (target == null) {
|
||||||
// Pfad berechnen (PathFinder falls vorhanden)
|
setTargetPhysicsEnabled(entry, true);
|
||||||
WorldPoint from = new WorldPoint(originPos.x, originPos.y, originPos.z);
|
return;
|
||||||
WorldPoint to = new WorldPoint(approachTarget.x, approachTarget.y, approachTarget.z);
|
|
||||||
if (pathFinder != null) {
|
|
||||||
currentPath = new ArrayList<>(pathFinder.findPath(from, to));
|
|
||||||
} else {
|
|
||||||
currentPath = new ArrayList<>(List.of(to));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bypass-Punkt einfügen wenn Sitzpunkt auf dem Weg liegt
|
|
||||||
insertBypassIfNeeded(from);
|
|
||||||
pathStep = 0;
|
|
||||||
|
|
||||||
phase = Phase.WALKING;
|
phase = Phase.WALKING;
|
||||||
log.info("[WorldInteractable] Annäherung an {} [{}]", entry.type(), entry.interactableId());
|
log.info("[WorldInteractable] Annäherung {} [{}]", entry.type(), entry.interactableId());
|
||||||
|
|
||||||
|
playerInput.navigateTo(target, CharacterNavigator.Speed.WALK,
|
||||||
|
this::onApproachArrived,
|
||||||
|
this::cancelInteraction);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Berechnet den Punkt, zu dem der Charakter läuft.
|
* Berechnet das Ziel für den Navigator:
|
||||||
* Bank: direkt zum Sitzpunkt (Pfeilspitze).
|
* – Bank: exakter Sitzpunkt (sitzX/sitzZ) — Charakter steht danach direkt am Sitzpunkt
|
||||||
* Bett: 1m in Pfeilrichtung vor dem Liegepunkt (Anfahrt von vorne).
|
* – Bett: 1m vor dem Liegepunkt in Blickrichtung (Anfahrt von vorne)
|
||||||
*/
|
*/
|
||||||
private Vector3f computeApproachTarget(InteractableEntry entry) {
|
private Vector3f computeApproachTarget(InteractableEntry entry) {
|
||||||
if (entry.type() == InteractableType.BED) {
|
if (entry.type() == InteractableType.BENCH) {
|
||||||
|
Bench bench = BenchIO.load(entry.interactableId()).orElse(null);
|
||||||
|
if (bench == null || !bench.isSitzSet()) {
|
||||||
|
log.warn("[WorldInteractable] Bank {} hat keinen Sitzpunkt.", entry.interactableId());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Vector3f(bench.getSitzX(), bench.getSitzY(), bench.getSitzZ());
|
||||||
|
} else {
|
||||||
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
||||||
if (bed != null && bed.isLiegeSet()) {
|
if (bed == null || !bed.isLiegeSet()) {
|
||||||
|
log.warn("[WorldInteractable] Bett {} hat keinen Liegepunkt.", entry.interactableId());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
float rotY = bed.getLiegeRotY();
|
float rotY = bed.getLiegeRotY();
|
||||||
interactableSitPt = new Vector3f(bed.getLiegeX(), bed.getLiegeY(), bed.getLiegeZ());
|
|
||||||
// Bett: 1m in Pfeilrichtung vor dem Liegepunkt anfahren
|
|
||||||
return new Vector3f(
|
return new Vector3f(
|
||||||
bed.getLiegeX() + (float) Math.cos(rotY),
|
bed.getLiegeX() + (float) Math.cos(rotY),
|
||||||
bed.getLiegeY(),
|
bed.getLiegeY(),
|
||||||
bed.getLiegeZ() + (float) Math.sin(rotY));
|
bed.getLiegeZ() + (float) Math.sin(rotY));
|
||||||
}
|
}
|
||||||
} else if (entry.type() == InteractableType.BENCH) {
|
|
||||||
Bench bench = BenchIO.load(entry.interactableId()).orElse(null);
|
|
||||||
if (bench != null && bench.isSitzSet()) {
|
|
||||||
interactableSitPt = new Vector3f(bench.getSitzX(), bench.getSitzY(), bench.getSitzZ());
|
|
||||||
// Bank: direkt zum Sitzpunkt (Pfeilspitze) laufen
|
|
||||||
return new Vector3f(bench.getSitzX(), bench.getSitzY(), bench.getSitzZ());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Fallback: 1m östlich des Objekts
|
// ── Am Ziel angekommen → drehen ────────────────────────────────────────────
|
||||||
interactableSitPt = null;
|
|
||||||
return new Vector3f(entry.worldX() + 1f, entry.worldY(), entry.worldZ());
|
private void onApproachArrived() {
|
||||||
|
phase = Phase.PLAY_ANIM;
|
||||||
|
InteractableEntry entry = entries.get(targetIdx);
|
||||||
|
float rotY = getSitFacingRotY(entry);
|
||||||
|
// Charakter dreht sich so dass der Rücken zur Bank / zum Bett zeigt
|
||||||
|
Vector3f sitDir = new Vector3f((float) Math.cos(rotY), 0f, (float) Math.sin(rotY));
|
||||||
|
playerInput.requestTurn(sitDir, 0.35f, () -> startSitAnim(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft ob der direkte Weg von {@code from} zum approachTarget durch den
|
* Gibt die Blickrichtung (radiant) des Charakters während des Sitzens / Liegens zurück.
|
||||||
* Sitzpunkt führt (nur relevant wenn Annährungspunkt ≠ Sitzpunkt, d.h. Bett).
|
* Bank: sitzRotY (Pfeilspitze = Charakter schaut in diese Richtung → Rücken zur Bank).
|
||||||
* Falls ja, wird ein Bypass-Punkt senkrecht eingefügt.
|
* Bett: liegeRotY.
|
||||||
*/
|
*/
|
||||||
private void insertBypassIfNeeded(WorldPoint from) {
|
private float getSitFacingRotY(InteractableEntry entry) {
|
||||||
if (interactableSitPt == null || approachTarget == null) return;
|
if (entry.type() == InteractableType.BENCH) {
|
||||||
|
Bench bench = BenchIO.load(entry.interactableId()).orElse(null);
|
||||||
float sx = interactableSitPt.x, sz = interactableSitPt.z;
|
return bench != null ? bench.getSitzRotY() : 0f;
|
||||||
float tx = approachTarget.x, tz = approachTarget.z;
|
} else {
|
||||||
|
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
||||||
// Wenn Annährungsziel = Sitzpunkt, ist kein Bypass nötig
|
return bed != null ? bed.getLiegeRotY() : 0f;
|
||||||
float aDx = tx - sx, aDz = tz - sz;
|
}
|
||||||
if (aDx * aDx + aDz * aDz < 0.1f) return;
|
|
||||||
|
|
||||||
float fx = from.x, fz = from.z;
|
|
||||||
|
|
||||||
// Projektion des Sitzpunkts auf die direkte Linie from→approachTarget
|
|
||||||
float dx = tx - fx, dz = tz - fz;
|
|
||||||
float lenSq = dx * dx + dz * dz;
|
|
||||||
if (lenSq < 0.001f) return;
|
|
||||||
|
|
||||||
float t = ((sx - fx) * dx + (sz - fz) * dz) / lenSq;
|
|
||||||
if (t < 0.05f || t > 0.95f) return;
|
|
||||||
|
|
||||||
float closestX = fx + t * dx;
|
|
||||||
float closestZ = fz + t * dz;
|
|
||||||
float distToPath = (float) Math.sqrt((sx - closestX) * (sx - closestX)
|
|
||||||
+ (sz - closestZ) * (sz - closestZ));
|
|
||||||
if (distToPath > 1.2f) return;
|
|
||||||
|
|
||||||
float faceX = sx - tx;
|
|
||||||
float faceZ = sz - tz;
|
|
||||||
float faceLen = (float) Math.sqrt(faceX * faceX + faceZ * faceZ);
|
|
||||||
if (faceLen > 0.001f) { faceX /= faceLen; faceZ /= faceLen; }
|
|
||||||
|
|
||||||
float perpX = -faceZ;
|
|
||||||
float perpZ = faceX;
|
|
||||||
|
|
||||||
float dot = (fx - sx) * perpX + (fz - sz) * perpZ;
|
|
||||||
float sign = dot >= 0f ? 1f : -1f;
|
|
||||||
|
|
||||||
WorldPoint bypass = new WorldPoint(
|
|
||||||
sx + perpX * sign * 2.5f,
|
|
||||||
from.y,
|
|
||||||
sz + perpZ * sign * 2.5f);
|
|
||||||
|
|
||||||
currentPath.add(currentPath.size() - 1, bypass);
|
|
||||||
log.info("[WorldInteractable] Bypass-Punkt eingefügt: ({}, {})", bypass.x, bypass.z);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startRestAnim() {
|
// ── Sitz-/Liegeanimation ──────────────────────────────────────────────────
|
||||||
playerInput.setAutopilotDirection(null);
|
|
||||||
phase = Phase.PLAY_ANIM;
|
|
||||||
|
|
||||||
InteractableEntry entry = entries.get(targetIdx);
|
|
||||||
float rotY = getRestRotY(entry);
|
|
||||||
|
|
||||||
// Zuerst Rücken zur Bank drehen, dann Sitz-/Liegeanimation
|
|
||||||
Vector3f sitDir = new Vector3f((float) Math.cos(rotY), 0f, (float) Math.sin(rotY));
|
|
||||||
playerInput.requestTurn(sitDir, 0.4f, () -> startSitAnim(entry));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startSitAnim(InteractableEntry entry) {
|
private void startSitAnim(InteractableEntry entry) {
|
||||||
boolean isBed = entry.type() == InteractableType.BED;
|
AnimationAction downAction = isBench(entry) ? AnimationAction.SIT_DOWN : AnimationAction.LIE_DOWN;
|
||||||
AnimationAction action = isBed ? AnimationAction.LIE_DOWN : AnimationAction.SIT_DOWN;
|
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
|
||||||
AnimationAction idleAction = isBed ? AnimationAction.LYING : AnimationAction.SITTING;
|
|
||||||
|
|
||||||
// duration=0 → PlayerInputControl ermittelt die echte Clip-Länge automatisch
|
playerInput.requestAnimation(downAction, 0f, () -> {
|
||||||
playerInput.requestAnimation(action, 0f, () -> {
|
snapToSitPos(entry);
|
||||||
if (isBed) teleportToRestPos(entry);
|
|
||||||
playerInput.lockInPlace();
|
playerInput.lockInPlace();
|
||||||
playerInput.playLockedAnimation(idleAction);
|
playerInput.playLockedAnimation(idleAction);
|
||||||
phase = Phase.RESTING;
|
phase = Phase.RESTING;
|
||||||
log.info("[WorldInteractable] Ruhezustand aktiv: {}", entry.type());
|
log.info("[WorldInteractable] Ruhezustand: {}", entry.type());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Snapped die Physik-Kapsel auf den exakten Sitz-/Liegepunkt. */
|
||||||
|
private void snapToSitPos(InteractableEntry entry) {
|
||||||
|
if (physicsChar == null) return;
|
||||||
|
if (isBench(entry)) {
|
||||||
|
Bench bench = BenchIO.load(entry.interactableId()).orElse(null);
|
||||||
|
if (bench != null && bench.isSitzSet()) {
|
||||||
|
physicsChar.setPhysicsLocation(
|
||||||
|
new Vector3f(bench.getSitzX(), bench.getSitzY(), bench.getSitzZ()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
||||||
|
if (bed != null && bed.isLiegeSet()) {
|
||||||
|
physicsChar.setPhysicsLocation(
|
||||||
|
new Vector3f(bed.getLiegeX(), bed.getLiegeY(), bed.getLiegeZ()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Aufstehen ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void startGetUp() {
|
private void startGetUp() {
|
||||||
if (targetIdx < 0 || targetIdx >= entries.size()) { phase = Phase.IDLE; return; }
|
if (targetIdx < 0 || targetIdx >= entries.size()) { phase = Phase.IDLE; return; }
|
||||||
|
|
||||||
InteractableEntry entry = entries.get(targetIdx);
|
InteractableEntry entry = entries.get(targetIdx);
|
||||||
boolean isBed = entry.type() == InteractableType.BED;
|
|
||||||
AnimationAction action = isBed ? AnimationAction.LIE_UP : AnimationAction.SIT_UP;
|
|
||||||
|
|
||||||
playerInput.unlockFromPlace();
|
playerInput.unlockFromPlace();
|
||||||
phase = Phase.GET_UP_ANIM;
|
phase = Phase.GET_UP_ANIM;
|
||||||
|
|
||||||
// Sink-Wert für SIT_UP/LIE_UP kommt ebenfalls aus AnimSet-Konfiguration
|
AnimationAction upAction = isBench(entry) ? AnimationAction.SIT_UP : AnimationAction.LIE_UP;
|
||||||
playerInput.requestAnimation(action, 0f, () -> {
|
|
||||||
// Kollision des Objekts nach dem Aufstehen wieder aktivieren
|
playerInput.requestAnimation(upAction, 0f, () -> {
|
||||||
if (targetIdx >= 0 && targetIdx < entries.size()) {
|
setTargetPhysicsEnabled(entry, true);
|
||||||
setTargetPhysicsEnabled(entries.get(targetIdx), true);
|
|
||||||
}
|
if (isBench(entry)) {
|
||||||
|
// Bank: Charakter bleibt stehen, Spieler übernimmt wieder
|
||||||
|
phase = Phase.IDLE;
|
||||||
|
targetIdx = -1;
|
||||||
|
log.info("[WorldInteractable] Bank verlassen.");
|
||||||
|
} else {
|
||||||
|
// Bett: zur Ausgangsposition zurücklaufen
|
||||||
phase = Phase.WALKING_BACK;
|
phase = Phase.WALKING_BACK;
|
||||||
walkTimer = 0f;
|
walkTimer = 0f;
|
||||||
log.info("[WorldInteractable] Rückkehr zur Ausgangsposition.");
|
log.info("[WorldInteractable] Rückkehr zur Ausgangsposition.");
|
||||||
|
if (originPos != null) {
|
||||||
|
playerInput.navigateTo(originPos, CharacterNavigator.Speed.WALK,
|
||||||
|
() -> { phase = Phase.IDLE; targetIdx = -1; },
|
||||||
|
() -> { phase = Phase.IDLE; targetIdx = -1; });
|
||||||
|
} else {
|
||||||
|
phase = Phase.IDLE;
|
||||||
|
targetIdx = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void teleportToRestPos(InteractableEntry entry) {
|
// ── Abbruch ───────────────────────────────────────────────────────────────
|
||||||
if (physicsChar == null || entry.type() != InteractableType.BED) return;
|
|
||||||
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
|
||||||
if (bed != null && bed.isLiegeSet()) {
|
|
||||||
physicsChar.setPhysicsLocation(new Vector3f(bed.getLiegeX(), bed.getLiegeY(), bed.getLiegeZ()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float getRestRotY(InteractableEntry entry) {
|
|
||||||
if (entry.type() == InteractableType.BED) {
|
|
||||||
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
|
||||||
if (bed != null) return bed.getLiegeRotY();
|
|
||||||
} else if (entry.type() == InteractableType.BENCH) {
|
|
||||||
Bench bench = BenchIO.load(entry.interactableId()).orElse(null);
|
|
||||||
if (bench != null) return bench.getSitzRotY();
|
|
||||||
}
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setTargetPhysicsEnabled(InteractableEntry entry, boolean enabled) {
|
|
||||||
WorldObjectsState wos = getApplication().getStateManager().getState(WorldObjectsState.class);
|
|
||||||
if (wos != null) wos.setInteractablePhysicsEnabled(entry.interactableId(), enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cancelInteraction() {
|
private void cancelInteraction() {
|
||||||
playerInput.setAutopilotDirection(null);
|
playerInput.stopNavigation();
|
||||||
if (phase == Phase.RESTING || phase == Phase.GET_UP_ANIM) {
|
if (phase == Phase.RESTING || phase == Phase.GET_UP_ANIM) {
|
||||||
playerInput.unlockFromPlace();
|
playerInput.unlockFromPlace();
|
||||||
}
|
}
|
||||||
// Kollision bei Abbruch immer wieder aktivieren
|
|
||||||
if (targetIdx >= 0 && targetIdx < entries.size()) {
|
if (targetIdx >= 0 && targetIdx < entries.size()) {
|
||||||
setTargetPhysicsEnabled(entries.get(targetIdx), true);
|
setTargetPhysicsEnabled(entries.get(targetIdx), true);
|
||||||
}
|
}
|
||||||
phase = Phase.IDLE;
|
phase = Phase.IDLE;
|
||||||
targetIdx = -1;
|
targetIdx = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static boolean isBench(InteractableEntry e) {
|
||||||
|
return e.type() == InteractableType.BENCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTargetPhysicsEnabled(InteractableEntry entry, boolean enabled) {
|
||||||
|
WorldObjectsState wos = getApplication().getStateManager().getState(WorldObjectsState.class);
|
||||||
|
if (wos != null) wos.setInteractablePhysicsEnabled(entry.interactableId(), enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user