Weiter am Kampfsystem gearbeitet, hier Schwertkampf und die animationen
This commit is contained in:
BIN
assets/imported/animations/Sword And Shield Attack.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Attack.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Idle.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Idle.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Run.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Run.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Slash 2.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Slash 2.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Walk.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Walk.fbx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,7 +9,12 @@
|
||||
"T-Pose",
|
||||
"Walking",
|
||||
"Jumping",
|
||||
"Sword And Shield Slash"
|
||||
"Sword And Shield Slash",
|
||||
"Sword And Shield Idle",
|
||||
"Sword And Shield Run",
|
||||
"Sword And Shield Walk",
|
||||
"Sword And Shield Attack",
|
||||
"Sword And Shield Slash 2"
|
||||
],
|
||||
"actionMap": {
|
||||
"DEFAULT": "Idle",
|
||||
@@ -22,7 +27,11 @@
|
||||
"IDLE": "Idle",
|
||||
"JUMP": "Jumping",
|
||||
"SWORD_ATTACK_1": "Sword And Shield Slash",
|
||||
"SWORD_ATTACK_2": "Sword And Shield Slash"
|
||||
"SWORD_IDLE": "Sword And Shield Idle",
|
||||
"SWORD_WALK": "Sword And Shield Walk",
|
||||
"SWORD_RUN": "Sword And Shield Run",
|
||||
"SWORD_ATTACK_3": "Sword And Shield Attack",
|
||||
"SWORD_ATTACK_2": "Sword And Shield Slash 2"
|
||||
},
|
||||
"previewModelPath": "Characters/Models/hero.j3o",
|
||||
"animOffsets": {
|
||||
@@ -67,5 +76,16 @@
|
||||
"SIT_UP",
|
||||
"IDLE"
|
||||
]
|
||||
}
|
||||
},
|
||||
"snapClips": [
|
||||
"Idle",
|
||||
"Walking",
|
||||
"Running",
|
||||
"Sprint",
|
||||
"Jumping",
|
||||
"Sword And Shield Idle",
|
||||
"Sword And Shield Run",
|
||||
"Sword And Shield Walk",
|
||||
"Sword And Shield Attack"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
#Tue Sep 15 12:31:37 CEST 2026
|
||||
#Sun Sep 20 15:31:32 CEST 2026
|
||||
attachedEmitters.count=0
|
||||
attachedLights.count=0
|
||||
attachedSounds.count=0
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
"worthGold": 0,
|
||||
"damage": 0,
|
||||
"grip": {
|
||||
"posX": 0.24,
|
||||
"posY": 0.05,
|
||||
"posX": 0.37,
|
||||
"posY": 0.08,
|
||||
"posZ": 0.0,
|
||||
"rotX": 0.0,
|
||||
"rotY": -90.0,
|
||||
"rotZ": 0.0
|
||||
"rotZ": 90.0
|
||||
},
|
||||
"modelRef": {
|
||||
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"
|
||||
|
||||
@@ -1562,17 +1562,22 @@ public class ModelEditorState extends BaseAppState {
|
||||
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||
head.setLocalRotation(headRot);
|
||||
|
||||
// Klingenflosse: dünnes flaches Rechteck senkrecht zum Schaft entlang +Y,
|
||||
// damit Rotation um Z (Roll) sichtbar ist. Breite 6cm, Dicke 2mm, entlang Schaft.
|
||||
// Griffachsen-Zylinder: runder Zylinder senkrecht zum Schaft (+Y),
|
||||
// zeigt die Roll-Richtung (ersetzt die frühere flache Klingenflosse).
|
||||
// JME3-Cylinder liegt standardmäßig auf +Z → -90° um X dreht ihn nach +Y.
|
||||
Material matFin = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
matFin.setColor("Color", new ColorRGBA(1f, 0.8f, 0.1f, 1f));
|
||||
Geometry fin = new Geometry("gFin", new Box(0.001f, 0.03f, shaftLen * 0.5f));
|
||||
fin.setMaterial(matFin);
|
||||
fin.setLocalTranslation(0f, 0.03f, shaftLen * 0.5f);
|
||||
float axisLen = 0.06f;
|
||||
Geometry gripAxis = new Geometry("gGripAxis", new Cylinder(4, 8, 0.006f, axisLen, true));
|
||||
gripAxis.setMaterial(matFin);
|
||||
Quaternion axisRot = new Quaternion();
|
||||
axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||
gripAxis.setLocalRotation(axisRot);
|
||||
gripAxis.setLocalTranslation(0f, axisLen * 0.5f, 0f);
|
||||
|
||||
group.attachChild(marker);
|
||||
group.attachChild(shaft);
|
||||
group.attachChild(fin);
|
||||
group.attachChild(gripAxis);
|
||||
group.attachChild(head);
|
||||
gripGizmoNode.attachChild(group);
|
||||
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
|
||||
@@ -51,6 +51,7 @@ public class AnimSetController implements ToolController {
|
||||
private ListView<String> animSetPartsListView;
|
||||
private java.util.Map<String, de.blight.game.animation.AnimSet.SubClipDef>
|
||||
editableSubClips = new java.util.LinkedHashMap<>();
|
||||
private java.util.Set<String> editableSnapClips = new java.util.LinkedHashSet<>();
|
||||
private ListView<String> animSetSequenceListView;
|
||||
private java.util.Map<String, java.util.List<String>>
|
||||
editableSequences = new java.util.LinkedHashMap<>();
|
||||
@@ -76,6 +77,16 @@ public class AnimSetController implements ToolController {
|
||||
animSetCurrentName = setName;
|
||||
animSetCurrentDir = setDir;
|
||||
currentAnimSet = animSet;
|
||||
editableSnapClips = new java.util.LinkedHashSet<>(animSet.getSnapClips());
|
||||
// Migration: wenn snapClips noch leer, Fallback-Clips (LOCOMOTION_ACTIONS) einmalig übernehmen
|
||||
// damit der Checkbox-Zustand die tatsächliche Snap-Konfiguration widerspiegelt.
|
||||
if (editableSnapClips.isEmpty()) {
|
||||
java.util.Set<String> locoFallback = java.util.Set.of(
|
||||
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
|
||||
animSet.getActionMap().forEach((action, clip) -> {
|
||||
if (locoFallback.contains(action)) editableSnapClips.add(clip);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -337,17 +348,14 @@ public class AnimSetController implements ToolController {
|
||||
animSetClipListView.setPrefHeight(180);
|
||||
editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips());
|
||||
editableSequences = new java.util.LinkedHashMap<>(animSet.getSequences());
|
||||
// editableSnapClips wird bereits in openWithSet gesetzt (inkl. Migration)
|
||||
ctx.input().animPreviewClipOffsets.clear();
|
||||
for (var en : animSet.getAnimOffsets().entrySet()) {
|
||||
de.blight.game.animation.AnimOffset o = en.getValue();
|
||||
ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
||||
}
|
||||
java.util.Set<String> locomotionActions = java.util.Set.of(
|
||||
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
|
||||
ctx.input().animPreviewLocomotionClips.clear();
|
||||
animSet.getActionMap().forEach((action, clip) -> {
|
||||
if (locomotionActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip);
|
||||
});
|
||||
ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
|
||||
|
||||
Path animRootDir = ctx.assetRoot().resolve("Characters");
|
||||
|
||||
@@ -454,10 +462,41 @@ public class AnimSetController implements ToolController {
|
||||
autoSaveAnimSet();
|
||||
});
|
||||
|
||||
CheckBox snapCB = new CheckBox("Root-Motion entfernen (auf der Stelle)");
|
||||
snapCB.setDisable(true);
|
||||
snapCB.setTooltip(new Tooltip("Bewegt sich die Animation durch den Raum (Walk/Run)?\nMit dieser Option bleibt der Charakter an Ort und Stelle\nund das Spiel übernimmt die Fortbewegung."));
|
||||
final boolean[] snapCbUpdating = {false};
|
||||
|
||||
animSetClipListView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, ov, nv) -> {
|
||||
snapCB.setDisable(nv == null);
|
||||
if (nv != null) {
|
||||
snapCbUpdating[0] = true;
|
||||
snapCB.setSelected(editableSnapClips.contains(nv));
|
||||
snapCbUpdating[0] = false;
|
||||
}
|
||||
});
|
||||
|
||||
snapCB.selectedProperty().addListener((obs, ov, nv) -> {
|
||||
if (snapCbUpdating[0]) return;
|
||||
String sel = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
if (nv) {
|
||||
editableSnapClips.add(sel);
|
||||
} else {
|
||||
editableSnapClips.remove(sel);
|
||||
}
|
||||
ctx.input().animPreviewLocomotionClips.clear();
|
||||
ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
|
||||
// Clip neu laden damit Snap-Änderung sofort in der Vorschau sichtbar wird
|
||||
ctx.input().animImportQueue.offer(findAnimClipPath(sel));
|
||||
autoSaveAnimSet();
|
||||
});
|
||||
|
||||
HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn);
|
||||
HBox.setHgrow(addClipBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
|
||||
inner.getChildren().addAll(animSetClipListView, clipBtns);
|
||||
inner.getChildren().addAll(animSetClipListView, snapCB, clipBtns);
|
||||
|
||||
// ── Clip-Teile (Sub-Clips) ─────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator());
|
||||
@@ -1172,12 +1211,9 @@ public class AnimSetController implements ToolController {
|
||||
de.blight.game.animation.AnimOffset o = en.getValue();
|
||||
ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
||||
}
|
||||
java.util.Set<String> locoActions = java.util.Set.of(
|
||||
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
|
||||
animSet.setSnapClips(new java.util.LinkedHashSet<>(editableSnapClips));
|
||||
ctx.input().animPreviewLocomotionClips.clear();
|
||||
animSet.getActionMap().forEach((action, clip) -> {
|
||||
if (locoActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip);
|
||||
});
|
||||
ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
|
||||
if (animSetModelCombo != null && animSetModelCombo.getValue() != null
|
||||
&& !animSetModelCombo.getValue().isBlank()) {
|
||||
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
||||
|
||||
@@ -38,12 +38,25 @@ public class AnimPlayer {
|
||||
private LinearBlendSpace blendSpace;
|
||||
private String pendingClip;
|
||||
private float blendElapsed;
|
||||
private float blendDuration;
|
||||
/** Einsprungzeit: Clip startet bei diesem Zeitwert statt bei 0 (überspringt z.B. das Ausholen). */
|
||||
private float pendingStartOffset;
|
||||
|
||||
public AnimPlayer(AnimComposer composer, SkinningControl skinning) {
|
||||
this.composer = composer;
|
||||
this.skinning = skinning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt einen Clip ab, optional ab einem Zeitversatz (z.B. Ausholen überspringen).
|
||||
*
|
||||
* @param startTimeOffset Clip startet ab dieser Zeit statt bei 0; 0 = von Anfang
|
||||
*/
|
||||
public boolean play(String clipName, float transition, float startTimeOffset) {
|
||||
this.pendingStartOffset = startTimeOffset;
|
||||
return play(clipName, transition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt einen Clip ab.
|
||||
* Bei laufendem Clip und {@code transition > 0}: nahtloser Übergang
|
||||
@@ -82,6 +95,7 @@ public class AnimPlayer {
|
||||
blendSpace = space;
|
||||
pendingClip = clipName;
|
||||
blendElapsed = 0f;
|
||||
blendDuration = Math.max(0.01f, transition);
|
||||
currentAction = blend;
|
||||
currentClip = null;
|
||||
log.debug("[AnimPlayer] Blend → '{}'", clipName);
|
||||
@@ -101,7 +115,7 @@ public class AnimPlayer {
|
||||
public void update(float tpf) {
|
||||
if (pendingClip == null || blendSpace == null) return;
|
||||
blendElapsed += tpf;
|
||||
float bt = Math.min(1f, blendElapsed / BLEND_DURATION);
|
||||
float bt = Math.min(1f, blendElapsed / blendDuration);
|
||||
blendSpace.setValue(bt);
|
||||
if (bt >= 1f) finishBlend();
|
||||
}
|
||||
@@ -129,13 +143,35 @@ public class AnimPlayer {
|
||||
|
||||
private boolean directPlay(String clipName) {
|
||||
try {
|
||||
Action act = composer.action(clipName);
|
||||
if (act instanceof BlendableAction ba) ba.setTransitionLength(0.0);
|
||||
// Diagnose: zeigt warum length=0 entsteht
|
||||
com.jme3.anim.AnimClip rawClip = composer.getAnimClip(clipName);
|
||||
if (rawClip != null) {
|
||||
com.jme3.anim.AnimTrack[] rawTracks = rawClip.getTracks();
|
||||
if (rawTracks != null && rawTracks.length > 0 && rawTracks[0] instanceof com.jme3.anim.TransformTrack tt) {
|
||||
float[] times = tt.getTimes();
|
||||
log.info("[AnimDiag] '{}' clipLen={}s tracks={} t0Len={}s times=[{}..{}] n={}",
|
||||
clipName, rawClip.getLength(), rawTracks.length, tt.getLength(),
|
||||
times != null && times.length > 0 ? times[0] : -1,
|
||||
times != null && times.length > 0 ? times[times.length-1] : -1,
|
||||
times != null ? times.length : 0);
|
||||
} else {
|
||||
log.info("[AnimDiag] '{}' clipLen={}s tracks={}", clipName, rawClip.getLength(),
|
||||
rawTracks != null ? rawTracks.length : -1);
|
||||
}
|
||||
} else {
|
||||
log.info("[AnimDiag] '{}' → clip NOT in animClipMap!", clipName);
|
||||
}
|
||||
|
||||
currentAction = composer.setCurrentAction(clipName);
|
||||
currentClip = clipName;
|
||||
pendingClip = null;
|
||||
blendSpace = null;
|
||||
log.debug("[AnimPlayer] direktPlay → '{}'", clipName);
|
||||
double len = currentAction != null ? currentAction.getLength() : -1;
|
||||
if (pendingStartOffset > 0f && currentAction != null && len > 0.01) {
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(pendingStartOffset % (float) len);
|
||||
}
|
||||
pendingStartOffset = 0f;
|
||||
log.info("[AnimPlayer] direktPlay → '{}' length={}s", clipName, len);
|
||||
return currentAction != null;
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimPlayer] direktPlay('{}') fehlgeschlagen: {}", clipName, e.getMessage());
|
||||
@@ -153,15 +189,15 @@ public class AnimPlayer {
|
||||
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
Action next = composer.action(clip);
|
||||
if (next instanceof BlendableAction ba) ba.setTransitionLength(0.0);
|
||||
currentAction = composer.setCurrentAction(clip);
|
||||
currentClip = clip;
|
||||
double len = currentAction != null ? currentAction.getLength() : 0.0;
|
||||
// AnimLayer.setTime: kein extra controlUpdate, kein loop=false-Seiteneffekt.
|
||||
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
|
||||
if (len > 0.01 && elapsed > 0) {
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(elapsed % len);
|
||||
if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) {
|
||||
float t = (pendingStartOffset + elapsed) % (float) len;
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t);
|
||||
pendingStartOffset = 0f;
|
||||
}
|
||||
log.debug("[AnimPlayer] finishBlend → '{}'", clip);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -7,8 +7,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Beschreibt ein Animations-Set: eine Liste von Clip-Namen sowie die
|
||||
@@ -32,6 +34,8 @@ public class AnimSet {
|
||||
private Map<String, SubClipDef> subClips = new LinkedHashMap<>();
|
||||
/** Ablauf-Definitionen: Name → geordnete Liste von Aktions-Namen (aus actionMap). */
|
||||
private Map<String, List<String>> sequences = new LinkedHashMap<>();
|
||||
/** Clip-Namen die snapRootBoneXZ erhalten sollen (Root-Motion entfernen, Bewegung auf der Stelle). */
|
||||
private Set<String> snapClips = new LinkedHashSet<>();
|
||||
|
||||
/** Beschreibt einen Sub-Clip: Zeitfenster [start, end] innerhalb eines kombinierten Quell-Clips. */
|
||||
public static class SubClipDef {
|
||||
@@ -60,6 +64,10 @@ public class AnimSet {
|
||||
return sequences != null ? sequences : new LinkedHashMap<>();
|
||||
}
|
||||
public void setSequences(Map<String, List<String>> sequences) { this.sequences = sequences; }
|
||||
public Set<String> getSnapClips() {
|
||||
return snapClips != null ? snapClips : new LinkedHashSet<>();
|
||||
}
|
||||
public void setSnapClips(Set<String> snapClips) { this.snapClips = snapClips; }
|
||||
|
||||
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
||||
public void save(Path setDir, String setName) throws IOException {
|
||||
|
||||
@@ -35,17 +35,17 @@ public enum AnimationAction {
|
||||
// ── Schwert ───────────────────────────────────────────────────────────────
|
||||
SWORD_ATTACK_1,
|
||||
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN,
|
||||
SWORD_ATTACK_2, SWORD_HEAVY_ATTACK,
|
||||
SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,
|
||||
SWORD_IDLE, SWORD_WALK, SWORD_RUN,
|
||||
|
||||
// ── Axt ───────────────────────────────────────────────────────────────────
|
||||
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_HIT_TAKEN,
|
||||
AXE_ATTACK_1, AXE_ATTACK_2, AXE_HEAVY_ATTACK,
|
||||
AXE_ATTACK_1, AXE_ATTACK_2, AXE_ATTACK_3, AXE_HEAVY_ATTACK,
|
||||
AXE_IDLE, AXE_WALK, AXE_RUN,
|
||||
|
||||
// ── Zweihänder ────────────────────────────────────────────────────────────
|
||||
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_HIT_TAKEN,
|
||||
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_HEAVY_ATTACK,
|
||||
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK,
|
||||
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
|
||||
|
||||
// ── Bogen ─────────────────────────────────────────────────────────────────
|
||||
@@ -98,6 +98,7 @@ public enum AnimationAction {
|
||||
case SWORD_BLOCK -> TextRegistry.resolve(null, key, "Block (Schwert)");
|
||||
case SWORD_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
|
||||
case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)");
|
||||
case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
|
||||
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
|
||||
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
||||
case SWORD_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)");
|
||||
@@ -109,6 +110,7 @@ public enum AnimationAction {
|
||||
case AXE_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Axt)");
|
||||
case AXE_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Axt)");
|
||||
case AXE_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Axt)");
|
||||
case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)");
|
||||
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
|
||||
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
|
||||
case AXE_WALK -> TextRegistry.resolve(null, key, "Gehen (Axt)");
|
||||
@@ -120,6 +122,7 @@ public enum AnimationAction {
|
||||
case TWO_HAND_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
|
||||
case TWO_HAND_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Zweihänder)");
|
||||
case TWO_HAND_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Zweihänder)");
|
||||
case TWO_HAND_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Zweihänder)");
|
||||
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
|
||||
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
|
||||
case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)");
|
||||
|
||||
@@ -43,10 +43,6 @@ public class AnimationLibrary extends BaseAppState {
|
||||
/** Clips die snapRootBoneXZ erhalten (Locomotion: Walk, Run, Sprint, …). */
|
||||
private final Set<String> snapClipNames = new LinkedHashSet<>();
|
||||
|
||||
/** Standard-Locomotion-Aktionen, deren gemappte Clips snapRootBoneXZ erhalten. */
|
||||
private static final Set<String> LOCOMOTION_ACTIONS = Set.of(
|
||||
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT"
|
||||
);
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -103,6 +99,14 @@ public class AnimationLibrary extends BaseAppState {
|
||||
|
||||
if (ac.getAnimClip(clipName) != null) return true; // bereits vorhanden
|
||||
|
||||
{
|
||||
com.jme3.anim.AnimTrack<?>[] st = src.getTracks();
|
||||
int sn0 = 0;
|
||||
if (st != null && st.length > 0 && st[0] instanceof TransformTrack stt)
|
||||
sn0 = stt.getTimes() != null ? stt.getTimes().length : 0;
|
||||
log.info("[AnimLib] applyTo pre-retarget '{}': srcLen={}s srcTracks={} srcT0Frames={}",
|
||||
clipName, src.getLength(), st != null ? st.length : 0, sn0);
|
||||
}
|
||||
AnimClip target;
|
||||
if (srcArm != null && srcArm != sc.getArmature()) {
|
||||
// Immer retarget() aufrufen – auch bei gleichen Knochen-Namen.
|
||||
@@ -131,8 +135,10 @@ public class AnimationLibrary extends BaseAppState {
|
||||
}
|
||||
|
||||
ac.addAnimClip(target);
|
||||
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
||||
if (clipName.equals("sit_down")) {
|
||||
int trackCount = target.getTracks() != null ? target.getTracks().length : 0;
|
||||
log.info("[AnimLib] Clip '{}' → AnimComposer '{}': length={}s tracks={}",
|
||||
clipName, model.getName(), target.getLength(), trackCount);
|
||||
if (clipName.equals("sit_down") || target.getLength() == 0) {
|
||||
dumpClipTracks(target);
|
||||
}
|
||||
return true;
|
||||
@@ -221,10 +227,8 @@ public class AnimationLibrary extends BaseAppState {
|
||||
snapClipNames.clear();
|
||||
try {
|
||||
AnimSet set = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName);
|
||||
set.getActionMap().forEach((action, clip) -> {
|
||||
if (LOCOMOTION_ACTIONS.contains(action)) snapClipNames.add(clip);
|
||||
});
|
||||
log.info("[AnimLib] snapRootBoneXZ für Locomotion-Clips: {}", snapClipNames);
|
||||
snapClipNames.addAll(set.getSnapClips());
|
||||
log.info("[AnimLib] snapRootBoneXZ für Clips: {}", snapClipNames);
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimLib] AnimSet '{}' nicht ladbar für Locomotion-Snap: {}", animSetName, e.getMessage());
|
||||
}
|
||||
@@ -320,9 +324,9 @@ public class AnimationLibrary extends BaseAppState {
|
||||
}
|
||||
|
||||
private void loadClipFromFile(Path file) {
|
||||
String fileName = file.getFileName().toString();
|
||||
String clipName = fileName.replaceFirst("\\.j3o$", "");
|
||||
String assetKey = "Characters/clips/" + fileName;
|
||||
String fileName = file.getFileName().toString();
|
||||
String fileClipKey = fileName.replaceFirst("\\.j3o$", "");
|
||||
String assetKey = "Characters/clips/" + fileName;
|
||||
|
||||
try {
|
||||
Spatial loaded = assetManager.loadModel(assetKey);
|
||||
@@ -334,11 +338,37 @@ public class AnimationLibrary extends BaseAppState {
|
||||
}
|
||||
Armature armature = sc != null ? sc.getArmature() : null;
|
||||
|
||||
AnimClip bestClip = null;
|
||||
for (String name : ac.getAnimClipsNames()) {
|
||||
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
|
||||
clips.put(name, animClip);
|
||||
if (armature != null) armatures.put(name, armature);
|
||||
log.info("[AnimLib] Clip geladen: '{}' aus {}", name, assetKey);
|
||||
com.jme3.anim.AnimTrack<?>[] rawTracks = animClip.getTracks();
|
||||
int n0 = 0;
|
||||
if (rawTracks != null && rawTracks.length > 0 && rawTracks[0] instanceof TransformTrack tt0) {
|
||||
n0 = tt0.getTimes() != null ? tt0.getTimes().length : 0;
|
||||
}
|
||||
log.info("[AnimLib] Clip geladen: '{}' aus {} | clipLen={}s tracks={} t0Frames={}",
|
||||
name, assetKey, animClip.getLength(),
|
||||
rawTracks != null ? rawTracks.length : 0, n0);
|
||||
if (bestClip == null || animClip.getLength() > bestClip.getLength()) {
|
||||
bestClip = animClip;
|
||||
}
|
||||
}
|
||||
// Kanonischen Dateiname-Key auf den längsten Clip mappen.
|
||||
// Verhindert dass ein leerer Null-Frame-Clip einen guten Clip überschreibt,
|
||||
// wenn die j3o mehrere Clips mit leicht unterschiedlichen Namen enthält
|
||||
// (z.B. "clip_Sword And Shield Slash" vs. "Sword And Shield Slash").
|
||||
if (bestClip != null) {
|
||||
AnimClip current = clips.get(fileClipKey);
|
||||
if (current == null || bestClip.getLength() > current.getLength()) {
|
||||
clips.put(fileClipKey, bestClip);
|
||||
if (armature != null) armatures.put(fileClipKey, armature);
|
||||
if (!fileClipKey.equals(bestClip.getName())) {
|
||||
log.info("[AnimLib] Key '{}' → '{}' (clipLen={}s, besser)",
|
||||
fileClipKey, bestClip.getName(), bestClip.getLength());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimLib] Fehler beim Laden von {}: {}", assetKey, e.getMessage());
|
||||
|
||||
@@ -81,11 +81,15 @@ public final class RetargetingSystem {
|
||||
if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint j)
|
||||
srcTrackMap.put(j.getName(), tt);
|
||||
|
||||
// Reference keyframe times
|
||||
// Reference keyframe times – pick the longest array (some bind-pose tracks have only 1 frame)
|
||||
float[] times = null;
|
||||
for (String name : nameMap.keySet()) {
|
||||
TransformTrack tt = srcTrackMap.get(name);
|
||||
if (tt != null) { times = tt.getTimes(); break; }
|
||||
if (tt != null && tt.getTimes() != null) {
|
||||
if (times == null || tt.getTimes().length > times.length) {
|
||||
times = tt.getTimes();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (times == null || times.length == 0) {
|
||||
log.warn("[Retarget] Keine Keyframe-Zeiten für '{}'", sourceClip.getName());
|
||||
@@ -398,18 +402,10 @@ public final class RetargetingSystem {
|
||||
|
||||
// ── Same-rig detection & redirect ────────────────────────────────────────
|
||||
|
||||
/** True nur wenn alle Namen exakt selbst-gemappt UND alle Bind-Posen übereinstimmen. */
|
||||
/** True wenn alle Namen exakt selbst-gemappt sind (gleiche Knochen-Namen → gleicher Rig-Typ). */
|
||||
private static boolean isSameRig(Map<String, String> nameMap, Armature src, Armature dst) {
|
||||
for (var e : nameMap.entrySet()) {
|
||||
if (!e.getKey().equals(e.getValue())) return false;
|
||||
Joint srcJ = src.getJoint(e.getKey());
|
||||
Joint dstJ = dst.getJoint(e.getValue());
|
||||
if (srcJ == null || dstJ == null) continue;
|
||||
Quaternion sb = srcJ.getInitialTransform() != null
|
||||
? srcJ.getInitialTransform().getRotation() : new Quaternion();
|
||||
Quaternion db = dstJ.getInitialTransform() != null
|
||||
? dstJ.getInitialTransform().getRotation() : new Quaternion();
|
||||
if (Math.abs(sb.dot(db)) < 0.9999f) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -34,10 +34,18 @@ public class PlayerInputControl {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
||||
|
||||
private static final float MOVE_SPEED = 0.07f;
|
||||
private static final float SPRINT_MULT = 1.5f;
|
||||
private static final float WALK_MULT = 0.5f;
|
||||
private static final float ROTATE_SPEED = 10f;
|
||||
private static final float MOVE_SPEED = 0.07f;
|
||||
private static final float SPRINT_MULT = 1.5f;
|
||||
private static final float WALK_MULT = 0.5f;
|
||||
private static final float ROTATE_SPEED = 10f;
|
||||
/** Kürzere Überblend-Zeit für Kampf-Animationen (Angriff fühlt sich "snappy" an). */
|
||||
private static final float COMBAT_BLEND = 0.05f;
|
||||
/** Zeit in Sekunden nach einem Angriff, in der der nächste Kombo-Schlag gedrückt werden kann. */
|
||||
private static final float COMBO_WINDOW = 0.6f;
|
||||
// Combo-Overlap-Utility: MELEE_COMBO_ACTIVE + COMBO_ATK2_OFFSET hier eintragen,
|
||||
// sobald die Animationen in Blender angeglichen wurden (passende Übergangsposen).
|
||||
// AnimPlayer.play(clip, transition, startOffset) und nextStartOffset in tryPlay() sind
|
||||
// bereits verdrahtet — nur die Konstanten reaktivieren und in handleCombatAttack() einsetzen.
|
||||
|
||||
private static final String ACT_DRAW_MELEE = "DrawMeleeWeapon";
|
||||
private static final String ACT_DRAW_RANGED = "DrawRangedWeapon";
|
||||
@@ -64,11 +72,17 @@ public class PlayerInputControl {
|
||||
private boolean weaponDrawn = false;
|
||||
private ItemSubCategory drawnWeaponType = null;
|
||||
private boolean isAiming = false;
|
||||
private boolean meleeAttackToggle = false;
|
||||
private boolean rightMouseHeld = false;
|
||||
private boolean combatAnimActive = false;
|
||||
private float combatAnimRemaining = 0f;
|
||||
private Runnable combatAnimCallback = null;
|
||||
/** Aktueller Schritt in der Kombo-Sequenz (0–3): 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK3-kritisch. */
|
||||
private int comboStep = 0;
|
||||
/** Zeitfenster nach einem Angriff, in dem der nächste Kombo-Schlag akzeptiert wird. */
|
||||
private boolean comboWindowActive = false;
|
||||
private float comboWindowRemaining = 0f;
|
||||
/** Letzter Angriff war ein kritischer Treffer (Kombo-Schritt 3). Für das Schadenssystem. */
|
||||
private boolean lastAttackCritical = false;
|
||||
|
||||
private AnimationLibrary animLib;
|
||||
private String animSetName;
|
||||
@@ -121,6 +135,7 @@ public class PlayerInputControl {
|
||||
private int jumpFrames = 0;
|
||||
private int groundGraceFrames = 0;
|
||||
private double nextTransitionLength = 0.0;
|
||||
private float nextStartOffset = 0f;
|
||||
|
||||
private boolean pickupActive = false;
|
||||
private float pickupRemaining = 0f;
|
||||
@@ -241,6 +256,7 @@ public class PlayerInputControl {
|
||||
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
|
||||
public boolean isWeaponDrawn() { return weaponDrawn; }
|
||||
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
||||
public boolean isLastAttackCritical() { return lastAttackCritical; }
|
||||
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
||||
|
||||
public boolean isPaused() { return paused; }
|
||||
@@ -255,6 +271,8 @@ public class PlayerInputControl {
|
||||
rightMouseHeld = false;
|
||||
combatAnimActive = false;
|
||||
combatAnimCallback = null;
|
||||
comboStep = 0;
|
||||
comboWindowActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,12 +675,32 @@ public class PlayerInputControl {
|
||||
// Kampf-Anim-Timer
|
||||
if (combatAnimActive) {
|
||||
combatAnimRemaining -= tpf;
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
if (combatAnimRemaining <= 0f) {
|
||||
combatAnimActive = false;
|
||||
currentAnim = null;
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
Runnable cb = combatAnimCallback;
|
||||
combatAnimCallback = null;
|
||||
if (cb != null) cb.run();
|
||||
if (!combatAnimActive && isMeleeType(drawnWeaponType)) {
|
||||
comboWindowActive = true;
|
||||
comboWindowRemaining = COMBO_WINDOW;
|
||||
log.info("[Combat] Kombo-Fenster geöffnet (step={} next={})", comboStep, comboStep);
|
||||
}
|
||||
} else {
|
||||
pollFootsteps(tpf);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Kombo-Fenster: läuft nach einem Nahkampfangriff; verfällt es → Kombo-Reset
|
||||
if (comboWindowActive) {
|
||||
comboWindowRemaining -= tpf;
|
||||
if (comboWindowRemaining <= 0f) {
|
||||
comboWindowActive = false;
|
||||
comboStep = 0;
|
||||
log.info("[Combat] Kombo-Fenster abgelaufen → Reset");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -670,7 +708,9 @@ public class PlayerInputControl {
|
||||
if (jumpFrames > 0) jumpFrames--;
|
||||
if (groundGraceFrames > 0) groundGraceFrames--;
|
||||
|
||||
if (!combatAnimActive) {
|
||||
// Während des Kombo-Fensters läuft die letzte Angriffs-Animation weiter —
|
||||
// kein Wechsel zu IDLE/WALK, sonst entsteht ein sichtbares Zucken beim nächsten Schlag.
|
||||
if (!combatAnimActive && !comboWindowActive) {
|
||||
AnimationAction target;
|
||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||
@@ -707,7 +747,8 @@ public class PlayerInputControl {
|
||||
}
|
||||
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
||||
log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName);
|
||||
if (runningClip != null) nextTransitionLength = 0.15;
|
||||
// Negativer Wert = Hard-Cut angefordert (kein Blend); 0 = noch nicht gesetzt → Default 0.15s
|
||||
if (runningClip != null && nextTransitionLength == 0.0) nextTransitionLength = 0.15;
|
||||
if (clip != null && tryPlay(clip)) return;
|
||||
if (action != AnimationAction.DEFAULT) {
|
||||
String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT);
|
||||
@@ -765,16 +806,25 @@ public class PlayerInputControl {
|
||||
}
|
||||
|
||||
private void handleCombatAttack() {
|
||||
if (!weaponDrawn || blockingAnimActive || combatAnimActive) return;
|
||||
if (!weaponDrawn || blockingAnimActive || combatAnimActive) {
|
||||
log.info("[Combat] BLOCKIERT: weaponDrawn={} blockingAnim={} combatAnim={}",
|
||||
weaponDrawn, blockingAnimActive, combatAnimActive);
|
||||
return;
|
||||
}
|
||||
if (isMeleeType(drawnWeaponType)) {
|
||||
AnimationAction atk = meleeAttackToggle
|
||||
? getMeleeAttack2Action(drawnWeaponType)
|
||||
: getMeleeAttack1Action(drawnWeaponType);
|
||||
meleeAttackToggle = !meleeAttackToggle;
|
||||
int step = comboStep;
|
||||
boolean isComboFollow = comboWindowActive; // war Kombo-Fenster offen?
|
||||
AnimationAction atk = getMeleeComboAction(drawnWeaponType, step);
|
||||
lastAttackCritical = (step == 3);
|
||||
comboStep = (step + 1) % 4;
|
||||
comboWindowActive = false;
|
||||
float dur = resolveClipLength(atk, 0.6f);
|
||||
// Kombo-Folgeschlag: kurzer Blend für sauberen Übergang; erster Schlag: Hard-Cut
|
||||
nextTransitionLength = isComboFollow ? COMBAT_BLEND : -1.0;
|
||||
log.info("[Combat] Kombo-Schritt {}: action={} dur={}s kritisch={}",
|
||||
step, atk, dur, lastAttackCritical);
|
||||
combatAnimActive = true;
|
||||
// Blend-out zu Idle startet BLEND_DURATION früher → kein sichtbares Einfrieren am letzten Frame
|
||||
combatAnimRemaining = Math.max(0.1f, dur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
||||
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
|
||||
combatAnimCallback = null;
|
||||
playAction(atk);
|
||||
currentAnim = atk;
|
||||
@@ -785,14 +835,16 @@ public class PlayerInputControl {
|
||||
float fireDur = resolveClipLength(fireAnim, 0.5f);
|
||||
float reloadDur = resolveClipLength(reloadAnim, 0.8f);
|
||||
combatAnimActive = true;
|
||||
combatAnimRemaining = Math.max(0.1f, fireDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
||||
combatAnimRemaining = Math.max(0.1f, fireDur - COMBAT_BLEND);
|
||||
combatAnimCallback = () -> {
|
||||
combatAnimActive = true;
|
||||
combatAnimRemaining = Math.max(0.1f, reloadDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
||||
combatAnimRemaining = Math.max(0.1f, reloadDur - COMBAT_BLEND);
|
||||
combatAnimCallback = null;
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
playAction(reloadAnim);
|
||||
currentAnim = reloadAnim;
|
||||
};
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
playAction(fireAnim);
|
||||
currentAnim = fireAnim;
|
||||
}
|
||||
@@ -909,6 +961,23 @@ public class PlayerInputControl {
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getMeleeAttack3Action(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_ATTACK_3;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_ATTACK_3;
|
||||
default -> AnimationAction.SWORD_ATTACK_3;
|
||||
};
|
||||
}
|
||||
|
||||
/** Kombo-Sequenz: 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK3 (kritisch). */
|
||||
private static AnimationAction getMeleeComboAction(ItemSubCategory type, int step) {
|
||||
return switch (step) {
|
||||
case 1 -> getMeleeAttack2Action(type);
|
||||
case 3 -> getMeleeAttack3Action(type);
|
||||
default -> getMeleeAttack1Action(type);
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getMeleeBlockAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_BLOCK;
|
||||
@@ -1185,9 +1254,11 @@ public class PlayerInputControl {
|
||||
if (animPlayer == null) {
|
||||
animPlayer = new AnimPlayer(animComposer, skinningControl);
|
||||
}
|
||||
float transLen = (float) nextTransitionLength;
|
||||
float transLen = (float) nextTransitionLength;
|
||||
float startOff = nextStartOffset;
|
||||
nextTransitionLength = 0.0;
|
||||
boolean ok = animPlayer.play(clip, transLen);
|
||||
nextStartOffset = 0f;
|
||||
boolean ok = animPlayer.play(clip, transLen, startOff);
|
||||
log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED");
|
||||
if (ok) {
|
||||
runningClip = animPlayer.getCurrentClip();
|
||||
|
||||
@@ -29,28 +29,33 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
|
||||
private static final String BONE_SPINE = "mixamorig:Spine2";
|
||||
private static final String BONE_HAND = "mixamorig:RightHand";
|
||||
private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
|
||||
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
|
||||
private static final float HALF_PI = (float) Math.PI * 0.5f;
|
||||
|
||||
// Grip-Zielposition im Spine2-lokalen Raum (Meter): Griff ~40cm über dem Brustbein
|
||||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.05f, 0.40f, -0.08f);
|
||||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
|
||||
/** Zusatz-Versatz beim gezückten Schwert in lokalem Waffen-Space (X=entlang Klinge, Y=entlang Griff, Z=senkrecht zur Schneide). */
|
||||
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.0f, 0.05f);
|
||||
|
||||
private final AssetManager assetManager;
|
||||
private final PlayerInputControl playerInput;
|
||||
private final MainCharacter mc;
|
||||
|
||||
private com.jme3.anim.SkinningControl skinCtrl;
|
||||
private Node backNode; // Spine2-Attachment-Node (im skalierten Char-Modell)
|
||||
private Node handNode; // RightHand-Attachment-Node
|
||||
private Node backNode;
|
||||
private Node handNode;
|
||||
private Node palmNodeRef;
|
||||
private int palmOffsetFrameDelay = 0;
|
||||
|
||||
// Bind-Pose-Rotation des RightHand-Knochens in Character-lokalem Raum (T-Pose).
|
||||
// Wird einmalig beim Initialisieren aus dem InverseBindMatrix abgeleitet.
|
||||
// Erlaubt, die Grip-Rotation aus dem Modell-Editor direkt im Spiel zu verwenden.
|
||||
private Quaternion handBindRotInChar;
|
||||
// Offset Handgelenk → Fingerknöchel in Hand-Bone-Local-Space.
|
||||
// Wird im 2. Frame aus den Runtime-Node-Positionen berechnet (skalierungs-unabhängig).
|
||||
private Vector3f palmOffsetLocal;
|
||||
|
||||
private Item lastEquippedWeapon;
|
||||
private boolean lastDrawnState = false;
|
||||
private Spatial weaponSpatial;
|
||||
private boolean gripDiagLogged = false;
|
||||
|
||||
public WeaponAttachmentControl(AssetManager assetManager,
|
||||
PlayerInputControl playerInput,
|
||||
@@ -62,38 +67,45 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
|
||||
@Override
|
||||
protected void controlUpdate(float tpf) {
|
||||
// Lazy init: SkinningControl erst nach setAnimationContext() verfügbar
|
||||
if (skinCtrl == null) {
|
||||
skinCtrl = playerInput.getSkinningControl();
|
||||
if (skinCtrl == null) return;
|
||||
try {
|
||||
backNode = skinCtrl.getAttachmentsNode(BONE_SPINE);
|
||||
log.info("[WeaponAttach] backNode ({}): {}", BONE_SPINE, backNode);
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_SPINE, e.getMessage());
|
||||
}
|
||||
try {
|
||||
handNode = skinCtrl.getAttachmentsNode(BONE_HAND);
|
||||
log.info("[WeaponAttach] handNode ({}): {}", BONE_HAND, handNode);
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_HAND, e.getMessage());
|
||||
}
|
||||
handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND);
|
||||
log.info("[WeaponAttach] handBindRotInChar: {}", handBindRotInChar);
|
||||
try {
|
||||
palmNodeRef = skinCtrl.getAttachmentsNode(BONE_PALM);
|
||||
palmOffsetFrameDelay = 2; // SkinningControl muss mindestens 1x laufen
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage());
|
||||
}
|
||||
log.info("[WeaponAttach] handBindRotInChar={}", handBindRotInChar);
|
||||
}
|
||||
|
||||
// Palm-Offset verzögert berechnen (SkinningControl muss mindestens 1x gelaufen sein)
|
||||
if (palmOffsetFrameDelay > 0 && palmNodeRef != null && handNode != null) {
|
||||
palmOffsetFrameDelay--;
|
||||
if (palmOffsetFrameDelay == 0) {
|
||||
palmOffsetLocal = computePalmOffsetFromNodes(handNode, palmNodeRef);
|
||||
log.info("[WeaponAttach] palmOffsetLocal (from nodes)={}", palmOffsetLocal);
|
||||
}
|
||||
}
|
||||
|
||||
Item current = mc.getEquippedMeleeWeapon();
|
||||
boolean drawn = playerInput.isWeaponDrawn();
|
||||
|
||||
if (current != lastEquippedWeapon) {
|
||||
log.info("[WeaponAttach] Waffe geändert: {} → {}",
|
||||
lastEquippedWeapon != null ? lastEquippedWeapon.getItemId() : "null",
|
||||
current != null ? current.getItemId() : "null");
|
||||
detachWeapon();
|
||||
lastEquippedWeapon = current;
|
||||
if (current != null) {
|
||||
loadWeapon(current);
|
||||
}
|
||||
if (current != null) loadWeapon(current);
|
||||
}
|
||||
|
||||
if (weaponSpatial != null) {
|
||||
@@ -111,12 +123,8 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
}
|
||||
try {
|
||||
weaponSpatial = assetManager.loadModel(item.getModelRef().getPath());
|
||||
// Wichtig: Waffe direkt am character-Node hängen (ohne geerbten 0.02-Scale
|
||||
// aus der Charakter-Modell-Hierarchie). Position/Rotation wird jedes Frame
|
||||
// aus den Bone-World-Transforms berechnet.
|
||||
((Node) spatial).attachChild(weaponSpatial);
|
||||
log.info("[WeaponAttach] Waffe geladen und an character-Node gehängt: {}",
|
||||
item.getModelRef().getPath());
|
||||
log.info("[WeaponAttach] Waffe geladen: {}", item.getModelRef().getPath());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeaponAttach] Waffenmodell nicht ladbar ({}): {}",
|
||||
item.getModelRef().getPath(), e.getMessage());
|
||||
@@ -130,31 +138,23 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
Node boneNode = drawn ? handNode : backNode;
|
||||
if (boneNode == null) return;
|
||||
|
||||
// Bone-World-Transform (korrekt, auch wenn Eltern skaliert sind)
|
||||
Vector3f boneWorldPos = boneNode.getWorldTranslation();
|
||||
Quaternion boneWorldRot = boneNode.getWorldRotation();
|
||||
|
||||
// In den lokalen Raum des character-Nodes transformieren
|
||||
// (character hat in der Regel nur Translation + Rotation, keinen Scale)
|
||||
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
|
||||
Vector3f boneInChar = charWorldRotInv.mult(
|
||||
boneWorldPos.subtract(spatial.getWorldTranslation()));
|
||||
Quaternion boneRotInChar = charWorldRotInv.mult(boneWorldRot);
|
||||
Vector3f charWorldPos = spatial.getWorldTranslation();
|
||||
|
||||
Vector3f boneInChar = charWorldRotInv.mult(
|
||||
boneNode.getWorldTranslation().subtract(charWorldPos));
|
||||
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
|
||||
|
||||
if (drawn) {
|
||||
applyHandTransform(boneInChar, boneRotInChar, grip);
|
||||
Vector3f attachInChar = (palmOffsetLocal != null)
|
||||
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
|
||||
: boneInChar;
|
||||
applyHandTransform(attachInChar, boneRotInChar, grip);
|
||||
} else {
|
||||
applyBackTransform(boneInChar, boneRotInChar, grip);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waffe in der Hand: Griff-Punkt soll am Handknochen liegen, Rotation aus ItemGrip.
|
||||
*
|
||||
* Die Grip-Rotation ist im Character-/Welt-Raum definiert (wie im Grip-Editor sichtbar).
|
||||
* Der Animations-Delta (Bewegung relativ zur Bind-Pose) wird zusätzlich aufaddiert,
|
||||
* damit die Waffe den Handknochen durch alle Animationen folgt.
|
||||
*/
|
||||
private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
||||
Quaternion gripRot = (grip != null)
|
||||
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ())
|
||||
@@ -163,21 +163,65 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||
: Vector3f.ZERO;
|
||||
|
||||
// animDelta = wie weit der Knochen sich von der Bind-Pose bewegt hat.
|
||||
// In der Bind-Pose (T-Pose / Idle) ist animDelta ≈ identity → weaponRot ≈ gripRot.
|
||||
// Während Angriffsanimationen dreht animDelta die Waffe mit der Hand mit.
|
||||
Quaternion animDelta = (handBindRotInChar != null)
|
||||
? boneRotInChar.mult(handBindRotInChar.inverse())
|
||||
: boneRotInChar;
|
||||
Quaternion weaponRot = animDelta.mult(gripRot.inverse());
|
||||
|
||||
Quaternion weaponRot = animDelta.mult(gripRot);
|
||||
if (!gripDiagLogged && weaponSpatial != null) {
|
||||
gripDiagLogged = true;
|
||||
float[] wa = weaponRot.toAngles(null);
|
||||
Vector3f bx = weaponRot.mult(Vector3f.UNIT_X);
|
||||
Vector3f by = weaponRot.mult(Vector3f.UNIT_Y);
|
||||
Vector3f bz = weaponRot.mult(Vector3f.UNIT_Z);
|
||||
float[] ad = animDelta.toAngles(null);
|
||||
float[] gr = gripRot.toAngles(null);
|
||||
log.info("[GripDiag] animDelta(°): x={} y={} z={}",
|
||||
Math.round(Math.toDegrees(ad[0])), Math.round(Math.toDegrees(ad[1])), Math.round(Math.toDegrees(ad[2])));
|
||||
log.info("[GripDiag] gripRot(°): x={} y={} z={}",
|
||||
Math.round(Math.toDegrees(gr[0])), Math.round(Math.toDegrees(gr[1])), Math.round(Math.toDegrees(gr[2])));
|
||||
log.info("[GripDiag] weaponRot(°): x={} y={} z={}",
|
||||
Math.round(Math.toDegrees(wa[0])), Math.round(Math.toDegrees(wa[1])), Math.round(Math.toDegrees(wa[2])));
|
||||
log.info("[GripDiag] Waffe +X in CharSpace: ({},{},{})", round1(bx.x), round1(bx.y), round1(bx.z));
|
||||
log.info("[GripDiag] Waffe +Y in CharSpace: ({},{},{})", round1(by.x), round1(by.y), round1(by.z));
|
||||
log.info("[GripDiag] Waffe +Z in CharSpace: ({},{},{})", round1(bz.x), round1(bz.y), round1(bz.z));
|
||||
// Vergleich RightHand vs RightHandMiddle1 – welcher Bone gibt die bessere Orientierung?
|
||||
Quaternion charWorldRotInv2 = spatial.getWorldRotation().inverse();
|
||||
if (palmNodeRef != null) {
|
||||
Quaternion palmRotInChar = charWorldRotInv2.mult(palmNodeRef.getWorldRotation());
|
||||
Quaternion palmBind = extractJointBindRot(skinCtrl, BONE_PALM);
|
||||
Quaternion palmDelta = (palmBind != null) ? palmRotInChar.mult(palmBind.inverse()) : palmRotInChar;
|
||||
float[] pd = palmDelta.toAngles(null);
|
||||
Vector3f px = palmDelta.mult(Vector3f.UNIT_X);
|
||||
Vector3f py = palmDelta.mult(Vector3f.UNIT_Y);
|
||||
log.info("[GripDiag] palmDelta(°) [Middle1]: x={} y={} z={}",
|
||||
Math.round(Math.toDegrees(pd[0])), Math.round(Math.toDegrees(pd[1])), Math.round(Math.toDegrees(pd[2])));
|
||||
log.info("[GripDiag] palm +X in CharSpace: ({},{},{})", round1(px.x), round1(px.y), round1(px.z));
|
||||
log.info("[GripDiag] palm +Y in CharSpace: ({},{},{})", round1(py.x), round1(py.y), round1(py.z));
|
||||
}
|
||||
}
|
||||
|
||||
// Waffenposition: Griff-Punkt liegt am Knochen-Ursprung
|
||||
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos));
|
||||
weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET));
|
||||
weaponSpatial.setLocalRotation(weaponRot);
|
||||
weaponSpatial.setLocalTranslation(bonePosInChar.subtract(weaponRot.mult(gripPos)));
|
||||
weaponSpatial.setLocalTranslation(weaponPos);
|
||||
}
|
||||
|
||||
private static float round1(float v) { return Math.round(v * 10f) / 10f; }
|
||||
|
||||
/**
|
||||
* Berechnet den Offset Handgelenk → Fingerknöchel in Hand-Bone-Local-Space
|
||||
* direkt aus den World-Positionen der Attachment-Nodes.
|
||||
* Dieser Ansatz umgeht alle InverseBindMatrix-Skalierungsprobleme.
|
||||
* Muss aufgerufen werden NACHDEM SkinningControl mindestens einmal gelaufen ist.
|
||||
*/
|
||||
private static Vector3f computePalmOffsetFromNodes(Node handNode, Node palmNode) {
|
||||
Vector3f deltaWorld = palmNode.getWorldTranslation().subtract(handNode.getWorldTranslation());
|
||||
// Delta in Hand-Bone-Local-Space ausdrücken (rotiert mit der Hand mit)
|
||||
Quaternion handWorldRotInv = handNode.getWorldRotation().inverse();
|
||||
return handWorldRotInv.mult(deltaWorld);
|
||||
}
|
||||
|
||||
/** Liest die Bind-Pose-Rotation eines Knochens (Model-Raum ≈ Character-lokal) aus dem InverseBindMatrix. */
|
||||
private static Quaternion extractJointBindRot(com.jme3.anim.SkinningControl sc, String jointName) {
|
||||
com.jme3.anim.Armature arm = sc.getArmature();
|
||||
if (arm == null) return null;
|
||||
@@ -190,22 +234,13 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
return bindMat.toRotationQuat().normalizeLocal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waffe am Rücken: BACK_GRIP_OFFSET über Spine2, Klinge zeigt nach unten.
|
||||
*/
|
||||
private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
||||
// fromAngles(0, 0, HALF_PI): Spine2-FBX-Flip kompensiert, Klinge zeigt nach unten
|
||||
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
|
||||
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
|
||||
Quaternion weaponRot = boneRotInChar.mult(backRot);
|
||||
|
||||
// Zielposition des Griff-Punktes: BACK_GRIP_OFFSET im Knochen-lokalen Raum
|
||||
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
|
||||
|
||||
Vector3f gripPos = (grip != null)
|
||||
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
|
||||
Vector3f gripPos = (grip != null)
|
||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||
: Vector3f.ZERO;
|
||||
|
||||
// weaponPos + weaponRot * gripPos = gripTarget
|
||||
weaponSpatial.setLocalRotation(weaponRot);
|
||||
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user