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

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -7,7 +7,9 @@
"Sprint", "Sprint",
"Stand To Sit", "Stand To Sit",
"T-Pose", "T-Pose",
"Walking" "Walking",
"Jumping",
"Sword And Shield Slash"
], ],
"actionMap": { "actionMap": {
"DEFAULT": "Idle", "DEFAULT": "Idle",
@@ -17,7 +19,10 @@
"SIT_DOWN": "Stand To Sit", "SIT_DOWN": "Stand To Sit",
"SITTING": "Sitting Idle", "SITTING": "Sitting Idle",
"SIT_UP": "Sit To Stand", "SIT_UP": "Sit To Stand",
"IDLE": "Idle" "IDLE": "Idle",
"JUMP": "Jumping",
"SWORD_ATTACK_1": "Sword And Shield Slash",
"SWORD_ATTACK_2": "Sword And Shield Slash"
}, },
"previewModelPath": "Characters/Models/hero.j3o", "previewModelPath": "Characters/Models/hero.j3o",
"animOffsets": { "animOffsets": {

View File

@@ -0,0 +1,30 @@
#Tue Sep 15 12:30:48 CEST 2026
attachedEmitters.count=0
attachedLights.count=0
attachedSounds.count=0
castShadow=true
category=
cullDistance=120.0
footstepSurface=
interactableOffsetX=0.0
interactableOffsetY=0.5
interactableOffsetZ=0.0
interactableRotY=0.0
interactableType=NONE
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
modelCategory=NONE
name=rostiges_kurzschwert
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=1.0
scaleY=1.0
scaleZ=1.0
solid=true
tags=
uniformScale=true

View File

@@ -0,0 +1,30 @@
#Tue Sep 15 12:31:37 CEST 2026
attachedEmitters.count=0
attachedLights.count=0
attachedSounds.count=0
castShadow=true
category=
cullDistance=120.0
footstepSurface=
interactableOffsetX=0.0
interactableOffsetY=0.5
interactableOffsetZ=0.0
interactableRotY=0.0
interactableType=NONE
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
modelCategory=NONE
name=rostiges_kurzschwert
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=1.0
scaleY=1.0
scaleZ=1.0
solid=false
tags=
uniformScale=true

View File

@@ -0,0 +1,25 @@
{
"itemId": "rostiges_kurzschwert",
"category": "WEAPON",
"subCategory": "SWORD",
"name": {
"id": "rostiges_kurzschwert.name"
},
"description": {
"id": "rostiges_kurzschwert.description"
},
"worthGold": 0,
"damage": 0,
"grip": {
"posX": 0.24,
"posY": 0.05,
"posZ": 0.0,
"rotX": 0.0,
"rotY": -90.0,
"rotZ": 0.0
},
"modelRef": {
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"
},
"consumable": false
}

View File

@@ -23,6 +23,12 @@ public class SaveGame {
public float x, y, z; public float x, y, z;
/** itemId → Anzahl */ /** itemId → Anzahl */
public Map<String, Integer> inventory = new LinkedHashMap<>(); public Map<String, Integer> inventory = new LinkedHashMap<>();
/** Ausgerüstete Nahkampfwaffe (itemId, nullable). */
public String equippedMeleeWeaponId;
/** Ausgerüstete Fernkampfwaffe (itemId, nullable). */
public String equippedRangedWeaponId;
/** Ausgerüsteter Schild (itemId, nullable). */
public String equippedShieldId;
} }
public static class WorldSave { public static class WorldSave {

View File

@@ -15,6 +15,8 @@ public class Item implements Interactable {
private TextReference name; private TextReference name;
private TextReference description; private TextReference description;
private int worthGold; private int worthGold;
private int damage;
private ItemGrip grip;
private ObjectReference modelRef; private ObjectReference modelRef;
private boolean consumable; private boolean consumable;
private List<ConsumableEffect> effects; private List<ConsumableEffect> effects;

View File

@@ -8,6 +8,9 @@ import static de.blight.common.model.ItemCategory.QUEST_ITEMS;
import static de.blight.common.model.ItemCategory.WEAPON; import static de.blight.common.model.ItemCategory.WEAPON;
import static de.blight.common.model.ItemSubCategory.ARMOR; import static de.blight.common.model.ItemSubCategory.ARMOR;
import static de.blight.common.model.ItemSubCategory.AXE; import static de.blight.common.model.ItemSubCategory.AXE;
import static de.blight.common.model.ItemSubCategory.BOW;
import static de.blight.common.model.ItemSubCategory.CROSSBOW;
import static de.blight.common.model.ItemSubCategory.MAGIC_CRYSTAL;
import static de.blight.common.model.ItemSubCategory.FOOD; import static de.blight.common.model.ItemSubCategory.FOOD;
import static de.blight.common.model.ItemSubCategory.HALBERD; import static de.blight.common.model.ItemSubCategory.HALBERD;
import static de.blight.common.model.ItemSubCategory.HELM; import static de.blight.common.model.ItemSubCategory.HELM;
@@ -36,7 +39,7 @@ public class ItemCategoryManager {
private static final Map<ItemCategory, List<ItemSubCategory>> mapping = new EnumMap<ItemCategory, List<ItemSubCategory>>(ItemCategory.class); private static final Map<ItemCategory, List<ItemSubCategory>> mapping = new EnumMap<ItemCategory, List<ItemSubCategory>>(ItemCategory.class);
static { static {
mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE)); mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, BOW, CROSSBOW, MAGIC_CRYSTAL));
mapping.put(GEAR, List.of(RING, NECKLACE, ARMOR, HELM, SHIELD)); mapping.put(GEAR, List.of(RING, NECKLACE, ARMOR, HELM, SHIELD));
mapping.put(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM)); mapping.put(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM));
mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM)); mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM));

View File

@@ -0,0 +1,12 @@
package de.blight.common.model;
import lombok.Getter;
import lombok.Setter;
/** Position und Ausrichtung des Griffes einer Waffe (in Metern / Grad). */
@Getter
@Setter
public class ItemGrip {
private float posX, posY, posZ;
private float rotX, rotY, rotZ;
}

View File

@@ -2,7 +2,7 @@ package de.blight.common.model;
public enum ItemSubCategory { public enum ItemSubCategory {
SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, //WEAPONS SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, BOW, CROSSBOW, MAGIC_CRYSTAL, //WEAPONS
RING, NECKLACE, ARMOR, HELM, SHIELD, //GEAR RING, NECKLACE, ARMOR, HELM, SHIELD, //GEAR
POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM, POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM,
QUEST_ITEM, QUEST_ITEM,

View File

@@ -44,6 +44,13 @@ public class MainCharacter extends GameCharacter {
private de.blight.common.model.abilities.Abilities abilities; private de.blight.common.model.abilities.Abilities abilities;
/** Aktuell ausgerüstete Nahkampfwaffe (SWORD, AXE oder TWO_HANDED_SWORD). */
private Item equippedMeleeWeapon;
/** Aktuell ausgerüstete Fernkampfwaffe (BOW, CROSSBOW oder MAGIC_CRYSTAL). */
private Item equippedRangedWeapon;
/** Schild-Slot – nur gültig wenn equippedMeleeWeapon.subCategory == SWORD. */
private Item equippedShield;
/** Gespielte Monolog-IDs – wird serialisiert, damit jeder Monolog nur einmal abgespielt wird. */ /** Gespielte Monolog-IDs – wird serialisiert, damit jeder Monolog nur einmal abgespielt wird. */
private Set<String> playedMonologueIds = new HashSet<>(); private Set<String> playedMonologueIds = new HashSet<>();
@@ -73,6 +80,27 @@ public class MainCharacter extends GameCharacter {
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private transient Queue<String> pendingDialogNpcIds = new ArrayDeque<>(); private transient Queue<String> pendingDialogNpcIds = new ArrayDeque<>();
public void equipMelee(Item item) {
equippedMeleeWeapon = item;
if (item == null || item.getSubCategory() != ItemSubCategory.SWORD) {
equippedShield = null;
}
}
public void equipRanged(Item item) {
equippedRangedWeapon = item;
}
public void equipShield(Item item) {
if (equippedMeleeWeapon != null && equippedMeleeWeapon.getSubCategory() == ItemSubCategory.SWORD) {
equippedShield = item;
}
}
public void unequipMelee() { equipMelee(null); }
public void unequipRanged() { equippedRangedWeapon = null; }
public void unequipShield() { equippedShield = null; }
public void handleDialogOption(DialogOption option) { public void handleDialogOption(DialogOption option) {
if (option.getRequiredItem() != null) { if (option.getRequiredItem() != null) {
getInventar().remove(option.getRequiredItem()); getInventar().remove(option.getRequiredItem());

View File

@@ -194,6 +194,10 @@ public class EditorApp extends Application {
colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; }); colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; });
terrainController.setGrassController(grassController); terrainController.setGrassController(grassController);
contentController = new de.blight.editor.toolset.content.ContentController(toolCtx); contentController = new de.blight.editor.toolset.content.ContentController(toolCtx);
contentController.setOnEnterContentMode(() -> {
if (assetController.isOverlayOpen()) assetController.closeOverlay();
assetController.hideTabBtn();
});
tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx); tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx);
gameController = new de.blight.editor.toolset.game.GameController(toolCtx); gameController = new de.blight.editor.toolset.game.GameController(toolCtx);
worldPanelController = new de.blight.editor.toolset.world.WorldPanelController(toolCtx, this::scheduleAutoSave); worldPanelController = new de.blight.editor.toolset.world.WorldPanelController(toolCtx, this::scheduleAutoSave);

View File

@@ -1167,6 +1167,18 @@ public class SharedInput {
/** Gesetzt vom JME-Thread nach Raycast-Klick, damit JFX-Spinner aktualisiert werden. */ /** Gesetzt vom JME-Thread nach Raycast-Klick, damit JFX-Spinner aktualisiert werden. */
public volatile boolean modelInteractablePosSetFromJme = false; public volatile boolean modelInteractablePosSetFromJme = false;
// ── Griff-Editor (Waffen-Attachment-Point) ────────────────────────────────
/** true → ModelEditorState zeigt den Grip-Gizmo statt des Interactable-Pfeils. */
public volatile boolean gripEditorActive = false;
public volatile float gripEditorPosX = 0f;
public volatile float gripEditorPosY = 0f;
public volatile float gripEditorPosZ = 0f;
public volatile float gripEditorRotX = 0f;
public volatile float gripEditorRotY = 0f;
public volatile float gripEditorRotZ = 0f;
/** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */
public volatile boolean gripEditorGizmoDirty = false;
/** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */ /** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */
public record SculptEdit(float screenX, float screenY, int action) {} public record SculptEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<SculptEdit> sculptEditQueue = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<SculptEdit> sculptEditQueue = new ConcurrentLinkedQueue<>();

View File

@@ -99,6 +99,11 @@ public class ModelEditorState extends BaseAppState {
private Node interactableArrowNode = null; private Node interactableArrowNode = null;
private float interactableOffsetX = 0f; private float interactableOffsetX = 0f;
private float interactableOffsetY = 0.5f; private float interactableOffsetY = 0.5f;
/** Grip-Gizmo: 15cm-Pfeil für Waffen-Attachment-Point. */
private Node gripGizmoNode = null;
private float gripPosX = 0f, gripPosY = 0f, gripPosZ = 0f;
private float gripRotX = 0f, gripRotY = 0f, gripRotZ = 0f;
private float interactableOffsetZ = 0f; private float interactableOffsetZ = 0f;
private float interactableRotY = 0f; private float interactableRotY = 0f;
@@ -232,6 +237,18 @@ public class ModelEditorState extends BaseAppState {
loadModel(openPath); loadModel(openPath);
} }
// Grip-Gizmo aktualisieren
if (input.gripEditorActive && input.gripEditorGizmoDirty) {
input.gripEditorGizmoDirty = false;
gripPosX = input.gripEditorPosX;
gripPosY = input.gripEditorPosY;
gripPosZ = input.gripEditorPosZ;
gripRotX = input.gripEditorRotX;
gripRotY = input.gripEditorRotY;
gripRotZ = input.gripEditorRotZ;
rebuildGripGizmo();
}
int removeLod = input.modelEditorRemoveEmbeddedLod; int removeLod = input.modelEditorRemoveEmbeddedLod;
if (removeLod > 0) { if (removeLod > 0) {
input.modelEditorRemoveEmbeddedLod = -1; input.modelEditorRemoveEmbeddedLod = -1;
@@ -705,6 +722,17 @@ public class ModelEditorState extends BaseAppState {
interactableOffsetZ = input.modelInteractableOffsetZ; interactableOffsetZ = input.modelInteractableOffsetZ;
interactableRotY = input.modelInteractableRotY; interactableRotY = input.modelInteractableRotY;
gripGizmoNode = new Node("gripGizmo");
gripGizmoNode.setCullHint(Spatial.CullHint.Always);
previewRoot.attachChild(gripGizmoNode);
gripPosX = input.gripEditorPosX;
gripPosY = input.gripEditorPosY;
gripPosZ = input.gripEditorPosZ;
gripRotX = input.gripEditorRotX;
gripRotY = input.gripEditorRotY;
gripRotZ = input.gripEditorRotZ;
orbitYaw = 30f; orbitYaw = 30f;
orbitPitch = 25f; orbitPitch = 25f;
} }
@@ -726,6 +754,8 @@ public class ModelEditorState extends BaseAppState {
embeddedLodSpatials = null; embeddedLodSpatials = null;
originalSpatial = null; originalSpatial = null;
interactableArrowNode = null; interactableArrowNode = null;
gripGizmoNode = null;
input.gripEditorActive = false;
camLight = null; camLight = null;
mainLight = null; mainLight = null;
treeLightMaterials.clear(); treeLightMaterials.clear();
@@ -1490,6 +1520,70 @@ public class ModelEditorState extends BaseAppState {
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
} }
/** Zeichnet / aktualisiert den 15cm-Grip-Gizmo (orangefarbener Pfeil). */
public void rebuildGripGizmo() {
if (gripGizmoNode == null) return;
gripGizmoNode.detachAllChildren();
float shaftLen = 0.12f;
float shaftRad = 0.008f;
float headRad = 0.022f;
float headLen = 0.03f; // 15cm gesamt: 12cm Schaft + 3cm Kopf
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(1f, 0.55f, 0f, 1f));
Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
matHead.setColor("Color", new ColorRGBA(1f, 0.3f, 0f, 1f));
Node group = new Node("gripGroup");
group.setLocalTranslation(gripPosX, gripPosY, gripPosZ);
Quaternion rot = new Quaternion();
rot.fromAngles(
com.jme3.math.FastMath.DEG_TO_RAD * gripRotX,
com.jme3.math.FastMath.DEG_TO_RAD * gripRotY,
com.jme3.math.FastMath.DEG_TO_RAD * gripRotZ);
group.setLocalRotation(rot);
// Kleiner Würfel am Griffpunkt
Geometry marker = new Geometry("gMarker", new Box(0.015f, 0.015f, 0.015f));
marker.setMaterial(mat);
// Schaft entlang +Z
Geometry shaft = new Geometry("gShaft", new Cylinder(4, 8, shaftRad, shaftLen, true));
shaft.setMaterial(mat);
shaft.setLocalTranslation(0f, 0f, shaftLen * 0.5f);
// Kegelspitze: Dome zeigt per Default nach +Y → 90° um X → zeigt nach +Z
Geometry head = new Geometry("gHead", new Dome(Vector3f.ZERO, 2, 8, headRad, false));
head.setMaterial(matHead);
head.setLocalTranslation(0f, 0f, shaftLen);
Quaternion headRot = new Quaternion();
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
head.setLocalRotation(headRot);
// Klingenflosse: dünnes flaches Rechteck senkrecht zum Schaft entlang +Y,
// damit Rotation um Z (Roll) sichtbar ist. Breite 6cm, Dicke 2mm, entlang Schaft.
Material matFin = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
matFin.setColor("Color", new ColorRGBA(1f, 0.8f, 0.1f, 1f));
Geometry fin = new Geometry("gFin", new Box(0.001f, 0.03f, shaftLen * 0.5f));
fin.setMaterial(matFin);
fin.setLocalTranslation(0f, 0.03f, shaftLen * 0.5f);
group.attachChild(marker);
group.attachChild(shaft);
group.attachChild(fin);
group.attachChild(head);
gripGizmoNode.attachChild(group);
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
}
/** Blendet den Grip-Gizmo ein oder aus. */
public void setGripGizmoVisible(boolean visible) {
if (gripGizmoNode != null)
gripGizmoNode.setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
/** Setzt den Pfeil sichtbar/unsichtbar. */ /** Setzt den Pfeil sichtbar/unsichtbar. */
public void setInteractableArrowVisible(boolean visible) { public void setInteractableArrowVisible(boolean visible) {
if (interactableArrowNode != null) if (interactableArrowNode != null)

View File

@@ -84,12 +84,18 @@ public class ContentController {
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
private Runnable onEnterContentMode;
private GripEditorController gripEditorController;
public ContentController(ToolContext ctx) { public ContentController(ToolContext ctx) {
this.ctx = ctx; this.ctx = ctx;
this.gripEditorController = new GripEditorController(ctx);
} }
// ── Öffentliche API ──────────────────────────────────────────────────────── // ── Öffentliche API ────────────────────────────────────────────────────────
public void setOnEnterContentMode(Runnable r) { this.onEnterContentMode = r; }
public void setDualWindowMenuItem(CheckMenuItem item) { public void setDualWindowMenuItem(CheckMenuItem item) {
this.dualWindowMenuItem = item; this.dualWindowMenuItem = item;
} }
@@ -206,6 +212,7 @@ public class ContentController {
/** 1-Fenster-Content-Modus aktivieren. */ /** 1-Fenster-Content-Modus aktivieren. */
public void switchToContentEditor() { public void switchToContentEditor() {
if (onEnterContentMode != null) onEnterContentMode.run();
ctx.setCurrentTool().accept("content"); ctx.setCurrentTool().accept("content");
if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(true); if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(true);
ctx.setMainToolbar().accept(buildSingleWindowContentToolBar()); ctx.setMainToolbar().accept(buildSingleWindowContentToolBar());
@@ -328,6 +335,7 @@ public class ContentController {
if (contentItemBtn != null) contentItemBtn.setSelected(true); if (contentItemBtn != null) contentItemBtn.setSelected(true);
de.blight.editor.ui.ItemEditorView view = de.blight.editor.ui.ItemEditorView view =
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
wireItemView(view);
setContentCenter(view); setContentCenter(view);
setContentRight(null); setContentRight(null);
if (selectItemId != null) view.selectItem(selectItemId); if (selectItemId != null) view.selectItem(selectItemId);
@@ -338,6 +346,7 @@ public class ContentController {
ctx.setMainToolbar().accept(tb); ctx.setMainToolbar().accept(tb);
de.blight.editor.ui.ItemEditorView view = de.blight.editor.ui.ItemEditorView view =
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
wireItemView(view);
ctx.setCenterView().accept(view); ctx.setCenterView().accept(view);
ctx.setRightPanel().accept(null); ctx.setRightPanel().accept(null);
if (selectItemId != null) view.selectItem(selectItemId); if (selectItemId != null) view.selectItem(selectItemId);
@@ -949,9 +958,26 @@ public class ContentController {
if (contentFractionNode != null) contentFractionNode.setExpanded(true); if (contentFractionNode != null) contentFractionNode.setExpanded(true);
} }
private void wireItemView(de.blight.editor.ui.ItemEditorView view) {
view.setOnIdChanged(this::refreshContentItemTree);
view.setGripEditorLauncher((modelPath, currentGrip, onSave) -> {
String itemId = view.getCurrentItemId();
gripEditorController.setOnReturn(() -> {
if (dualWindowMode) {
buildItemViewInContent();
if (activeItemView != null) activeItemView.selectItem(itemId);
} else {
switchToItemEditor(itemId);
}
});
gripEditorController.open(modelPath, currentGrip, onSave);
});
}
private void buildItemViewInContent() { private void buildItemViewInContent() {
collapseAllContentSections(); collapseAllContentSections();
activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
wireItemView(activeItemView);
setContentCenter(activeItemView); setContentCenter(activeItemView);
setContentRight(null); setContentRight(null);
setContentBottom(buildItemLegend()); setContentBottom(buildItemLegend());

View File

@@ -0,0 +1,220 @@
package de.blight.editor.toolset.content;
import de.blight.common.model.ItemGrip;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import de.blight.editor.state.ModelEditorState;
import de.blight.editor.ui.ItemEditorView;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import java.util.function.Consumer;
/**
* Öffnet den Modell-Editor-Viewport mit einem 15cm-Griff-Gizmo (orangefarbener Pfeil).
* Position und Rotation des Griffes werden über 6 Spinner eingestellt.
*/
public class GripEditorController implements ItemEditorView.GripEditorLauncher {
private final ToolContext ctx;
private Spinner<Double> posXSpin, posYSpin, posZSpin;
private Spinner<Double> rotXSpin, rotYSpin, rotZSpin;
private Consumer<ItemGrip> pendingOnSave;
private Runnable onReturn;
private boolean suppressSpinnerEvents = false;
public void setOnReturn(Runnable r) { this.onReturn = r; }
public GripEditorController(ToolContext ctx) {
this.ctx = ctx;
}
// ── GripEditorLauncher ────────────────────────────────────────────────────
@Override
public void open(String modelPath, ItemGrip currentGrip, Consumer<ItemGrip> onSave) {
pendingOnSave = onSave;
SharedInput input = ctx.input();
input.gripEditorActive = true;
// Grip-Werte aus Item übernehmen (oder Defaults)
ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip();
input.gripEditorPosX = g.getPosX();
input.gripEditorPosY = g.getPosY();
input.gripEditorPosZ = g.getPosZ();
input.gripEditorRotX = g.getRotX();
input.gripEditorRotY = g.getRotY();
input.gripEditorRotZ = g.getRotZ();
input.gripEditorGizmoDirty = true;
// Waffenmodell im Modell-Editor laden
if (modelPath != null && !modelPath.isBlank()) {
input.modelEditorOpenPath = modelPath;
}
input.activeLayer = SharedInput.LAYER_MODEL_EDITOR;
// Hauptfenster: JME-Viewport + Grip-Panel
ctx.setCenterView().accept(ctx.viewportSupplier().get());
ctx.setCurrentTool().accept("gripEditor");
ctx.setMainToolbar().accept(buildToolbar());
ctx.setRightPanel().accept(buildPanel(g));
ctx.setLeftPanel().accept(null);
ctx.setBottomPanel().accept(null);
}
// ── Toolbar ───────────────────────────────────────────────────────────────
private ToolBar buildToolbar() {
Button cancelBtn = new Button("← Zurück");
cancelBtn.setOnAction(e -> onCancel());
Label title = new Label("Griff-Editor");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
ToolBar tb = new ToolBar();
tb.getItems().addAll(cancelBtn, new Separator(Orientation.VERTICAL), title);
return tb;
}
// ── Panel ─────────────────────────────────────────────────────────────────
private VBox buildPanel(ItemGrip g) {
posXSpin = doubleSpin(g.getPosX(), -2.0, 2.0, 0.01, "m");
posYSpin = doubleSpin(g.getPosY(), -2.0, 2.0, 0.01, "m");
posZSpin = doubleSpin(g.getPosZ(), -2.0, 2.0, 0.01, "m");
rotXSpin = doubleSpin(g.getRotX(), -180.0, 180.0, 1.0, "°");
rotYSpin = doubleSpin(g.getRotY(), -180.0, 180.0, 1.0, "°");
rotZSpin = doubleSpin(g.getRotZ(), -180.0, 180.0, 1.0, "°");
posXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
posYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
posZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
rotZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged());
Button saveBtn = new Button("Speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-background-color: #2a5a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand; -fx-font-weight: bold;");
saveBtn.setOnAction(e -> onSave());
Button cancelBtn = new Button("Abbrechen");
cancelBtn.setMaxWidth(Double.MAX_VALUE);
cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;");
cancelBtn.setOnAction(e -> onCancel());
VBox panel = new VBox(8);
panel.setPadding(new Insets(12));
panel.setStyle("-fx-background-color: #252535;");
panel.setPrefWidth(220);
panel.getChildren().addAll(
styledTitle("Position (m)"),
row("X:", posXSpin),
row("Y:", posYSpin),
row("Z:", posZSpin),
new Separator(),
styledTitle("Rotation (°)"),
row("X:", rotXSpin),
row("Y:", rotYSpin),
row("Z:", rotZSpin),
new Separator(),
styledHint("Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."),
new Separator(),
saveBtn,
cancelBtn
);
return panel;
}
// ── Spinner-Events ────────────────────────────────────────────────────────
private void onSpinnerChanged() {
if (suppressSpinnerEvents) return;
SharedInput input = ctx.input();
input.gripEditorPosX = posXSpin.getValue().floatValue();
input.gripEditorPosY = posYSpin.getValue().floatValue();
input.gripEditorPosZ = posZSpin.getValue().floatValue();
input.gripEditorRotX = rotXSpin.getValue().floatValue();
input.gripEditorRotY = rotYSpin.getValue().floatValue();
input.gripEditorRotZ = rotZSpin.getValue().floatValue();
input.gripEditorGizmoDirty = true;
}
// ── Save / Cancel ─────────────────────────────────────────────────────────
private void onSave() {
ItemGrip grip = new ItemGrip();
grip.setPosX(posXSpin.getValue().floatValue());
grip.setPosY(posYSpin.getValue().floatValue());
grip.setPosZ(posZSpin.getValue().floatValue());
grip.setRotX(rotXSpin.getValue().floatValue());
grip.setRotY(rotYSpin.getValue().floatValue());
grip.setRotZ(rotZSpin.getValue().floatValue());
ctx.input().gripEditorActive = false;
Consumer<ItemGrip> save = pendingOnSave;
Runnable ret = onReturn;
pendingOnSave = null;
onReturn = null;
if (save != null) save.accept(grip);
if (ret != null) ret.run(); else ctx.switchToWorld().run();
}
private void onCancel() {
ctx.input().gripEditorActive = false;
Runnable ret = onReturn;
pendingOnSave = null;
onReturn = null;
if (ret != null) ret.run(); else ctx.switchToWorld().run();
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static Spinner<Double> doubleSpin(float initial, double min, double max, double step, String unit) {
SpinnerValueFactory.DoubleSpinnerValueFactory vf =
new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initial, step);
vf.setConverter(new javafx.util.StringConverter<>() {
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.2f", v) + " " + unit; }
@Override public Double fromString(String s) {
try { return Double.parseDouble(s.replace(unit, "").replace(",", ".").trim()); }
catch (NumberFormatException e) { return vf.getValue(); }
}
});
Spinner<Double> sp = new Spinner<>(vf);
sp.setEditable(true);
sp.setMaxWidth(Double.MAX_VALUE);
return sp;
}
private static Label styledTitle(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #88aacc;");
return l;
}
private static Label styledHint(String text) {
Label l = new Label(text);
l.setWrapText(true);
l.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
return l;
}
private static HBox row(String labelText, Spinner<Double> spinner) {
Label lbl = new Label(labelText);
lbl.setMinWidth(20);
lbl.setStyle("-fx-text-fill: #aaa;");
HBox.setHgrow(spinner, Priority.ALWAYS);
HBox box = new HBox(6, lbl, spinner);
box.setAlignment(Pos.CENTER_LEFT);
return box;
}
}

View File

@@ -4,6 +4,7 @@ import de.blight.common.model.CharacterStat;
import de.blight.common.model.ConsumableEffect; import de.blight.common.model.ConsumableEffect;
import de.blight.common.model.Item; import de.blight.common.model.Item;
import de.blight.common.model.ItemCategory; import de.blight.common.model.ItemCategory;
import de.blight.common.model.ItemGrip;
import de.blight.common.model.ItemCategoryManager; import de.blight.common.model.ItemCategoryManager;
import de.blight.common.model.ItemIO; import de.blight.common.model.ItemIO;
import de.blight.common.model.ItemSubCategory; import de.blight.common.model.ItemSubCategory;
@@ -42,14 +43,23 @@ public class ItemEditorView extends BorderPane {
private TextField idField; private TextField idField;
private ComboBox<ItemCategory> catCombo; private ComboBox<ItemCategory> catCombo;
private ComboBox<ItemSubCategory> subCatCombo; private ComboBox<ItemSubCategory> subCatCombo;
private TextField nameField; private Label nameLabel;
private TextField descField; private Label descLabel;
private Spinner<Integer> goldSpinner; private Spinner<Integer> goldSpinner;
private TextField modelRefField; private Spinner<Integer> damageSpinner;
private VBox formContainer; private VBox weaponSection;
private CheckBox consumableCheck; private TextField modelRefField;
private VBox consumablesSection; private VBox formContainer;
private VBox effectsRows; private CheckBox consumableCheck;
private VBox consumablesSection;
private VBox effectsRows;
public interface GripEditorLauncher {
void open(String modelPath, ItemGrip currentGrip, java.util.function.Consumer<ItemGrip> onSave);
}
private Runnable onIdChanged;
private GripEditorLauncher gripEditorLauncher;
private final PauseTransition savePause = new PauseTransition(Duration.millis(600)); private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
@@ -59,7 +69,7 @@ public class ItemEditorView extends BorderPane {
public ItemEditorView(Path itemDir) { public ItemEditorView(Path itemDir) {
this.itemDir = itemDir; this.itemDir = itemDir;
this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot this.assetRoot = itemDir.getParent();
setStyle("-fx-background-color: #1e1e2e;"); setStyle("-fx-background-color: #1e1e2e;");
reload(); reload();
@@ -77,6 +87,10 @@ public class ItemEditorView extends BorderPane {
// ── Öffentliche API ─────────────────────────────────────────────────────── // ── Öffentliche API ───────────────────────────────────────────────────────
public void setOnIdChanged(Runnable r) { this.onIdChanged = r; }
public void setGripEditorLauncher(GripEditorLauncher l) { this.gripEditorLauncher = l; }
public String getCurrentItemId() { return current != null ? current.getItemId() : null; }
public void selectItem(String itemId) { public void selectItem(String itemId) {
if (itemId == null) return; if (itemId == null) return;
items.stream() items.stream()
@@ -129,21 +143,6 @@ public class ItemEditorView extends BorderPane {
}; };
} }
// ── Legend ────────────────────────────────────────────────────────────────
private HBox buildLegend() {
HBox box = new HBox(8);
box.setAlignment(Pos.CENTER_LEFT);
for (ItemCategory cat : ItemCategory.values()) {
Label dot = new Label("■");
dot.setStyle("-fx-text-fill: " + categoryColor(cat) + "; -fx-font-size: 10;");
Label lbl = new Label(cat.name());
lbl.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
box.getChildren().addAll(dot, lbl);
}
return box;
}
// ── Form construction ───────────────────────────────────────────────────── // ── Form construction ─────────────────────────────────────────────────────
private VBox buildForm() { private VBox buildForm() {
@@ -196,7 +195,6 @@ public class ItemEditorView extends BorderPane {
} }
}); });
// Wenn Kategorie wechselt: SubKategorie-Liste aktualisieren
catCombo.valueProperty().addListener((obs, oldCat, newCat) -> { catCombo.valueProperty().addListener((obs, oldCat, newCat) -> {
ItemSubCategory prevSub = subCatCombo.getValue(); ItemSubCategory prevSub = subCatCombo.getValue();
subCatCombo.getItems().setAll( subCatCombo.getItems().setAll(
@@ -207,21 +205,47 @@ public class ItemEditorView extends BorderPane {
} else { } else {
subCatCombo.setValue(null); subCatCombo.setValue(null);
} }
updateWeaponSectionVisibility();
scheduleSave();
});
subCatCombo.valueProperty().addListener((obs, o, n) -> {
updateWeaponSectionVisibility();
scheduleSave(); scheduleSave();
}); });
subCatCombo.valueProperty().addListener((obs, o, n) -> scheduleSave());
nameField = new TextField(); // Name und Beschreibung: read-only (auto-generierte TextReference-Schlüssel)
nameField.setPromptText("TextReference-Schlüssel"); nameLabel = new Label();
nameField.textProperty().addListener((obs, o, n) -> scheduleSave()); nameLabel.setStyle("-fx-text-fill: #777; -fx-font-style: italic;");
descField = new TextField(); nameLabel.setMaxWidth(Double.MAX_VALUE);
descField.setPromptText("TextReference-Schlüssel"); descLabel = new Label();
descField.textProperty().addListener((obs, o, n) -> scheduleSave()); descLabel.setStyle("-fx-text-fill: #777; -fx-font-style: italic;");
descLabel.setMaxWidth(Double.MAX_VALUE);
goldSpinner = new Spinner<>(0, 999999, 0); goldSpinner = new Spinner<>(0, 999999, 0);
goldSpinner.setEditable(true); goldSpinner.setEditable(true);
goldSpinner.setMaxWidth(Double.MAX_VALUE); goldSpinner.setMaxWidth(Double.MAX_VALUE);
goldSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave()); goldSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
// ── Waffen-Sektion ────────────────────────────────────────────────────
damageSpinner = new Spinner<>(0, 99999, 0);
damageSpinner.setEditable(true);
damageSpinner.setMaxWidth(Double.MAX_VALUE);
damageSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
Button gripBtn = new Button("Griff definieren...");
gripBtn.setMaxWidth(Double.MAX_VALUE);
gripBtn.setStyle("-fx-background-color: #3a3a5a; -fx-text-fill: #ddddff; -fx-cursor: hand;");
gripBtn.setOnAction(e -> openGripEditor());
weaponSection = new VBox(6,
new Separator(),
sectionTitle("Kampfwerte"),
row("Schaden:", damageSpinner),
gripBtn
);
weaponSection.setVisible(false);
weaponSection.setManaged(false);
modelRefField = new TextField(); modelRefField = new TextField();
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)"); modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
modelRefField.setEditable(false); modelRefField.setEditable(false);
@@ -231,10 +255,6 @@ public class ItemEditorView extends BorderPane {
pickModelBtn.setTooltip(new Tooltip("Modell auswählen")); pickModelBtn.setTooltip(new Tooltip("Modell auswählen"));
pickModelBtn.setOnAction(e -> pickModel()); pickModelBtn.setOnAction(e -> pickModel());
HBox modelRow = new HBox(4, modelRefField, pickModelBtn);
HBox.setHgrow(modelRefField, Priority.ALWAYS);
modelRow.setAlignment(Pos.CENTER_LEFT);
Button clearModelBtn = new Button("✕"); Button clearModelBtn = new Button("✕");
clearModelBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;"); clearModelBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
clearModelBtn.setTooltip(new Tooltip("Modell-Referenz entfernen")); clearModelBtn.setTooltip(new Tooltip("Modell-Referenz entfernen"));
@@ -273,12 +293,13 @@ public class ItemEditorView extends BorderPane {
row("SubKategorie:", subCatCombo), row("SubKategorie:", subCatCombo),
new Separator(), new Separator(),
sectionTitle("Texte"), sectionTitle("Texte"),
row("Name:", nameField), row("Name:", nameLabel),
row("Beschreibung:", descField), row("Beschreibung:", descLabel),
new Separator(), new Separator(),
sectionTitle("Werte"), sectionTitle("Werte"),
row("Wert (Gold):", goldSpinner), row("Wert (Gold):", goldSpinner),
row("Modell:", modelFullRow), row("Modell:", modelFullRow),
weaponSection,
new Separator(), new Separator(),
consumableCheck, consumableCheck,
consumablesSection consumablesSection
@@ -286,6 +307,24 @@ public class ItemEditorView extends BorderPane {
return form; return form;
} }
private void openGripEditor() {
if (current == null || gripEditorLauncher == null) return;
String modelPath = modelRefField.getText().trim();
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), grip -> {
current.setGrip(grip);
persistCurrent();
});
}
private void updateWeaponSectionVisibility() {
ItemCategory cat = catCombo.getValue();
ItemSubCategory sub = subCatCombo.getValue();
boolean isWeapon = cat == ItemCategory.WEAPON
&& sub != ItemSubCategory.SHIELD;
weaponSection.setVisible(isWeapon);
weaponSection.setManaged(isWeapon);
}
// ── Modell-Auswahl ──────────────────────────────────────────────────────── // ── Modell-Auswahl ────────────────────────────────────────────────────────
private void pickModel() { private void pickModel() {
@@ -296,8 +335,10 @@ public class ItemEditorView extends BorderPane {
private void onIdCommitted() { private void onIdCommitted() {
String id = idField.getText().trim(); String id = idField.getText().trim();
if (id.isBlank()) return; if (id.isBlank()) return;
if (nameField.getText().isBlank()) nameField.setText(id + ".name"); nameLabel.setText(id + ".name");
if (descField.getText().isBlank()) descField.setText(id + ".description"); descLabel.setText(id + ".description");
scheduleSave();
if (onIdChanged != null) onIdChanged.run();
} }
// ── Auto-save ───────────────────────────────────────────────────────────── // ── Auto-save ─────────────────────────────────────────────────────────────
@@ -338,11 +379,13 @@ public class ItemEditorView extends BorderPane {
private void loadFormFromItem(Item item) { private void loadFormFromItem(Item item) {
idField.setText(safe(item.getItemId())); idField.setText(safe(item.getItemId()));
catCombo.setValue(item.getCategory()); // löst Listener aus → subCatCombo befüllt catCombo.setValue(item.getCategory());
subCatCombo.setValue(item.getSubCategory()); subCatCombo.setValue(item.getSubCategory());
nameField.setText(item.getName() != null ? item.getName().id() : ""); String id = safe(item.getItemId());
descField.setText(item.getDescription() != null ? item.getDescription().id() : ""); nameLabel.setText(item.getName() != null ? item.getName().id() : (id.isBlank() ? "" : id + ".name"));
descLabel.setText(item.getDescription() != null ? item.getDescription().id() : (id.isBlank() ? "" : id + ".description"));
goldSpinner.getValueFactory().setValue(item.getWorthGold()); goldSpinner.getValueFactory().setValue(item.getWorthGold());
damageSpinner.getValueFactory().setValue(item.getDamage());
ObjectReference ref = item.getModelRef(); ObjectReference ref = item.getModelRef();
modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : ""); modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : "");
consumableCheck.setSelected(item.isConsumable()); consumableCheck.setSelected(item.isConsumable());
@@ -352,15 +395,19 @@ public class ItemEditorView extends BorderPane {
addEffectRow(effect.getStat(), effect.getValue()); addEffectRow(effect.getStat(), effect.getValue());
} }
} }
updateWeaponSectionVisibility();
} }
private void saveFormToItem(Item item) { private void saveFormToItem(Item item) {
item.setItemId(idField.getText().trim()); item.setItemId(idField.getText().trim());
item.setCategory(catCombo.getValue()); item.setCategory(catCombo.getValue());
item.setSubCategory(subCatCombo.getValue()); item.setSubCategory(subCatCombo.getValue());
item.setName(ref(nameField)); String nameKey = nameLabel.getText().trim();
item.setDescription(ref(descField)); String descKey = descLabel.getText().trim();
item.setName(nameKey.isBlank() ? null : new TextReference(nameKey));
item.setDescription(descKey.isBlank() ? null : new TextReference(descKey));
item.setWorthGold(goldSpinner.getValue()); item.setWorthGold(goldSpinner.getValue());
item.setDamage(damageSpinner.getValue());
String mr = modelRefField.getText().trim(); String mr = modelRefField.getText().trim();
item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr)); item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr));
item.setConsumable(consumableCheck.isSelected()); item.setConsumable(consumableCheck.isSelected());
@@ -378,14 +425,16 @@ public class ItemEditorView extends BorderPane {
private void clearForm() { private void clearForm() {
idField.clear(); idField.clear();
catCombo.setValue(null); // setzt subCatCombo via Listener zurück catCombo.setValue(null);
subCatCombo.setValue(null); subCatCombo.setValue(null);
nameField.clear(); nameLabel.setText("");
descField.clear(); descLabel.setText("");
goldSpinner.getValueFactory().setValue(0); goldSpinner.getValueFactory().setValue(0);
damageSpinner.getValueFactory().setValue(0);
modelRefField.clear(); modelRefField.clear();
consumableCheck.setSelected(false); consumableCheck.setSelected(false);
effectsRows.getChildren().clear(); effectsRows.getChildren().clear();
updateWeaponSectionVisibility();
} }
// ── List operations ─────────────────────────────────────────────────────── // ── List operations ───────────────────────────────────────────────────────
@@ -453,9 +502,4 @@ public class ItemEditorView extends BorderPane {
} }
private static String safe(String s) { return s != null ? s : ""; } private static String safe(String s) { return s != null ? s : ""; }
private static TextReference ref(TextField f) {
String s = f.getText().trim();
return s.isBlank() ? null : new TextReference(s);
}
} }

View File

@@ -31,7 +31,37 @@ public enum AnimationAction {
DISAPPOINT, DISAPPOINT,
SECRET, SECRET,
TALK_SITTING, 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. */ /** Lesbare Bezeichnung für UI-Anzeige, via TextRegistry aufgelöst. */
public String displayName() { public String displayName() {
@@ -61,7 +91,69 @@ public enum AnimationAction {
case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht"); case DISAPPOINT -> TextRegistry.resolve(null, key, "Enttäuscht");
case SECRET -> TextRegistry.resolve(null, key, "Geheimnis"); case SECRET -> TextRegistry.resolve(null, key, "Geheimnis");
case TALK_SITTING -> TextRegistry.resolve(null, key, "Reden (Sitzend)"); 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 inventory = KeyInput.KEY_I;
public int quicksave = KeyInput.KEY_F5; public int quicksave = KeyInput.KEY_F5;
public int character = KeyInput.KEY_C; 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. */ /** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
public static final String[][] ENTRIES = { public static final String[][] ENTRIES = {
@@ -34,8 +37,11 @@ public class KeyBindings {
{"turnRight", "Kamera rechts"}, {"turnRight", "Kamera rechts"},
{"inventory", "Inventar"}, {"inventory", "Inventar"},
{"quicksave", "Schnellspeichern"}, {"quicksave", "Schnellspeichern"},
{"character", "Charakter"}, {"character", "Charakter"},
{"quests", "Questlog"}, {"quests", "Questlog"},
{"drawMeleeWeapon", "Nahkampfwaffe ziehen"},
{"drawRangedWeapon", "Fernkampfwaffe ziehen"},
{"sheathWeapon", "Waffe wegstecken"},
}; };
public int get(String fieldName) { public int get(String fieldName) {

View File

@@ -3,8 +3,12 @@ package de.blight.game.control;
import com.jme3.anim.AnimComposer; import com.jme3.anim.AnimComposer;
import com.jme3.bullet.control.CharacterControl; import com.jme3.bullet.control.CharacterControl;
import com.jme3.input.InputManager; import com.jme3.input.InputManager;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger; 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.Quaternion;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
@@ -35,8 +39,16 @@ public class PlayerInputControl {
private static final float WALK_MULT = 0.5f; private static final float WALK_MULT = 0.5f;
private static final float ROTATE_SPEED = 10f; private static final float ROTATE_SPEED = 10f;
private static final String[] ACTION_NAMES = private static final String ACT_DRAW_MELEE = "DrawMeleeWeapon";
{"Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk"}; 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 InputManager inputManager;
private final Camera cam; private final Camera cam;
@@ -47,6 +59,17 @@ public class PlayerInputControl {
private boolean forward, backward, left, right, sprint, walk; private boolean forward, backward, left, right, sprint, walk;
private boolean paused = false; 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 AnimationLibrary animLib;
private String animSetName; private String animSetName;
private Path assetRoot; private Path assetRoot;
@@ -139,6 +162,11 @@ public class PlayerInputControl {
case "Sprint" -> sprint = isPressed; case "Sprint" -> sprint = isPressed;
case "Walk" -> walk = isPressed; case "Walk" -> walk = isPressed;
case "Jump" -> { if (isPressed && physicsChar != null) { physicsChar.jump(); jumpFrames = 12; } } 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 boolean isPaused() { return paused; }
public void setPaused(boolean paused) { public void setPaused(boolean paused) {
@@ -219,6 +252,9 @@ public class PlayerInputControl {
autopilotDir = null; autopilotDir = null;
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
stopWading(); stopWading();
rightMouseHeld = false;
combatAnimActive = false;
combatAnimCallback = null;
} }
} }
@@ -463,13 +499,18 @@ public class PlayerInputControl {
} }
private void registerMappings(KeyBindings kb) { private void registerMappings(KeyBindings kb) {
inputManager.addMapping("Forward", new KeyTrigger(kb.forward)); inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
inputManager.addMapping("Backward", new KeyTrigger(kb.backward)); inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
inputManager.addMapping("Left", new KeyTrigger(kb.left)); inputManager.addMapping("Left", new KeyTrigger(kb.left));
inputManager.addMapping("Right", new KeyTrigger(kb.right)); inputManager.addMapping("Right", new KeyTrigger(kb.right));
inputManager.addMapping("Jump", new KeyTrigger(kb.jump)); inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint)); inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
inputManager.addMapping("Walk", new KeyTrigger(kb.walk)); 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); inputManager.addListener(actionListener, ACTION_NAMES);
} }
@@ -598,9 +639,9 @@ public class PlayerInputControl {
if (moving) { if (moving) {
scratchMoveDir.normalizeLocal(); scratchMoveDir.normalizeLocal();
float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT float speed = (inDeepWater || walk) ? MOVE_SPEED * WALK_MULT
: sprint ? MOVE_SPEED * SPRINT_MULT : (sprint && !weaponDrawn) ? MOVE_SPEED * SPRINT_MULT
: MOVE_SPEED; : MOVE_SPEED;
physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed)); physicsChar.setWalkDirection(scratchWalk.set(scratchMoveDir).multLocal(speed));
if (visual != null) { if (visual != null) {
@@ -613,24 +654,44 @@ public class PlayerInputControl {
physicsChar.setWalkDirection(Vector3f.ZERO); 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 // Animations-Auswahl
if (jumpFrames > 0) jumpFrames--; if (jumpFrames > 0) jumpFrames--;
if (groundGraceFrames > 0) groundGraceFrames--; if (groundGraceFrames > 0) groundGraceFrames--;
AnimationAction target; if (!combatAnimActive) {
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) { AnimationAction target;
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP; if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
} else if (moving) { target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
target = (inDeepWater || walk) ? AnimationAction.WALK } else if (moving) {
: sprint ? AnimationAction.SPRINT target = (inDeepWater || walk) ? AnimationAction.WALK
: AnimationAction.RUN; : sprint ? AnimationAction.SPRINT
} else { : AnimationAction.RUN;
target = AnimationAction.IDLE; } else {
} target = AnimationAction.IDLE;
}
if (target != currentAnim) { target = resolveWeaponAction(target);
playAction(target);
currentAnim = target; if (weaponDrawn && rightMouseHeld && isMeleeType(drawnWeaponType)) {
target = getMeleeBlockAction(drawnWeaponType);
}
if (target != currentAnim) {
playAction(target);
currentAnim = target;
}
} }
pollFootsteps(tpf); 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, public void setFootstepSystem(de.blight.game.audio.FootstepSystem fs,
java.util.function.BiFunction<Float, Float, float[]> query) { java.util.function.BiFunction<Float, Float, float[]> query) {
this.footstepSystem = fs; 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(); MainCharacter mc = findMainCharacter();
if (mc != null) { 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 // SaveGameState mit Charakter und Positions-Lieferant verknüpfen
de.blight.game.state.SaveGameState saveState = de.blight.game.state.SaveGameState saveState =
app.getStateManager().getState(de.blight.game.state.SaveGameState.class); 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.AnalogListener;
import com.jme3.input.controls.KeyTrigger; import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger; import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.scene.Node; 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.MainCharacter;
import de.blight.common.model.XPHelper; import de.blight.common.model.XPHelper;
import de.blight.common.model.abilities.Abilities; import de.blight.common.model.abilities.Abilities;
@@ -46,9 +49,10 @@ public class CharacterState extends OverlayState {
// ── Input ───────────────────────────────────────────────────────────────── // ── Input ─────────────────────────────────────────────────────────────────
private static final String MAP_TOGGLE = "_CharToggle"; private static final String MAP_TOGGLE = "_CharToggle";
private static final String MAP_SCROLL_UP = "_CharScrollUp"; private static final String MAP_SCROLL_UP = "_CharScrollUp";
private static final String MAP_SCROLL_DOWN = "_CharScrollDown"; private static final String MAP_SCROLL_DOWN = "_CharScrollDown";
private static final String MAP_EQUIP_CLICK = "_CharEquipClick";
// ── Scroll-Zustand ──────────────────────────────────────────────────────── // ── Scroll-Zustand ────────────────────────────────────────────────────────
@@ -64,6 +68,11 @@ public class CharacterState extends OverlayState {
private final MainCharacter mc; 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) {} private record Skill(String name, int lvl, int maxLvl, String[][] abils) {}
public CharacterState(MainCharacter mc) { 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_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
app.getInputManager().addMapping(MAP_SCROLL_DOWN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true)); app.getInputManager().addMapping(MAP_SCROLL_DOWN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
app.getInputManager().addListener(scrollAnalog, MAP_SCROLL_UP, MAP_SCROLL_DOWN); 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 @Override
@@ -89,13 +100,18 @@ public class CharacterState extends OverlayState {
app.getInputManager().deleteMapping(MAP_TOGGLE); app.getInputManager().deleteMapping(MAP_TOGGLE);
app.getInputManager().deleteMapping(MAP_SCROLL_UP); app.getInputManager().deleteMapping(MAP_SCROLL_UP);
app.getInputManager().deleteMapping(MAP_SCROLL_DOWN); app.getInputManager().deleteMapping(MAP_SCROLL_DOWN);
app.getInputManager().deleteMapping(MAP_EQUIP_CLICK);
} }
@Override @Override
protected void buildContent(Node panel, float scale) { protected void buildContent(Node panel, float scale) {
meleeSlotBounds = null;
shieldSlotBounds = null;
rangedSlotBounds = null;
float divX = PX + PAD + LEFT_W + 6f; float divX = PX + PAD + LEFT_W + 6f;
addQuad(panel, divX, PY + PAD, 1f, PH - HDR_H - PAD * 2, UiTheme.COL_DIVIDER, -17f); 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); buildAbilities(panel);
} }
@@ -120,9 +136,22 @@ public class CharacterState extends OverlayState {
rebuildSkills(); 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 ─────────────────────────────────────────────────── // ── Linke Spalte: Werte ───────────────────────────────────────────────────
private void buildStats(Node panel) { private float buildStats(Node panel) {
float x = PX + PAD; float x = PX + PAD;
float y = PY + PH - HDR_H - 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, "HP", hpReg, x, y); y -= 16;
regenLine(panel, "Stamina", stReg, x, y); y -= 16; regenLine(panel, "Stamina", stReg, x, y); y -= 16;
regenLine(panel, "Mana", mnReg, x, y); regenLine(panel, "Mana", mnReg, x, y);
return y;
} }
private float statBar(Node parent, String label, int cur, int max, ColorRGBA col, 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; 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) { private void regenLine(Node parent, String label, int val, float x, float y) {
ColorRGBA col = val > 0 ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED; ColorRGBA col = val > 0 ? UiTheme.COL_UNLOCKED : UiTheme.COL_LOCKED;
BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_BODY, UiTheme.COL_MUTED); BitmapText lblTxt = crispText(label + ":", UiTheme.FONT_BODY, UiTheme.COL_MUTED);

View File

@@ -225,16 +225,43 @@ public class InventoryState extends OverlayState {
// ── Item-Grid ───────────────────────────────────────────────────────────── // ── 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() { private void buildGrid() {
gridNode = new Node("inv-grid"); gridNode = new Node("inv-grid");
panel.attachChild(gridNode); panel.attachChild(gridNode);
float savedContentTop = contentTop;
if (activeTab == ItemCategory.WEAPON) {
buildWeaponSlots(gridNode);
contentTop -= 2 * (WEAPON_SLOT_H + 4);
}
if (activeItems.isEmpty()) { if (activeItems.isEmpty()) {
BitmapText empty = crispText("Keine Items vorhanden", UiTheme.FONT_HEADING, UiTheme.COL_MUTED); BitmapText empty = crispText("Keine Items vorhanden", UiTheme.FONT_HEADING, UiTheme.COL_MUTED);
float ey = contentBottom + (contentTop - contentBottom) / 2f + 10; float ey = contentBottom + (contentTop - contentBottom) / 2f + 10;
float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f; float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f;
empty.setLocalTranslation(ex, ey, -17); empty.setLocalTranslation(ex, ey, -17);
gridNode.attachChild(empty); gridNode.attachChild(empty);
contentTop = savedContentTop;
return; return;
} }
@@ -278,6 +305,7 @@ public class InventoryState extends OverlayState {
if (col != 0) { virtY -= CELL_GAP; } if (col != 0) { virtY -= CELL_GAP; }
virtY -= CELL_GAP; virtY -= CELL_GAP;
} }
contentTop = savedContentTop;
} }
private void rebuildGrid() { private void rebuildGrid() {
@@ -297,7 +325,8 @@ public class InventoryState extends OverlayState {
int rows = (int) Math.ceil(g.size() / (double) COLS); int rows = (int) Math.ceil(g.size() / (double) COLS);
totalH += rows * (CELL_H + CELL_GAP); 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); return Math.max(0f, totalH - contentH);
} }
@@ -392,6 +421,9 @@ public class InventoryState extends OverlayState {
case STAFF -> "Stäbe"; case STAFF -> "Stäbe";
case HALBERD -> "Hellebarden"; case HALBERD -> "Hellebarden";
case AXE -> "Äxte"; case AXE -> "Äxte";
case BOW -> "Bögen";
case CROSSBOW -> "Armbrüste";
case MAGIC_CRYSTAL -> "Magiekristalle";
case RING -> "Ringe"; case RING -> "Ringe";
case NECKLACE -> "Halsketten"; case NECKLACE -> "Halsketten";
case ARMOR -> "Rüstungen"; 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 { try {
SaveGameIO.save(save); SaveGameIO.save(save);
} catch (IOException e) { } catch (IOException e) {

View File

@@ -267,6 +267,7 @@ public class WorldInteractableState extends BaseAppState {
// ── Annäherung ──────────────────────────────────────────────────────────── // ── Annäherung ────────────────────────────────────────────────────────────
private void startApproach(int idx) { private void startApproach(int idx) {
if (playerInput.isWeaponDrawn()) return;
targetIdx = idx; targetIdx = idx;
walkTimer = 0f; walkTimer = 0f;
originPos = physicsChar.getPhysicsLocation().clone(); 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()); 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, inputManager.addMapping(INTERACT_ACTION,
@@ -421,6 +435,7 @@ public class WorldItemsState extends BaseAppState {
// ── Pickup-Logik ────────────────────────────────────────────────────────── // ── Pickup-Logik ──────────────────────────────────────────────────────────
private void tryPickup() { private void tryPickup() {
if (playerInput.isWeaponDrawn()) return;
log.info("[WorldItems] tryPickup: hoveredIdx={} physicsChar={}", hoveredIdx, physicsChar != null ? "ok" : "NULL"); log.info("[WorldItems] tryPickup: hoveredIdx={} physicsChar={}", hoveredIdx, physicsChar != null ? "ok" : "NULL");
if (hoveredIdx < 0 || physicsChar == null) return; if (hoveredIdx < 0 || physicsChar == null) return;

View File

@@ -147,6 +147,8 @@ quest.kuehles_bier.success
quest.magnetiterz.description quest.magnetiterz.description
quest.magnetiterz.name quest.magnetiterz.name
quest.magnetiterz.success quest.magnetiterz.success
rostiges_kurzschwert.description
rostiges_kurzschwert.name
silas.name silas.name
strandgut.description strandgut.description
strandgut.name strandgut.name

View File

@@ -0,0 +1,2 @@
# uuid itemId x y z
fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693