Kampfsystem für Zweihänder erweitert

This commit is contained in:
2026-10-06 21:48:44 +02:00
parent 62931e3273
commit 9ff911eaa6
54 changed files with 258 additions and 47 deletions

View File

@@ -664,18 +664,10 @@ public class AnimPreviewState extends BaseAppState {
// snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary.
boolean isJ3oSource = animAssetPath.matches("(?i).*\\.j3o$");
// FBX-Import erfordert ein geladenes Charakter-Modell.
// Ohne Ziel-Armatur kann normalizeAndRetargetClip die $AssimpFbx$_Translation-Orphan-Joints
// nicht in den Hips-Track falten → Hips-Translation geht verloren → Charakter schrumpft.
if (!isJ3oSource && srcArm != null && dstArm == null) {
input.animPreviewStatus =
"Fehler: FBX-Import erfordert ein geladenes Charakter-Modell. " +
"Bitte zuerst das Modell laden, dann die Animation importieren.";
LOG.warn("[AnimPreview] FBX-Import ohne Charakter-Modell abgebrochen: {}", animAssetPath);
return;
}
com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm;
// Kein Charakter-Modell geladen: Normalisierung gegen eigenes Skelett (Orphan-Fold + Scale-Strip).
// Das erzeugte j3o ist später per redirectAnimClipByName auf jedes kompatible Rig anwendbar.
com.jme3.anim.Armature effectiveDst = dstArm != null ? dstArm : srcArm;
com.jme3.anim.Armature snapArm = effectiveDst;
int saved = 0;
for (AnimClip clip : sourceAC.getAnimClips()) {
@@ -688,7 +680,7 @@ public class AnimPreviewState extends BaseAppState {
}
}
AnimClip result;
if (!retarget) {
if (!retarget && (isJ3oSource || srcArm == null)) {
result = clip;
} else if (isJ3oSource) {
// j3o-Clips wurden bereits beim FBX-Import normalisiert (in Modell-Raum gespeichert).
@@ -696,12 +688,15 @@ public class AnimPreviewState extends BaseAppState {
result = redirectAnimClipByName(clip, dstArm);
} else {
// FBX-Import: Normalisierung in Modell-Raum (Orphan-Fold + Bind-Pose-Delta).
// Das resultierende j3o ist dann mit plain redirectAnimClipByName kompatibel.
result = normalizeAndRetargetClip(clip, dstArm);
// Kein Charakter-Modell → effectiveDst == srcArm → reine Normalisierung ohne Retarget.
result = normalizeAndRetargetClip(clip, effectiveDst);
if (result == null) {
LOG.warn("[AnimPreview] normalizeAndRetarget: keine Joints gemappt, versuche Retargeting");
result = de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm);
if (dstArm != null) {
result = de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm);
}
}
if (result == null) result = clip;
}
if (result == null) continue;

View File

@@ -997,6 +997,11 @@ public class AnimSetController implements ToolController {
return;
}
// ── Aktions-Combo mit Suchfeld ────────────────────────────────────────
javafx.scene.control.TextField actionSearch = new javafx.scene.control.TextField();
actionSearch.setPromptText("Aktion suchen…");
actionSearch.setMaxWidth(Double.MAX_VALUE);
ComboBox<de.blight.game.animation.AnimationAction> actionCombo = new ComboBox<>();
actionCombo.getItems().addAll(available);
javafx.util.Callback<javafx.scene.control.ListView<de.blight.game.animation.AnimationAction>,
@@ -1013,29 +1018,85 @@ public class AnimSetController implements ToolController {
actionCombo.setMaxWidth(Double.MAX_VALUE);
actionCombo.getSelectionModel().selectFirst();
ComboBox<String> clipCombo = new ComboBox<>();
actionSearch.textProperty().addListener((obs, ov, nv) -> {
String lower = nv == null ? "" : nv.toLowerCase();
actionCombo.getItems().setAll(available.stream()
.filter(a -> lower.isEmpty()
|| a.displayName().toLowerCase().contains(lower)
|| a.name().toLowerCase().contains(lower))
.collect(java.util.stream.Collectors.toList()));
if (!actionCombo.getItems().isEmpty()) actionCombo.getSelectionModel().selectFirst();
});
// ── Clip-Combo mit Suchfeld ───────────────────────────────────────────
java.util.List<String> allClipNames = new java.util.ArrayList<>(animSetClipListView.getItems());
for (String subName : editableSubClips.keySet()) {
if (!allClipNames.contains(subName)) allClipNames.add(subName);
}
javafx.scene.control.TextField clipSearch = new javafx.scene.control.TextField();
clipSearch.setPromptText("Clip suchen…");
clipSearch.setMaxWidth(Double.MAX_VALUE);
ComboBox<String> clipCombo = new ComboBox<>();
clipCombo.getItems().addAll(allClipNames);
clipCombo.setMaxWidth(Double.MAX_VALUE);
clipCombo.getSelectionModel().selectFirst();
clipSearch.textProperty().addListener((obs, ov, nv) -> {
String lower = nv == null ? "" : nv.toLowerCase();
clipCombo.getItems().setAll(allClipNames.stream()
.filter(c -> lower.isEmpty() || c.toLowerCase().contains(lower))
.collect(java.util.stream.Collectors.toList()));
if (!clipCombo.getItems().isEmpty()) clipCombo.getSelectionModel().selectFirst();
});
// ── Layout ────────────────────────────────────────────────────────────
VBox actionBox = new VBox(4, actionSearch, actionCombo);
VBox clipBox = new VBox(4, clipSearch, clipCombo);
javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane();
grid.setHgap(8); grid.setVgap(6);
grid.add(new Label("Aktion:"), 0, 0); grid.add(actionCombo, 1, 0);
grid.add(new Label("Clip:"), 0, 1); grid.add(clipCombo, 1, 1);
grid.setHgap(8); grid.setVgap(8);
grid.add(new Label("Aktion:"), 0, 0); grid.add(actionBox, 1, 0);
grid.add(new Label("Clip:"), 0, 1); grid.add(clipBox, 1, 1);
javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints();
cc.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), cc);
CheckBox weitereCB = new CheckBox("Weitere zuordnen");
VBox content = new VBox(12, grid, weitereCB);
Dialog<ButtonType> dlg = new Dialog<>();
dlg.initOwner(ctx.stageSupplier().get());
dlg.setTitle("Aktion zuordnen");
ButtonType ok = new ButtonType("Hinzufügen", ButtonBar.ButtonData.OK_DONE);
dlg.getDialogPane().getButtonTypes().addAll(ok, ButtonType.CANCEL);
dlg.getDialogPane().setContent(grid);
dlg.getDialogPane().setContent(content);
dlg.getDialogPane().setPrefWidth(480);
// Wenn "Weitere zuordnen" aktiv: Dialog offen lassen, nächste Aktion auto-selektieren.
javafx.scene.Node okNode = dlg.getDialogPane().lookupButton(ok);
okNode.addEventFilter(javafx.event.ActionEvent.ACTION, ev -> {
if (!weitereCB.isSelected()) return;
ev.consume();
var action = actionCombo.getValue();
var clip = clipCombo.getValue();
if (action == null || clip == null) return;
String entry = action.name() + " → " + clip;
if (!animSetActionListView.getItems().contains(entry)) {
animSetActionListView.getItems().add(entry);
autoSaveAnimSet();
}
available.remove(action);
String lower = actionSearch.getText() == null ? "" : actionSearch.getText().toLowerCase();
actionCombo.getItems().setAll(available.stream()
.filter(a -> lower.isEmpty()
|| a.displayName().toLowerCase().contains(lower)
|| a.name().toLowerCase().contains(lower))
.collect(java.util.stream.Collectors.toList()));
if (!actionCombo.getItems().isEmpty()) actionCombo.getSelectionModel().selectFirst();
});
dlg.showAndWait().ifPresent(bt -> {
if (bt != ok) return;
var action = actionCombo.getValue();