Weiter am Kampfsystem gearbeitet - hier: Schwert und Schild
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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)");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user