Weiter am Kampfsystem gearbeitet - hier: Schwert und Schild

This commit is contained in:
2026-09-20 22:17:29 +02:00
parent 836b5d3d99
commit ef061b2726
49 changed files with 1463 additions and 366 deletions

View File

@@ -41,12 +41,23 @@ public class AnimPlayer {
private float blendDuration;
/** Einsprungzeit: Clip startet bei diesem Zeitwert statt bei 0 (überspringt z.B. das Ausholen). */
private float pendingStartOffset;
/** Wenn true: Clip rückwärts abspielen (speed=-1, Start am Ende). */
private boolean pendingReversed;
public AnimPlayer(AnimComposer composer, SkinningControl skinning) {
this.composer = composer;
this.skinning = skinning;
}
/**
* Spielt einen Clip rückwärts ab (speed=-1, startet am Ende).
* Geeignet für IDLE_TO_BLOCK rückwärts beim Loslassen der Blocktaste.
*/
public boolean playReversed(String clipName, float transition) {
this.pendingReversed = true;
return play(clipName, transition);
}
/**
* Spielt einen Clip ab, optional ab einem Zeitversatz (z.B. Ausholen überspringen).
*
@@ -167,10 +178,15 @@ public class AnimPlayer {
pendingClip = null;
blendSpace = null;
double len = currentAction != null ? currentAction.getLength() : -1;
if (pendingStartOffset > 0f && currentAction != null && len > 0.01) {
if (pendingReversed && currentAction != null && len > 0.01) {
currentAction.setSpeed(-1f);
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len);
pendingReversed = false;
} else if (pendingStartOffset > 0f && currentAction != null && len > 0.01) {
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(pendingStartOffset % (float) len);
}
pendingStartOffset = 0f;
pendingReversed = false;
log.info("[AnimPlayer] direktPlay → '{}' length={}s", clipName, len);
return currentAction != null;
} catch (Exception e) {
@@ -192,13 +208,17 @@ public class AnimPlayer {
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 || pendingStartOffset > 0)) {
if (pendingReversed && currentAction != null && len > 0.01) {
currentAction.setSpeed(-1f);
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len);
pendingReversed = false;
} else if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) {
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
float t = (pendingStartOffset + elapsed) % (float) len;
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t);
pendingStartOffset = 0f;
}
pendingReversed = false;
log.debug("[AnimPlayer] finishBlend → '{}'", clip);
} catch (Exception e) {
log.warn("[AnimPlayer] finishBlend('{}') fehlgeschlagen: {}", clip, e.getMessage());

View File

@@ -34,18 +34,21 @@ public enum AnimationAction {
// ── Schwert ───────────────────────────────────────────────────────────────
SWORD_ATTACK_1,
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN,
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK,
SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,
SWORD_IDLE_TO_BLOCK, SWORD_COUNTERATTACK, SWORD_BLOCK_IMPACT, SWORD_IDLE_IMPACT,
SWORD_IDLE, SWORD_WALK, SWORD_RUN,
// ── Axt ───────────────────────────────────────────────────────────────────
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_HIT_TAKEN,
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
AXE_ATTACK_1, AXE_ATTACK_2, AXE_ATTACK_3, AXE_HEAVY_ATTACK,
AXE_IDLE_TO_BLOCK, AXE_COUNTERATTACK, AXE_BLOCK_IMPACT, AXE_IDLE_IMPACT,
AXE_IDLE, AXE_WALK, AXE_RUN,
// ── Zweihänder ────────────────────────────────────────────────────────────
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_HIT_TAKEN,
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK,
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK,
TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_COUNTERATTACK, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT,
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
// ── Bogen ─────────────────────────────────────────────────────────────────
@@ -96,35 +99,44 @@ public enum AnimationAction {
case SWORD_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Schwert)");
case SWORD_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Schwert)");
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_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
case SWORD_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Schwert)");
case SWORD_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Schwert)");
case SWORD_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Schwert)");
case SWORD_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
case SWORD_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)");
case SWORD_RUN -> TextRegistry.resolve(null, key, "Laufen (Schwert)");
case AXE_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Axt)");
case AXE_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Axt)");
case AXE_BLOCK -> TextRegistry.resolve(null, key, "Block (Axt)");
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_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)");
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
case AXE_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Axt)");
case AXE_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Axt)");
case AXE_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Axt)");
case AXE_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Axt)");
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
case AXE_WALK -> TextRegistry.resolve(null, key, "Gehen (Axt)");
case AXE_RUN -> TextRegistry.resolve(null, key, "Laufen (Axt)");
case TWO_HAND_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Zweihänder)");
case TWO_HAND_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Zweihänder)");
case TWO_HAND_BLOCK -> TextRegistry.resolve(null, key, "Block (Zweihänder)");
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_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
case TWO_HAND_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Zweihänder)");
case TWO_HAND_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Zweihänder)");
case TWO_HAND_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Zweihänder)");
case TWO_HAND_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)");
case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)");

View File

@@ -42,6 +42,8 @@ public class PlayerInputControl {
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;
/** Maximale Haltezeit für einen Kurz-Klick (Konterangriff); darüber = Block-Eingabe. */
private static final float BLOCK_TAP_MAX = 0.20f;
// 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
@@ -67,15 +69,20 @@ public class PlayerInputControl {
private boolean forward, backward, left, right, sprint, walk;
private boolean paused = false;
private enum BlockPhase { NONE, WAITING, ENTERING, HOLDING, EXITING }
// ── Kampf-Zustand ─────────────────────────────────────────────────────────
private MainCharacter mainCharacter = null;
private boolean weaponDrawn = false;
private ItemSubCategory drawnWeaponType = null;
private boolean isAiming = false;
private boolean rightMouseHeld = false;
private boolean combatAnimActive = false;
private float combatAnimRemaining = 0f;
private Runnable combatAnimCallback = null;
private BlockPhase blockPhase = BlockPhase.NONE;
private float blockPhaseTimer = 0f;
private boolean combatAnimActive = false;
private float combatAnimRemaining = 0f;
private Runnable combatAnimCallback = null;
private boolean combatAnimSkipComboWindow = false;
/** 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. */
@@ -136,6 +143,7 @@ public class PlayerInputControl {
private int groundGraceFrames = 0;
private double nextTransitionLength = 0.0;
private float nextStartOffset = 0f;
private boolean nextPlayReversed = false;
private boolean pickupActive = false;
private float pickupRemaining = 0f;
@@ -269,6 +277,7 @@ public class PlayerInputControl {
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
stopWading();
rightMouseHeld = false;
blockPhase = BlockPhase.NONE;
combatAnimActive = false;
combatAnimCallback = null;
comboStep = 0;
@@ -682,8 +691,10 @@ public class PlayerInputControl {
nextTransitionLength = COMBAT_BLEND;
Runnable cb = combatAnimCallback;
combatAnimCallback = null;
boolean skipWindow = combatAnimSkipComboWindow;
combatAnimSkipComboWindow = false;
if (cb != null) cb.run();
if (!combatAnimActive && isMeleeType(drawnWeaponType)) {
if (!skipWindow && !combatAnimActive && isMeleeType(drawnWeaponType)) {
comboWindowActive = true;
comboWindowRemaining = COMBO_WINDOW;
log.info("[Combat] Kombo-Fenster geöffnet (step={} next={})", comboStep, comboStep);
@@ -704,13 +715,14 @@ public class PlayerInputControl {
}
}
updateBlockPhase(tpf);
// Animations-Auswahl
if (jumpFrames > 0) jumpFrames--;
if (groundGraceFrames > 0) groundGraceFrames--;
// 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) {
// Block-Phase verwaltet ihre eigenen Animationen.
if (!combatAnimActive && !isBlocking()) {
AnimationAction target;
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
@@ -724,10 +736,6 @@ public class PlayerInputControl {
target = resolveWeaponAction(target);
if (weaponDrawn && rightMouseHeld && isMeleeType(drawnWeaponType)) {
target = getMeleeBlockAction(drawnWeaponType);
}
if (target != currentAnim) {
playAction(target);
currentAnim = target;
@@ -796,6 +804,7 @@ public class PlayerInputControl {
if (drawnWeaponType == null) { weaponDrawn = false; return; }
isAiming = false;
rightMouseHeld = false;
blockPhase = BlockPhase.NONE;
AnimationAction sheathAnim = getSheathAction(drawnWeaponType);
float dur = resolveClipLength(sheathAnim, 0.5f);
requestAnimation(sheathAnim, dur, () -> {
@@ -806,9 +815,9 @@ public class PlayerInputControl {
}
private void handleCombatAttack() {
if (!weaponDrawn || blockingAnimActive || combatAnimActive) {
log.info("[Combat] BLOCKIERT: weaponDrawn={} blockingAnim={} combatAnim={}",
weaponDrawn, blockingAnimActive, combatAnimActive);
if (!weaponDrawn || blockingAnimActive || combatAnimActive || isBlocking()) {
log.info("[Combat] BLOCKIERT: weaponDrawn={} blockingAnim={} combatAnim={} blocking={}",
weaponDrawn, blockingAnimActive, combatAnimActive, isBlocking());
return;
}
if (isMeleeType(drawnWeaponType)) {
@@ -853,12 +862,103 @@ public class PlayerInputControl {
private void handleCombatAlt(boolean isPressed) {
if (!weaponDrawn || blockingAnimActive) return;
if (isMeleeType(drawnWeaponType)) {
rightMouseHeld = isPressed;
if (isPressed) {
if (blockPhase == BlockPhase.NONE) {
blockPhase = BlockPhase.WAITING;
blockPhaseTimer = 0f;
}
} else {
switch (blockPhase) {
case WAITING -> {
blockPhase = BlockPhase.NONE;
playCounterAttack();
}
case ENTERING, HOLDING -> startBlockExit();
default -> {}
}
}
} else if (isRangedType(drawnWeaponType) && isPressed) {
isAiming = !isAiming;
}
}
private void updateBlockPhase(float tpf) {
switch (blockPhase) {
case WAITING -> {
blockPhaseTimer += tpf;
if (blockPhaseTimer >= BLOCK_TAP_MAX) {
if (hasShieldEquipped()) {
startBlockEnter();
} else {
blockPhase = BlockPhase.NONE;
}
}
}
case ENTERING -> {
blockPhaseTimer -= tpf;
if (blockPhaseTimer <= 0f) {
blockPhase = BlockPhase.HOLDING;
AnimationAction blockAct = getMeleeBlockAction(drawnWeaponType);
nextTransitionLength = COMBAT_BLEND;
playAction(blockAct);
currentAnim = blockAct;
}
}
case EXITING -> {
blockPhaseTimer -= tpf;
if (blockPhaseTimer <= 0f) {
blockPhase = BlockPhase.NONE;
currentAnim = null;
}
}
default -> {}
}
}
private void startBlockEnter() {
blockPhase = BlockPhase.ENTERING;
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
float dur = resolveClipLength(idleToBlock, 0.5f);
blockPhaseTimer = dur;
nextTransitionLength = COMBAT_BLEND;
playAction(idleToBlock);
currentAnim = idleToBlock;
}
private void startBlockExit() {
blockPhase = BlockPhase.EXITING;
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
float dur = resolveClipLength(idleToBlock, 0.5f);
blockPhaseTimer = dur;
nextTransitionLength = COMBAT_BLEND;
nextPlayReversed = true;
playAction(idleToBlock);
currentAnim = idleToBlock;
}
private void playCounterAttack() {
if (combatAnimActive) return;
AnimationAction counterAct = getMeleeCounterAttackAction(drawnWeaponType);
float dur = resolveClipLength(counterAct, 0.7f);
nextTransitionLength = COMBAT_BLEND;
combatAnimActive = true;
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
combatAnimCallback = null;
combatAnimSkipComboWindow = true;
playAction(counterAct);
currentAnim = counterAct;
}
public boolean isBlocking() {
return blockPhase == BlockPhase.ENTERING
|| blockPhase == BlockPhase.HOLDING
|| blockPhase == BlockPhase.EXITING;
}
private boolean hasShieldEquipped() {
return mainCharacter != null && mainCharacter.getEquippedShield() != null;
}
private AnimationAction resolveWeaponAction(AnimationAction base) {
if (!weaponDrawn || drawnWeaponType == null) return base;
return switch (base) {
@@ -986,6 +1086,38 @@ public class PlayerInputControl {
};
}
private static AnimationAction getMeleeIdleToBlockAction(ItemSubCategory s) {
return switch (s) {
case AXE -> AnimationAction.AXE_IDLE_TO_BLOCK;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_TO_BLOCK;
default -> AnimationAction.SWORD_IDLE_TO_BLOCK;
};
}
private static AnimationAction getMeleeCounterAttackAction(ItemSubCategory s) {
return switch (s) {
case AXE -> AnimationAction.AXE_COUNTERATTACK;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_COUNTERATTACK;
default -> AnimationAction.SWORD_COUNTERATTACK;
};
}
public static AnimationAction getMeleeBlockImpactAction(ItemSubCategory s) {
return switch (s) {
case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT;
default -> AnimationAction.SWORD_BLOCK_IMPACT;
};
}
public static AnimationAction getMeleeIdleImpactAction(ItemSubCategory s) {
return switch (s) {
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT;
default -> AnimationAction.SWORD_IDLE_IMPACT;
};
}
private static AnimationAction getRangedDrawAction(ItemSubCategory s) {
return switch (s) {
case CROSSBOW -> AnimationAction.CROSSBOW_DRAW;
@@ -1254,12 +1386,17 @@ public class PlayerInputControl {
if (animPlayer == null) {
animPlayer = new AnimPlayer(animComposer, skinningControl);
}
float transLen = (float) nextTransitionLength;
float startOff = nextStartOffset;
float transLen = (float) nextTransitionLength;
float startOff = nextStartOffset;
boolean reversed = nextPlayReversed;
nextTransitionLength = 0.0;
nextStartOffset = 0f;
boolean ok = animPlayer.play(clip, transLen, startOff);
log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED");
nextPlayReversed = false;
boolean ok = reversed
? animPlayer.playReversed(clip, transLen)
: animPlayer.play(clip, transLen, startOff);
log.info("[Anim] AnimPlayer.{}('{}') transLen={} → {}", reversed ? "playReversed" : "play",
clip, transLen, ok ? "OK" : "FAILED");
if (ok) {
runningClip = animPlayer.getCurrentClip();
applyAnimOffset(clip);

View File

@@ -27,9 +27,10 @@ public class WeaponAttachmentControl extends AbstractControl {
private static final Logger log = LoggerFactory.getLogger(WeaponAttachmentControl.class);
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 String BONE_SPINE = "mixamorig:Spine2";
private static final String BONE_HAND = "mixamorig:RightHand";
private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
private static final float HALF_PI = (float) Math.PI * 0.5f;
@@ -37,6 +38,18 @@ public class WeaponAttachmentControl extends AbstractControl {
/** 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);
/** Schild am Rücken: Griff unterhalb der Schultern, parallel zum Boden, Schild über dem Schwert. */
private static final Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f);
// Schild am Arm (gezückt): Rotation relativ zur Bind-Pose des Unterarm-Knochens.
// Werte: 0f = keine Drehung, HALF_PI = +90°, -HALF_PI = -90°
private static final float SHIELD_ARM_ROT_X = -HALF_PI;
private static final float SHIELD_ARM_ROT_Y = 0f;
private static final float SHIELD_ARM_ROT_Z = 0f;
// Verschiebung entlang des Unterarms Richtung Ellbogen (lokale −Y des LeftHand-Bones).
private static final float SHIELD_ARM_ELBOW_OFFSET = 0.25f;
private final AssetManager assetManager;
private final PlayerInputControl playerInput;
private final MainCharacter mc;
@@ -57,6 +70,12 @@ public class WeaponAttachmentControl extends AbstractControl {
private Spatial weaponSpatial;
private boolean gripDiagLogged = false;
private Item lastEquippedShield;
private Spatial shieldSpatial;
private Node leftForeArmNode;
private Quaternion leftForeArmBindRotInChar;
public WeaponAttachmentControl(AssetManager assetManager,
PlayerInputControl playerInput,
MainCharacter mc) {
@@ -87,6 +106,12 @@ public class WeaponAttachmentControl extends AbstractControl {
} catch (Exception e) {
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage());
}
try {
leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM);
} catch (Exception e) {
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage());
}
leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM);
log.info("[WeaponAttach] handBindRotInChar={}", handBindRotInChar);
}
@@ -100,6 +125,7 @@ public class WeaponAttachmentControl extends AbstractControl {
}
Item current = mc.getEquippedMeleeWeapon();
Item shield = mc.getEquippedShield();
boolean drawn = playerInput.isWeaponDrawn();
if (current != lastEquippedWeapon) {
@@ -112,6 +138,16 @@ public class WeaponAttachmentControl extends AbstractControl {
lastDrawnState = drawn;
updateWeaponTransform(drawn, current != null ? current.getGrip() : null);
}
if (shield != lastEquippedShield) {
detachShield();
lastEquippedShield = shield;
if (shield != null) loadShield(shield);
}
if (shieldSpatial != null) {
updateShieldTransform(drawn, shield);
}
}
// ── Laden ────────────────────────────────────────────────────────────────
@@ -245,6 +281,73 @@ public class WeaponAttachmentControl extends AbstractControl {
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
}
// ── Schild ────────────────────────────────────────────────────────────────
private void loadShield(Item item) {
if (item.getModelRef() == null || item.getModelRef().getPath() == null) {
log.warn("[WeaponAttach] Kein modelRef an Schild '{}'.", item.getItemId());
return;
}
try {
shieldSpatial = assetManager.loadModel(item.getModelRef().getPath());
((Node) spatial).attachChild(shieldSpatial);
log.info("[WeaponAttach] Schild geladen: {}", item.getModelRef().getPath());
} catch (Exception e) {
log.warn("[WeaponAttach] Schildmodell nicht ladbar ({}): {}",
item.getModelRef().getPath(), e.getMessage());
shieldSpatial = null;
}
}
private void updateShieldTransform(boolean drawn, Item shield) {
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
Vector3f charWorldPos = spatial.getWorldTranslation();
ItemGrip grip = shield != null ? shield.getGrip() : null;
if (drawn && leftForeArmNode != null) {
Vector3f boneInChar = charWorldRotInv.mult(
leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation());
applyShieldArmTransform(boneInChar, boneRotInChar, grip);
shieldSpatial.setCullHint(Spatial.CullHint.Inherit);
} else if (!drawn && backNode != null) {
Vector3f boneInChar = charWorldRotInv.mult(
backNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(backNode.getWorldRotation());
applyShieldBackTransform(boneInChar, boneRotInChar, grip);
shieldSpatial.setCullHint(Spatial.CullHint.Inherit);
} else {
shieldSpatial.setCullHint(Spatial.CullHint.Always);
}
}
private void applyShieldArmTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
Quaternion animDelta = (leftForeArmBindRotInChar != null)
? boneRotInChar.mult(leftForeArmBindRotInChar.inverse())
: boneRotInChar;
Quaternion armRot = new Quaternion().fromAngles(SHIELD_ARM_ROT_X, SHIELD_ARM_ROT_Y, SHIELD_ARM_ROT_Z);
Quaternion shieldRot = animDelta.mult(armRot);
Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO;
// Ankerpunkt Richtung Ellbogen verschieben (lokale −Y des LeftHand-Bones)
Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -SHIELD_ARM_ELBOW_OFFSET, 0f)));
shieldSpatial.setLocalRotation(shieldRot);
shieldSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
}
private void applyShieldBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
// 180° um Y: Modell-+X → Spine-X (Griff nach links), Modell-+Z → Spine-−Z (flach am Rücken)
Quaternion backRot = new Quaternion().fromAngles(0f, (float) Math.PI, 0f);
Quaternion shieldRot = boneRotInChar.mult(backRot);
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(SHIELD_BACK_OFFSET));
Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO;
shieldSpatial.setLocalRotation(shieldRot);
shieldSpatial.setLocalTranslation(gripTarget.subtract(shieldRot.mult(gripPos)));
}
// ── Detach ────────────────────────────────────────────────────────────────
private void detachWeapon() {
@@ -255,6 +358,13 @@ public class WeaponAttachmentControl extends AbstractControl {
lastDrawnState = false;
}
private void detachShield() {
if (shieldSpatial != null) {
shieldSpatial.removeFromParent();
shieldSpatial = null;
}
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {}
}

View File

@@ -186,8 +186,7 @@ public class WorldInteractableState extends BaseAppState {
private final ActionListener interactListener = (name, isPressed, tpf) -> {
if (!isPressed || phase != Phase.IDLE) return;
int idx = findNearestInRange();
if (idx >= 0) startApproach(idx);
if (hoveredIdx >= 0) startApproach(hoveredIdx);
};
private final ActionListener getUpListener = (name, isPressed, tpf) -> {
@@ -247,22 +246,6 @@ public class WorldInteractableState extends BaseAppState {
return new Vector3f(e.worldX(), e.worldY(), e.worldZ());
}
// ── Suche nächstes Interactable ───────────────────────────────────────────
private int findNearestInRange() {
Vector3f pos = physicsChar.getPhysicsLocation();
int bestIdx = -1;
float bestDist = Float.MAX_VALUE;
for (int i = 0; i < entries.size(); i++) {
InteractableEntry e = entries.get(i);
float dx = e.worldX() - pos.x;
float dz = e.worldZ() - pos.z;
float d = (float) Math.sqrt(dx * dx + dz * dz);
float range = (e.type() == InteractableType.BENCH) ? BENCH_RANGE : BED_RANGE;
if (d < range && d < bestDist) { bestDist = d; bestIdx = i; }
}
return bestIdx;
}
// ── Annäherung ────────────────────────────────────────────────────────────

View File

@@ -153,6 +153,9 @@ public class WorldItemsState extends BaseAppState {
for (PlacedItem pi : items) {
Spatial s = buildVisual(pi);
s.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
com.jme3.math.Quaternion q = new com.jme3.math.Quaternion();
q.fromAngles(pi.rotX(), pi.rotY(), pi.rotZ());
s.setLocalRotation(q);
s.setUserData("itemId", pi.itemId());
itemsRoot.attachChild(s);
visuals.add(s);