Beginn des Kampf Systems
This commit is contained in:
@@ -31,7 +31,37 @@ public enum AnimationAction {
|
||||
DISAPPOINT,
|
||||
SECRET,
|
||||
TALK_SITTING,
|
||||
SWORD_ATTACK_1;
|
||||
|
||||
// ── Schwert ───────────────────────────────────────────────────────────────
|
||||
SWORD_ATTACK_1,
|
||||
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN,
|
||||
SWORD_ATTACK_2, 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_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_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
|
||||
|
||||
// ── Bogen ─────────────────────────────────────────────────────────────────
|
||||
BOW_DRAW, BOW_SHEATHE, BOW_HIT_TAKEN,
|
||||
BOW_AIM, BOW_FIRE, BOW_RELOAD,
|
||||
BOW_IDLE, BOW_WALK, BOW_RUN,
|
||||
|
||||
// ── Armbrust ──────────────────────────────────────────────────────────────
|
||||
CROSSBOW_DRAW, CROSSBOW_SHEATHE, CROSSBOW_HIT_TAKEN,
|
||||
CROSSBOW_AIM, CROSSBOW_FIRE, CROSSBOW_RELOAD,
|
||||
CROSSBOW_IDLE, CROSSBOW_WALK, CROSSBOW_RUN,
|
||||
|
||||
// ── Magiekristall ─────────────────────────────────────────────────────────
|
||||
CRYSTAL_DRAW, CRYSTAL_SHEATHE, CRYSTAL_HIT_TAKEN,
|
||||
CRYSTAL_AIM, CRYSTAL_FIRE, CRYSTAL_RELOAD,
|
||||
CRYSTAL_IDLE, CRYSTAL_WALK, CRYSTAL_RUN;
|
||||
|
||||
/** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */
|
||||
public String displayName() {
|
||||
@@ -61,7 +91,69 @@ public enum AnimationAction {
|
||||
case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht");
|
||||
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
|
||||
case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)");
|
||||
case SWORD_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1(Schwert)");
|
||||
|
||||
case SWORD_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Schwert)");
|
||||
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_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)");
|
||||
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_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)");
|
||||
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_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)");
|
||||
case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)");
|
||||
|
||||
case BOW_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Bogen)");
|
||||
case BOW_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Bogen)");
|
||||
case BOW_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Bogen)");
|
||||
case BOW_AIM -> TextRegistry.resolve(null, key, "Zielen (Bogen)");
|
||||
case BOW_FIRE -> TextRegistry.resolve(null, key, "Abfeuern (Bogen)");
|
||||
case BOW_RELOAD -> TextRegistry.resolve(null, key, "Nachladen (Bogen)");
|
||||
case BOW_IDLE -> TextRegistry.resolve(null, key, "Stehen (Bogen)");
|
||||
case BOW_WALK -> TextRegistry.resolve(null, key, "Gehen (Bogen)");
|
||||
case BOW_RUN -> TextRegistry.resolve(null, key, "Laufen (Bogen)");
|
||||
|
||||
case CROSSBOW_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Armbrust)");
|
||||
case CROSSBOW_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Armbrust)");
|
||||
case CROSSBOW_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Armbrust)");
|
||||
case CROSSBOW_AIM -> TextRegistry.resolve(null, key, "Zielen (Armbrust)");
|
||||
case CROSSBOW_FIRE -> TextRegistry.resolve(null, key, "Abfeuern (Armbrust)");
|
||||
case CROSSBOW_RELOAD -> TextRegistry.resolve(null, key, "Nachladen (Armbrust)");
|
||||
case CROSSBOW_IDLE -> TextRegistry.resolve(null, key, "Stehen (Armbrust)");
|
||||
case CROSSBOW_WALK -> TextRegistry.resolve(null, key, "Gehen (Armbrust)");
|
||||
case CROSSBOW_RUN -> TextRegistry.resolve(null, key, "Laufen (Armbrust)");
|
||||
|
||||
case CRYSTAL_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Kristall)");
|
||||
case CRYSTAL_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Kristall)");
|
||||
case CRYSTAL_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Kristall)");
|
||||
case CRYSTAL_AIM -> TextRegistry.resolve(null, key, "Zielen (Kristall)");
|
||||
case CRYSTAL_FIRE -> TextRegistry.resolve(null, key, "Abfeuern (Kristall)");
|
||||
case CRYSTAL_RELOAD -> TextRegistry.resolve(null, key, "Nachladen (Kristall)");
|
||||
case CRYSTAL_IDLE -> TextRegistry.resolve(null, key, "Stehen (Kristall)");
|
||||
case CRYSTAL_WALK -> TextRegistry.resolve(null, key, "Gehen (Kristall)");
|
||||
case CRYSTAL_RUN -> TextRegistry.resolve(null, key, "Laufen (Kristall)");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,10 @@ public class KeyBindings {
|
||||
public int inventory = KeyInput.KEY_I;
|
||||
public int quicksave = KeyInput.KEY_F5;
|
||||
public int character = KeyInput.KEY_C;
|
||||
public int quests = KeyInput.KEY_V;
|
||||
public int quests = KeyInput.KEY_V;
|
||||
public int drawMeleeWeapon = KeyInput.KEY_1;
|
||||
public int drawRangedWeapon = KeyInput.KEY_2;
|
||||
public int sheathWeapon = KeyInput.KEY_X;
|
||||
|
||||
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
|
||||
public static final String[][] ENTRIES = {
|
||||
@@ -34,8 +37,11 @@ public class KeyBindings {
|
||||
{"turnRight", "Kamera rechts"},
|
||||
{"inventory", "Inventar"},
|
||||
{"quicksave", "Schnellspeichern"},
|
||||
{"character", "Charakter"},
|
||||
{"quests", "Questlog"},
|
||||
{"character", "Charakter"},
|
||||
{"quests", "Questlog"},
|
||||
{"drawMeleeWeapon", "Nahkampfwaffe ziehen"},
|
||||
{"drawRangedWeapon", "Fernkampfwaffe ziehen"},
|
||||
{"sheathWeapon", "Waffe wegstecken"},
|
||||
};
|
||||
|
||||
public int get(String fieldName) {
|
||||
|
||||
@@ -3,8 +3,12 @@ package de.blight.game.control;
|
||||
import com.jme3.anim.AnimComposer;
|
||||
import com.jme3.bullet.control.CharacterControl;
|
||||
import com.jme3.input.InputManager;
|
||||
import com.jme3.input.MouseInput;
|
||||
import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
import com.jme3.input.controls.MouseButtonTrigger;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
@@ -35,8 +39,16 @@ public class PlayerInputControl {
|
||||
private static final float WALK_MULT = 0.5f;
|
||||
private static final float ROTATE_SPEED = 10f;
|
||||
|
||||
private static final String[] ACTION_NAMES =
|
||||
{"Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk"};
|
||||
private static final String ACT_DRAW_MELEE = "DrawMeleeWeapon";
|
||||
private static final String ACT_DRAW_RANGED = "DrawRangedWeapon";
|
||||
private static final String ACT_SHEATH = "SheathWeapon";
|
||||
private static final String ACT_COMBAT_ATK = "CombatAttack";
|
||||
private static final String ACT_COMBAT_ALT = "CombatAltAction";
|
||||
|
||||
private static final String[] ACTION_NAMES = {
|
||||
"Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk",
|
||||
ACT_DRAW_MELEE, ACT_DRAW_RANGED, ACT_SHEATH, ACT_COMBAT_ATK, ACT_COMBAT_ALT
|
||||
};
|
||||
|
||||
private final InputManager inputManager;
|
||||
private final Camera cam;
|
||||
@@ -47,6 +59,17 @@ public class PlayerInputControl {
|
||||
private boolean forward, backward, left, right, sprint, walk;
|
||||
private boolean paused = false;
|
||||
|
||||
// ── Kampf-Zustand ─────────────────────────────────────────────────────────
|
||||
private MainCharacter mainCharacter = null;
|
||||
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;
|
||||
|
||||
private AnimationLibrary animLib;
|
||||
private String animSetName;
|
||||
private Path assetRoot;
|
||||
@@ -139,6 +162,11 @@ public class PlayerInputControl {
|
||||
case "Sprint" -> sprint = isPressed;
|
||||
case "Walk" -> walk = isPressed;
|
||||
case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } }
|
||||
case ACT_DRAW_MELEE -> { if (isPressed) handleDrawMelee(); }
|
||||
case ACT_DRAW_RANGED -> { if (isPressed) handleDrawRanged(); }
|
||||
case ACT_SHEATH -> { if (isPressed && weaponDrawn && !blockingAnimActive) sheathCurrentWeapon(); }
|
||||
case ACT_COMBAT_ATK -> { if (isPressed) handleCombatAttack(); }
|
||||
case ACT_COMBAT_ALT -> handleCombatAlt(isPressed);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,6 +238,11 @@ public class PlayerInputControl {
|
||||
}
|
||||
}
|
||||
|
||||
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
|
||||
public boolean isWeaponDrawn() { return weaponDrawn; }
|
||||
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
||||
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
||||
|
||||
public boolean isPaused() { return paused; }
|
||||
|
||||
public void setPaused(boolean paused) {
|
||||
@@ -219,6 +252,9 @@ public class PlayerInputControl {
|
||||
autopilotDir = null;
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
stopWading();
|
||||
rightMouseHeld = false;
|
||||
combatAnimActive = false;
|
||||
combatAnimCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,13 +499,18 @@ public class PlayerInputControl {
|
||||
}
|
||||
|
||||
private void registerMappings(KeyBindings kb) {
|
||||
inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
|
||||
inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
|
||||
inputManager.addMapping("Left", new KeyTrigger(kb.left));
|
||||
inputManager.addMapping("Right", new KeyTrigger(kb.right));
|
||||
inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
|
||||
inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
|
||||
inputManager.addMapping("Walk", new KeyTrigger(kb.walk));
|
||||
inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
|
||||
inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
|
||||
inputManager.addMapping("Left", new KeyTrigger(kb.left));
|
||||
inputManager.addMapping("Right", new KeyTrigger(kb.right));
|
||||
inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
|
||||
inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
|
||||
inputManager.addMapping("Walk", new KeyTrigger(kb.walk));
|
||||
inputManager.addMapping(ACT_DRAW_MELEE, new KeyTrigger(kb.drawMeleeWeapon));
|
||||
inputManager.addMapping(ACT_DRAW_RANGED, new KeyTrigger(kb.drawRangedWeapon));
|
||||
inputManager.addMapping(ACT_SHEATH, new KeyTrigger(kb.sheathWeapon));
|
||||
inputManager.addMapping(ACT_COMBAT_ATK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||
inputManager.addMapping(ACT_COMBAT_ALT, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
|
||||
inputManager.addListener(actionListener, ACTION_NAMES);
|
||||
}
|
||||
|
||||
@@ -598,9 +639,9 @@ public class PlayerInputControl {
|
||||
|
||||
if (moving) {
|
||||
scratchMoveDir.normalizeLocal();
|
||||
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
||||
: sprint ? MOVE_SPEED * SPRINT_MULT
|
||||
: MOVE_SPEED;
|
||||
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
|
||||
: (sprint && !weaponDrawn) ? MOVE_SPEED * SPRINT_MULT
|
||||
: MOVE_SPEED;
|
||||
physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed));
|
||||
|
||||
if (visual != null) {
|
||||
@@ -613,24 +654,44 @@ public class PlayerInputControl {
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
}
|
||||
|
||||
// Kampf-Anim-Timer
|
||||
if (combatAnimActive) {
|
||||
combatAnimRemaining -= tpf;
|
||||
if (combatAnimRemaining <= 0f) {
|
||||
combatAnimActive = false;
|
||||
currentAnim = null;
|
||||
Runnable cb = combatAnimCallback;
|
||||
combatAnimCallback = null;
|
||||
if (cb != null) cb.run();
|
||||
}
|
||||
}
|
||||
|
||||
// Animations-Auswahl
|
||||
if (jumpFrames > 0) jumpFrames--;
|
||||
if (groundGraceFrames > 0) groundGraceFrames--;
|
||||
|
||||
AnimationAction target;
|
||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||
} else if (moving) {
|
||||
target = (inDeepWater || walk) ? AnimationAction.WALK
|
||||
: sprint ? AnimationAction.SPRINT
|
||||
: AnimationAction.RUN;
|
||||
} else {
|
||||
target = AnimationAction.IDLE;
|
||||
}
|
||||
if (!combatAnimActive) {
|
||||
AnimationAction target;
|
||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||
} else if (moving) {
|
||||
target = (inDeepWater || walk) ? AnimationAction.WALK
|
||||
: sprint ? AnimationAction.SPRINT
|
||||
: AnimationAction.RUN;
|
||||
} else {
|
||||
target = AnimationAction.IDLE;
|
||||
}
|
||||
|
||||
if (target != currentAnim) {
|
||||
playAction(target);
|
||||
currentAnim = target;
|
||||
target = resolveWeaponAction(target);
|
||||
|
||||
if (weaponDrawn && rightMouseHeld && isMeleeType(drawnWeaponType)) {
|
||||
target = getMeleeBlockAction(drawnWeaponType);
|
||||
}
|
||||
|
||||
if (target != currentAnim) {
|
||||
playAction(target);
|
||||
currentAnim = target;
|
||||
}
|
||||
}
|
||||
|
||||
pollFootsteps(tpf);
|
||||
@@ -654,6 +715,244 @@ public class PlayerInputControl {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kampf-Hilfsmethoden ───────────────────────────────────────────────────
|
||||
|
||||
private void handleDrawMelee() {
|
||||
if (mainCharacter == null || mainCharacter.getEquippedMeleeWeapon() == null) return;
|
||||
if (blockingAnimActive || combatAnimActive) return;
|
||||
ItemSubCategory sub = mainCharacter.getEquippedMeleeWeapon().getSubCategory();
|
||||
if (!weaponDrawn) {
|
||||
AnimationAction drawAnim = getMeleeDrawAction(sub);
|
||||
float dur = resolveClipLength(drawAnim, 0.5f);
|
||||
requestAnimation(drawAnim, dur, () -> {
|
||||
weaponDrawn = true;
|
||||
drawnWeaponType = sub;
|
||||
currentAnim = null;
|
||||
});
|
||||
} else if (isMeleeType(drawnWeaponType)) {
|
||||
sheathCurrentWeapon();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDrawRanged() {
|
||||
if (mainCharacter == null || mainCharacter.getEquippedRangedWeapon() == null) return;
|
||||
if (blockingAnimActive || combatAnimActive) return;
|
||||
ItemSubCategory sub = mainCharacter.getEquippedRangedWeapon().getSubCategory();
|
||||
if (!weaponDrawn) {
|
||||
AnimationAction drawAnim = getRangedDrawAction(sub);
|
||||
float dur = resolveClipLength(drawAnim, 0.5f);
|
||||
requestAnimation(drawAnim, dur, () -> {
|
||||
weaponDrawn = true;
|
||||
drawnWeaponType = sub;
|
||||
currentAnim = null;
|
||||
});
|
||||
} else if (isRangedType(drawnWeaponType)) {
|
||||
sheathCurrentWeapon();
|
||||
}
|
||||
}
|
||||
|
||||
private void sheathCurrentWeapon() {
|
||||
if (drawnWeaponType == null) { weaponDrawn = false; return; }
|
||||
isAiming = false;
|
||||
rightMouseHeld = false;
|
||||
AnimationAction sheathAnim = getSheathAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(sheathAnim, 0.5f);
|
||||
requestAnimation(sheathAnim, dur, () -> {
|
||||
weaponDrawn = false;
|
||||
drawnWeaponType = null;
|
||||
currentAnim = null;
|
||||
});
|
||||
}
|
||||
|
||||
private void handleCombatAttack() {
|
||||
if (!weaponDrawn || blockingAnimActive || combatAnimActive) return;
|
||||
if (isMeleeType(drawnWeaponType)) {
|
||||
AnimationAction atk = meleeAttackToggle
|
||||
? getMeleeAttack2Action(drawnWeaponType)
|
||||
: getMeleeAttack1Action(drawnWeaponType);
|
||||
meleeAttackToggle = !meleeAttackToggle;
|
||||
float dur = resolveClipLength(atk, 0.6f);
|
||||
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);
|
||||
combatAnimCallback = null;
|
||||
playAction(atk);
|
||||
currentAnim = atk;
|
||||
} else if (isRangedType(drawnWeaponType) && isAiming) {
|
||||
isAiming = false;
|
||||
AnimationAction fireAnim = getRangedFireAction(drawnWeaponType);
|
||||
AnimationAction reloadAnim = getRangedReloadAction(drawnWeaponType);
|
||||
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);
|
||||
combatAnimCallback = () -> {
|
||||
combatAnimActive = true;
|
||||
combatAnimRemaining = Math.max(0.1f, reloadDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
||||
combatAnimCallback = null;
|
||||
playAction(reloadAnim);
|
||||
currentAnim = reloadAnim;
|
||||
};
|
||||
playAction(fireAnim);
|
||||
currentAnim = fireAnim;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCombatAlt(boolean isPressed) {
|
||||
if (!weaponDrawn || blockingAnimActive) return;
|
||||
if (isMeleeType(drawnWeaponType)) {
|
||||
rightMouseHeld = isPressed;
|
||||
} else if (isRangedType(drawnWeaponType) && isPressed) {
|
||||
isAiming = !isAiming;
|
||||
}
|
||||
}
|
||||
|
||||
private AnimationAction resolveWeaponAction(AnimationAction base) {
|
||||
if (!weaponDrawn || drawnWeaponType == null) return base;
|
||||
return switch (base) {
|
||||
case IDLE -> weaponIdleAction();
|
||||
case WALK -> weaponWalkAction();
|
||||
case RUN, SPRINT -> weaponRunAction();
|
||||
default -> base;
|
||||
};
|
||||
}
|
||||
|
||||
private AnimationAction weaponIdleAction() {
|
||||
if (isAiming) {
|
||||
return switch (drawnWeaponType) {
|
||||
case BOW -> AnimationAction.BOW_AIM;
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_AIM;
|
||||
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_AIM;
|
||||
default -> weaponStandAction();
|
||||
};
|
||||
}
|
||||
return weaponStandAction();
|
||||
}
|
||||
|
||||
private AnimationAction weaponStandAction() {
|
||||
return switch (drawnWeaponType) {
|
||||
case SWORD -> AnimationAction.SWORD_IDLE;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE;
|
||||
case AXE -> AnimationAction.AXE_IDLE;
|
||||
case BOW -> AnimationAction.BOW_IDLE;
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_IDLE;
|
||||
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_IDLE;
|
||||
default -> AnimationAction.IDLE;
|
||||
};
|
||||
}
|
||||
|
||||
private AnimationAction weaponWalkAction() {
|
||||
return switch (drawnWeaponType) {
|
||||
case SWORD -> AnimationAction.SWORD_WALK;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_WALK;
|
||||
case AXE -> AnimationAction.AXE_WALK;
|
||||
case BOW -> AnimationAction.BOW_WALK;
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_WALK;
|
||||
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_WALK;
|
||||
default -> AnimationAction.WALK;
|
||||
};
|
||||
}
|
||||
|
||||
private AnimationAction weaponRunAction() {
|
||||
return switch (drawnWeaponType) {
|
||||
case SWORD -> AnimationAction.SWORD_RUN;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_RUN;
|
||||
case AXE -> AnimationAction.AXE_RUN;
|
||||
case BOW -> AnimationAction.BOW_RUN;
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_RUN;
|
||||
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_RUN;
|
||||
default -> AnimationAction.RUN;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean isMeleeType(ItemSubCategory s) {
|
||||
return s == ItemSubCategory.SWORD
|
||||
|| s == ItemSubCategory.AXE
|
||||
|| s == ItemSubCategory.TWO_HANDED_SWORD;
|
||||
}
|
||||
|
||||
private static boolean isRangedType(ItemSubCategory s) {
|
||||
return s == ItemSubCategory.BOW
|
||||
|| s == ItemSubCategory.CROSSBOW
|
||||
|| s == ItemSubCategory.MAGIC_CRYSTAL;
|
||||
}
|
||||
|
||||
private static AnimationAction getMeleeDrawAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_DRAW;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_DRAW;
|
||||
default -> AnimationAction.SWORD_DRAW;
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getMeleySheathAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_SHEATHE;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_SHEATHE;
|
||||
default -> AnimationAction.SWORD_SHEATHE;
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getMeleeAttack1Action(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_ATTACK_1;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_ATTACK_1;
|
||||
default -> AnimationAction.SWORD_ATTACK_1;
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getMeleeAttack2Action(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_ATTACK_2;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_ATTACK_2;
|
||||
default -> AnimationAction.SWORD_ATTACK_2;
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getMeleeBlockAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case AXE -> AnimationAction.AXE_BLOCK;
|
||||
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK;
|
||||
default -> AnimationAction.SWORD_BLOCK;
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getRangedDrawAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_DRAW;
|
||||
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_DRAW;
|
||||
default -> AnimationAction.BOW_DRAW;
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getRangedSheathAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_SHEATHE;
|
||||
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_SHEATHE;
|
||||
default -> AnimationAction.BOW_SHEATHE;
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getRangedFireAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_FIRE;
|
||||
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_FIRE;
|
||||
default -> AnimationAction.BOW_FIRE;
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getRangedReloadAction(ItemSubCategory s) {
|
||||
return switch (s) {
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_RELOAD;
|
||||
case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_RELOAD;
|
||||
default -> AnimationAction.BOW_RELOAD;
|
||||
};
|
||||
}
|
||||
|
||||
private static AnimationAction getSheathAction(ItemSubCategory s) {
|
||||
return isMeleeType(s) ? getMeleySheathAction(s) : getRangedSheathAction(s);
|
||||
}
|
||||
|
||||
public void setFootstepSystem(de.blight.game.audio.FootstepSystem fs,
|
||||
java.util.function.BiFunction<Float, Float, float[]> query) {
|
||||
this.footstepSystem = fs;
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
package de.blight.game.control;
|
||||
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.math.Matrix4f;
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.ItemGrip;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Hängt die ausgerüstete Waffe an den Charakter:
|
||||
* - weggesteckt → Spine2-Knochen (Rücken, Klinge zeigt nach unten)
|
||||
* - gezückt → RightHand-Knochen (unter Berücksichtigung des gespeicherten ItemGrip)
|
||||
*
|
||||
* Die Waffe wird direkt am character-Node (kein geerbter Scale) als Kind eingehängt
|
||||
* und bekommt jeden Frame die korrekte Position/Rotation aus dem Bone-World-Transform.
|
||||
*/
|
||||
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 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 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
|
||||
|
||||
// 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;
|
||||
|
||||
private Item lastEquippedWeapon;
|
||||
private boolean lastDrawnState = false;
|
||||
private Spatial weaponSpatial;
|
||||
|
||||
public WeaponAttachmentControl(AssetManager assetManager,
|
||||
PlayerInputControl playerInput,
|
||||
MainCharacter mc) {
|
||||
this.assetManager = assetManager;
|
||||
this.playerInput = playerInput;
|
||||
this.mc = mc;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
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 (weaponSpatial != null) {
|
||||
lastDrawnState = drawn;
|
||||
updateWeaponTransform(drawn, current != null ? current.getGrip() : null);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Laden ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void loadWeapon(Item item) {
|
||||
if (item.getModelRef() == null || item.getModelRef().getPath() == null) {
|
||||
log.warn("[WeaponAttach] Kein modelRef an Item '{}'.", item.getItemId());
|
||||
return;
|
||||
}
|
||||
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());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeaponAttach] Waffenmodell nicht ladbar ({}): {}",
|
||||
item.getModelRef().getPath(), e.getMessage());
|
||||
weaponSpatial = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Transform-Update (jedes Frame) ────────────────────────────────────────
|
||||
|
||||
private void updateWeaponTransform(boolean drawn, ItemGrip grip) {
|
||||
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);
|
||||
|
||||
if (drawn) {
|
||||
applyHandTransform(boneInChar, 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())
|
||||
: new Quaternion();
|
||||
Vector3f gripPos = (grip != null)
|
||||
? 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);
|
||||
|
||||
// Waffenposition: Griff-Punkt liegt am Knochen-Ursprung
|
||||
weaponSpatial.setLocalRotation(weaponRot);
|
||||
weaponSpatial.setLocalTranslation(bonePosInChar.subtract(weaponRot.mult(gripPos)));
|
||||
}
|
||||
|
||||
/** 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;
|
||||
com.jme3.anim.Joint joint = arm.getJoint(jointName);
|
||||
if (joint == null) return null;
|
||||
Matrix4f invBind = joint.getInverseModelBindMatrix();
|
||||
if (invBind == null) return null;
|
||||
Matrix4f bindMat = invBind.invert();
|
||||
if (bindMat == null) return null;
|
||||
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 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)
|
||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||
: Vector3f.ZERO;
|
||||
|
||||
// weaponPos + weaponRot * gripPos = gripTarget
|
||||
weaponSpatial.setLocalRotation(weaponRot);
|
||||
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
|
||||
}
|
||||
|
||||
// ── Detach ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void detachWeapon() {
|
||||
if (weaponSpatial != null) {
|
||||
weaponSpatial.removeFromParent();
|
||||
weaponSpatial = null;
|
||||
}
|
||||
lastDrawnState = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||
}
|
||||
@@ -361,6 +361,8 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
MainCharacter mc = findMainCharacter();
|
||||
if (mc != null) {
|
||||
playerInput.setMainCharacter(mc);
|
||||
character.addControl(new de.blight.game.control.WeaponAttachmentControl(assetManager, playerInput, mc));
|
||||
// SaveGameState mit Charakter und Positions-Lieferant verknüpfen
|
||||
de.blight.game.state.SaveGameState saveState =
|
||||
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
|
||||
|
||||
@@ -8,8 +8,11 @@ import com.jme3.input.controls.ActionListener;
|
||||
import com.jme3.input.controls.AnalogListener;
|
||||
import com.jme3.input.controls.KeyTrigger;
|
||||
import com.jme3.input.controls.MouseAxisTrigger;
|
||||
import com.jme3.input.controls.MouseButtonTrigger;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.scene.Node;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import de.blight.common.model.XPHelper;
|
||||
import de.blight.common.model.abilities.Abilities;
|
||||
@@ -46,9 +49,10 @@ public class CharacterState extends OverlayState {
|
||||
|
||||
// ── Input ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private static final String MAP_TOGGLE = "_CharToggle";
|
||||
private static final String MAP_SCROLL_UP = "_CharScrollUp";
|
||||
private static final String MAP_SCROLL_DOWN = "_CharScrollDown";
|
||||
private static final String MAP_TOGGLE = "_CharToggle";
|
||||
private static final String MAP_SCROLL_UP = "_CharScrollUp";
|
||||
private static final String MAP_SCROLL_DOWN = "_CharScrollDown";
|
||||
private static final String MAP_EQUIP_CLICK = "_CharEquipClick";
|
||||
|
||||
// ── Scroll-Zustand ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -64,6 +68,11 @@ public class CharacterState extends OverlayState {
|
||||
|
||||
private final MainCharacter mc;
|
||||
|
||||
// ── Equipment-Slot-Bounds (für Klick-Erkennung) ───────────────────────────
|
||||
private float[] meleeSlotBounds = null;
|
||||
private float[] shieldSlotBounds = null;
|
||||
private float[] rangedSlotBounds = null;
|
||||
|
||||
private record Skill(String name, int lvl, int maxLvl, String[][] abils) {}
|
||||
|
||||
public CharacterState(MainCharacter mc) {
|
||||
@@ -82,6 +91,8 @@ public class CharacterState extends OverlayState {
|
||||
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
|
||||
app.getInputManager().addMapping(MAP_SCROLL_DOWN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
|
||||
app.getInputManager().addListener(scrollAnalog, MAP_SCROLL_UP, MAP_SCROLL_DOWN);
|
||||
app.getInputManager().addMapping(MAP_EQUIP_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||
app.getInputManager().addListener(equipClickListener, MAP_EQUIP_CLICK);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,13 +100,18 @@ public class CharacterState extends OverlayState {
|
||||
app.getInputManager().deleteMapping(MAP_TOGGLE);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
|
||||
app.getInputManager().deleteMapping(MAP_SCROLL_DOWN);
|
||||
app.getInputManager().deleteMapping(MAP_EQUIP_CLICK);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void buildContent(Node panel, float scale) {
|
||||
meleeSlotBounds = null;
|
||||
shieldSlotBounds = null;
|
||||
rangedSlotBounds = null;
|
||||
float divX = PX + PAD + LEFT_W + 6f;
|
||||
addQuad(panel, divX, PY + PAD, 1f, PH - HDR_H - PAD * 2, UiTheme.COL_DIVIDER, -17f);
|
||||
buildStats(panel);
|
||||
float equipY = buildStats(panel);
|
||||
buildEquipment(panel, equipY);
|
||||
buildAbilities(panel);
|
||||
}
|
||||
|
||||
@@ -120,9 +136,22 @@ public class CharacterState extends OverlayState {
|
||||
rebuildSkills();
|
||||
};
|
||||
|
||||
private final ActionListener equipClickListener = (name, isPressed, tpf) -> {
|
||||
if (!isPressed || !isEnabled()) return;
|
||||
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||
if (hitTest(v, meleeSlotBounds)) { cycleEquipMelee(); return; }
|
||||
if (hitTest(v, shieldSlotBounds)) { cycleEquipShield(); return; }
|
||||
if (hitTest(v, rangedSlotBounds)) { cycleEquipRanged(); }
|
||||
};
|
||||
|
||||
private static boolean hitTest(float[] v, float[] b) {
|
||||
if (b == null) return false;
|
||||
return v[0] >= b[0] && v[0] <= b[0] + b[2] && v[1] >= b[1] && v[1] <= b[1] + b[3];
|
||||
}
|
||||
|
||||
// ── Linke Spalte: Werte ───────────────────────────────────────────────────
|
||||
|
||||
private void buildStats(Node panel) {
|
||||
private float buildStats(Node panel) {
|
||||
float x = PX + PAD;
|
||||
float y = PY + PH - HDR_H - PAD;
|
||||
|
||||
@@ -172,6 +201,7 @@ public class CharacterState extends OverlayState {
|
||||
regenLine(panel, "HP", hpReg, x, y); y -= 16;
|
||||
regenLine(panel, "Stamina", stReg, x, y); y -= 16;
|
||||
regenLine(panel, "Mana", mnReg, x, y);
|
||||
return y;
|
||||
}
|
||||
|
||||
private float statBar(Node parent, String label, int cur, int max, ColorRGBA col,
|
||||
@@ -191,6 +221,109 @@ public class CharacterState extends OverlayState {
|
||||
return y - BAR_H_STAT - 10;
|
||||
}
|
||||
|
||||
// ── Ausrüstungs-Panel ─────────────────────────────────────────────────────
|
||||
|
||||
private void buildEquipment(Node panel, float startY) {
|
||||
float x = PX + PAD;
|
||||
float y = startY - 26f;
|
||||
|
||||
addQuad(panel, x, y, LEFT_W - 6, 1f, UiTheme.COL_DIVIDER, -17f);
|
||||
y -= 14;
|
||||
|
||||
BitmapText lbl = crispText("Ausrüstung", UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
|
||||
lbl.setLocalTranslation(x, y, -17);
|
||||
panel.attachChild(lbl);
|
||||
y -= 20;
|
||||
|
||||
String meleeName = mc.getEquippedMeleeWeapon() != null
|
||||
? mc.getEquippedMeleeWeapon().getDisplayText() : "—";
|
||||
meleeSlotBounds = equipSlotRow(panel, "Nahkampf", meleeName, x, y, LEFT_W - 6);
|
||||
y -= 26;
|
||||
|
||||
boolean hasSword = mc.getEquippedMeleeWeapon() != null
|
||||
&& mc.getEquippedMeleeWeapon().getSubCategory() == ItemSubCategory.SWORD;
|
||||
if (hasSword) {
|
||||
String shieldName = mc.getEquippedShield() != null
|
||||
? mc.getEquippedShield().getDisplayText() : "—";
|
||||
shieldSlotBounds = equipSlotRow(panel, "Schild", shieldName, x, y, LEFT_W - 6);
|
||||
y -= 26;
|
||||
} else {
|
||||
shieldSlotBounds = null;
|
||||
}
|
||||
|
||||
String rangedName = mc.getEquippedRangedWeapon() != null
|
||||
? mc.getEquippedRangedWeapon().getDisplayText() : "—";
|
||||
rangedSlotBounds = equipSlotRow(panel, "Fernkampf", rangedName, x, y, LEFT_W - 6);
|
||||
}
|
||||
|
||||
private float[] equipSlotRow(Node panel, String label, String value,
|
||||
float x, float y, float w) {
|
||||
float slotH = 22f;
|
||||
addQuad(panel, x, y - slotH, w, slotH, UiTheme.COL_CARD_BRD, -19f);
|
||||
addQuad(panel, x+1, y - slotH + 1, w-2, slotH-2, UiTheme.COL_CARD, -18f);
|
||||
|
||||
BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_LABEL, UiTheme.COL_MUTED);
|
||||
lblTxt.setLocalTranslation(x + 4, y - 5, -17f);
|
||||
panel.attachChild(lblTxt);
|
||||
|
||||
String clipped = value.length() > 16 ? value.substring(0, 15) + "…" : value;
|
||||
BitmapText valTxt = crispText(clipped, UiTheme.FONT_SMALL, UiTheme.COL_WHITE);
|
||||
valTxt.setLocalTranslation(x + 76, y - 5, -17f);
|
||||
panel.attachChild(valTxt);
|
||||
|
||||
BitmapText arr = crispText("»", UiTheme.FONT_TINY, UiTheme.COL_MUTED);
|
||||
arr.setLocalTranslation(x + w - 16, y - 5, -17f);
|
||||
panel.attachChild(arr);
|
||||
|
||||
return new float[]{ x, y - slotH, w, slotH };
|
||||
}
|
||||
|
||||
private void cycleEquipMelee() {
|
||||
java.util.List<Item> options = new java.util.ArrayList<>();
|
||||
options.add(null);
|
||||
if (mc.getInventar() != null) {
|
||||
mc.getInventar().getItems().keySet().stream()
|
||||
.filter(i -> i.getSubCategory() == ItemSubCategory.SWORD
|
||||
|| i.getSubCategory() == ItemSubCategory.AXE
|
||||
|| i.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD)
|
||||
.sorted(java.util.Comparator.comparing(Item::getDisplayText))
|
||||
.forEach(options::add);
|
||||
}
|
||||
int idx = options.indexOf(mc.getEquippedMeleeWeapon());
|
||||
mc.equipMelee(options.get((idx + 1) % options.size()));
|
||||
setEnabled(false); setEnabled(true);
|
||||
}
|
||||
|
||||
private void cycleEquipShield() {
|
||||
java.util.List<Item> options = new java.util.ArrayList<>();
|
||||
options.add(null);
|
||||
if (mc.getInventar() != null) {
|
||||
mc.getInventar().getItems().keySet().stream()
|
||||
.filter(i -> i.getSubCategory() == ItemSubCategory.SHIELD)
|
||||
.sorted(java.util.Comparator.comparing(Item::getDisplayText))
|
||||
.forEach(options::add);
|
||||
}
|
||||
int idx = options.indexOf(mc.getEquippedShield());
|
||||
mc.equipShield(options.get((idx + 1) % options.size()));
|
||||
setEnabled(false); setEnabled(true);
|
||||
}
|
||||
|
||||
private void cycleEquipRanged() {
|
||||
java.util.List<Item> options = new java.util.ArrayList<>();
|
||||
options.add(null);
|
||||
if (mc.getInventar() != null) {
|
||||
mc.getInventar().getItems().keySet().stream()
|
||||
.filter(i -> i.getSubCategory() == ItemSubCategory.BOW
|
||||
|| i.getSubCategory() == ItemSubCategory.CROSSBOW
|
||||
|| i.getSubCategory() == ItemSubCategory.MAGIC_CRYSTAL)
|
||||
.sorted(java.util.Comparator.comparing(Item::getDisplayText))
|
||||
.forEach(options::add);
|
||||
}
|
||||
int idx = options.indexOf(mc.getEquippedRangedWeapon());
|
||||
mc.equipRanged(options.get((idx + 1) % options.size()));
|
||||
setEnabled(false); setEnabled(true);
|
||||
}
|
||||
|
||||
private void regenLine(Node parent, String label, int val, float x, float y) {
|
||||
ColorRGBA col = val > 0 ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED;
|
||||
BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||
|
||||
@@ -225,16 +225,43 @@ public class InventoryState extends OverlayState {
|
||||
|
||||
// ── Item-Grid ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static final float WEAPON_SLOT_H = 26f;
|
||||
|
||||
private void buildWeaponSlots(Node parent) {
|
||||
float x = contentLeft;
|
||||
float y = contentTop;
|
||||
float w = COLS * (CELL_W + CELL_GAP) - CELL_GAP;
|
||||
|
||||
String[] labels = {"Slot 1 — Nahkampf", "Slot 2 — Fernkampf"};
|
||||
Item[] items = {mc.getEquippedMeleeWeapon(), mc.getEquippedRangedWeapon()};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
y -= WEAPON_SLOT_H;
|
||||
addQuad(parent, x, y, w, WEAPON_SLOT_H, UiTheme.COL_SECTION, -18f);
|
||||
String slotTxt = labels[i] + ": " + (items[i] != null ? items[i].getDisplayText() : "—");
|
||||
BitmapText t = crispText(slotTxt, UiTheme.FONT_BODY, UiTheme.COL_MUTED);
|
||||
t.setLocalTranslation(x + 8, y + WEAPON_SLOT_H - 7, -17f);
|
||||
parent.attachChild(t);
|
||||
y -= 4;
|
||||
}
|
||||
}
|
||||
|
||||
private void buildGrid() {
|
||||
gridNode = new Node("inv-grid");
|
||||
panel.attachChild(gridNode);
|
||||
|
||||
float savedContentTop = contentTop;
|
||||
if (activeTab == ItemCategory.WEAPON) {
|
||||
buildWeaponSlots(gridNode);
|
||||
contentTop -= 2 * (WEAPON_SLOT_H + 4);
|
||||
}
|
||||
|
||||
if (activeItems.isEmpty()) {
|
||||
BitmapText empty = crispText("Keine Items vorhanden", UiTheme.FONT_HEADING, UiTheme.COL_MUTED);
|
||||
float ey = contentBottom + (contentTop - contentBottom) / 2f + 10;
|
||||
float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f;
|
||||
empty.setLocalTranslation(ex, ey, -17);
|
||||
gridNode.attachChild(empty);
|
||||
contentTop = savedContentTop;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -278,6 +305,7 @@ public class InventoryState extends OverlayState {
|
||||
if (col != 0) { virtY -= CELL_GAP; }
|
||||
virtY -= CELL_GAP;
|
||||
}
|
||||
contentTop = savedContentTop;
|
||||
}
|
||||
|
||||
private void rebuildGrid() {
|
||||
@@ -297,7 +325,8 @@ public class InventoryState extends OverlayState {
|
||||
int rows = (int) Math.ceil(g.size() / (double) COLS);
|
||||
totalH += rows * (CELL_H + CELL_GAP);
|
||||
}
|
||||
float contentH = contentTop - contentBottom;
|
||||
float slotOffset = (activeTab == ItemCategory.WEAPON) ? 2 * (WEAPON_SLOT_H + 4) : 0f;
|
||||
float contentH = contentTop - contentBottom - slotOffset;
|
||||
return Math.max(0f, totalH - contentH);
|
||||
}
|
||||
|
||||
@@ -392,6 +421,9 @@ public class InventoryState extends OverlayState {
|
||||
case STAFF -> "Stäbe";
|
||||
case HALBERD -> "Hellebarden";
|
||||
case AXE -> "Äxte";
|
||||
case BOW -> "Bögen";
|
||||
case CROSSBOW -> "Armbrüste";
|
||||
case MAGIC_CRYSTAL -> "Magiekristalle";
|
||||
case RING -> "Ringe";
|
||||
case NECKLACE -> "Halsketten";
|
||||
case ARMOR -> "Rüstungen";
|
||||
|
||||
@@ -124,6 +124,11 @@ public class SaveGameState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// Ausgerüstete Waffen
|
||||
save.character.equippedMeleeWeaponId = mc.getEquippedMeleeWeapon() != null ? mc.getEquippedMeleeWeapon().getItemId() : null;
|
||||
save.character.equippedRangedWeaponId = mc.getEquippedRangedWeapon() != null ? mc.getEquippedRangedWeapon().getItemId() : null;
|
||||
save.character.equippedShieldId = mc.getEquippedShield() != null ? mc.getEquippedShield().getItemId() : null;
|
||||
|
||||
try {
|
||||
SaveGameIO.save(save);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -267,6 +267,7 @@ public class WorldInteractableState extends BaseAppState {
|
||||
// ── Annäherung ────────────────────────────────────────────────────────────
|
||||
|
||||
private void startApproach(int idx) {
|
||||
if (playerInput.isWeaponDrawn()) return;
|
||||
targetIdx = idx;
|
||||
walkTimer = 0f;
|
||||
originPos = physicsChar.getPhysicsLocation().clone();
|
||||
|
||||
@@ -173,6 +173,20 @@ public class WorldItemsState extends BaseAppState {
|
||||
}
|
||||
log.info("[WorldItems] Inventar wiederhergestellt ({} Einträge).", cs.inventory.size());
|
||||
}
|
||||
|
||||
// Ausgerüstete Waffen wiederherstellen
|
||||
if (cs.equippedMeleeWeaponId != null) {
|
||||
Item m = itemDefs.get(cs.equippedMeleeWeaponId);
|
||||
if (m != null) mainCharacter.equipMelee(m);
|
||||
}
|
||||
if (cs.equippedRangedWeaponId != null) {
|
||||
Item r = itemDefs.get(cs.equippedRangedWeaponId);
|
||||
if (r != null) mainCharacter.equipRanged(r);
|
||||
}
|
||||
if (cs.equippedShieldId != null) {
|
||||
Item s = itemDefs.get(cs.equippedShieldId);
|
||||
if (s != null) mainCharacter.equipShield(s);
|
||||
}
|
||||
}
|
||||
|
||||
inputManager.addMapping(INTERACT_ACTION,
|
||||
@@ -421,6 +435,7 @@ public class WorldItemsState extends BaseAppState {
|
||||
// ── Pickup-Logik ──────────────────────────────────────────────────────────
|
||||
|
||||
private void tryPickup() {
|
||||
if (playerInput.isWeaponDrawn()) return;
|
||||
log.info("[WorldItems] tryPickup: hoveredIdx={} physicsChar={}", hoveredIdx, physicsChar != null ? "ok" : "NULL");
|
||||
if (hoveredIdx < 0 || physicsChar == null) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user