Weiter Ingame an den animationen gepfeilt

This commit is contained in:
2026-08-19 19:37:59 +02:00
parent ec0865d09b
commit c221c26a23
6 changed files with 52 additions and 81 deletions

View File

@@ -2,35 +2,27 @@
"chapter": 0, "chapter": 0,
"level": 0, "level": 0,
"xp": 0, "xp": 0,
"currentHp": 100, "currentHp": 0,
"currentStamina": 100, "currentStamina": 0,
"currentMana": 100, "currentMana": 0,
"maxHp": 100, "maxHp": 0,
"maxStamina": 100, "maxStamina": 0,
"maxMana": 100, "maxMana": 0,
"openHpRegeneration": 0, "openHpRegeneration": 0,
"openManaRegeneration": 0, "openManaRegeneration": 0,
"openStaminaRegeneration": 0, "openStaminaRegeneration": 0,
"restorationValue": 10, "restorationValue": 10,
"abilities": { "playedMonologueIds": [],
"lvlMagic": 1, "firedTriggerIds": [],
"lvlStaffCombat": 1, "visitedZoneIds": [],
"lvlSwordsmanship": 1, "gameVariables": {},
"lvlArchery": 1, "memberFractionIds": [],
"lvlHeavyWeapons": 1,
"lvlCrossbow": 1,
"lvlThievery": 0,
"lvlAlchemy": 0,
"lvlEngineering": 0,
"lvlSmithery": 0,
"lvlEnchanting": 0
},
"listeners": [], "listeners": [],
"characterId": "hero", "characterId": "hero",
"name": { "name": {
"id": "hero.name" "id": "hero.name"
}, },
"modelPath": "Models/Chars/mainchar.j3o", "modelPath": "Characters/Models/hero.j3o",
"animSetPath": "human", "animSetPath": "human",
"type": "MAIN_CHARACTER" "type": "MAIN_CHARACTER"
} }

View File

@@ -11410,6 +11410,11 @@ public class EditorApp extends Application {
saveCurrentAnimSet(animSetCurrentName, animSetCurrentDir); saveCurrentAnimSet(animSetCurrentName, animSetCurrentDir);
} }
private void autoSaveCharacter() {
if (charIdField == null || charIdField.getText().trim().isBlank()) return;
saveCharacter(ASSET_ROOT.resolve("Characters"));
}
private void saveCurrentAnimSet(String setName, Path setDir) { private void saveCurrentAnimSet(String setName, Path setDir) {
if (animSetClipListView == null) { if (animSetClipListView == null) {
return; return;
@@ -12037,16 +12042,6 @@ public class EditorApp extends Application {
VBox inner = new VBox(8); VBox inner = new VBox(8);
inner.setPadding(new Insets(10)); inner.setPadding(new Insets(10));
Button newCharBtn = new Button("Neuer Charakter");
newCharBtn.setMaxWidth(Double.MAX_VALUE);
newCharBtn.setOnAction(e -> clearCharacterForm());
Button importCharModelBtn = new Button("Character-Modell importieren (FBX)…");
importCharModelBtn.setMaxWidth(Double.MAX_VALUE);
importCharModelBtn.setStyle("-fx-font-weight: bold;");
importCharModelBtn.setOnAction(e -> handleCharacterModelImport(importCharModelBtn.getScene().getWindow()));
inner.getChildren().addAll(newCharBtn, importCharModelBtn);
charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen"); charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen");
charEditorStatusLabel.setWrapText(true); charEditorStatusLabel.setWrapText(true);
@@ -12097,18 +12092,6 @@ public class EditorApp extends Application {
refreshCharAnimSetCombo(); refreshCharAnimSetCombo();
charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet()); charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet());
Button stripClipsBtn = new Button("Eingebettete Clips löschen");
stripClipsBtn.setMaxWidth(Double.MAX_VALUE);
stripClipsBtn.setDisable(true);
charModelCombo.valueProperty().addListener((obs, ov, nv) ->
stripClipsBtn.setDisable(nv == null || nv.isBlank()));
stripClipsBtn.setOnAction(e -> {
String modelPath = charModelCombo.getValue();
if (modelPath == null || modelPath.isBlank()) return;
if (charEditorStatusLabel != null)
charEditorStatusLabel.setText("Entferne eingebettete Clips…");
input.animStripClipsRequest.set(modelPath);
});
charEditContainer.getChildren().addAll( charEditContainer.getChildren().addAll(
dl.apply("ID:"), charIdField, dl.apply("ID:"), charIdField,
@@ -12202,8 +12185,7 @@ public class EditorApp extends Application {
charEditContainer.getChildren().add(charNpcSection); charEditContainer.getChildren().add(charNpcSection);
charEditContainer.getChildren().addAll( charEditContainer.getChildren().addAll(
dl.apply("Modell:"), charModelCombo, dl.apply("Modell:"), charModelCombo,
dl.apply("Anim-Set:"), charAnimSetCombo, dl.apply("Anim-Set:"), charAnimSetCombo
stripClipsBtn
); );
// ── Aktions-Zuweisung ───────────────────────────────────────────────── // ── Aktions-Zuweisung ─────────────────────────────────────────────────
@@ -12214,13 +12196,20 @@ public class EditorApp extends Application {
charActionLabelsBox.getChildren().add(actionHint); charActionLabelsBox.getChildren().add(actionHint);
charEditContainer.getChildren().add(charActionLabelsBox); charEditContainer.getChildren().add(charActionLabelsBox);
// ── Speichern ───────────────────────────────────────────────────────── // ── Auto-Save-Listener ────────────────────────────────────────────────
charEditContainer.getChildren().add(new Separator()); charIdField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
Button saveCharBtn = new Button("Charakter speichern"); charTypeCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
saveCharBtn.setMaxWidth(Double.MAX_VALUE); charModelCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
saveCharBtn.setStyle("-fx-font-weight:bold;-fx-background-color:#3a7a3a;-fx-text-fill:white;"); charAnimSetCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
saveCharBtn.setOnAction(e -> saveCharacter(charDir)); charFractionCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
charEditContainer.getChildren().add(saveCharBtn); charStatusCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
charTraderCheck.selectedProperty().addListener((obs, ov, nv) -> autoSaveCharacter());
charTraderItemsView.getItems().addListener(
(javafx.collections.ListChangeListener<String>) c -> autoSaveCharacter());
charMsgFriendly.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
charMsgNeutral.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
charMsgEnraged.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
charMsgEnemy.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); });
inner.getChildren().add(charEditContainer); inner.getChildren().add(charEditContainer);

View File

@@ -336,21 +336,14 @@ public class AnimationLibrary extends BaseAppState {
return null; return null;
} }
// Clips mit Vorwärts-Root-Motion in Local-Y (rennen, gehen, springen):
// Y wird auf 0 eingefroren → kein Drift. Alle anderen Clips: Y bleibt frei (Hinsetzen usw.)
private static final java.util.Set<String> LOCOMOTION_CLIPS = java.util.Set.of(
"running", "walking", "sprinting", "running_jump"
);
/** /**
* Entfernt Root-Motion aus dem flachsten Bone mit Translation-Track (typischerweise Hips). * Entfernt Root-Motion aus dem flachsten Bone mit Translation-Track (typischerweise Hips).
* Nur dieser eine Bone wird modifiziert — alle anderen Bones bleiben unverändert. * Nur dieser eine Bone wird modifiziert — alle anderen Bones bleiben unverändert.
* *
* In diesen Mixamo-Exporten ist die Achsenbelegung des Hips-Bones: * Achsenbelegung dieses Mixamo-Exports im Hips-Bone-Local-Space:
* Local X → seitlich → wird auf 0 eingefroren (kein Seiten-Drift) * Local X → JME3-X (seitlich) → wird auf 0 eingefroren
* Local Y → vorwärts → wird auf 0 eingefroren für Lauf-Clips (kein Vorwärts-Drift) * Local Y → JME3-Y (Höhe oben) → IMMER frei lassen (Charakter-Höhe erhalten)
* bleibt frei für Sitz/Stand-Clips (leichte Neigungsbewegung) * Local Z → JME3-Z (vorwärts) → wird auf 0 eingefroren (alle Clips auf der Stelle)
* Local Z → Höhe → IMMER frei lassen (Charakter-Höhe und Setz-Bewegung erhalten)
*/ */
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) { public static AnimClip snapRootBoneXZ(AnimClip clip, Armature armature) {
@@ -367,8 +360,6 @@ public class AnimationLibrary extends BaseAppState {
} }
if (minDepth == Integer.MAX_VALUE) return clip; if (minDepth == Integer.MAX_VALUE) return clip;
boolean isLocomotion = LOCOMOTION_CLIPS.contains(clip.getName().toLowerCase(java.util.Locale.ROOT));
List<AnimTrack<?>> newTracks = new ArrayList<>(); List<AnimTrack<?>> newTracks = new ArrayList<>();
boolean modified = false; boolean modified = false;
for (AnimTrack<?> track : clip.getTracks()) { for (AnimTrack<?> track : clip.getTracks()) {
@@ -383,11 +374,6 @@ public class AnimationLibrary extends BaseAppState {
continue; continue;
} }
// Y-Normalisierung für Nicht-Lauf-Clips: Frame-0 auf Bind-Pose-Y
float bindY = j.getInitialTransform().getTranslation().y;
float frame0Y = translations[0].y;
float yOffset = bindY - frame0Y;
// Diagnostik // Diagnostik
float yMin = Float.MAX_VALUE, yMax = -Float.MAX_VALUE; float yMin = Float.MAX_VALUE, yMax = -Float.MAX_VALUE;
float xRange = 0, zRange = 0; float xRange = 0, zRange = 0;
@@ -397,21 +383,18 @@ public class AnimationLibrary extends BaseAppState {
xRange = Math.max(xRange, Math.abs(t.x - translations[0].x)); xRange = Math.max(xRange, Math.abs(t.x - translations[0].x));
zRange = Math.max(zRange, Math.abs(t.z - translations[0].z)); zRange = Math.max(zRange, Math.abs(t.z - translations[0].z));
} }
log.info("[AnimLib] snap '{}' root='{}' locomotion={} bindY={} frame0Y={} yOffset={} yRange={} xRange={} zRange={}", log.info("[AnimLib] snap '{}' root='{}' xRange={} yRange={} zRange={}",
clip.getName(), j.getName(), isLocomotion, clip.getName(), j.getName(),
String.format("%.3f", bindY), String.format("%.3f", frame0Y), String.format("%.3f", xRange),
String.format("%.3f", yOffset), String.format("%.3f", yMax - yMin), String.format("%.3f", yMax - yMin),
String.format("%.3f", xRange), String.format("%.3f", zRange)); String.format("%.3f", zRange));
Vector3f[] snapped = new Vector3f[translations.length]; Vector3f[] snapped = new Vector3f[translations.length];
for (int i = 0; i < translations.length; i++) { for (int i = 0; i < translations.length; i++) {
float newY = isLocomotion
? 0f // Lauf-Clips: Y einfrieren (Vorwärts-Drift weg)
: (translations[i].y + yOffset); // Rest: Y normalisiert (Neigung erhalten)
snapped[i] = new Vector3f( snapped[i] = new Vector3f(
0f, // X immer 0 (Seiten-Drift weg) 0f, // X = 0 (kein seitlicher Drift)
newY, translations[i].y, // Y frei (Höhe; Y=oben in bone-local / JME3-Y)
translations[i].z // Z immer frei (Höhe und Setz-Bewegung erhalten) 0f // Z = 0 (kein Vorwärts-Drift; Z=vorwärts in bone-local)
); );
} }
newTracks.add(new TransformTrack(j, tt.getTimes(), snapped, tt.getRotations(), tt.getScales())); newTracks.add(new TransformTrack(j, tt.getTimes(), snapped, tt.getRotations(), tt.getScales()));

View File

@@ -168,7 +168,7 @@ public class PlayerInputControl {
visualBaseTranslation = visual.getLocalTranslation().clone(); visualBaseTranslation = visual.getLocalTranslation().clone();
} }
try { try {
AnimSet as = AnimSet.load(assetRoot.resolve("animations/sets"), animSetName); AnimSet as = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName);
animOffsets = as.getAnimOffsets(); animOffsets = as.getAnimOffsets();
log.info("[AnimCtx] {} Anim-Offset-Einträge geladen.", animOffsets.size()); log.info("[AnimCtx] {} Anim-Offset-Einträge geladen.", animOffsets.size());
} catch (Exception e) { } catch (Exception e) {
@@ -629,6 +629,7 @@ public class PlayerInputControl {
} }
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action); String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName); log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName);
if (runningClip != null) nextTransitionLength = 0.15;
if (clip != null && tryPlay(clip)) return; if (clip != null && tryPlay(clip)) return;
if (action != AnimationAction.DEFAULT) { if (action != AnimationAction.DEFAULT) {
String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT); String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT);

View File

@@ -277,6 +277,12 @@ public class CharacterNavigator {
} }
if (clip == null) return; if (clip == null) return;
if (!animLib.applyTo(clip, visual)) return; if (!animLib.applyTo(clip, visual)) return;
if (currentAnim != null) {
com.jme3.anim.tween.action.Action nextAction = animComposer.action(clip);
if (nextAction instanceof com.jme3.anim.tween.action.BlendableAction ba) {
ba.setTransitionLength(0.15);
}
}
com.jme3.anim.tween.action.Action act = animComposer.setCurrentAction(clip); com.jme3.anim.tween.action.Action act = animComposer.setCurrentAction(clip);
if (act == null) return; if (act == null) return;
if (skinningControl != null && !skinningControl.isEnabled()) { if (skinningControl != null && !skinningControl.isEnabled()) {