diff --git a/blight-assets/src/main/resources/Models/Chars/mainchar.j3o b/blight-assets/src/main/resources/Models/Chars/mainchar.j3o index d291e97..467a92c 100644 Binary files a/blight-assets/src/main/resources/Models/Chars/mainchar.j3o and b/blight-assets/src/main/resources/Models/Chars/mainchar.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/idle.j3o b/blight-assets/src/main/resources/animations/clips/idle.j3o index 3339791..aac8cab 100644 Binary files a/blight-assets/src/main/resources/animations/clips/idle.j3o and b/blight-assets/src/main/resources/animations/clips/idle.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/pickup.j3o b/blight-assets/src/main/resources/animations/clips/pickup.j3o index d565c94..394b217 100644 Binary files a/blight-assets/src/main/resources/animations/clips/pickup.j3o and b/blight-assets/src/main/resources/animations/clips/pickup.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/running.j3o b/blight-assets/src/main/resources/animations/clips/running.j3o index 4aff2bc..5a89ac2 100644 Binary files a/blight-assets/src/main/resources/animations/clips/running.j3o and b/blight-assets/src/main/resources/animations/clips/running.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/running_jump.j3o b/blight-assets/src/main/resources/animations/clips/running_jump.j3o index 17a245f..228a147 100644 Binary files a/blight-assets/src/main/resources/animations/clips/running_jump.j3o and b/blight-assets/src/main/resources/animations/clips/running_jump.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/sitting_floor.j3o b/blight-assets/src/main/resources/animations/clips/sitting_floor.j3o index 9e91b05..d6d0833 100644 Binary files a/blight-assets/src/main/resources/animations/clips/sitting_floor.j3o and b/blight-assets/src/main/resources/animations/clips/sitting_floor.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/sprinting.j3o b/blight-assets/src/main/resources/animations/clips/sprinting.j3o index a8d4f23..d184823 100644 Binary files a/blight-assets/src/main/resources/animations/clips/sprinting.j3o and b/blight-assets/src/main/resources/animations/clips/sprinting.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/stand_up.j3o b/blight-assets/src/main/resources/animations/clips/stand_up.j3o index 0103cae..1812335 100644 Binary files a/blight-assets/src/main/resources/animations/clips/stand_up.j3o and b/blight-assets/src/main/resources/animations/clips/stand_up.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/walking.j3o b/blight-assets/src/main/resources/animations/clips/walking.j3o index 757854f..1bdf353 100644 Binary files a/blight-assets/src/main/resources/animations/clips/walking.j3o and b/blight-assets/src/main/resources/animations/clips/walking.j3o differ diff --git a/blight-assets/src/main/resources/animations/sets/human.animset.json b/blight-assets/src/main/resources/animations/sets/human.animset.json index 3a1683c..083ea5c 100644 --- a/blight-assets/src/main/resources/animations/sets/human.animset.json +++ b/blight-assets/src/main/resources/animations/sets/human.animset.json @@ -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 diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 07fb64c..63ba3c9 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -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; } diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 83b5786..82a6a9f 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -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; diff --git a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java index c77274c..82dd32e 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java @@ -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); } diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java index cc79805..28311a9 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java @@ -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. */ diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java index 3f6ba7b..7eb436c 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java @@ -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); } diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index 4e98872..f422b51 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -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> 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 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 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; } } diff --git a/blight-game/src/main/java/de/blight/game/navigation/CharacterNavigator.java b/blight-game/src/main/java/de/blight/game/navigation/CharacterNavigator.java index e305a74..ae282d3 100644 --- a/blight-game/src/main/java/de/blight/game/navigation/CharacterNavigator.java +++ b/blight-game/src/main/java/de/blight/game/navigation/CharacterNavigator.java @@ -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) { diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 7325aca..f884136 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -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 diff --git a/blight-game/src/main/java/de/blight/game/state/GrassState.java b/blight-game/src/main/java/de/blight/game/state/GrassState.java index 6d02198..429b002 100644 --- a/blight-game/src/main/java/de/blight/game/state/GrassState.java +++ b/blight-game/src/main/java/de/blight/game/state/GrassState.java @@ -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); } diff --git a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java index 679d4a0..d8828cf 100644 --- a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java +++ b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java @@ -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) { diff --git a/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java b/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java index afd8a07..9504734 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java @@ -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) {