Beginn des Kampf Systems

This commit is contained in:
2026-09-15 17:19:21 +02:00
parent 1d6f43f9ce
commit bdaeed056c
34 changed files with 1448 additions and 93 deletions

View File

@@ -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)");
};
}
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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) {}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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";

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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;