Bank-Sitz-Fix: clearKfOffset-Timing, Approach-Distanz, Animationsübergänge
- clearKfOffset() erst nach get_up-Animation (Callback) statt sofort beim Start → kein Slide des Visuals während der Aufsteh-Animation - Approach-Distanz zur Bank um 17.5cm verkürzt (läuft näher ran, sitzt tiefer) - blockingAnimRemaining um 1 Frame (1/60s) gekürzt → verhindert Extra-Keyframe-Hold am Animationsende (noch zu beobachten) - Diverses aus vorheriger Session: AnimSet-Editor, Navigation, Assets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -29,23 +29,23 @@
|
||||
},
|
||||
"previewModelPath": "Models/Chars/mainchar.j3o",
|
||||
"motionKeyframes": {
|
||||
"get_up_sitting": [
|
||||
{
|
||||
"time": 0.0,
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"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,
|
||||
"tz": -0.5,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
}
|
||||
],
|
||||
"get_up_sitting": [
|
||||
{
|
||||
"time": 0.0,
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.5,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
|
||||
@@ -603,6 +603,17 @@ public class EditorApp extends Application {
|
||||
updateSpawnFields(input.pickedSpawnInfo);
|
||||
}
|
||||
|
||||
// Modell-Editor: gebakte Scale aus j3o erkannt → Spinner aktualisieren
|
||||
if (input.modelEditorBakedScaleDetected) {
|
||||
input.modelEditorBakedScaleDetected = false;
|
||||
double bsX = input.modelEditorScaleX;
|
||||
double bsY = input.modelEditorScaleY;
|
||||
double bsZ = input.modelEditorScaleZ;
|
||||
if (modelEditorSpinX != null) modelEditorSpinX.getValueFactory().setValue(bsX);
|
||||
if (modelEditorSpinY != null) modelEditorSpinY.getValueFactory().setValue(bsY);
|
||||
if (modelEditorSpinZ != null) modelEditorSpinZ.getValueFactory().setValue(bsZ);
|
||||
}
|
||||
|
||||
// Modell-Editor: Bounds-Aktualisierung
|
||||
if (input.modelEditorBoundsReady) {
|
||||
input.modelEditorBoundsReady = false;
|
||||
@@ -5999,6 +6010,8 @@ public class EditorApp extends Application {
|
||||
input.modelInteractableOffsetY = meta.interactableOffsetY();
|
||||
input.modelInteractableOffsetZ = meta.interactableOffsetZ();
|
||||
input.modelInteractableRotY = meta.interactableRotY();
|
||||
input.modelInteractableActive = meta.interactableType() == de.blight.common.model.InteractableType.BED
|
||||
|| meta.interactableType() == de.blight.common.model.InteractableType.BENCH;
|
||||
input.modelInteractableOffsetChanged = true;
|
||||
|
||||
Label restPosHint = new Label("Klicke auf das Modell um den Ruhepunkt zu setzen:");
|
||||
@@ -6116,8 +6129,9 @@ public class EditorApp extends Application {
|
||||
|| nv == de.blight.common.model.InteractableType.BENCH;
|
||||
restPointBox.setVisible(show);
|
||||
restPointBox.setManaged(show);
|
||||
// Pfeil ein-/ausblenden — über SharedInput-Flag signalisieren
|
||||
input.modelInteractableOffsetChanged = show;
|
||||
// Pfeil ein-/ausblenden — immer rebuilden, damit Sichtbarkeit aktualisiert wird
|
||||
input.modelInteractableActive = show;
|
||||
input.modelInteractableOffsetChanged = true;
|
||||
});
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────
|
||||
@@ -6654,8 +6668,9 @@ public class EditorApp extends Application {
|
||||
de.blight.common.model.InteractableType interactableType,
|
||||
float interactableOffsetX, float interactableOffsetY,
|
||||
float interactableOffsetZ, float interactableRotY) {
|
||||
// Scale wird in j3o eingebrannt → Meta bekommt immer 1.0 (kein doppelter Scale beim Laden)
|
||||
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||
name, category, tags, sx, sy, sz, uniform,
|
||||
name, category, tags, 1f, 1f, 1f, uniform,
|
||||
pivotY, placeY, solid, cast, receive, rndMin, rndMax,
|
||||
lod1Path, lod2Path, 30f, 80f, 120f,
|
||||
lights, emitters,
|
||||
@@ -6712,12 +6727,22 @@ public class EditorApp extends Application {
|
||||
// Asset-Tree aktualisieren
|
||||
input.refreshAssets = true;
|
||||
|
||||
// Thumbnail generieren (JME3-Thread liest das Flag und rendert)
|
||||
java.nio.file.Path finalJ3o = category.isEmpty() ? absolutePath
|
||||
: ASSET_ROOT.resolve("Models")
|
||||
.resolve(java.nio.file.Path.of(category.replace('/', java.io.File.separatorChar)))
|
||||
.resolve(name.isEmpty() ? absolutePath.getFileName().toString()
|
||||
: name.replaceAll("[\\\\/:*?\"<>|]", "_") + ".j3o");
|
||||
|
||||
// Scale in j3o einbrennen (JME3-Thread) – muss vor Thumbnail passieren
|
||||
if (sx != 1f || sy != 1f || sz != 1f) {
|
||||
input.modelEditorScaleX = sx;
|
||||
input.modelEditorScaleY = sy;
|
||||
input.modelEditorScaleZ = sz;
|
||||
input.modelEditorBakeScalePath = finalJ3o;
|
||||
input.modelEditorBakeScaleRequest = true;
|
||||
}
|
||||
|
||||
// Thumbnail generieren (JME3-Thread liest das Flag und rendert)
|
||||
input.modelEditorThumbnailRequest = finalJ3o;
|
||||
}
|
||||
|
||||
|
||||
@@ -674,6 +674,13 @@ public class SharedInput {
|
||||
*/
|
||||
public volatile java.nio.file.Path modelEditorThumbnailRequest = null;
|
||||
|
||||
/** JFX → JME: Scale in j3o einbrennen (für animierte Modelle als Spatial-Transform, sonst Vertex-Bake). */
|
||||
public volatile boolean modelEditorBakeScaleRequest = false;
|
||||
public volatile java.nio.file.Path modelEditorBakeScalePath = null;
|
||||
|
||||
/** JME → JFX: j3o hatte eine gebakte Scale, die von der Meta abwich – Spinner aktualisieren. */
|
||||
public volatile boolean modelEditorBakedScaleDetected = false;
|
||||
|
||||
/** JME → JFX: true wenn das geladene Modell eingebettete LOD-Kinder hat (kein separater Pfad nötig). */
|
||||
public volatile boolean modelEditorHasEmbeddedLods = false;
|
||||
|
||||
@@ -882,6 +889,8 @@ public class SharedInput {
|
||||
public volatile float modelInteractableOffsetZ = 0f;
|
||||
public volatile float modelInteractableRotY = 0f;
|
||||
public volatile boolean modelInteractableOffsetChanged = false;
|
||||
/** true wenn Interactable-Typ aktiv (BED/BENCH) – steuert Pfeil-Sichtbarkeit. */
|
||||
public volatile boolean modelInteractableActive = false;
|
||||
/** Gesetzt vom JME-Thread nach Raycast-Klick, damit JFX-Spinner aktualisiert werden. */
|
||||
public volatile boolean modelInteractablePosSetFromJme = false;
|
||||
|
||||
|
||||
@@ -225,10 +225,9 @@ public class AnimPreviewState extends BaseAppState {
|
||||
previewTarget.z + FastMath.cos(rotY) * FastMath.cos(rotX) * dist));
|
||||
c.lookAt(previewTarget, Vector3f.UNIT_Y);
|
||||
|
||||
// Achsen: Größe proportional zur Kameradistanz, immer am Weltpunkt (0,0,0)
|
||||
// Achsen: feste 1m Weltlänge (Schaft = 0.5 lokal → Scale 2.0)
|
||||
if (axesNode != null) {
|
||||
float s = previewCamDist * input.animPreviewZoom * 0.18f;
|
||||
axesNode.setLocalScale(s);
|
||||
axesNode.setLocalScale(2.0f);
|
||||
axesNode.setLocalTranslation(Vector3f.ZERO);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.jme3.scene.shape.Box;
|
||||
import com.jme3.scene.shape.Cylinder;
|
||||
import com.jme3.scene.shape.Dome;
|
||||
import com.jme3.scene.shape.Sphere;
|
||||
import com.jme3.anim.SkinningControl;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
import org.slf4j.Logger;
|
||||
@@ -219,6 +220,18 @@ public class ModelEditorState extends BaseAppState {
|
||||
input.modelEditorAttachedEmitters);
|
||||
}
|
||||
|
||||
// Scale in j3o einbrennen (vor Thumbnail, damit Thumbnail die gebackene Datei erhält)
|
||||
if (input.modelEditorBakeScaleRequest) {
|
||||
input.modelEditorBakeScaleRequest = false;
|
||||
Path bakePath = input.modelEditorBakeScalePath;
|
||||
if (bakePath != null) {
|
||||
bakeScaleIntoModel(bakePath,
|
||||
input.modelEditorScaleX,
|
||||
input.modelEditorScaleY,
|
||||
input.modelEditorScaleZ);
|
||||
}
|
||||
}
|
||||
|
||||
// Thumbnail auf Anforderung generieren
|
||||
Path thumbReq = input.modelEditorThumbnailRequest;
|
||||
if (thumbReq != null && modelWrapper != null) {
|
||||
@@ -350,6 +363,18 @@ public class ModelEditorState extends BaseAppState {
|
||||
modelWrapper.attachChild(box);
|
||||
}
|
||||
|
||||
// Gebakte Spatial-Scale erkennen: j3o hat explizit gesetzte Scale, Meta sagt 1.0
|
||||
// → Scale aus j3o übernehmen und JavaFX-Spinner aktualisieren lassen
|
||||
com.jme3.math.Vector3f jScale = modelWrapper.getChildren().isEmpty()
|
||||
? com.jme3.math.Vector3f.UNIT_XYZ
|
||||
: modelWrapper.getChild(0).getLocalScale();
|
||||
if (Math.abs(jScale.y - 1f) > 0.001f && Math.abs(input.modelEditorScaleY - 1f) < 0.001f) {
|
||||
input.modelEditorScaleX = jScale.x;
|
||||
input.modelEditorScaleY = jScale.y;
|
||||
input.modelEditorScaleZ = jScale.z;
|
||||
input.modelEditorBakedScaleDetected = true;
|
||||
}
|
||||
|
||||
// Skalierung aus SharedInput anwenden
|
||||
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||
applyPivot(input.modelEditorPivotY);
|
||||
@@ -767,6 +792,48 @@ public class ModelEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scale-Bake ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Brennt den Scale (sx,sy,sz) in die j3o-Datei ein.
|
||||
* Animierte Modelle (SkinningControl vorhanden): Scale als Spatial-Transform gespeichert.
|
||||
* Statische Modelle: Scale in Vertex-Positionen gebacken (wie ModelImportState beim Import).
|
||||
*/
|
||||
private void bakeScaleIntoModel(Path j3oPath, float sx, float sy, float sz) {
|
||||
try {
|
||||
BinaryImporter importer = BinaryImporter.getInstance();
|
||||
importer.setAssetManager(app.getAssetManager());
|
||||
Savable savable = importer.load(j3oPath.toFile());
|
||||
if (!(savable instanceof Spatial root)) {
|
||||
log.warn("[ModelEditor] Bake: kein Spatial in {}", j3oPath.getFileName());
|
||||
return;
|
||||
}
|
||||
if (hasSkinningControl(root)) {
|
||||
root.setLocalScale(sx, sy, sz);
|
||||
log.info("[ModelEditor] Animiert: Scale ({},{},{}) als Spatial-Transform gespeichert", sx, sy, sz);
|
||||
} else {
|
||||
root.setLocalScale(sx, sy, sz);
|
||||
ModelImportState.stripControls(root);
|
||||
ModelImportState.bakeTransform(root, new Matrix4f());
|
||||
log.info("[ModelEditor] Statisch: Scale ({},{},{}) in Vertices gebacken", sx, sy, sz);
|
||||
}
|
||||
BinaryExporter.getInstance().save(root, j3oPath.toFile());
|
||||
log.info("[ModelEditor] j3o nach Bake gespeichert: {}", j3oPath.getFileName());
|
||||
} catch (Exception e) {
|
||||
log.error("[ModelEditor] Scale-Bake fehlgeschlagen: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasSkinningControl(Spatial s) {
|
||||
if (s.getControl(SkinningControl.class) != null) return true;
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial c : n.getChildren()) {
|
||||
if (hasSkinningControl(c)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Thumbnail ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void generateThumbnail(Path j3oPath) {
|
||||
@@ -842,7 +909,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
group.attachChild(shaft);
|
||||
group.attachChild(head);
|
||||
interactableArrowNode.attachChild(group);
|
||||
interactableArrowNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
interactableArrowNode.setCullHint(
|
||||
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
/** Setzt den Pfeil sichtbar/unsichtbar. */
|
||||
|
||||
@@ -619,7 +619,7 @@ public class ModelImportState extends BaseAppState {
|
||||
bakeTransform(s, new Matrix4f());
|
||||
}
|
||||
|
||||
private static void bakeTransform(Spatial s, Matrix4f accum) {
|
||||
static void bakeTransform(Spatial s, Matrix4f accum) {
|
||||
Matrix4f localMat = new Matrix4f();
|
||||
s.getLocalTransform().toTransformMatrix(localMat);
|
||||
Matrix4f combined = accum.mult(localMat);
|
||||
@@ -637,7 +637,7 @@ public class ModelImportState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyMatrixToMesh(Geometry g, Matrix4f mat) {
|
||||
static void applyMatrixToMesh(Geometry g, Matrix4f mat) {
|
||||
Mesh newMesh = g.getMesh().deepClone();
|
||||
|
||||
FloatBuffer pos = newMesh.getFloatBuffer(VertexBuffer.Type.Position);
|
||||
@@ -673,7 +673,7 @@ public class ModelImportState extends BaseAppState {
|
||||
g.setMesh(newMesh);
|
||||
}
|
||||
|
||||
private static Matrix3f buildNormalMatrix(Matrix4f mat) {
|
||||
static Matrix3f buildNormalMatrix(Matrix4f mat) {
|
||||
Matrix3f m3 = new Matrix3f(
|
||||
mat.m00, mat.m01, mat.m02,
|
||||
mat.m10, mat.m11, mat.m12,
|
||||
@@ -683,7 +683,7 @@ public class ModelImportState extends BaseAppState {
|
||||
return m3;
|
||||
}
|
||||
|
||||
private static void stripControls(Spatial s) {
|
||||
static void stripControls(Spatial s) {
|
||||
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
||||
if (s instanceof Node n) n.getChildren().forEach(ModelImportState::stripControls);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.Spatial;
|
||||
import de.blight.game.animation.AnimKeyframe;
|
||||
import de.blight.game.animation.AnimSet;
|
||||
import de.blight.game.animation.AnimationAction;
|
||||
import de.blight.game.animation.AnimationLibrary;
|
||||
import de.blight.game.animation.RetargetingSystem;
|
||||
@@ -20,6 +22,9 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PlayerInputControl {
|
||||
|
||||
@@ -53,6 +58,15 @@ public class PlayerInputControl {
|
||||
|
||||
private com.jme3.anim.SkinningControl skinningControl = null;
|
||||
|
||||
// ── Motion-Keyframe-Offsets ───────────────────────────────────────────────
|
||||
private Map<String, List<AnimKeyframe>> motionKeyframes = new LinkedHashMap<>();
|
||||
/** Basis-Local-Translation des Visual-Nodes; wird beim Laden des AnimSet einmalig gespeichert. */
|
||||
private Vector3f visualBaseTranslation = new Vector3f();
|
||||
private final Vector3f kfOffsetCurrent = new Vector3f();
|
||||
private final Vector3f kfOffsetTarget = new Vector3f();
|
||||
// Lineare Geschwindigkeit des KF-Offsets in m/s; 0 = kein aktiver Lerp
|
||||
private float kfOffsetSpeed = 0f;
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────
|
||||
private CharacterNavigator navigator = null;
|
||||
private de.blight.game.navigation.PathFinder navPathFinder = null;
|
||||
@@ -127,6 +141,17 @@ public class PlayerInputControl {
|
||||
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
||||
skinningControl = findSkinningControl(visual);
|
||||
log.info("[AnimCtx] SkinningControl gefunden: {}", skinningControl != null);
|
||||
if (visual != null) {
|
||||
visualBaseTranslation = visual.getLocalTranslation().clone();
|
||||
}
|
||||
try {
|
||||
AnimSet as = AnimSet.load(assetRoot.resolve("animations/sets"), animSetName);
|
||||
motionKeyframes = as.getMotionKeyframes();
|
||||
log.info("[AnimCtx] {} Motion-KF-Einträge geladen.", motionKeyframes.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimCtx] AnimSet-KF nicht ladbar: {}", e.getMessage());
|
||||
motionKeyframes = new LinkedHashMap<>();
|
||||
}
|
||||
// Navigator (neu) aufbauen sobald alle Abhängigkeiten bereit sind
|
||||
if (navPathFinder != null && navTerrain != null && physicsChar != null && visual != null) {
|
||||
navigator = new CharacterNavigator(physicsChar, visual, navPathFinder, navTerrain);
|
||||
@@ -194,7 +219,7 @@ public class PlayerInputControl {
|
||||
duration = resolveClipLength(action, 1.5f);
|
||||
}
|
||||
blockingAnimActive = true;
|
||||
blockingAnimRemaining = duration;
|
||||
blockingAnimRemaining = Math.max(duration - (1f / 60f), 0f);
|
||||
blockingAnimTotal = duration;
|
||||
blockingAnimCallback = onComplete;
|
||||
autopilotDir = null;
|
||||
@@ -268,6 +293,15 @@ public class PlayerInputControl {
|
||||
*/
|
||||
public void navigateTo(Vector3f target, CharacterNavigator.Speed speed,
|
||||
Runnable onArrival, Runnable onFailed) {
|
||||
navigateTo(target, speed, -1f, onArrival, onFailed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie {@link #navigateTo} mit explizitem Ankunftsradius (Meter).
|
||||
* Werte <= 0 verwenden den Navigator-Standard (0.45m).
|
||||
*/
|
||||
public void navigateTo(Vector3f target, CharacterNavigator.Speed speed, float arriveRadius,
|
||||
Runnable onArrival, Runnable onFailed) {
|
||||
if (navigator == null) {
|
||||
log.warn("[Nav] navigateTo: kein CharacterNavigator – setNavigationSources() vorher aufrufen.");
|
||||
if (onFailed != null) onFailed.run();
|
||||
@@ -276,7 +310,12 @@ public class PlayerInputControl {
|
||||
forward = backward = left = right = false;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
autopilotDir = null;
|
||||
navigator.navigateTo(target, speed, onArrival, onFailed);
|
||||
clearMotionKfOffset();
|
||||
if (arriveRadius > 0f) {
|
||||
navigator.navigateTo(target, speed, arriveRadius, onArrival, onFailed);
|
||||
} else {
|
||||
navigator.navigateTo(target, speed, onArrival, onFailed);
|
||||
}
|
||||
}
|
||||
|
||||
/** Bricht eine laufende Navigation ab (kein Callback). */
|
||||
@@ -291,8 +330,6 @@ public class PlayerInputControl {
|
||||
/**
|
||||
* Spielt eine Animations-Aktion als Dauer-Loop während des Ruhezustands.
|
||||
* Nur sinnvoll nach {@link #lockInPlace()}.
|
||||
* Hat die Aktion Motion Keyframes, wird der erste Keyframe (time=0) als statischer
|
||||
* Versatz auf den Visual-Node angewendet.
|
||||
*/
|
||||
public void playLockedAnimation(AnimationAction action) {
|
||||
playAction(action);
|
||||
@@ -313,6 +350,19 @@ public class PlayerInputControl {
|
||||
public void update(float tpf) {
|
||||
if (physicsChar == null) return;
|
||||
|
||||
if (visual != null && kfOffsetSpeed > 0f) {
|
||||
Vector3f delta = kfOffsetTarget.subtract(kfOffsetCurrent);
|
||||
float dist = delta.length();
|
||||
float step = kfOffsetSpeed * tpf;
|
||||
if (dist <= step) {
|
||||
kfOffsetCurrent.set(kfOffsetTarget);
|
||||
kfOffsetSpeed = 0f;
|
||||
} else {
|
||||
kfOffsetCurrent.addLocal(delta.normalizeLocal().multLocal(step));
|
||||
}
|
||||
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(kfOffsetCurrent));
|
||||
}
|
||||
|
||||
if (paused) {
|
||||
if (autopilotDir != null) {
|
||||
autopilotDir = null;
|
||||
@@ -490,6 +540,38 @@ public class PlayerInputControl {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyMotionKfOffset(String clip) {
|
||||
if (visual == null) return;
|
||||
List<AnimKeyframe> kfs = (clip != null) ? motionKeyframes.get(clip) : null;
|
||||
if (kfs == null || kfs.isEmpty()) { clearMotionKfOffset(); return; }
|
||||
AnimKeyframe kf = kfs.get(0);
|
||||
Quaternion facing = visual.getLocalRotation().clone();
|
||||
Vector3f worldOffset = facing.mult(new Vector3f(kf.tx, kf.ty, kf.tz));
|
||||
kfOffsetTarget.set(worldOffset);
|
||||
kfOffsetCurrent.set(worldOffset);
|
||||
kfOffsetSpeed = 0f;
|
||||
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(kfOffsetCurrent));
|
||||
log.info("[KF] Clip '{}' → Offset ({},{},{}) sofort", clip, worldOffset.x, worldOffset.y, worldOffset.z);
|
||||
}
|
||||
|
||||
public void clearKfOffset() {
|
||||
log.info("[KF] clearKfOffset() → Lerp zu 0, current=({},{},{})", kfOffsetCurrent.x, kfOffsetCurrent.y, kfOffsetCurrent.z);
|
||||
kfOffsetTarget.set(0, 0, 0);
|
||||
kfOffsetSpeed = 5.0f; // 0.25m in ~3 Frames bei 60fps
|
||||
}
|
||||
|
||||
private void clearMotionKfOffset() {
|
||||
clearKfOffset();
|
||||
}
|
||||
|
||||
/** Gibt die konfigurierte Bank-Approach-Distanz (|sitting.tz|) zurück; 0.25m als Fallback. */
|
||||
public float getBenchApproachDist() {
|
||||
List<AnimKeyframe> kfs = motionKeyframes.get("sitting");
|
||||
if (kfs == null || kfs.isEmpty()) return 0.25f;
|
||||
float tz = kfs.get(0).tz;
|
||||
return Math.abs(tz) > 0.01f ? Math.abs(tz) : 0.25f;
|
||||
}
|
||||
|
||||
private boolean tryPlay(String clip) {
|
||||
if (animComposer == null || !animLib.applyTo(clip, visual)) {
|
||||
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
||||
@@ -507,6 +589,7 @@ public class PlayerInputControl {
|
||||
log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip);
|
||||
}
|
||||
runningClip = clip;
|
||||
applyMotionKfOffset(clip);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,10 @@ public class CharacterNavigator {
|
||||
|
||||
// ── Tuneable ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** Ankunftsradius zum finalen Ziel (m). */
|
||||
private static final float ARRIVE_RADIUS = 0.45f;
|
||||
/** Standard-Ankunftsradius zum finalen Ziel (m). */
|
||||
private static final float DEFAULT_ARRIVE_RADIUS = 0.45f;
|
||||
/** Aktueller Ankunftsradius; kann pro navigateTo-Aufruf überschrieben werden. */
|
||||
private float arriveRadius = DEFAULT_ARRIVE_RADIUS;
|
||||
/** Radius zum Weiterspringen auf den nächsten Wegpunkt (m). */
|
||||
private static final float WAYPOINT_RADIUS = 0.9f;
|
||||
/** Rotationsgeschwindigkeit (rad/s). */
|
||||
@@ -133,6 +135,16 @@ public class CharacterNavigator {
|
||||
*/
|
||||
public void navigateTo(Vector3f target, Speed speed,
|
||||
Runnable onArrival, Runnable onFailed) {
|
||||
navigateTo(target, speed, DEFAULT_ARRIVE_RADIUS, onArrival, onFailed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie {@link #navigateTo} mit explizitem Ankunftsradius.
|
||||
* Kleinere Werte (z. B. 0.05m für Sitzpunkte) reduzieren den Versatz beim End-Snap.
|
||||
*/
|
||||
public void navigateTo(Vector3f target, Speed speed, float customArriveRadius,
|
||||
Runnable onArrival, Runnable onFailed) {
|
||||
this.arriveRadius = customArriveRadius;
|
||||
this.speed = speed;
|
||||
this.onArrival = onArrival;
|
||||
this.onFailed = onFailed;
|
||||
@@ -158,7 +170,7 @@ public class CharacterNavigator {
|
||||
|
||||
/** Wie {@link #navigateTo} ohne Fehler-Callback. */
|
||||
public void navigateTo(Vector3f target, Speed speed, Runnable onArrival) {
|
||||
navigateTo(target, speed, onArrival, null);
|
||||
navigateTo(target, speed, DEFAULT_ARRIVE_RADIUS, onArrival, null);
|
||||
}
|
||||
|
||||
/** Bricht die Navigation sofort ab, kein Callback wird ausgeführt. */
|
||||
@@ -190,7 +202,7 @@ public class CharacterNavigator {
|
||||
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; }
|
||||
if (gdx * gdx + gdz * gdz <= arriveRadius * arriveRadius) { arrive(); return; }
|
||||
|
||||
// ── Wegpunkte überspringen die bereits passiert sind ──────────────────
|
||||
while (pathStep < path.size() - 1) {
|
||||
|
||||
@@ -340,7 +340,13 @@ public class WorldScene extends BaseAppState {
|
||||
float modelHeight = yRange[1] - yRange[0];
|
||||
log.info("[WorldScene] Vertex-Y-Range: min={} max={} height={}", yRange[0], yRange[1], modelHeight);
|
||||
float offsetY;
|
||||
if (modelHeight > 0.1f) {
|
||||
com.jme3.math.Vector3f savedScale = loaded.getLocalScale();
|
||||
if (Math.abs(savedScale.y - 1f) > 0.001f) {
|
||||
// Im j3o eingebrannte Skalierung verwenden (nicht auto-berechnen)
|
||||
float scale = savedScale.y;
|
||||
offsetY = -(0.9f + scale * yRange[0]);
|
||||
log.info("[WorldScene] Charakter: gespeicherte Skalierung {}x offsetY={}", scale, offsetY);
|
||||
} else if (modelHeight > 0.1f) {
|
||||
float scale = 1.8f / modelHeight;
|
||||
loaded.setLocalScale(scale);
|
||||
// Füße des Modells (scale * minY in loaded-Local) auf Kapsel-Unterkante legen
|
||||
|
||||
@@ -208,7 +208,8 @@ public class GrassState extends BaseAppState
|
||||
node.attachChild(geo);
|
||||
}
|
||||
if (node.getChildren().isEmpty()) return;
|
||||
node.setCullHint(Spatial.CullHint.Always); // sichtbar erst wenn LOD0
|
||||
boolean visibleNow = terrainChunkState.getChunkLod(cx, cz) == 0;
|
||||
node.setCullHint(visibleNow ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
chunkNodes[idx] = node;
|
||||
grassNode.attachChild(node);
|
||||
}
|
||||
|
||||
@@ -257,6 +257,13 @@ public class TerrainChunkState extends BaseAppState {
|
||||
public void addChunkListener(ChunkListener l) { listeners.add(l); }
|
||||
public void removeChunkListener(ChunkListener l) { listeners.remove(l); }
|
||||
|
||||
/** Aktueller LOD eines Chunks (cx/cz in Chunk-Koordinaten), -1 = nicht geladen/versteckt. */
|
||||
public int getChunkLod(int cx, int cz) {
|
||||
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
|
||||
if (ci < 0 || ci >= TOTAL) return -1;
|
||||
return chunkLod[ci];
|
||||
}
|
||||
|
||||
// ── Chunk-Mesh-Aufbau ─────────────────────────────────────────────────────
|
||||
|
||||
private void rebuildChunkMesh(int cx, int cz, int lod, int[] targetLod) {
|
||||
|
||||
@@ -57,6 +57,14 @@ public class WorldInteractableState extends BaseAppState {
|
||||
private static final float BED_RANGE = 6f;
|
||||
private static final float WALK_TIMEOUT = 12f;
|
||||
|
||||
// Nach dem Aufstehen: Bank-Physik erst re-enablen wenn Charakter weit genug weg
|
||||
private static final float BENCH_REENABLE_DIST_SQ = 0.36f; // 0.6m Radius
|
||||
private static final float BENCH_REENABLE_TIMEOUT = 8f;
|
||||
private String benchPendingId = null;
|
||||
private float benchPendingX = 0f;
|
||||
private float benchPendingZ = 0f;
|
||||
private float benchPendingTimer = 0f;
|
||||
|
||||
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
||||
|
||||
private final KeyBindings keyBindings;
|
||||
@@ -146,9 +154,16 @@ public class WorldInteractableState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
// Navigation läuft im CharacterNavigator (PlayerInputControl.update).
|
||||
// Nur walkTimer für Bett-Rückkehr brauchen wir noch.
|
||||
if (phase == Phase.WALKING_BACK) walkTimer += tpf;
|
||||
if (benchPendingId != null) {
|
||||
benchPendingTimer += tpf;
|
||||
Vector3f pos = physicsChar.getPhysicsLocation();
|
||||
float dx = pos.x - benchPendingX;
|
||||
float dz = pos.z - benchPendingZ;
|
||||
if (dx * dx + dz * dz >= BENCH_REENABLE_DIST_SQ || benchPendingTimer >= BENCH_REENABLE_TIMEOUT) {
|
||||
flushBenchReEnable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Listener ─────────────────────────────────────────────────────────────
|
||||
@@ -197,10 +212,14 @@ public class WorldInteractableState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
if (benchPendingId != null && benchPendingId.equals(entry.interactableId())) {
|
||||
benchPendingId = null; // gleiche Bank – wird gleich wieder disabled
|
||||
}
|
||||
phase = Phase.WALKING;
|
||||
log.info("[WorldInteractable] Annäherung {} [{}]", entry.type(), entry.interactableId());
|
||||
|
||||
playerInput.navigateTo(target, CharacterNavigator.Speed.WALK,
|
||||
float radius = isBench(entry) ? 0.05f : -1f;
|
||||
playerInput.navigateTo(target, CharacterNavigator.Speed.WALK, radius,
|
||||
this::onApproachArrived,
|
||||
this::cancelInteraction);
|
||||
}
|
||||
@@ -217,7 +236,13 @@ public class WorldInteractableState extends BaseAppState {
|
||||
log.warn("[WorldInteractable] Bank {} hat keinen Sitzpunkt.", entry.interactableId());
|
||||
return null;
|
||||
}
|
||||
return new Vector3f(bench.getSitzX(), bench.getSitzY(), bench.getSitzZ());
|
||||
// Approach 17.5cm kürzer als KF-Offset → Charakter läuft näher ran, Visual landet tiefer zur Bank.
|
||||
float approachDist = Math.max(playerInput.getBenchApproachDist() - 0.175f, 0.05f);
|
||||
float rotY = bench.getSitzRotY();
|
||||
float dx = (float) Math.cos(rotY) * approachDist;
|
||||
float dz = (float) Math.sin(rotY) * approachDist;
|
||||
log.info("[WorldInteractable] Bank approach: {}m von Sitzpunkt", approachDist);
|
||||
return new Vector3f(bench.getSitzX() + dx, bench.getSitzY(), bench.getSitzZ() + dz);
|
||||
} else {
|
||||
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
||||
if (bed == null || !bed.isLiegeSet()) {
|
||||
@@ -238,7 +263,6 @@ public class WorldInteractableState extends BaseAppState {
|
||||
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));
|
||||
}
|
||||
@@ -265,7 +289,7 @@ public class WorldInteractableState extends BaseAppState {
|
||||
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
|
||||
|
||||
playerInput.requestAnimation(downAction, 0f, () -> {
|
||||
snapToSitPos(entry);
|
||||
if (!isBench(entry)) snapToSitPos(entry); // Bank: Physik bleibt am Approach-Punkt
|
||||
playerInput.lockInPlace();
|
||||
playerInput.playLockedAnimation(idleAction);
|
||||
phase = Phase.RESTING;
|
||||
@@ -303,10 +327,12 @@ public class WorldInteractableState extends BaseAppState {
|
||||
AnimationAction upAction = isBench(entry) ? AnimationAction.SIT_UP : AnimationAction.LIE_UP;
|
||||
|
||||
playerInput.requestAnimation(upAction, 0f, () -> {
|
||||
setTargetPhysicsEnabled(entry, true);
|
||||
|
||||
if (isBench(entry)) {
|
||||
// Bank: Charakter bleibt stehen, Spieler übernimmt wieder
|
||||
playerInput.clearKfOffset();
|
||||
benchPendingId = entry.interactableId();
|
||||
benchPendingX = entry.worldX();
|
||||
benchPendingZ = entry.worldZ();
|
||||
benchPendingTimer = 0f;
|
||||
phase = Phase.IDLE;
|
||||
targetIdx = -1;
|
||||
log.info("[WorldInteractable] Bank verlassen.");
|
||||
@@ -337,10 +363,18 @@ public class WorldInteractableState extends BaseAppState {
|
||||
if (targetIdx >= 0 && targetIdx < entries.size()) {
|
||||
setTargetPhysicsEnabled(entries.get(targetIdx), true);
|
||||
}
|
||||
flushBenchReEnable();
|
||||
phase = Phase.IDLE;
|
||||
targetIdx = -1;
|
||||
}
|
||||
|
||||
private void flushBenchReEnable() {
|
||||
if (benchPendingId == null) return;
|
||||
WorldObjectsState wos = getApplication().getStateManager().getState(WorldObjectsState.class);
|
||||
if (wos != null) wos.setInteractablePhysicsEnabled(benchPendingId, true);
|
||||
benchPendingId = null;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static boolean isBench(InteractableEntry e) {
|
||||
|
||||
Reference in New Issue
Block a user