Animationen jetzt heil und Keyframes eingebaut
This commit is contained in:
Binary file not shown.
@@ -6,13 +6,13 @@
|
|||||||
"pickup",
|
"pickup",
|
||||||
"running",
|
"running",
|
||||||
"running_jump",
|
"running_jump",
|
||||||
"sit_down",
|
|
||||||
"sitting",
|
"sitting",
|
||||||
"sitting_floor",
|
"sitting_floor",
|
||||||
"sprinting",
|
"sprinting",
|
||||||
"stand_up",
|
"stand_up",
|
||||||
"tpose",
|
"tpose",
|
||||||
"walking"
|
"walking",
|
||||||
|
"sit_down_bench"
|
||||||
],
|
],
|
||||||
"actionMap": {
|
"actionMap": {
|
||||||
"DEFAULT": "tpose",
|
"DEFAULT": "tpose",
|
||||||
@@ -23,11 +23,22 @@
|
|||||||
"RUNNING_JUMP": "running_jump",
|
"RUNNING_JUMP": "running_jump",
|
||||||
"JUMP": "idle_jump",
|
"JUMP": "idle_jump",
|
||||||
"PICK_UP": "pickup",
|
"PICK_UP": "pickup",
|
||||||
"SIT_DOWN": "sit_down",
|
|
||||||
"SIT_UP": "stand_up",
|
"SIT_UP": "stand_up",
|
||||||
"SITTING": "sitting"
|
"SITTING": "sitting",
|
||||||
|
"SIT_DOWN": "sit_down_bench"
|
||||||
},
|
},
|
||||||
"previewModelPath": "Models/Chars/mainchar.j3o",
|
"previewModelPath": "Models/Chars/mainchar.j3o",
|
||||||
"sinkMap": {},
|
"motionKeyframes": {
|
||||||
"anchorBoneMap": {}
|
"sit_down_bench": [
|
||||||
|
{
|
||||||
|
"time": 0.0,
|
||||||
|
"tx": 0.0,
|
||||||
|
"ty": 0.0,
|
||||||
|
"tz": 0.25,
|
||||||
|
"rx": 0.0,
|
||||||
|
"ry": 0.0,
|
||||||
|
"rz": 0.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -152,15 +152,17 @@ public class EditorApp extends Application {
|
|||||||
// AnimSet-Editor
|
// AnimSet-Editor
|
||||||
private ListView<String> animSetClipListView;
|
private ListView<String> animSetClipListView;
|
||||||
private ListView<String> animSetActionListView;
|
private ListView<String> animSetActionListView;
|
||||||
private ListView<String> animSetSinkListView;
|
|
||||||
private ListView<String> animSetAnchorBoneListView;
|
|
||||||
private String animSetPendingPlayClip = null;
|
private String animSetPendingPlayClip = null;
|
||||||
private ComboBox<String> animSetModelCombo;
|
private ComboBox<String> animSetModelCombo;
|
||||||
private boolean animSetDirty = false;
|
private boolean animSetDirty = false;
|
||||||
private String animSetCurrentName = null;
|
private String animSetCurrentName = null;
|
||||||
private Path animSetCurrentDir = null;
|
private Path animSetCurrentDir = null;
|
||||||
private java.util.List<String> animJointNames = new java.util.ArrayList<>();
|
// Motion-Keyframe-Editor (innerhalb AnimSet-Editor)
|
||||||
private Label animSetBonesLabel;
|
private javafx.scene.control.ListView<de.blight.game.animation.AnimKeyframe> animSetKfListView;
|
||||||
|
private javafx.collections.ObservableList<de.blight.game.animation.AnimKeyframe> animSetKfList =
|
||||||
|
javafx.collections.FXCollections.observableArrayList();
|
||||||
|
private java.util.Map<String, java.util.List<de.blight.game.animation.AnimKeyframe>>
|
||||||
|
animSetMotionKeyframes = new java.util.LinkedHashMap<>();
|
||||||
|
|
||||||
// Character-Editor-Zustand
|
// Character-Editor-Zustand
|
||||||
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
||||||
@@ -453,19 +455,6 @@ public class EditorApp extends Application {
|
|||||||
animClipListView.getItems().setAll(newClips);
|
animClipListView.getItems().setAll(newClips);
|
||||||
if (!newClips.isEmpty()) animClipListView.getSelectionModel().selectFirst();
|
if (!newClips.isEmpty()) animClipListView.getSelectionModel().selectFirst();
|
||||||
}
|
}
|
||||||
java.util.List<String> newJoints = input.animPreviewJointNames.getAndSet(null);
|
|
||||||
if (newJoints != null) {
|
|
||||||
animJointNames = new java.util.ArrayList<>(newJoints);
|
|
||||||
if (animSetBonesLabel != null) {
|
|
||||||
if (animJointNames.isEmpty()) {
|
|
||||||
animSetBonesLabel.setText("Kein Armature gefunden");
|
|
||||||
animSetBonesLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #c66;");
|
|
||||||
} else {
|
|
||||||
animSetBonesLabel.setText(animJointNames.size() + " Joints geladen");
|
|
||||||
animSetBonesLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #6a6;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// AnimSet-Editor: nach Clip-Load automatisch abspielen
|
// AnimSet-Editor: nach Clip-Load automatisch abspielen
|
||||||
if (newClips != null && animSetPendingPlayClip != null) {
|
if (newClips != null && animSetPendingPlayClip != null) {
|
||||||
input.animPreviewPlayClip = animSetPendingPlayClip;
|
input.animPreviewPlayClip = animSetPendingPlayClip;
|
||||||
@@ -8276,7 +8265,25 @@ public class EditorApp extends Application {
|
|||||||
removeClipBtn.setDisable(true);
|
removeClipBtn.setDisable(true);
|
||||||
|
|
||||||
animSetClipListView.getSelectionModel().selectedItemProperty()
|
animSetClipListView.getSelectionModel().selectedItemProperty()
|
||||||
.addListener((obs, ov, nv) -> removeClipBtn.setDisable(nv == null));
|
.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));
|
||||||
|
}
|
||||||
|
// Keyframes des neuen Clips laden
|
||||||
|
if (animSetKfList != null) {
|
||||||
|
animSetKfList.clear();
|
||||||
|
if (nv != null) {
|
||||||
|
var kfs = animSetMotionKeyframes.getOrDefault(nv, new java.util.ArrayList<>());
|
||||||
|
animSetKfList.setAll(kfs);
|
||||||
|
// Clip direkt in Vorschau abspielen
|
||||||
|
input.animPreviewPlayClip = nv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// KF-Bereich aktivieren/deaktivieren
|
||||||
|
if (animSetKfListView != null) animSetKfListView.setDisable(nv == null);
|
||||||
|
});
|
||||||
|
|
||||||
addClipBtn.setOnAction(e -> {
|
addClipBtn.setOnAction(e -> {
|
||||||
org.slf4j.Logger _log = org.slf4j.LoggerFactory.getLogger("AnimSetClipScan");
|
org.slf4j.Logger _log = org.slf4j.LoggerFactory.getLogger("AnimSetClipScan");
|
||||||
@@ -8395,196 +8402,72 @@ public class EditorApp extends Application {
|
|||||||
HBox.setHgrow(removeActionBtn, Priority.ALWAYS);
|
HBox.setHgrow(removeActionBtn, Priority.ALWAYS);
|
||||||
inner.getChildren().addAll(animSetActionListView, actionBtns);
|
inner.getChildren().addAll(animSetActionListView, actionBtns);
|
||||||
|
|
||||||
// ── Bone-Anchoring ────────────────────────────────────────────────────
|
// ── Motion Keyframes ──────────────────────────────────────────────────
|
||||||
inner.getChildren().addAll(new Separator(), sectionTitle("Bone-Anchoring"), new Separator());
|
inner.getChildren().addAll(new Separator(), sectionTitle("Motion Keyframes"), new Separator());
|
||||||
|
|
||||||
Label anchorHint = new Label("Pro Aktion: Knochen angeben, der auf seiner Welt-Y fixiert bleibt (z. B. SIT_DOWN → foot.l). Hat Vorrang vor manuellem Sink.");
|
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).");
|
||||||
anchorHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
kfHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
||||||
anchorHint.setWrapText(true);
|
kfHint.setWrapText(true);
|
||||||
animSetBonesLabel = new Label(animJointNames.isEmpty() ? "Kein Modell geladen" : animJointNames.size() + " Joints geladen");
|
|
||||||
animSetBonesLabel.setStyle("-fx-font-size: 10; -fx-text-fill: " + (animJointNames.isEmpty() ? "#888;" : "#6a6;"));
|
|
||||||
|
|
||||||
animSetAnchorBoneListView = new ListView<>();
|
// Keyframes aus AnimSet laden
|
||||||
animSetAnchorBoneListView.setPrefHeight(110);
|
animSetMotionKeyframes = new java.util.LinkedHashMap<>();
|
||||||
if (animSet.getAnchorBoneMap() != null) {
|
for (var entry : animSet.getMotionKeyframes().entrySet()) {
|
||||||
for (var e2 : animSet.getAnchorBoneMap().entrySet()) {
|
animSetMotionKeyframes.put(entry.getKey(), new java.util.ArrayList<>(entry.getValue()));
|
||||||
animSetAnchorBoneListView.getItems().add(e2.getKey() + " → " + e2.getValue());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
animSetKfList = javafx.collections.FXCollections.observableArrayList();
|
||||||
|
animSetKfList.addListener((javafx.collections.ListChangeListener<de.blight.game.animation.AnimKeyframe>)
|
||||||
|
change -> updateAnimPreviewKfOffset());
|
||||||
|
|
||||||
Button addAnchorBtn = new Button("+ Hinzufügen…");
|
// ListView mit Zusammenfassung, Doppelklick öffnet Edit-Dialog
|
||||||
Button removeAnchorBtn = new Button("- Entfernen");
|
animSetKfListView = new javafx.scene.control.ListView<>(animSetKfList);
|
||||||
addAnchorBtn.setMaxWidth(Double.MAX_VALUE);
|
animSetKfListView.setPrefHeight(150);
|
||||||
removeAnchorBtn.setMaxWidth(Double.MAX_VALUE);
|
animSetKfListView.setPlaceholder(new Label("Keine Keyframes – [+ Keyframe] zum Hinzufügen"));
|
||||||
removeAnchorBtn.setDisable(true);
|
animSetKfListView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() {
|
||||||
animSetAnchorBoneListView.getSelectionModel().selectedItemProperty()
|
@Override protected void updateItem(de.blight.game.animation.AnimKeyframe kf, boolean empty) {
|
||||||
.addListener((obs, ov, nv) -> removeAnchorBtn.setDisable(nv == null));
|
super.updateItem(kf, empty);
|
||||||
|
if (empty || kf == null) { setText(null); return; }
|
||||||
addAnchorBtn.setOnAction(e -> {
|
setText(String.format("%.3fs | TX%+.3f TY%+.3f TZ%+.3f | RX%+.1f° RY%+.1f° RZ%+.1f°",
|
||||||
// Pending joint names aus JME3-Thread abholen (falls Timer sie noch nicht konsumiert hat)
|
kf.time, kf.tx, kf.ty, kf.tz, kf.rx, kf.ry, kf.rz));
|
||||||
java.util.List<String> fresh = input.animPreviewJointNames.getAndSet(null);
|
|
||||||
if (fresh != null) {
|
|
||||||
animJointNames = new java.util.ArrayList<>(fresh);
|
|
||||||
if (animSetBonesLabel != null) {
|
|
||||||
animSetBonesLabel.setText(animJointNames.isEmpty() ? "Kein Armature gefunden" : animJointNames.size() + " Joints geladen");
|
|
||||||
animSetBonesLabel.setStyle("-fx-font-size: 10; -fx-text-fill: " + (animJointNames.isEmpty() ? "#c66;" : "#6a6;"));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
ComboBox<de.blight.game.animation.AnimationAction> anchorActionCombo = new ComboBox<>();
|
|
||||||
anchorActionCombo.getItems().addAll(de.blight.game.animation.AnimationAction.values());
|
|
||||||
javafx.util.Callback<javafx.scene.control.ListView<de.blight.game.animation.AnimationAction>,
|
|
||||||
javafx.scene.control.ListCell<de.blight.game.animation.AnimationAction>> acf =
|
|
||||||
lv -> new javafx.scene.control.ListCell<>() {
|
|
||||||
@Override protected void updateItem(de.blight.game.animation.AnimationAction it, boolean empty) {
|
|
||||||
super.updateItem(it, empty);
|
|
||||||
setText(empty || it == null ? null : it.displayName() + " (" + it.name() + ")");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
anchorActionCombo.setCellFactory(acf);
|
|
||||||
anchorActionCombo.setButtonCell(acf.call(null));
|
|
||||||
anchorActionCombo.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
anchorActionCombo.getSelectionModel().selectFirst();
|
|
||||||
|
|
||||||
// Joint-Auswahl: ComboBox mit geladenen Namen, editierbar als Fallback
|
|
||||||
ComboBox<String> boneCombo = new ComboBox<>();
|
|
||||||
boneCombo.setEditable(true);
|
|
||||||
boneCombo.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
if (animJointNames.isEmpty()) {
|
|
||||||
boneCombo.setPromptText("Joint-Name (erst Modell laden)");
|
|
||||||
} else {
|
|
||||||
boneCombo.getItems().addAll(animJointNames);
|
|
||||||
boneCombo.setPromptText("Joint auswählen…");
|
|
||||||
}
|
|
||||||
|
|
||||||
javafx.scene.layout.GridPane anchorGrid = new javafx.scene.layout.GridPane();
|
|
||||||
anchorGrid.setHgap(8); anchorGrid.setVgap(6);
|
|
||||||
anchorGrid.add(new Label("Aktion:"), 0, 0); anchorGrid.add(anchorActionCombo, 1, 0);
|
|
||||||
anchorGrid.add(new Label("Joint-Name:"), 0, 1); anchorGrid.add(boneCombo, 1, 1);
|
|
||||||
javafx.scene.layout.ColumnConstraints anchorCc = new javafx.scene.layout.ColumnConstraints();
|
|
||||||
anchorCc.setHgrow(Priority.ALWAYS);
|
|
||||||
anchorGrid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), anchorCc);
|
|
||||||
|
|
||||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> anchorDlg = new javafx.scene.control.Dialog<>();
|
|
||||||
anchorDlg.setTitle("Bone-Anchoring konfigurieren");
|
|
||||||
javafx.scene.control.ButtonType okAnchor = new javafx.scene.control.ButtonType("Setzen",
|
|
||||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
|
||||||
anchorDlg.getDialogPane().getButtonTypes().addAll(okAnchor, javafx.scene.control.ButtonType.CANCEL);
|
|
||||||
anchorDlg.getDialogPane().setContent(anchorGrid);
|
|
||||||
anchorDlg.showAndWait().ifPresent(bt -> {
|
|
||||||
if (bt != okAnchor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var selAction = anchorActionCombo.getValue();
|
|
||||||
String bone = boneCombo.getEditor().getText();
|
|
||||||
if (selAction == null || bone == null || bone.isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String newEntry = selAction.name() + " → " + bone.trim();
|
|
||||||
animSetAnchorBoneListView.getItems().removeIf(it -> it.startsWith(selAction.name() + " → "));
|
|
||||||
animSetAnchorBoneListView.getItems().add(newEntry);
|
|
||||||
animSetDirty = true;
|
|
||||||
});
|
});
|
||||||
|
animSetKfListView.setOnMouseClicked(ev -> {
|
||||||
|
if (ev.getClickCount() == 2) {
|
||||||
|
de.blight.game.animation.AnimKeyframe sel =
|
||||||
|
animSetKfListView.getSelectionModel().getSelectedItem();
|
||||||
|
if (sel != null) showAnimKfDialog(sel);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
removeAnchorBtn.setOnAction(e -> {
|
// initial deaktiviert – wird durch Clip-Selektion gesteuert
|
||||||
String sel = animSetAnchorBoneListView.getSelectionModel().getSelectedItem();
|
animSetKfListView.setDisable(true);
|
||||||
|
|
||||||
|
Button addKfBtn = new Button("+ Keyframe");
|
||||||
|
Button removeKfBtn = new Button("- Entfernen");
|
||||||
|
addKfBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
removeKfBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
addKfBtn.setDisable(true);
|
||||||
|
removeKfBtn.setDisable(true);
|
||||||
|
animSetKfListView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, ov, nv) -> removeKfBtn.setDisable(nv == null));
|
||||||
|
// addKfBtn folgt Clip-Selektion
|
||||||
|
animSetClipListView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, ov, nv) -> addKfBtn.setDisable(nv == null));
|
||||||
|
|
||||||
|
addKfBtn.setOnAction(e -> showAnimKfDialog(null));
|
||||||
|
|
||||||
|
removeKfBtn.setOnAction(e -> {
|
||||||
|
de.blight.game.animation.AnimKeyframe sel =
|
||||||
|
animSetKfListView.getSelectionModel().getSelectedItem();
|
||||||
if (sel != null) {
|
if (sel != null) {
|
||||||
animSetAnchorBoneListView.getItems().remove(sel);
|
animSetKfList.remove(sel);
|
||||||
animSetDirty = true;
|
animSetDirty = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
HBox anchorBtns = new HBox(6, addAnchorBtn, removeAnchorBtn);
|
HBox kfBtns = new HBox(6, addKfBtn, removeKfBtn);
|
||||||
HBox.setHgrow(addAnchorBtn, Priority.ALWAYS);
|
HBox.setHgrow(addKfBtn, Priority.ALWAYS);
|
||||||
HBox.setHgrow(removeAnchorBtn, Priority.ALWAYS);
|
HBox.setHgrow(removeKfBtn, Priority.ALWAYS);
|
||||||
inner.getChildren().addAll(anchorHint, animSetBonesLabel, animSetAnchorBoneListView, anchorBtns);
|
inner.getChildren().addAll(kfHint, animSetKfListView, kfBtns);
|
||||||
|
|
||||||
// ── Sink-Konfiguration (Fallback) ─────────────────────────────────────
|
|
||||||
inner.getChildren().addAll(new Separator(), sectionTitle("Manueller Sink-Fallback"), new Separator());
|
|
||||||
|
|
||||||
Label sinkHint = new Label("Root-Motion-Ersatz: Körper senkt/hebt sich während der Animation.\nNegativ = nach unten (Setzen), Positiv = nach oben.");
|
|
||||||
sinkHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
|
||||||
sinkHint.setWrapText(true);
|
|
||||||
|
|
||||||
animSetSinkListView = new ListView<>();
|
|
||||||
animSetSinkListView.setPrefHeight(120);
|
|
||||||
if (animSet.getSinkMap() != null) {
|
|
||||||
for (var e2 : animSet.getSinkMap().entrySet()) {
|
|
||||||
animSetSinkListView.getItems().add(e2.getKey() + " → " + e2.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button addSinkBtn = new Button("+ Setzen…");
|
|
||||||
Button removeSinkBtn = new Button("- Entfernen");
|
|
||||||
addSinkBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
removeSinkBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
removeSinkBtn.setDisable(true);
|
|
||||||
animSetSinkListView.getSelectionModel().selectedItemProperty()
|
|
||||||
.addListener((obs, ov, nv) -> removeSinkBtn.setDisable(nv == null));
|
|
||||||
|
|
||||||
addSinkBtn.setOnAction(e -> {
|
|
||||||
ComboBox<de.blight.game.animation.AnimationAction> actionSinkCombo = new ComboBox<>();
|
|
||||||
actionSinkCombo.getItems().addAll(de.blight.game.animation.AnimationAction.values());
|
|
||||||
javafx.util.Callback<javafx.scene.control.ListView<de.blight.game.animation.AnimationAction>,
|
|
||||||
javafx.scene.control.ListCell<de.blight.game.animation.AnimationAction>> cf2 =
|
|
||||||
lv -> new javafx.scene.control.ListCell<>() {
|
|
||||||
@Override protected void updateItem(de.blight.game.animation.AnimationAction it, boolean empty) {
|
|
||||||
super.updateItem(it, empty);
|
|
||||||
setText(empty || it == null ? null : it.displayName() + " (" + it.name() + ")");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
actionSinkCombo.setCellFactory(cf2);
|
|
||||||
actionSinkCombo.setButtonCell(cf2.call(null));
|
|
||||||
actionSinkCombo.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
actionSinkCombo.getSelectionModel().selectFirst();
|
|
||||||
|
|
||||||
Spinner<Double> sinkSpinner = new Spinner<>(-3.0, 3.0, 0.0, 0.05);
|
|
||||||
sinkSpinner.setEditable(true);
|
|
||||||
sinkSpinner.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
|
|
||||||
javafx.scene.layout.GridPane sinkGrid = new javafx.scene.layout.GridPane();
|
|
||||||
sinkGrid.setHgap(8); sinkGrid.setVgap(6);
|
|
||||||
sinkGrid.add(new Label("Aktion:"), 0, 0); sinkGrid.add(actionSinkCombo, 1, 0);
|
|
||||||
sinkGrid.add(new Label("Versatz (m):"), 0, 1); sinkGrid.add(sinkSpinner, 1, 1);
|
|
||||||
javafx.scene.layout.ColumnConstraints sinkCc = new javafx.scene.layout.ColumnConstraints();
|
|
||||||
sinkCc.setHgrow(Priority.ALWAYS);
|
|
||||||
sinkGrid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), sinkCc);
|
|
||||||
|
|
||||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> sinkDlg = new javafx.scene.control.Dialog<>();
|
|
||||||
sinkDlg.setTitle("Sink-Wert setzen");
|
|
||||||
javafx.scene.control.ButtonType okSink = new javafx.scene.control.ButtonType("Setzen",
|
|
||||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
|
||||||
sinkDlg.getDialogPane().getButtonTypes().addAll(okSink, javafx.scene.control.ButtonType.CANCEL);
|
|
||||||
sinkDlg.getDialogPane().setContent(sinkGrid);
|
|
||||||
sinkDlg.showAndWait().ifPresent(bt -> {
|
|
||||||
if (bt != okSink) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var selAction = actionSinkCombo.getValue();
|
|
||||||
if (selAction == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
double val = sinkSpinner.getValue();
|
|
||||||
String newEntry = selAction.name() + " → " + val;
|
|
||||||
// Bestehenden Eintrag für diese Aktion ersetzen
|
|
||||||
animSetSinkListView.getItems().removeIf(it -> it.startsWith(selAction.name() + " → "));
|
|
||||||
animSetSinkListView.getItems().add(newEntry);
|
|
||||||
animSetDirty = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
removeSinkBtn.setOnAction(e -> {
|
|
||||||
String sel = animSetSinkListView.getSelectionModel().getSelectedItem();
|
|
||||||
if (sel != null) {
|
|
||||||
animSetSinkListView.getItems().remove(sel);
|
|
||||||
animSetDirty = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
HBox sinkBtns = new HBox(6, addSinkBtn, removeSinkBtn);
|
|
||||||
HBox.setHgrow(addSinkBtn, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(removeSinkBtn, Priority.ALWAYS);
|
|
||||||
inner.getChildren().addAll(sinkHint, animSetSinkListView, sinkBtns);
|
|
||||||
|
|
||||||
// ── Vorschau ─────────────────────────────────────────────────────────
|
// ── Vorschau ─────────────────────────────────────────────────────────
|
||||||
inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator());
|
inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator());
|
||||||
@@ -8644,6 +8527,117 @@ public class EditorApp extends Application {
|
|||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Öffnet den Keyframe-Dialog (null = Hinzufügen, non-null = Bearbeiten). */
|
||||||
|
private void showAnimKfDialog(de.blight.game.animation.AnimKeyframe 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;
|
||||||
|
float initRX = isAdd ? 0f : existing.rx;
|
||||||
|
float initRY = isAdd ? 0f : existing.ry;
|
||||||
|
float initRZ = isAdd ? 0f : existing.rz;
|
||||||
|
|
||||||
|
Spinner<Double> spTime = new Spinner<>(0.0, maxTime, initTime, 0.05);
|
||||||
|
Spinner<Double> spTX = new Spinner<>(-10.0, 10.0, initTX, 0.05);
|
||||||
|
Spinner<Double> spTY = new Spinner<>(-10.0, 10.0, initTY, 0.05);
|
||||||
|
Spinner<Double> spTZ = new Spinner<>(-10.0, 10.0, initTZ, 0.05);
|
||||||
|
Spinner<Double> spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0);
|
||||||
|
Spinner<Double> spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0);
|
||||||
|
Spinner<Double> spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0);
|
||||||
|
for (Spinner<Double> sp : new Spinner[]{spTime, 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};
|
||||||
|
javafx.scene.layout.GridPane kfGrid = new javafx.scene.layout.GridPane();
|
||||||
|
kfGrid.setHgap(8); kfGrid.setVgap(5);
|
||||||
|
for (int i = 0; i < sps.length; i++) {
|
||||||
|
kfGrid.add(new Label(rows[i][0]), 0, i);
|
||||||
|
kfGrid.add(sps[i], 1, i);
|
||||||
|
}
|
||||||
|
javafx.scene.layout.ColumnConstraints kfCC = new javafx.scene.layout.ColumnConstraints();
|
||||||
|
kfCC.setHgrow(Priority.ALWAYS);
|
||||||
|
kfGrid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), kfCC);
|
||||||
|
|
||||||
|
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> kfDlg =
|
||||||
|
new javafx.scene.control.Dialog<>();
|
||||||
|
kfDlg.setTitle(isAdd ? "Keyframe hinzufügen" : "Keyframe bearbeiten");
|
||||||
|
javafx.scene.control.ButtonType okKf = new javafx.scene.control.ButtonType(
|
||||||
|
isAdd ? "Hinzufügen" : "Übernehmen",
|
||||||
|
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||||
|
kfDlg.getDialogPane().getButtonTypes().addAll(okKf, javafx.scene.control.ButtonType.CANCEL);
|
||||||
|
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,
|
||||||
|
spTX.getValue().floatValue(), spTY.getValue().floatValue(), spTZ.getValue().floatValue(),
|
||||||
|
spRX.getValue().floatValue(), spRY.getValue().floatValue(), spRZ.getValue().floatValue());
|
||||||
|
animSetKfList.add(kf);
|
||||||
|
} else {
|
||||||
|
existing.time = t;
|
||||||
|
existing.tx = spTX.getValue().floatValue();
|
||||||
|
existing.ty = spTY.getValue().floatValue();
|
||||||
|
existing.tz = spTZ.getValue().floatValue();
|
||||||
|
existing.rx = spRX.getValue().floatValue();
|
||||||
|
existing.ry = spRY.getValue().floatValue();
|
||||||
|
existing.rz = spRZ.getValue().floatValue();
|
||||||
|
}
|
||||||
|
animSetKfList.sort(java.util.Comparator.comparingDouble(k -> k.time));
|
||||||
|
if (animSetKfListView != null) animSetKfListView.refresh();
|
||||||
|
animSetDirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spielt den Clip der gegebenen Aktion in der Vorschau ab, um animPreviewCurrentClipDuration zu setzen. */
|
||||||
|
private void autoPreviewClipForAction(de.blight.game.animation.AnimationAction action) {
|
||||||
|
if (animSetActionListView == null || action == null) return;
|
||||||
|
String prefix = action.name() + " → ";
|
||||||
|
for (String entry : animSetActionListView.getItems()) {
|
||||||
|
if (entry.startsWith(prefix)) {
|
||||||
|
String clip = entry.substring(prefix.length()).trim();
|
||||||
|
if (!clip.isEmpty()) {
|
||||||
|
input.animPreviewPlayClip = clip;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAnimPreviewKfOffset() {
|
||||||
|
if (animSetKfList == null || animSetKfList.isEmpty()) {
|
||||||
|
input.animPreviewKfTx = 0f;
|
||||||
|
input.animPreviewKfTy = 0f;
|
||||||
|
input.animPreviewKfTz = 0f;
|
||||||
|
input.animPreviewKfActive = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void previewSelectedClip() {
|
private void previewSelectedClip() {
|
||||||
if (animSetClipListView == null) return;
|
if (animSetClipListView == null) return;
|
||||||
String clip = animSetClipListView.getSelectionModel().getSelectedItem();
|
String clip = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||||
@@ -8749,28 +8743,21 @@ public class EditorApp extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
animSet.setActionMap(actionMap);
|
animSet.setActionMap(actionMap);
|
||||||
java.util.Map<String, Float> sinkMap = new java.util.LinkedHashMap<>();
|
// Motion Keyframes: aktuell sichtbare Liste (des selektierten Clips) sichern, dann schreiben
|
||||||
if (animSetSinkListView != null) {
|
if (animSetClipListView != null) {
|
||||||
for (String it : animSetSinkListView.getItems()) {
|
String selClip = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||||
String[] parts = it.split(" → ", 2);
|
if (selClip != null) {
|
||||||
if (parts.length == 2) {
|
animSetMotionKeyframes.put(selClip, new java.util.ArrayList<>(animSetKfList));
|
||||||
try {
|
|
||||||
sinkMap.put(parts[0], Float.parseFloat(parts[1]));
|
|
||||||
} catch (NumberFormatException ignored) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
java.util.Map<String, java.util.List<de.blight.game.animation.AnimKeyframe>> kfFinal =
|
||||||
animSet.setSinkMap(sinkMap);
|
new java.util.LinkedHashMap<>();
|
||||||
java.util.Map<String, String> anchorBoneMap = new java.util.LinkedHashMap<>();
|
for (var kfEntry : animSetMotionKeyframes.entrySet()) {
|
||||||
if (animSetAnchorBoneListView != null) {
|
if (kfEntry.getValue() != null && !kfEntry.getValue().isEmpty()) {
|
||||||
for (String it : animSetAnchorBoneListView.getItems()) {
|
kfFinal.put(kfEntry.getKey(), kfEntry.getValue());
|
||||||
String[] parts = it.split(" → ", 2);
|
|
||||||
if (parts.length == 2) {
|
|
||||||
anchorBoneMap.put(parts[0], parts[1]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
animSet.setMotionKeyframes(kfFinal);
|
||||||
animSet.setAnchorBoneMap(anchorBoneMap);
|
|
||||||
// Vorschau-Modell-Pfad beibehalten
|
// Vorschau-Modell-Pfad beibehalten
|
||||||
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
|
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
|
||||||
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
||||||
|
|||||||
@@ -567,9 +567,16 @@ public class SharedInput {
|
|||||||
/** JME3 → JavaFX: Clip-Namen nach Modell-Laden. getAndSet(null) konsumiert. */
|
/** JME3 → JavaFX: Clip-Namen nach Modell-Laden. getAndSet(null) konsumiert. */
|
||||||
public final java.util.concurrent.atomic.AtomicReference<java.util.List<String>>
|
public final java.util.concurrent.atomic.AtomicReference<java.util.List<String>>
|
||||||
animPreviewClips = new java.util.concurrent.atomic.AtomicReference<>();
|
animPreviewClips = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
/** JME3 → JavaFX: Joint-Namen des geladenen Armatures (für Bone-Anchoring-Auswahl). */
|
/** JME3 → JavaFX: Joint-Namen des geladenen Armatures. */
|
||||||
public final java.util.concurrent.atomic.AtomicReference<java.util.List<String>>
|
public final java.util.concurrent.atomic.AtomicReference<java.util.List<String>>
|
||||||
animPreviewJointNames = new java.util.concurrent.atomic.AtomicReference<>();
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.
|
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.
|
||||||
|
|||||||
@@ -232,6 +232,16 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
axesNode.setLocalTranslation(Vector3f.ZERO);
|
axesNode.setLocalTranslation(Vector3f.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Motion-Keyframe-Vorschau: Versatz auf Modell anwenden (TX/TY/TZ in Welt-Koordinaten)
|
||||||
|
if (currentModel != null) {
|
||||||
|
if (input.animPreviewKfActive) {
|
||||||
|
currentModel.setLocalTranslation(
|
||||||
|
input.animPreviewKfTx, input.animPreviewKfTy, input.animPreviewKfTz);
|
||||||
|
} else {
|
||||||
|
currentModel.setLocalTranslation(Vector3f.ZERO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
previewScene.updateLogicalState(tpf);
|
previewScene.updateLogicalState(tpf);
|
||||||
previewScene.updateGeometricState();
|
previewScene.updateGeometricState();
|
||||||
}
|
}
|
||||||
@@ -297,13 +307,15 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
previewSC != null ? "ok" : "NULL");
|
previewSC != null ? "ok" : "NULL");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kamera: immer auf Hüfthöhe (0, 1, 0) zielen; Distanz aus BoundingBox
|
// Kamera: immer auf Hüfthöhe (0, 1, 0) zielen; Distanz aus BoundingBox.
|
||||||
|
// WICHTIG: BoundingBox wird VOR dem ersten SC-Update berechnet (T-Pose noch nicht sichtbar).
|
||||||
|
// Deshalb Minimum von 3f + 2m Puffer, damit die Kamera nicht im Körper startet.
|
||||||
model.updateGeometricState();
|
model.updateGeometricState();
|
||||||
if (model.getWorldBound() instanceof BoundingBox bb) {
|
if (model.getWorldBound() instanceof BoundingBox bb) {
|
||||||
float ext = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
float ext = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
||||||
previewCamDist = ext * 2.8f;
|
previewCamDist = Math.max(ext * 2.8f, 3f) + 2f;
|
||||||
} else {
|
} else {
|
||||||
previewCamDist = 3f;
|
previewCamDist = 5f;
|
||||||
}
|
}
|
||||||
previewTarget.set(0, 1, 0);
|
previewTarget.set(0, 1, 0);
|
||||||
input.animPreviewZoom = 1.0f;
|
input.animPreviewZoom = 1.0f;
|
||||||
@@ -381,6 +393,7 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
currentClipName = clipName;
|
currentClipName = clipName;
|
||||||
if (currentAction != null) {
|
if (currentAction != null) {
|
||||||
currentAction.setSpeed(input.animPreviewSpeed);
|
currentAction.setSpeed(input.animPreviewSpeed);
|
||||||
|
input.animPreviewCurrentClipDuration = (float) currentAction.getLength();
|
||||||
LOG.info("[AnimPreview] Play '{}' length={}", clipName, currentAction.getLength());
|
LOG.info("[AnimPreview] Play '{}' length={}", clipName, currentAction.getLength());
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,15 +26,8 @@ public class AnimSet {
|
|||||||
private Map<String, String> actionMap = new LinkedHashMap<>();
|
private Map<String, String> actionMap = new LinkedHashMap<>();
|
||||||
/** Zuletzt im Editor verwendeter Modell-Pfad (relativ zu Assets-Root). Wird beim Öffnen auto-geladen. */
|
/** Zuletzt im Editor verwendeter Modell-Pfad (relativ zu Assets-Root). Wird beim Öffnen auto-geladen. */
|
||||||
private String previewModelPath = null;
|
private String previewModelPath = null;
|
||||||
/** Vertikaler Versatz des Visual-Nodes während der jeweiligen Animation (Root-Motion-Ersatz, Fallback). */
|
/** Manueller Positions-/Rotations-Versatz: Aktion → sortierte Keyframe-Liste. */
|
||||||
private Map<String, Float> sinkMap = new LinkedHashMap<>();
|
private Map<String, List<AnimKeyframe>> motionKeyframes = new LinkedHashMap<>();
|
||||||
/**
|
|
||||||
* Pro Aktion konfigurierbarer Anchor-Knochen (z. B. SIT_DOWN → "foot.l", PICK_UP → "hand.r").
|
|
||||||
* Wenn für eine Aktion ein Eintrag vorhanden ist, wird Bone-Anchoring verwendet:
|
|
||||||
* der Knochen bleibt auf seiner Welt-Y vor der Animation fixiert.
|
|
||||||
* Überschreibt sinkMap für diese Aktion.
|
|
||||||
*/
|
|
||||||
private Map<String, String> anchorBoneMap = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
public List<String> getClips() { return clips; }
|
public List<String> getClips() { return clips; }
|
||||||
public void setClips(List<String> clips) { this.clips = clips; }
|
public void setClips(List<String> clips) { this.clips = clips; }
|
||||||
@@ -42,10 +35,12 @@ public class AnimSet {
|
|||||||
public void setActionMap(Map<String, String> actionMap) { this.actionMap = actionMap; }
|
public void setActionMap(Map<String, String> actionMap) { this.actionMap = actionMap; }
|
||||||
public String getPreviewModelPath() { return previewModelPath; }
|
public String getPreviewModelPath() { return previewModelPath; }
|
||||||
public void setPreviewModelPath(String previewModelPath) { this.previewModelPath = previewModelPath; }
|
public void setPreviewModelPath(String previewModelPath) { this.previewModelPath = previewModelPath; }
|
||||||
public Map<String, Float> getSinkMap() { return sinkMap != null ? sinkMap : new LinkedHashMap<>(); }
|
public Map<String, List<AnimKeyframe>> getMotionKeyframes() {
|
||||||
public void setSinkMap(Map<String, Float> sinkMap) { this.sinkMap = sinkMap; }
|
return motionKeyframes != null ? motionKeyframes : new LinkedHashMap<>();
|
||||||
public Map<String, String> getAnchorBoneMap() { return anchorBoneMap != null ? anchorBoneMap : new LinkedHashMap<>(); }
|
}
|
||||||
public void setAnchorBoneMap(Map<String, String> anchorBoneMap) { this.anchorBoneMap = anchorBoneMap; }
|
public void setMotionKeyframes(Map<String, List<AnimKeyframe>> motionKeyframes) {
|
||||||
|
this.motionKeyframes = motionKeyframes;
|
||||||
|
}
|
||||||
|
|
||||||
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
||||||
public void save(Path setDir, String setName) throws IOException {
|
public void save(Path setDir, String setName) throws IOException {
|
||||||
|
|||||||
@@ -49,16 +49,8 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
private AnimComposer animComposer;
|
private AnimComposer animComposer;
|
||||||
private String runningClip;
|
private String runningClip;
|
||||||
private java.util.Map<String, Float> animSinkMap = java.util.Map.of();
|
|
||||||
private java.util.Map<String, String> animAnchorBoneMap = java.util.Map.of();
|
|
||||||
|
|
||||||
/** Bone-Anchoring: SkinningControl + Referenz-Position vor der Animation (Model-Space, alle Achsen). */
|
|
||||||
private com.jme3.anim.SkinningControl skinningControl = null;
|
private com.jme3.anim.SkinningControl skinningControl = null;
|
||||||
private Vector3f preAnimAnchorBoneModel = null;
|
|
||||||
private Vector3f preAnimVisualTranslation = null;
|
|
||||||
private String currentAnchorBone = null;
|
|
||||||
private boolean boneAnchorWarnLogged = false;
|
|
||||||
private int boneAnchorLogFrames = 0;
|
|
||||||
private int jumpFrames = 0;
|
private int jumpFrames = 0;
|
||||||
private boolean pickupActive = false;
|
private boolean pickupActive = false;
|
||||||
private float pickupRemaining = 0f;
|
private float pickupRemaining = 0f;
|
||||||
@@ -69,14 +61,13 @@ public class PlayerInputControl {
|
|||||||
private float blockingAnimTotal = 0f;
|
private float blockingAnimTotal = 0f;
|
||||||
private Runnable blockingAnimCallback = null;
|
private Runnable blockingAnimCallback = null;
|
||||||
|
|
||||||
/**
|
/** Manueller Motion-Override: pro Aktion konfigurierbare Keyframe-Liste. */
|
||||||
* Vertikaler Versatz des Visual-Nodes während einer blockierenden Animation
|
private java.util.Map<String, java.util.List<de.blight.game.animation.AnimKeyframe>>
|
||||||
* (Root-Motion-Ersatz: Körper senkt sich beim Setzen, hebt sich beim Aufstehen).
|
motionKeyframesMap = java.util.Map.of();
|
||||||
* visualSinkCurrent wird pro Frame interpoliert.
|
private java.util.List<de.blight.game.animation.AnimKeyframe>
|
||||||
*/
|
currentMotionKfs = null;
|
||||||
private float visualSinkStart = 0f;
|
private Vector3f preMotionTranslation = null;
|
||||||
private float visualSinkTarget = 0f;
|
private Quaternion preMotionRotation = null;
|
||||||
private float visualSinkCurrent = 0f;
|
|
||||||
|
|
||||||
/** Drehung auf der Stelle (kein Vorwärtsbewegen, nur Rotation). */
|
/** Drehung auf der Stelle (kein Vorwärtsbewegen, nur Rotation). */
|
||||||
private boolean turnActive = false;
|
private boolean turnActive = false;
|
||||||
@@ -125,21 +116,19 @@ public class PlayerInputControl {
|
|||||||
this.runningClip = null;
|
this.runningClip = null;
|
||||||
this.animComposer = (visual != null) ? RetargetingSystem.findAnimComposer(visual) : null;
|
this.animComposer = (visual != null) ? RetargetingSystem.findAnimComposer(visual) : null;
|
||||||
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
||||||
// SinkMap + AnchorBoneMap aus AnimSet laden
|
skinningControl = findSkinningControl(visual);
|
||||||
|
log.info("[AnimCtx] SkinningControl gefunden: {}", skinningControl != null);
|
||||||
if (animSetName != null && assetRoot != null) {
|
if (animSetName != null && assetRoot != null) {
|
||||||
try {
|
try {
|
||||||
java.nio.file.Path setDir = assetRoot.resolve("animations").resolve("sets");
|
java.nio.file.Path setDir = assetRoot.resolve("animations").resolve("sets");
|
||||||
de.blight.game.animation.AnimSet set = de.blight.game.animation.AnimSet.load(setDir, animSetName);
|
de.blight.game.animation.AnimSet set = de.blight.game.animation.AnimSet.load(setDir, animSetName);
|
||||||
animSinkMap = set.getSinkMap();
|
motionKeyframesMap = set.getMotionKeyframes();
|
||||||
animAnchorBoneMap = set.getAnchorBoneMap();
|
log.info("[AnimCtx] MotionKeyframes geladen: {} Aktionen: {}",
|
||||||
|
motionKeyframesMap.size(), motionKeyframesMap.keySet());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
animSinkMap = java.util.Map.of();
|
motionKeyframesMap = java.util.Map.of();
|
||||||
animAnchorBoneMap = java.util.Map.of();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
skinningControl = findSkinningControl(visual);
|
|
||||||
log.info("[AnimCtx] SkinningControl gefunden: {}, AnchorBoneMap: {}",
|
|
||||||
skinningControl != null, animAnchorBoneMap);
|
|
||||||
if (animSetName != null) {
|
if (animSetName != null) {
|
||||||
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.IDLE);
|
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.IDLE);
|
||||||
if (clip != null && tryPlay(clip)) {
|
if (clip != null && tryPlay(clip)) {
|
||||||
@@ -200,31 +189,25 @@ public class PlayerInputControl {
|
|||||||
if (duration <= 0f) {
|
if (duration <= 0f) {
|
||||||
duration = resolveClipLength(action, 1.5f);
|
duration = resolveClipLength(action, 1.5f);
|
||||||
}
|
}
|
||||||
// Bone-Anchoring: pro Aktion konfigurierten Knochen laden und Referenz-Y einfrieren
|
String kfClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
||||||
currentAnchorBone = animAnchorBoneMap.get(action.name());
|
currentMotionKfs = kfClip != null ? motionKeyframesMap.get(kfClip) : null;
|
||||||
if (currentAnchorBone != null && !currentAnchorBone.isBlank()) {
|
if (currentMotionKfs != null && !currentMotionKfs.isEmpty() && visual != null) {
|
||||||
preAnimAnchorBoneModel = getBoneModelPos(currentAnchorBone);
|
// KF-Werte sind absolute Positionen im Charakter-Lokalraum (nicht additiv)
|
||||||
preAnimVisualTranslation = visual != null ? visual.getLocalTranslation().clone() : new Vector3f();
|
preMotionTranslation = Vector3f.ZERO;
|
||||||
boneAnchorWarnLogged = false;
|
preMotionRotation = visual.getLocalRotation().clone();
|
||||||
boneAnchorLogFrames = 0;
|
|
||||||
log.info("[BoneAnchor] Aktion={} Knochen='{}' preModelY={} (null={})",
|
|
||||||
action.name(), currentAnchorBone,
|
|
||||||
preAnimAnchorBoneModel != null ? preAnimAnchorBoneModel.y : Float.NaN,
|
|
||||||
preAnimAnchorBoneModel == null);
|
|
||||||
} else {
|
} else {
|
||||||
currentAnchorBone = null;
|
// Keine Keyframes: visuelle Verschiebung aus vorheriger Keyframe-Aktion zurücksetzen
|
||||||
preAnimAnchorBoneModel = null;
|
if (visual != null) {
|
||||||
preAnimVisualTranslation = null;
|
visual.setLocalTranslation(Vector3f.ZERO);
|
||||||
// Fallback: manuellen Sink aus AnimSet-Konfiguration laden
|
|
||||||
if (animSinkMap.containsKey(action.name())) {
|
|
||||||
visualSinkTarget = animSinkMap.get(action.name());
|
|
||||||
}
|
}
|
||||||
|
currentMotionKfs = null;
|
||||||
|
preMotionTranslation = null;
|
||||||
|
preMotionRotation = null;
|
||||||
}
|
}
|
||||||
blockingAnimActive = true;
|
blockingAnimActive = true;
|
||||||
blockingAnimRemaining = duration;
|
blockingAnimRemaining = duration;
|
||||||
blockingAnimTotal = duration;
|
blockingAnimTotal = duration;
|
||||||
blockingAnimCallback = onComplete;
|
blockingAnimCallback = onComplete;
|
||||||
visualSinkStart = visualSinkCurrent;
|
|
||||||
autopilotDir = null;
|
autopilotDir = null;
|
||||||
forward = backward = left = right = false;
|
forward = backward = left = right = false;
|
||||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
@@ -232,14 +215,6 @@ public class PlayerInputControl {
|
|||||||
currentAnim = action;
|
currentAnim = action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Überschreibt das Sink-Ziel für die nächste {@link #requestAnimation}-Animation manuell.
|
|
||||||
* Hat Vorrang vor der AnimSet-Konfiguration wenn VOR requestAnimation aufgerufen.
|
|
||||||
*/
|
|
||||||
public void setNextAnimationSink(float targetY) {
|
|
||||||
this.visualSinkTarget = targetY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Liefert die Länge des Clips für {@code action} in Sekunden, oder {@code fallback} wenn nicht ermittelbar. */
|
/** Liefert die Länge des Clips für {@code action} in Sekunden, oder {@code fallback} wenn nicht ermittelbar. */
|
||||||
private float resolveClipLength(AnimationAction action, float fallback) {
|
private float resolveClipLength(AnimationAction action, float fallback) {
|
||||||
if (animComposer == null || animLib == null || animSetName == null) {
|
if (animComposer == null || animLib == null || animSetName == null) {
|
||||||
@@ -295,10 +270,27 @@ public class PlayerInputControl {
|
|||||||
/**
|
/**
|
||||||
* Spielt eine Animations-Aktion als Dauer-Loop während des Ruhezustands.
|
* Spielt eine Animations-Aktion als Dauer-Loop während des Ruhezustands.
|
||||||
* Nur sinnvoll nach {@link #lockInPlace()}.
|
* Nur sinnvoll nach {@link #lockInPlace()}.
|
||||||
|
* Hat die Aktion Motion Keyframes, wird der erste Keyframe (time=0) als statischer
|
||||||
|
* Versatz auf den Visual-Node angewendet.
|
||||||
*/
|
*/
|
||||||
public void playLockedAnimation(AnimationAction action) {
|
public void playLockedAnimation(AnimationAction action) {
|
||||||
playAction(action);
|
playAction(action);
|
||||||
currentAnim = action;
|
currentAnim = action;
|
||||||
|
String lockedClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
||||||
|
java.util.List<de.blight.game.animation.AnimKeyframe> kfs =
|
||||||
|
lockedClip != null ? motionKeyframesMap.get(lockedClip) : null;
|
||||||
|
log.info("[AnimKF] playLockedAnimation({}) clip='{}' → KFs gefunden: {}",
|
||||||
|
action, lockedClip, kfs != null ? kfs.size() : "null");
|
||||||
|
if (kfs != null && !kfs.isEmpty() && visual != null) {
|
||||||
|
currentMotionKfs = kfs;
|
||||||
|
preMotionTranslation = Vector3f.ZERO;
|
||||||
|
preMotionRotation = visual.getLocalRotation().clone();
|
||||||
|
applyMotionKeyframes(0f);
|
||||||
|
log.info("[AnimKF] Offset angewendet: visual.localTranslation={}", visual.getLocalTranslation());
|
||||||
|
currentMotionKfs = null;
|
||||||
|
preMotionTranslation = null;
|
||||||
|
preMotionRotation = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerMappings(KeyBindings kb) {
|
private void registerMappings(KeyBindings kb) {
|
||||||
@@ -366,34 +358,22 @@ public class PlayerInputControl {
|
|||||||
if (blockingAnimActive) {
|
if (blockingAnimActive) {
|
||||||
blockingAnimRemaining -= tpf;
|
blockingAnimRemaining -= tpf;
|
||||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
|
float elapsed = blockingAnimTotal - blockingAnimRemaining;
|
||||||
// Visuellen Versatz anpassen: Foot-Anchoring hat Vorrang vor manuellem Sink
|
applyMotionKeyframes(elapsed);
|
||||||
if (visual != null) {
|
|
||||||
if (currentAnchorBone != null && preAnimAnchorBoneModel != null) {
|
|
||||||
// Bone-Anchoring: 3D-Delta im Model-Space messen und als Visual-Offset anwenden.
|
|
||||||
// Model-Space ist unabhängig vom Visual-Shift → keine Rückkopplung.
|
|
||||||
applyBoneAnchorOffset(currentAnchorBone);
|
|
||||||
} else if (blockingAnimTotal > 0f) {
|
|
||||||
// Fallback: manueller Sink interpoliert
|
|
||||||
float t = Math.max(0f, Math.min(1f, 1f - blockingAnimRemaining / blockingAnimTotal));
|
|
||||||
visualSinkCurrent = visualSinkStart + (visualSinkTarget - visualSinkStart) * t;
|
|
||||||
applyVisualSink();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockingAnimRemaining <= 0f) {
|
if (blockingAnimRemaining <= 0f) {
|
||||||
blockingAnimActive = false;
|
blockingAnimActive = false;
|
||||||
if (currentAnchorBone != null && preAnimAnchorBoneModel != null) {
|
applyMotionKeyframes(blockingAnimTotal); // Endwert einrasten
|
||||||
// Bone-Anchoring: letzten Kompensationswert einrasten
|
currentMotionKfs = null;
|
||||||
applyBoneAnchorOffset(currentAnchorBone);
|
preMotionTranslation = null;
|
||||||
} else {
|
preMotionRotation = null;
|
||||||
// Fallback: Zielwert einrasten
|
|
||||||
visualSinkCurrent = visualSinkTarget;
|
|
||||||
applyVisualSink();
|
|
||||||
}
|
|
||||||
Runnable cb = blockingAnimCallback;
|
Runnable cb = blockingAnimCallback;
|
||||||
blockingAnimCallback = null;
|
blockingAnimCallback = null;
|
||||||
if (cb != null) cb.run();
|
if (cb != null) cb.run();
|
||||||
|
// Kein Ruhezustand nach der Animation → visuellen Versatz zurücksetzen
|
||||||
|
if (!lockedInPlace && visual != null) {
|
||||||
|
visual.setLocalTranslation(Vector3f.ZERO);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -489,67 +469,6 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert die aktuelle Welt-Y des angegebenen Joints, oder NaN wenn nicht ermittelbar.
|
|
||||||
* Liest den Joint aus dem SkinningControl (nach AnimComposer-Update = aktueller Frame).
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Gibt die Position des Joints im Model-Space des Armatures zurück.
|
|
||||||
* Bewusst KEIN Welt-Transform: sonst entsteht eine Rückkopplung mit dem Visual-Offset,
|
|
||||||
* weil der Visual-Node-Shift den Welt-Transform des Knochens beeinflusst.
|
|
||||||
*/
|
|
||||||
private Vector3f getBoneModelPos(String boneName) {
|
|
||||||
if (skinningControl == null || boneName == null || boneName.isBlank()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
com.jme3.anim.Armature armature = skinningControl.getArmature();
|
|
||||||
if (armature == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
com.jme3.anim.Joint joint = armature.getJoint(boneName);
|
|
||||||
if (joint == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return joint.getModelTransform().getTranslation().clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Berechnet den Y-Offset des Anchor-Knochens gegenüber seiner Startposition
|
|
||||||
* (in Model-Space, keine Rückkopplung mit dem Visual-Shift) und setzt die
|
|
||||||
* Local-Y des Visual-Nodes so, dass der Knochen vertikal fixiert bleibt.
|
|
||||||
*
|
|
||||||
* Nur Y wird kompensiert. X/Z-Drift im Model-Space liegt in einem anderen
|
|
||||||
* Koordinatensystem als der Visual-Node (Blender-Export-Rotation) und würde
|
|
||||||
* den Charakter horizontal verschieben — das ist falsch.
|
|
||||||
*
|
|
||||||
* Formel: visual.localY = preAnimVisualY + (preAnimBone.y - currentBone.y) * scale
|
|
||||||
*/
|
|
||||||
private void applyBoneAnchorOffset(String boneName) {
|
|
||||||
if (visual == null || preAnimAnchorBoneModel == null || preAnimVisualTranslation == null) {
|
|
||||||
if (!boneAnchorWarnLogged) {
|
|
||||||
log.warn("[BoneAnchor] applyBoneAnchorOffset abgebrochen: visual={} preModel={} preVis={}",
|
|
||||||
visual != null, preAnimAnchorBoneModel, preAnimVisualTranslation);
|
|
||||||
boneAnchorWarnLogged = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Vector3f current = getBoneModelPos(boneName);
|
|
||||||
if (current == null) {
|
|
||||||
if (!boneAnchorWarnLogged) {
|
|
||||||
log.warn("[BoneAnchor] Knochen '{}' nicht im Armature gefunden (skinningControl={})",
|
|
||||||
boneName, skinningControl != null);
|
|
||||||
boneAnchorWarnLogged = true;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
float scale = skinningControl != null && skinningControl.getSpatial() != null
|
|
||||||
? skinningControl.getSpatial().getWorldScale().y : 1f;
|
|
||||||
float newY = preAnimVisualTranslation.y + (preAnimAnchorBoneModel.y - current.y) * scale;
|
|
||||||
visualSinkCurrent = newY;
|
|
||||||
com.jme3.math.Vector3f t = visual.getLocalTranslation();
|
|
||||||
visual.setLocalTranslation(t.x, newY, t.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Durchsucht den Szenegraphen rekursiv nach dem ersten SkinningControl. */
|
/** Durchsucht den Szenegraphen rekursiv nach dem ersten SkinningControl. */
|
||||||
private com.jme3.anim.SkinningControl findSkinningControl(Spatial s) {
|
private com.jme3.anim.SkinningControl findSkinningControl(Spatial s) {
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
@@ -570,14 +489,59 @@ public class PlayerInputControl {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyVisualSink() {
|
/**
|
||||||
if (visual == null) {
|
* Interpoliert die Motion-Keyframes der laufenden Aktion für den Zeitpunkt {@code time}
|
||||||
return;
|
* und setzt Translation + Rotation des Visual-Nodes.
|
||||||
|
* TX/TZ werden im Charakter-lokalen Raum (Rotation zu Animations-Start) angewendet.
|
||||||
|
* Dabei gilt: positive TX = rechts vom Charakter, positives TZ = vor dem Charakter
|
||||||
|
* (= weg von der Bank), negatives TZ = hinter den Charakter (= zur Bank hin).
|
||||||
|
* TY ist Welt-Y. RX/RY/RZ sind additiv zur Startrotation.
|
||||||
|
*/
|
||||||
|
private void applyMotionKeyframes(float time) {
|
||||||
|
if (currentMotionKfs == null || currentMotionKfs.isEmpty()) return;
|
||||||
|
if (preMotionRotation == null || visual == null) return;
|
||||||
|
|
||||||
|
de.blight.game.animation.AnimKeyframe before = null, after = null;
|
||||||
|
for (de.blight.game.animation.AnimKeyframe kf : currentMotionKfs) {
|
||||||
|
if (kf.time <= time) { before = kf; }
|
||||||
|
else if (after == null) { after = kf; break; }
|
||||||
}
|
}
|
||||||
com.jme3.math.Vector3f t = visual.getLocalTranslation();
|
|
||||||
visual.setLocalTranslation(t.x, visualSinkCurrent, t.z);
|
float tx, ty, tz, rx, ry, rz;
|
||||||
|
if (before == null && after == null) { return; }
|
||||||
|
else if (before == null) {
|
||||||
|
tx = after.tx; ty = after.ty; tz = after.tz;
|
||||||
|
rx = after.rx; ry = after.ry; rz = after.rz;
|
||||||
|
} else if (after == null) {
|
||||||
|
tx = before.tx; ty = before.ty; tz = before.tz;
|
||||||
|
rx = before.rx; ry = before.ry; rz = before.rz;
|
||||||
|
} else {
|
||||||
|
float t = Math.max(0f, Math.min(1f,
|
||||||
|
(time - before.time) / (after.time - before.time)));
|
||||||
|
tx = lerp(before.tx, after.tx, t);
|
||||||
|
ty = lerp(before.ty, after.ty, t);
|
||||||
|
tz = lerp(before.tz, after.tz, t);
|
||||||
|
rx = lerp(before.rx, after.rx, t);
|
||||||
|
ry = lerp(before.ry, after.ry, t);
|
||||||
|
rz = lerp(before.rz, after.rz, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TX/TZ im Charakter-lokalen Raum: preMotionRotation dreht den Offset in Welt-Raum.
|
||||||
|
// Konvention: local -Z = forward (lookAt-Konvention), also tz negativ = hinter Charakter.
|
||||||
|
Vector3f localXZ = preMotionRotation.mult(new Vector3f(tx, 0f, tz));
|
||||||
|
visual.setLocalTranslation(localXZ.x, ty, localXZ.z);
|
||||||
|
|
||||||
|
// Rotation: additiv zur Startrotation via SLERP der Euler-Offsets
|
||||||
|
Quaternion rotOffset = new Quaternion();
|
||||||
|
rotOffset.fromAngles(
|
||||||
|
rx * FastMath.DEG_TO_RAD,
|
||||||
|
ry * FastMath.DEG_TO_RAD,
|
||||||
|
rz * FastMath.DEG_TO_RAD);
|
||||||
|
visual.setLocalRotation(preMotionRotation.mult(rotOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
||||||
|
|
||||||
private boolean tryPlay(String clip) {
|
private boolean tryPlay(String clip) {
|
||||||
if (animComposer == null || !animLib.applyTo(clip, visual)) {
|
if (animComposer == null || !animLib.applyTo(clip, visual)) {
|
||||||
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
||||||
|
|||||||
@@ -399,9 +399,8 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
AnimationAction idleAction = isBed ? AnimationAction.LYING : AnimationAction.SITTING;
|
AnimationAction idleAction = isBed ? AnimationAction.LYING : AnimationAction.SITTING;
|
||||||
|
|
||||||
// duration=0 → PlayerInputControl ermittelt die echte Clip-Länge automatisch
|
// duration=0 → PlayerInputControl ermittelt die echte Clip-Länge automatisch
|
||||||
// Sink-Wert kommt aus AnimSet-Konfiguration (Animationseditor)
|
|
||||||
playerInput.requestAnimation(action, 0f, () -> {
|
playerInput.requestAnimation(action, 0f, () -> {
|
||||||
teleportToRestPos(entry);
|
if (isBed) teleportToRestPos(entry);
|
||||||
playerInput.lockInPlace();
|
playerInput.lockInPlace();
|
||||||
playerInput.playLockedAnimation(idleAction);
|
playerInput.playLockedAnimation(idleAction);
|
||||||
phase = Phase.RESTING;
|
phase = Phase.RESTING;
|
||||||
@@ -432,19 +431,10 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void teleportToRestPos(InteractableEntry entry) {
|
private void teleportToRestPos(InteractableEntry entry) {
|
||||||
if (physicsChar == null) return;
|
if (physicsChar == null || entry.type() != InteractableType.BED) return;
|
||||||
if (entry.type() == InteractableType.BED) {
|
|
||||||
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
Bed bed = BedIO.load(entry.interactableId()).orElse(null);
|
||||||
if (bed != null && bed.isLiegeSet())
|
if (bed != null && bed.isLiegeSet()) {
|
||||||
physicsChar.setPhysicsLocation(new Vector3f(bed.getLiegeX(), bed.getLiegeY(), bed.getLiegeZ()));
|
physicsChar.setPhysicsLocation(new Vector3f(bed.getLiegeX(), bed.getLiegeY(), bed.getLiegeZ()));
|
||||||
} else if (entry.type() == InteractableType.BENCH) {
|
|
||||||
// X/Z aus dem Sitzpunkt, Y bleibt bei der aktuellen Physik-Position (Charakter ist
|
|
||||||
// bereits auf Bodenhöhe und durch Terrain geerdet — kein Sprung nach oben)
|
|
||||||
Bench bench = BenchIO.load(entry.interactableId()).orElse(null);
|
|
||||||
if (bench != null && bench.isSitzSet()) {
|
|
||||||
float currentY = physicsChar.getPhysicsLocation().y;
|
|
||||||
physicsChar.setPhysicsLocation(new Vector3f(bench.getSitzX(), currentY, bench.getSitzZ()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user