From 63aa7aa10484299aeb1a2f17fbcb14c27ce678a2 Mon Sep 17 00:00:00 2001 From: Mario Date: Sun, 28 Jun 2026 22:59:36 +0200 Subject: [PATCH] =?UTF-8?q?Commit=20vor=20=C3=84nderung=20Umstellung=20auf?= =?UTF-8?q?=20eine=20einzelne=20Animation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../animations/sets/human.animset.json | 26 +- .../main/java/de/blight/editor/EditorApp.java | 186 +++++++------- .../java/de/blight/editor/SharedInput.java | 10 +- .../blight/editor/state/AnimPreviewState.java | 6 +- .../blight/game/animation/AnimKeyframe.java | 27 --- .../de/blight/game/animation/AnimSet.java | 12 +- .../blight/game/animation/FootIKControl.java | 226 ------------------ .../game/control/PlayerInputControl.java | 125 ++++++---- .../game/state/WorldInteractableState.java | 51 +++- 9 files changed, 227 insertions(+), 442 deletions(-) delete mode 100644 blight-game/src/main/java/de/blight/game/animation/AnimKeyframe.java delete mode 100644 blight-game/src/main/java/de/blight/game/animation/FootIKControl.java 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 c3aba07..961be6e 100644 --- a/blight-assets/src/main/resources/animations/sets/human.animset.json +++ b/blight-assets/src/main/resources/animations/sets/human.animset.json @@ -30,28 +30,8 @@ "REVIVE": "alive_again" }, "previewModelPath": "Models/Chars/mainchar.j3o", - "motionKeyframes": { - "sitting": [ - { - "time": 0.0, - "tx": 0.0, - "ty": 0.0, - "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 - } - ] + "animOffsets": { + "sitting": {"tx": 0.0, "ty": 0.0, "tz": -0.5, "rx": 0.0, "ry": 0.0, "rz": 0.0}, + "get_up_sitting": {"tx": 0.0, "ty": 0.0, "tz": -0.5, "rx": 0.0, "ry": 0.0, "rz": 0.0} } } \ No newline at end of file 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 63ba3c9..d7997f9 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -157,12 +157,12 @@ public class EditorApp extends Application { private boolean animSetDirty = false; private String animSetCurrentName = null; private Path animSetCurrentDir = null; - // Motion-Keyframe-Editor (innerhalb AnimSet-Editor) - private javafx.scene.control.ListView animSetKfListView; - private javafx.collections.ObservableList animSetKfList = + // Anim-Offset-Editor (innerhalb AnimSet-Editor) + private javafx.scene.control.ListView animSetOffsetListView; + private javafx.collections.ObservableList animSetOffsetList = javafx.collections.FXCollections.observableArrayList(); - private java.util.Map> - animSetMotionKeyframes = new java.util.LinkedHashMap<>(); + private java.util.Map + animSetOffsets = new java.util.LinkedHashMap<>(); // Character-Editor-Zustand private de.blight.editor.ui.DialogEditorView dialogEditorView; @@ -8292,22 +8292,24 @@ public class EditorApp extends Application { animSetClipListView.getSelectionModel().selectedItemProperty() .addListener((obs, ov, nv) -> { removeClipBtn.setDisable(nv == null); - // Keyframes des alten Clips sichern - if (ov != null && animSetKfList != null) { - animSetMotionKeyframes.put(ov, new java.util.ArrayList<>(animSetKfList)); + // Offset des alten Clips sichern + if (ov != null && animSetOffsetList != null && !animSetOffsetList.isEmpty()) { + animSetOffsets.put(ov, animSetOffsetList.get(0)); + } else if (ov != null) { + animSetOffsets.remove(ov); } - // Keyframes des neuen Clips laden - if (animSetKfList != null) { - animSetKfList.clear(); + // Offset des neuen Clips laden + if (animSetOffsetList != null) { + animSetOffsetList.clear(); if (nv != null) { - var kfs = animSetMotionKeyframes.getOrDefault(nv, new java.util.ArrayList<>()); - animSetKfList.setAll(kfs); + de.blight.game.animation.AnimOffset off = animSetOffsets.get(nv); + if (off != null) animSetOffsetList.setAll(off); // Clip direkt in Vorschau abspielen input.animPreviewPlayClip = nv; } } - // KF-Bereich aktivieren/deaktivieren - if (animSetKfListView != null) animSetKfListView.setDisable(nv == null); + // Offset-Bereich aktivieren/deaktivieren + if (animSetOffsetListView != null) animSetOffsetListView.setDisable(nv == null); }); addClipBtn.setOnAction(e -> { @@ -8427,64 +8429,61 @@ public class EditorApp extends Application { HBox.setHgrow(removeActionBtn, Priority.ALWAYS); inner.getChildren().addAll(animSetActionListView, actionBtns); - // ── Motion Keyframes ────────────────────────────────────────────────── - inner.getChildren().addAll(new Separator(), sectionTitle("Motion Keyframes"), new Separator()); + // ── Anim-Offsets ────────────────────────────────────────────────────── + inner.getChildren().addAll(new Separator(), sectionTitle("Anim-Offsets"), new Separator()); - Label kfHint = new Label("TX/TZ = charakter-lokal (seitlich/vorwärts), TY = Welt-Y (hoch/runter). RX/RY/RZ in Grad, additiv zur Startrotation. Zeit ≤ Clip-Dauer (Clip vorab in Vorschau abspielen)."); + Label kfHint = new Label("TX/TZ = charakter-lokal (seitlich/vorwärts), TY = Welt-Y (hoch/runter). RX/RY/RZ in Grad, additiv zur Startrotation."); kfHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); kfHint.setWrapText(true); - // Keyframes aus AnimSet laden - animSetMotionKeyframes = new java.util.LinkedHashMap<>(); - for (var entry : animSet.getMotionKeyframes().entrySet()) { - animSetMotionKeyframes.put(entry.getKey(), new java.util.ArrayList<>(entry.getValue())); - } - animSetKfList = javafx.collections.FXCollections.observableArrayList(); - animSetKfList.addListener((javafx.collections.ListChangeListener) - change -> updateAnimPreviewKfOffset()); + // Offsets aus AnimSet laden + animSetOffsets = new java.util.LinkedHashMap<>(animSet.getAnimOffsets()); + animSetOffsetList = javafx.collections.FXCollections.observableArrayList(); + animSetOffsetList.addListener((javafx.collections.ListChangeListener) + change -> updateAnimPreviewOffset()); - // ListView mit Zusammenfassung, Doppelklick öffnet Edit-Dialog - animSetKfListView = new javafx.scene.control.ListView<>(animSetKfList); - animSetKfListView.setPrefHeight(150); - animSetKfListView.setPlaceholder(new Label("Keine Keyframes – [+ Keyframe] zum Hinzufügen")); - animSetKfListView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { - @Override protected void updateItem(de.blight.game.animation.AnimKeyframe kf, boolean empty) { - super.updateItem(kf, empty); - if (empty || kf == null) { setText(null); return; } - setText(String.format("%.3fs | TX%+.3f TY%+.3f TZ%+.3f | RX%+.1f° RY%+.1f° RZ%+.1f°", - kf.time, kf.tx, kf.ty, kf.tz, kf.rx, kf.ry, kf.rz)); + // ListView zeigt den einen Offset des gewählten Clips; Doppelklick öffnet Edit-Dialog + animSetOffsetListView = new javafx.scene.control.ListView<>(animSetOffsetList); + animSetOffsetListView.setPrefHeight(60); + animSetOffsetListView.setPlaceholder(new Label("Kein Offset – [+ Offset] zum Setzen")); + animSetOffsetListView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { + @Override protected void updateItem(de.blight.game.animation.AnimOffset off, boolean empty) { + super.updateItem(off, empty); + if (empty || off == null) { setText(null); return; } + setText(String.format("TX%+.3f TY%+.3f TZ%+.3f | RX%+.1f° RY%+.1f° RZ%+.1f°", + off.tx, off.ty, off.tz, off.rx, off.ry, off.rz)); } }); - animSetKfListView.setOnMouseClicked(ev -> { + animSetOffsetListView.setOnMouseClicked(ev -> { if (ev.getClickCount() == 2) { - de.blight.game.animation.AnimKeyframe sel = - animSetKfListView.getSelectionModel().getSelectedItem(); - if (sel != null) showAnimKfDialog(sel); + de.blight.game.animation.AnimOffset sel = + animSetOffsetListView.getSelectionModel().getSelectedItem(); + if (sel != null) showAnimOffsetDialog(sel); } }); // initial deaktiviert – wird durch Clip-Selektion gesteuert - animSetKfListView.setDisable(true); + animSetOffsetListView.setDisable(true); - Button addKfBtn = new Button("+ Keyframe"); + Button addKfBtn = new Button("+ Offset"); Button removeKfBtn = new Button("- Entfernen"); addKfBtn.setMaxWidth(Double.MAX_VALUE); removeKfBtn.setMaxWidth(Double.MAX_VALUE); addKfBtn.setDisable(true); removeKfBtn.setDisable(true); - animSetKfListView.getSelectionModel().selectedItemProperty() + animSetOffsetListView.getSelectionModel().selectedItemProperty() .addListener((obs, ov, nv) -> removeKfBtn.setDisable(nv == null)); - // addKfBtn folgt Clip-Selektion + // addKfBtn folgt Clip-Selektion (nur wenn noch kein Offset gesetzt) animSetClipListView.getSelectionModel().selectedItemProperty() .addListener((obs, ov, nv) -> addKfBtn.setDisable(nv == null)); - addKfBtn.setOnAction(e -> showAnimKfDialog(null)); + addKfBtn.setOnAction(e -> showAnimOffsetDialog(null)); removeKfBtn.setOnAction(e -> { - de.blight.game.animation.AnimKeyframe sel = - animSetKfListView.getSelectionModel().getSelectedItem(); + de.blight.game.animation.AnimOffset sel = + animSetOffsetListView.getSelectionModel().getSelectedItem(); if (sel != null) { - animSetKfList.remove(sel); + animSetOffsetList.remove(sel); animSetDirty = true; } }); @@ -8492,7 +8491,7 @@ public class EditorApp extends Application { HBox kfBtns = new HBox(6, addKfBtn, removeKfBtn); HBox.setHgrow(addKfBtn, Priority.ALWAYS); HBox.setHgrow(removeKfBtn, Priority.ALWAYS); - inner.getChildren().addAll(kfHint, animSetKfListView, kfBtns); + inner.getChildren().addAll(kfHint, animSetOffsetListView, kfBtns); // ── Vorschau ───────────────────────────────────────────────────────── inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator()); @@ -8552,16 +8551,10 @@ public class EditorApp extends Application { return panel; } - /** Öffnet den Keyframe-Dialog (null = Hinzufügen, non-null = Bearbeiten). */ - private void showAnimKfDialog(de.blight.game.animation.AnimKeyframe existing) { + /** Öffnet den Offset-Dialog (null = Hinzufügen, non-null = Bearbeiten). */ + private void showAnimOffsetDialog(de.blight.game.animation.AnimOffset existing) { boolean isAdd = (existing == null); - // Zeitlimit: zuletzt gemessene Clip-Dauer aus Preview, sonst 999 - float maxTime = input.animPreviewCurrentClipDuration > 0.01f - ? input.animPreviewCurrentClipDuration : 999.0f; - float initTime = isAdd - ? (animSetKfList.isEmpty() ? 0f : Math.min(animSetKfList.get(animSetKfList.size() - 1).time + 0.5f, maxTime)) - : existing.time; float initTX = isAdd ? 0f : existing.tx; float initTY = isAdd ? 0f : existing.ty; float initTZ = isAdd ? 0f : existing.tz; @@ -8569,25 +8562,19 @@ public class EditorApp extends Application { float initRY = isAdd ? 0f : existing.ry; float initRZ = isAdd ? 0f : existing.rz; - Spinner spTime = new Spinner<>(0.0, maxTime, initTime, 0.05); - Spinner spTX = new Spinner<>(-10.0, 10.0, initTX, 0.05); - Spinner spTY = new Spinner<>(-10.0, 10.0, initTY, 0.05); - Spinner spTZ = new Spinner<>(-10.0, 10.0, initTZ, 0.05); - Spinner spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0); - Spinner spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0); - Spinner spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0); - for (Spinner sp : new Spinner[]{spTime, spTX, spTY, spTZ, spRX, spRY, spRZ}) { + Spinner spTX = new Spinner<>(-10.0, 10.0, initTX, 0.05); + Spinner spTY = new Spinner<>(-10.0, 10.0, initTY, 0.05); + Spinner spTZ = new Spinner<>(-10.0, 10.0, initTZ, 0.05); + Spinner spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0); + Spinner spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0); + Spinner spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0); + for (Spinner sp : new Spinner[]{spTX, spTY, spTZ, spRX, spRY, spRZ}) { sp.setEditable(true); sp.setMaxWidth(Double.MAX_VALUE); } - String timeLabel = maxTime < 999f - ? String.format("Zeit (0 – %.2fs):", maxTime) - : "Zeit (s):"; - String[][] rows = { - {timeLabel}, {"TX (m):"}, {"TY (m):"}, {"TZ (m):"}, {"RX (°):"}, {"RY (°):"}, {"RZ (°):"} - }; - Spinner[] sps = {spTime, spTX, spTY, spTZ, spRX, spRY, spRZ}; + String[][] rows = {{"TX (m):"}, {"TY (m):"}, {"TZ (m):"}, {"RX (°):"}, {"RY (°):"}, {"RZ (°):"}}; + Spinner[] sps = {spTX, spTY, spTZ, spRX, spRY, spRZ}; javafx.scene.layout.GridPane kfGrid = new javafx.scene.layout.GridPane(); kfGrid.setHgap(8); kfGrid.setVgap(5); for (int i = 0; i < sps.length; i++) { @@ -8600,7 +8587,7 @@ public class EditorApp extends Application { javafx.scene.control.Dialog kfDlg = new javafx.scene.control.Dialog<>(); - kfDlg.setTitle(isAdd ? "Keyframe hinzufügen" : "Keyframe bearbeiten"); + kfDlg.setTitle(isAdd ? "Offset hinzufügen" : "Offset bearbeiten"); javafx.scene.control.ButtonType okKf = new javafx.scene.control.ButtonType( isAdd ? "Hinzufügen" : "Übernehmen", javafx.scene.control.ButtonBar.ButtonData.OK_DONE); @@ -8608,15 +8595,12 @@ public class EditorApp extends Application { kfDlg.getDialogPane().setContent(kfGrid); kfDlg.showAndWait().ifPresent(bt -> { if (bt != okKf) return; - float t = (float) Math.min(spTime.getValue(), maxTime); if (isAdd) { - de.blight.game.animation.AnimKeyframe kf = new de.blight.game.animation.AnimKeyframe( - t, + de.blight.game.animation.AnimOffset off = new de.blight.game.animation.AnimOffset( spTX.getValue().floatValue(), spTY.getValue().floatValue(), spTZ.getValue().floatValue(), spRX.getValue().floatValue(), spRY.getValue().floatValue(), spRZ.getValue().floatValue()); - animSetKfList.add(kf); + animSetOffsetList.setAll(off); } else { - existing.time = t; existing.tx = spTX.getValue().floatValue(); existing.ty = spTY.getValue().floatValue(); existing.tz = spTZ.getValue().floatValue(); @@ -8624,8 +8608,7 @@ public class EditorApp extends Application { existing.ry = spRY.getValue().floatValue(); existing.rz = spRZ.getValue().floatValue(); } - animSetKfList.sort(java.util.Comparator.comparingDouble(k -> k.time)); - if (animSetKfListView != null) animSetKfListView.refresh(); + if (animSetOffsetListView != null) animSetOffsetListView.refresh(); animSetDirty = true; }); } @@ -8645,21 +8628,18 @@ public class EditorApp extends Application { } } - private void updateAnimPreviewKfOffset() { - if (animSetKfList == null || animSetKfList.isEmpty()) { - input.animPreviewKfTx = 0f; - input.animPreviewKfTy = 0f; - input.animPreviewKfTz = 0f; - input.animPreviewKfActive = false; + private void updateAnimPreviewOffset() { + if (animSetOffsetList == null || animSetOffsetList.isEmpty()) { + input.animPreviewOffsetTx = 0f; + input.animPreviewOffsetTy = 0f; + input.animPreviewOffsetTz = 0f; + input.animPreviewOffsetActive = false; } else { - de.blight.game.animation.AnimKeyframe first = animSetKfList.get(0); - for (de.blight.game.animation.AnimKeyframe k : animSetKfList) { - if (k.time < first.time) { first = k; } - } - input.animPreviewKfTx = first.tx; - input.animPreviewKfTy = first.ty; - input.animPreviewKfTz = first.tz; - input.animPreviewKfActive = true; + de.blight.game.animation.AnimOffset off = animSetOffsetList.get(0); + input.animPreviewOffsetTx = off.tx; + input.animPreviewOffsetTy = off.ty; + input.animPreviewOffsetTz = off.tz; + input.animPreviewOffsetActive = true; } } @@ -8768,21 +8748,25 @@ public class EditorApp extends Application { } } animSet.setActionMap(actionMap); - // Motion Keyframes: aktuell sichtbare Liste (des selektierten Clips) sichern, dann schreiben + // Anim-Offsets: aktuell sichtbaren Offset (des selektierten Clips) sichern, dann schreiben if (animSetClipListView != null) { String selClip = animSetClipListView.getSelectionModel().getSelectedItem(); if (selClip != null) { - animSetMotionKeyframes.put(selClip, new java.util.ArrayList<>(animSetKfList)); + if (!animSetOffsetList.isEmpty()) { + animSetOffsets.put(selClip, animSetOffsetList.get(0)); + } else { + animSetOffsets.remove(selClip); + } } } - java.util.Map> kfFinal = + java.util.Map offsetFinal = new java.util.LinkedHashMap<>(); - for (var kfEntry : animSetMotionKeyframes.entrySet()) { - if (kfEntry.getValue() != null && !kfEntry.getValue().isEmpty()) { - kfFinal.put(kfEntry.getKey(), kfEntry.getValue()); + for (var entry : animSetOffsets.entrySet()) { + if (entry.getValue() != null) { + offsetFinal.put(entry.getKey(), entry.getValue()); } } - animSet.setMotionKeyframes(kfFinal); + animSet.setAnimOffsets(offsetFinal); // Vorschau-Modell-Pfad beibehalten if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) { animSet.setPreviewModelPath(animSetModelCombo.getValue()); 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 82a6a9f..2d99511 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -572,11 +572,11 @@ public class SharedInput { animPreviewJointNames = new java.util.concurrent.atomic.AtomicReference<>(); /** JME3 → JavaFX: Länge des zuletzt gestarteten Clips in Sekunden (0 = unbekannt). */ public volatile float animPreviewCurrentClipDuration = 0f; - /** JavaFX → JME3: Motion-Keyframe-Vorschau-Versatz (erster KF, tx/ty/tz in Metern). */ - public volatile float animPreviewKfTx = 0f; - public volatile float animPreviewKfTy = 0f; - public volatile float animPreviewKfTz = 0f; - public volatile boolean animPreviewKfActive = false; + /** JavaFX → JME3: Anim-Offset-Vorschau (tx/ty/tz in Metern). */ + public volatile float animPreviewOffsetTx = 0f; + public volatile float animPreviewOffsetTy = 0f; + public volatile float animPreviewOffsetTz = 0f; + public volatile boolean animPreviewOffsetActive = false; /** * JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells. 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 82dd32e..8c86b0b 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 @@ -231,11 +231,11 @@ public class AnimPreviewState extends BaseAppState { axesNode.setLocalTranslation(Vector3f.ZERO); } - // Motion-Keyframe-Vorschau: Versatz auf Modell anwenden (TX/TY/TZ in Welt-Koordinaten) + // Anim-Offset-Vorschau: Versatz auf Modell anwenden (TX/TY/TZ in Welt-Koordinaten) if (currentModel != null) { - if (input.animPreviewKfActive) { + if (input.animPreviewOffsetActive) { currentModel.setLocalTranslation( - input.animPreviewKfTx, input.animPreviewKfTy, input.animPreviewKfTz); + input.animPreviewOffsetTx, input.animPreviewOffsetTy, input.animPreviewOffsetTz); } else { currentModel.setLocalTranslation(Vector3f.ZERO); } diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimKeyframe.java b/blight-game/src/main/java/de/blight/game/animation/AnimKeyframe.java deleted file mode 100644 index 5a115e5..0000000 --- a/blight-game/src/main/java/de/blight/game/animation/AnimKeyframe.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.blight.game.animation; - -/** - * Ein Keyframe für manuellen Positions-/Rotations-Versatz während einer blockierenden Animation. - * - * TX/TZ: Versatz in Charakter-lokalem Raum (TX=seitlich, TZ=vorwärts relativ zur Blickrichtung). - * TY: Versatz in Welt-Y (hoch/runter). - * RX/RY/RZ: Additiver Rotations-Versatz in Grad (Euler XYZ, relativ zur Startrotation). - * - * Keyframes einer Aktion werden nach {@code time} sortiert und linear interpoliert. - */ -public class AnimKeyframe { - - public float time; // Sekunden seit Animations-Start - public float tx, ty, tz; // Positions-Versatz (Meter) - public float rx, ry, rz; // Rotations-Versatz (Grad) - - public AnimKeyframe() {} - - public AnimKeyframe(float time, - float tx, float ty, float tz, - float rx, float ry, float rz) { - this.time = time; - this.tx = tx; this.ty = ty; this.tz = tz; - this.rx = rx; this.ry = ry; this.rz = rz; - } -} diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimSet.java b/blight-game/src/main/java/de/blight/game/animation/AnimSet.java index b0e1a94..9fd1c0d 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimSet.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimSet.java @@ -26,8 +26,8 @@ public class AnimSet { private Map actionMap = new LinkedHashMap<>(); /** Zuletzt im Editor verwendeter Modell-Pfad (relativ zu Assets-Root). Wird beim Öffnen auto-geladen. */ private String previewModelPath = null; - /** Manueller Positions-/Rotations-Versatz: Aktion → sortierte Keyframe-Liste. */ - private Map> motionKeyframes = new LinkedHashMap<>(); + /** Manueller Positions-/Rotations-Versatz pro Clip-Name. */ + private Map animOffsets = new LinkedHashMap<>(); public List getClips() { return clips; } public void setClips(List clips) { this.clips = clips; } @@ -35,11 +35,11 @@ public class AnimSet { public void setActionMap(Map actionMap) { this.actionMap = actionMap; } public String getPreviewModelPath() { return previewModelPath; } public void setPreviewModelPath(String previewModelPath) { this.previewModelPath = previewModelPath; } - public Map> getMotionKeyframes() { - return motionKeyframes != null ? motionKeyframes : new LinkedHashMap<>(); + public Map getAnimOffsets() { + return animOffsets != null ? animOffsets : new LinkedHashMap<>(); } - public void setMotionKeyframes(Map> motionKeyframes) { - this.motionKeyframes = motionKeyframes; + public void setAnimOffsets(Map animOffsets) { + this.animOffsets = animOffsets; } /** Speichert dieses Set als {@code .animset.json} im Verzeichnis {@code setDir}. */ diff --git a/blight-game/src/main/java/de/blight/game/animation/FootIKControl.java b/blight-game/src/main/java/de/blight/game/animation/FootIKControl.java deleted file mode 100644 index f4d9496..0000000 --- a/blight-game/src/main/java/de/blight/game/animation/FootIKControl.java +++ /dev/null @@ -1,226 +0,0 @@ -package de.blight.game.animation; - -import com.jme3.anim.Armature; -import com.jme3.anim.Joint; -import com.jme3.math.FastMath; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; -import com.jme3.renderer.RenderManager; -import com.jme3.renderer.ViewPort; -import com.jme3.scene.Spatial; -import com.jme3.scene.control.AbstractControl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 2-Bone-Foot-IK – hält die Füße an ihrer World-Space-Position fest. - * - * Muss NACH AnimComposer und SkinningControl zur Spatial hinzugefügt werden, - * damit controlUpdate() nach der Animations-Anwendung läuft. - */ -public final class FootIKControl extends AbstractControl { - - private static final Logger log = LoggerFactory.getLogger(FootIKControl.class); - - private final Armature armature; - - private final Joint leftThigh, leftCalf, leftFoot; - private final Joint rightThigh, rightCalf, rightFoot; - - // IK-Ziele in World-Space; null = IK inaktiv - private Vector3f leftWorldTarget = null; - private Vector3f rightWorldTarget = null; - - private static final float MIN_CORRECTION_DIST = 0.005f; - - private int debugFramesLeft = 0; - - public FootIKControl(Armature armature) { - this.armature = armature; - leftThigh = findJoint(armature, "L_Thigh", "LeftUpLeg", "mixamorig:LeftUpLeg"); - leftCalf = findJoint(armature, "L_Calf", "LeftLeg", "mixamorig:LeftLeg"); - leftFoot = findJoint(armature, "L_Foot", "LeftFoot", "mixamorig:LeftFoot"); - rightThigh = findJoint(armature, "R_Thigh", "RightUpLeg", "mixamorig:RightUpLeg"); - rightCalf = findJoint(armature, "R_Calf", "RightLeg", "mixamorig:RightLeg"); - rightFoot = findJoint(armature, "R_Foot", "RightFoot", "mixamorig:RightFoot"); - log.info("[FootIK] Init: L={} R={}", - leftFoot != null ? leftFoot.getName() : "n/a", - rightFoot != null ? rightFoot.getName() : "n/a"); - } - - /** - * Fixiert die Füße an ihrer aktuellen World-Space-Position. - * Muss aufgerufen werden NACHDEM die neue Animation gesetzt wurde, - * aber BEVOR der kfOffset die Armature verschoben hat. - */ - public void lockFeetAtCurrentWorldPos() { - Spatial sp = getSpatial(); - if (sp == null || leftFoot == null || rightFoot == null) { - return; - } - leftWorldTarget = sp.localToWorld( - leftFoot.getModelTransform().getTranslation().clone(), new Vector3f()); - rightWorldTarget = sp.localToWorld( - rightFoot.getModelTransform().getTranslation().clone(), new Vector3f()); - debugFramesLeft = 10; - log.info("[FootIK] LockWorld: L={} R={}", fv(leftWorldTarget), fv(rightWorldTarget)); - } - - public void releaseFeet() { - leftWorldTarget = null; - rightWorldTarget = null; - debugFramesLeft = 0; - log.info("[FootIK] Freigegeben"); - } - - public boolean isActive() { - return leftWorldTarget != null || rightWorldTarget != null; - } - - // ── Control-Update ──────────────────────────────────────────────────────── - - @Override - protected void controlUpdate(float tpf) { - if (leftWorldTarget == null && rightWorldTarget == null) { - return; - } - - Spatial sp = getSpatial(); - if (sp == null) { - return; - } - - boolean dbg = debugFramesLeft > 0; - if (dbg) { - debugFramesLeft--; - } - - if (leftWorldTarget != null && leftThigh != null && leftCalf != null && leftFoot != null) { - Vector3f modelTarget = sp.worldToLocal(leftWorldTarget, new Vector3f()); - solveLeg(leftThigh, leftCalf, leftFoot, modelTarget, dbg, "L"); - } - if (rightWorldTarget != null && rightThigh != null && rightCalf != null && rightFoot != null) { - Vector3f modelTarget = sp.worldToLocal(rightWorldTarget, new Vector3f()); - solveLeg(rightThigh, rightCalf, rightFoot, modelTarget, dbg, "R"); - } - - armature.update(); - } - - @Override - protected void controlRender(RenderManager rm, ViewPort vp) {} - - // ── 2-Bone-IK-Solver ───────────────────────────────────────────────────── - - private void solveLeg(Joint thigh, Joint calf, Joint foot, - Vector3f target, boolean dbg, String side) { - Vector3f A = thigh.getModelTransform().getTranslation().clone(); - Vector3f B = calf.getModelTransform().getTranslation().clone(); - Vector3f C = foot.getModelTransform().getTranslation().clone(); - - float L1 = A.distance(B); - float L2 = B.distance(C); - if (L1 < 0.0001f || L2 < 0.0001f) { - return; - } - - float footErr = C.distance(target); - if (dbg) { - log.info("[FootIK][{}] A={} B={} C={} T={} err={} L1={} L2={}", - side, fv(A), fv(B), fv(C), fv(target), - String.format("%.4f", footErr), - String.format("%.3f", L1), String.format("%.3f", L2)); - } - if (footErr < MIN_CORRECTION_DIST) { - return; - } - - float d = A.distance(target); - d = FastMath.clamp(d, Math.abs(L1 - L2) + 0.0001f, L1 + L2 - 0.0001f); - - float cosA = (L1 * L1 + d * d - L2 * L2) / (2f * L1 * d); - float angleA = FastMath.acos(FastMath.clamp(cosA, -1f, 1f)); - - Vector3f dirAT = target.subtract(A).normalizeLocal(); - Vector3f kneeDir = B.subtract(A).normalizeLocal(); - float proj = dirAT.dot(kneeDir); - Vector3f perp = kneeDir.subtract(dirAT.mult(proj)); - if (perp.lengthSquared() < 0.0001f) { - perp = findPerp(dirAT); - } - perp.normalizeLocal(); - - Vector3f B_new = A.add( - dirAT.mult(L1 * FastMath.cos(angleA)).addLocal( - perp.mult(L1 * FastMath.sin(angleA))) - ); - - // Oberschenkel rotieren - Vector3f curThighDir = B.subtract(A).normalizeLocal(); - Vector3f dstThighDir = B_new.subtract(A).normalizeLocal(); - Quaternion thighArc = rotArc(curThighDir, dstThighDir); - Quaternion thighModelRot = thigh.getModelTransform().getRotation().clone(); - Quaternion newThighModelRot = thighArc.mult(thighModelRot); - Joint thighParent = thigh.getParent(); - Quaternion parentRot = thighParent != null - ? thighParent.getModelTransform().getRotation() : new Quaternion(); - thigh.setLocalRotation(parentRot.inverse().mult(newThighModelRot)); - - // Unterschenkel rotieren — Calf-Richtung NACH der Thigh-Rotation verwenden - Vector3f curCalfDirOrig = C.subtract(B).normalizeLocal(); - Vector3f curCalfDirRotated = thighArc.mult(curCalfDirOrig); - Vector3f dstCalfDir = target.subtract(B_new).normalizeLocal(); - Quaternion calfArc = rotArc(curCalfDirRotated, dstCalfDir); - Quaternion oldCalfLocalRot = calf.getLocalRotation().clone(); - Quaternion actualCalfModel = newThighModelRot.mult(oldCalfLocalRot); - Quaternion newCalfModelRot = calfArc.mult(actualCalfModel); - calf.setLocalRotation(newThighModelRot.inverse().mult(newCalfModelRot)); - - if (dbg) { - float thighAngle = thighArc.toAngleAxis(new Vector3f()) * FastMath.RAD_TO_DEG; - float calfAngle = calfArc.toAngleAxis(new Vector3f()) * FastMath.RAD_TO_DEG; - log.info("[FootIK][{}] B_new={} thigh={}° calf={}°", - side, fv(B_new), - String.format("%.1f", thighAngle), - String.format("%.1f", calfAngle)); - } - } - - // ── Hilfsmethoden ──────────────────────────────────────────────────────── - - private static Quaternion rotArc(Vector3f from, Vector3f to) { - float dot = FastMath.clamp(from.dot(to), -1f, 1f); - if (dot > 0.9999f) { - return new Quaternion(); - } - if (dot < -0.9999f) { - return new Quaternion().fromAngleAxis(FastMath.PI, findPerp(from)); - } - Vector3f axis = from.cross(to).normalizeLocal(); - Quaternion q = new Quaternion(); - q.fromAngleAxis(FastMath.acos(dot), axis); - return q; - } - - private static Vector3f findPerp(Vector3f v) { - Vector3f p = v.cross(Vector3f.UNIT_X); - if (p.lengthSquared() < 0.0001f) { - p = v.cross(Vector3f.UNIT_Z); - } - return p.normalizeLocal(); - } - - private static Joint findJoint(Armature arm, String... names) { - for (String n : names) { - Joint j = arm.getJoint(n); - if (j != null) { - return j; - } - } - return null; - } - - private static String fv(Vector3f v) { - return String.format("(%.3f,%.3f,%.3f)", v.x, v.y, v.z); - } -} 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 3c514ae..8628a86 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,7 +9,7 @@ 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.AnimOffset; import de.blight.game.animation.AnimSet; import de.blight.game.animation.AnimationAction; import de.blight.game.animation.AnimationLibrary; @@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory; import java.nio.file.Path; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; public class PlayerInputControl { @@ -58,20 +57,21 @@ public class PlayerInputControl { private com.jme3.anim.SkinningControl skinningControl = null; - // ── Motion-Keyframe-Offsets ─────────────────────────────────────────────── - private Map> motionKeyframes = new LinkedHashMap<>(); + // ── Anim-Offsets ───────────────────────────────────────────────────────── + private Map animOffsets = 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; + private final Vector3f animOffsetCurrent = new Vector3f(); + private final Vector3f animOffsetTarget = new Vector3f(); + private float animOffsetSpeed = 0f; // ── 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 int groundGraceFrames = 0; + private double nextTransitionLength = 0.0; private boolean pickupActive = false; private float pickupRemaining = 0f; @@ -146,11 +146,11 @@ public class PlayerInputControl { } try { AnimSet as = AnimSet.load(assetRoot.resolve("animations/sets"), animSetName); - motionKeyframes = as.getMotionKeyframes(); - log.info("[AnimCtx] {} Motion-KF-Einträge geladen.", motionKeyframes.size()); + animOffsets = as.getAnimOffsets(); + log.info("[AnimCtx] {} Anim-Offset-Einträge geladen.", animOffsets.size()); } catch (Exception e) { log.warn("[AnimCtx] AnimSet-KF nicht ladbar: {}", e.getMessage()); - motionKeyframes = new LinkedHashMap<>(); + animOffsets = new LinkedHashMap<>(); } // Navigator (neu) aufbauen sobald alle Abhängigkeiten bereit sind if (navPathFinder != null && navTerrain != null && physicsChar != null && visual != null) { @@ -219,7 +219,7 @@ public class PlayerInputControl { duration = resolveClipLength(action, 1.5f); } blockingAnimActive = true; - blockingAnimRemaining = duration + (1f / 60f); + blockingAnimRemaining = duration; blockingAnimTotal = duration; blockingAnimCallback = onComplete; autopilotDir = null; @@ -310,7 +310,7 @@ public class PlayerInputControl { forward = backward = left = right = false; if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); autopilotDir = null; - clearMotionKfOffset(); + clearAnimOffset(); if (arriveRadius > 0f) { navigator.navigateTo(target, speed, arriveRadius, onArrival, onFailed); } else { @@ -350,17 +350,17 @@ public class PlayerInputControl { public void update(float tpf) { if (physicsChar == null) return; - if (visual != null && kfOffsetSpeed > 0f) { - Vector3f delta = kfOffsetTarget.subtract(kfOffsetCurrent); + if (visual != null && animOffsetSpeed > 0f) { + Vector3f delta = animOffsetTarget.subtract(animOffsetCurrent); float dist = delta.length(); - float step = kfOffsetSpeed * tpf; + float step = animOffsetSpeed * tpf; if (dist <= step) { - kfOffsetCurrent.set(kfOffsetTarget); - kfOffsetSpeed = 0f; + animOffsetCurrent.set(animOffsetTarget); + animOffsetSpeed = 0f; } else { - kfOffsetCurrent.addLocal(delta.normalizeLocal().multLocal(step)); + animOffsetCurrent.addLocal(delta.normalizeLocal().multLocal(step)); } - visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(kfOffsetCurrent)); + visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent)); } if (paused) { @@ -485,9 +485,10 @@ public class PlayerInputControl { // Animations-Auswahl if (jumpFrames > 0) jumpFrames--; + if (groundGraceFrames > 0) groundGraceFrames--; AnimationAction target; - if (jumpFrames > 0 || !physicsChar.onGround()) { + if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) { target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP; } else if (moving) { target = walk ? AnimationAction.WALK @@ -540,36 +541,66 @@ public class PlayerInputControl { return null; } - private void applyMotionKfOffset(String clip) { + private void applyAnimOffset(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); + AnimOffset off = (clip != null) ? animOffsets.get(clip) : null; + if (off == null) { clearAnimOffset(); return; } Quaternion facing = visual.getLocalRotation().clone(); - Vector3f worldOffset = facing.mult(new Vector3f(kf.tx, kf.ty, kf.tz)); - kfOffsetTarget.set(worldOffset); - if (kfOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) { - kfOffsetSpeed = 5.0f; + Vector3f worldOffset = facing.mult(new Vector3f(off.tx, off.ty, off.tz)); + animOffsetTarget.set(worldOffset); + if (animOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) { + animOffsetSpeed = 5.0f; } - log.info("[KF] Clip '{}' → Offset-Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z); + log.info("[AnimOffset] Clip '{}' → Ziel ({},{},{})", 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; + public void clearAnimOffset() { + log.info("[AnimOffset] clearAnimOffset() → Lerp zu 0, current=({},{},{})", animOffsetCurrent.x, animOffsetCurrent.y, animOffsetCurrent.z); + animOffsetTarget.set(0, 0, 0); + animOffsetSpeed = 5.0f; } - private void clearMotionKfOffset() { - clearKfOffset(); + /** + * Setzt den Visual-Versatz sofort (kein Lerp), ohne den Physik-Körper zu bewegen. + * tx/ty/tz in charakter-lokalem Raum (tz = vorwärts/rückwärts in Blickrichtung). + */ + public void setAnimOffsetInstant(float tx, float ty, float tz) { + if (visual == null) return; + Quaternion facing = visual.getLocalRotation().clone(); + Vector3f worldOffset = facing.mult(new Vector3f(tx, ty, tz)); + animOffsetTarget.set(worldOffset); + animOffsetCurrent.set(worldOffset); + animOffsetSpeed = 0f; + visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent)); + log.info("[AnimOffset] Instant: ({},{},{}) → world ({},{},{})", tx, ty, tz, worldOffset.x, worldOffset.y, worldOffset.z); + } + + /** Setzt den Visual-Versatz sofort auf 0 zurück (kein Lerp). */ + public void clearAnimOffsetInstant() { + animOffsetTarget.set(0, 0, 0); + animOffsetCurrent.set(0, 0, 0); + animOffsetSpeed = 0f; + if (visual != null) { + visual.setLocalTranslation(visualBaseTranslation.clone()); + } + log.info("[AnimOffset] Instant-Clear"); + } + + /** Verhindert für {@code frames} Frames, dass die JUMP-Animation durch kurzes onGround=false nach Teleport ausgelöst wird. */ + public void setGroundGrace(int frames) { + groundGraceFrames = frames; + } + + /** Setzt eine einmalige Überblend-Zeit für den nächsten Animations-Wechsel (sanfter Skelett-Übergang). */ + public void setNextAnimTransition(double seconds) { + nextTransitionLength = seconds; } /** 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; + AnimOffset off = animOffsets.get("sitting"); + if (off == null) return 0.25f; + return Math.abs(off.tz) > 0.01f ? Math.abs(off.tz) : 0.25f; } private boolean tryPlay(String clip) { @@ -577,6 +608,16 @@ public class PlayerInputControl { log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip); return false; } + // Optionale Überblend-Zeit (einmalig, wird nach Verwendung auf 0 zurückgesetzt) + double transLen = nextTransitionLength; + nextTransitionLength = 0.0; + if (transLen > 0.0) { + com.jme3.anim.tween.action.Action nextAction = animComposer.action(clip); + if (nextAction instanceof com.jme3.anim.tween.action.BlendableAction) { + ((com.jme3.anim.tween.action.BlendableAction) nextAction).setTransitionLength(transLen); + log.info("[Anim] Transition {} → '{}' über {:.2f}s", runningClip, clip, transLen); + } + } // Erst Action setzen, DANN SkinningControl aktivieren – // vermeidet 1 Frame in Bind-Pose × Armature-Rx90° = liegender Charakter. com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip); @@ -589,7 +630,7 @@ public class PlayerInputControl { log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip); } runningClip = clip; - applyMotionKfOffset(clip); + applyAnimOffset(clip); return true; } } 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 9504734..64f0a8f 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 @@ -9,6 +9,7 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.input.controls.MouseButtonTrigger; import com.jme3.math.Vector3f; +import com.jme3.scene.Spatial; import de.blight.common.PlacedModel; import de.blight.common.PlacedModelIO; import de.blight.common.model.Bed; @@ -53,17 +54,20 @@ public class WorldInteractableState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(WorldInteractableState.class); - private static final float BENCH_RANGE = 5f; - private static final float BED_RANGE = 6f; - private static final float WALK_TIMEOUT = 12f; + private static final float BENCH_RANGE = 5f; + private static final float BED_RANGE = 6f; + private static final float WALK_TIMEOUT = 12f; + private static final float BENCH_SIT_MOVE_DIST = 0.5f; // 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; + private String benchPendingId = null; + private float benchPendingX = 0f; + private float benchPendingZ = 0f; + private float benchPendingTimer = 0f; + /** Blickrichtung des Charakters beim Hinsetzen (weg von der Bank); für Positions-Versatz nach Anim. */ + private Vector3f currentBenchSitDir = null; // ── Abhängigkeiten ──────────────────────────────────────────────────────── @@ -264,6 +268,9 @@ public class WorldInteractableState extends BaseAppState { InteractableEntry entry = entries.get(targetIdx); float rotY = getSitFacingRotY(entry); Vector3f sitDir = new Vector3f((float) Math.cos(rotY), 0f, (float) Math.sin(rotY)); + if (isBench(entry)) { + currentBenchSitDir = sitDir.clone(); + } playerInput.requestTurn(sitDir, 0.35f, () -> startSitAnim(entry)); } @@ -289,8 +296,20 @@ public class WorldInteractableState extends BaseAppState { AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING; playerInput.requestAnimation(downAction, 0f, () -> { - if (!isBench(entry)) snapToSitPos(entry); // Bank: Physik bleibt am Approach-Punkt + if (isBench(entry) && currentBenchSitDir != null) { + Vector3f cur = physicsChar.getPhysicsLocation(); + Vector3f delta = currentBenchSitDir.mult(-BENCH_SIT_MOVE_DIST); + physicsChar.setPhysicsLocation(cur.add(delta)); + // Sofortiges Spatial-Update verhindert 1-Frame-Kamerazuckeln + Spatial phySpatial = physicsChar.getSpatial(); + if (phySpatial != null) { + phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta)); + } + log.info("[WorldInteractable] Charakter 50cm zur Bank verschoben."); + } + if (!isBench(entry)) snapToSitPos(entry); playerInput.lockInPlace(); + if (isBench(entry)) playerInput.setNextAnimTransition(0.2); playerInput.playLockedAnimation(idleAction); phase = Phase.RESTING; log.info("[WorldInteractable] Ruhezustand: {}", entry.type()); @@ -328,7 +347,21 @@ public class WorldInteractableState extends BaseAppState { playerInput.requestAnimation(upAction, 0f, () -> { if (isBench(entry)) { - playerInput.clearKfOffset(); + // Nach stand_up_bench: Charakter 50cm von Bank wegbewegen + if (currentBenchSitDir != null) { + Vector3f cur = physicsChar.getPhysicsLocation(); + Vector3f delta = currentBenchSitDir.mult(BENCH_SIT_MOVE_DIST); + physicsChar.setPhysicsLocation(cur.add(delta)); + Spatial phySpatial = physicsChar.getSpatial(); + if (phySpatial != null) { + phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta)); + } + currentBenchSitDir = null; + playerInput.setGroundGrace(4); + playerInput.setNextAnimTransition(0.2); + log.info("[WorldInteractable] Charakter 50cm von Bank wegbewegt."); + } + playerInput.clearAnimOffset(); benchPendingId = entry.interactableId(); benchPendingX = entry.worldX(); benchPendingZ = entry.worldZ();