diff --git a/assets/imported/animations/Sword And Shield Slash.fbx b/assets/imported/animations/Sword And Shield Slash.fbx new file mode 100644 index 0000000..d6793db Binary files /dev/null and b/assets/imported/animations/Sword And Shield Slash.fbx differ diff --git a/assets/imported/models/rostiges_kurzschwert.glb b/assets/imported/models/rostiges_kurzschwert.glb new file mode 100644 index 0000000..8b218b6 Binary files /dev/null and b/assets/imported/models/rostiges_kurzschwert.glb differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/imported/rostiges_kurzschwert.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/imported/rostiges_kurzschwert.j3o.thumb.png new file mode 100644 index 0000000..dba4e1d Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/imported/rostiges_kurzschwert.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/waffen/schwerter/rostiges_kurzschwert.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/waffen/schwerter/rostiges_kurzschwert.j3o.thumb.png new file mode 100644 index 0000000..3db202a Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/waffen/schwerter/rostiges_kurzschwert.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Slash.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Slash.j3o new file mode 100644 index 0000000..ff95fd4 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Slash.j3o differ diff --git a/blight-assets/src/main/resources/Characters/sets/human.animset.json b/blight-assets/src/main/resources/Characters/sets/human.animset.json index 4b0ebdc..22283f4 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -7,7 +7,9 @@ "Sprint", "Stand To Sit", "T-Pose", - "Walking" + "Walking", + "Jumping", + "Sword And Shield Slash" ], "actionMap": { "DEFAULT": "Idle", @@ -17,7 +19,10 @@ "SIT_DOWN": "Stand To Sit", "SITTING": "Sitting Idle", "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", "animOffsets": { diff --git a/blight-assets/src/main/resources/Models/imported/rostiges_kurzschwert.j3o.meta b/blight-assets/src/main/resources/Models/imported/rostiges_kurzschwert.j3o.meta new file mode 100644 index 0000000..5b857d0 --- /dev/null +++ b/blight-assets/src/main/resources/Models/imported/rostiges_kurzschwert.j3o.meta @@ -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 diff --git a/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o new file mode 100644 index 0000000..c97810d Binary files /dev/null and b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o differ diff --git a/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o.meta b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o.meta new file mode 100644 index 0000000..9a19eb7 --- /dev/null +++ b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o.meta @@ -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 diff --git a/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item b/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item new file mode 100644 index 0000000..89b1af7 --- /dev/null +++ b/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item @@ -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 +} \ No newline at end of file diff --git a/blight-common/src/main/java/de/blight/common/SaveGame.java b/blight-common/src/main/java/de/blight/common/SaveGame.java index 167dfd6..ebfad44 100644 --- a/blight-common/src/main/java/de/blight/common/SaveGame.java +++ b/blight-common/src/main/java/de/blight/common/SaveGame.java @@ -23,6 +23,12 @@ public class SaveGame { public float x, y, z; /** itemId → Anzahl */ public Map 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 { diff --git a/blight-common/src/main/java/de/blight/common/model/Item.java b/blight-common/src/main/java/de/blight/common/model/Item.java index 298c900..5a7d37e 100644 --- a/blight-common/src/main/java/de/blight/common/model/Item.java +++ b/blight-common/src/main/java/de/blight/common/model/Item.java @@ -15,6 +15,8 @@ public class Item implements Interactable { private TextReference name; private TextReference description; private int worthGold; + private int damage; + private ItemGrip grip; private ObjectReference modelRef; private boolean consumable; private List effects; diff --git a/blight-common/src/main/java/de/blight/common/model/ItemCategoryManager.java b/blight-common/src/main/java/de/blight/common/model/ItemCategoryManager.java index aa0ec3d..759e17b 100644 --- a/blight-common/src/main/java/de/blight/common/model/ItemCategoryManager.java +++ b/blight-common/src/main/java/de/blight/common/model/ItemCategoryManager.java @@ -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.ItemSubCategory.ARMOR; 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.HALBERD; import static de.blight.common.model.ItemSubCategory.HELM; @@ -36,7 +39,7 @@ public class ItemCategoryManager { private static final Map> mapping = new EnumMap>(ItemCategory.class); 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(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM)); mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM)); diff --git a/blight-common/src/main/java/de/blight/common/model/ItemGrip.java b/blight-common/src/main/java/de/blight/common/model/ItemGrip.java new file mode 100644 index 0000000..1b65baa --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/model/ItemGrip.java @@ -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; +} diff --git a/blight-common/src/main/java/de/blight/common/model/ItemSubCategory.java b/blight-common/src/main/java/de/blight/common/model/ItemSubCategory.java index 66ecf26..240e1d6 100644 --- a/blight-common/src/main/java/de/blight/common/model/ItemSubCategory.java +++ b/blight-common/src/main/java/de/blight/common/model/ItemSubCategory.java @@ -2,7 +2,7 @@ package de.blight.common.model; 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 POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM, QUEST_ITEM, diff --git a/blight-common/src/main/java/de/blight/common/model/MainCharacter.java b/blight-common/src/main/java/de/blight/common/model/MainCharacter.java index 06e647b..626c9dd 100644 --- a/blight-common/src/main/java/de/blight/common/model/MainCharacter.java +++ b/blight-common/src/main/java/de/blight/common/model/MainCharacter.java @@ -44,6 +44,13 @@ public class MainCharacter extends GameCharacter { 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. */ private Set playedMonologueIds = new HashSet<>(); @@ -73,6 +80,27 @@ public class MainCharacter extends GameCharacter { @Setter(AccessLevel.NONE) private transient Queue 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) { if (option.getRequiredItem() != null) { getInventar().remove(option.getRequiredItem()); diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 9bca28f..c412507 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -194,6 +194,10 @@ public class EditorApp extends Application { colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; }); terrainController.setGrassController(grassController); 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); gameController = new de.blight.editor.toolset.game.GameController(toolCtx); worldPanelController = new de.blight.editor.toolset.world.WorldPanelController(toolCtx, this::scheduleAutoSave); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 6eb4734..af2b992 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -1167,6 +1167,18 @@ public class SharedInput { /** Gesetzt vom JME-Thread nach Raycast-Klick, damit JFX-Spinner aktualisiert werden. */ 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. */ public record SculptEdit(float screenX, float screenY, int action) {} public final ConcurrentLinkedQueue sculptEditQueue = new ConcurrentLinkedQueue<>(); diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java index 543e1cf..e8726fb 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java @@ -99,6 +99,11 @@ public class ModelEditorState extends BaseAppState { private Node interactableArrowNode = null; private float interactableOffsetX = 0f; 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 interactableRotY = 0f; @@ -232,6 +237,18 @@ public class ModelEditorState extends BaseAppState { 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; if (removeLod > 0) { input.modelEditorRemoveEmbeddedLod = -1; @@ -705,6 +722,17 @@ public class ModelEditorState extends BaseAppState { interactableOffsetZ = input.modelInteractableOffsetZ; 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; orbitPitch = 25f; } @@ -726,6 +754,8 @@ public class ModelEditorState extends BaseAppState { embeddedLodSpatials = null; originalSpatial = null; interactableArrowNode = null; + gripGizmoNode = null; + input.gripEditorActive = false; camLight = null; mainLight = null; treeLightMaterials.clear(); @@ -1490,6 +1520,70 @@ public class ModelEditorState extends BaseAppState { 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. */ public void setInteractableArrowVisible(boolean visible) { if (interactableArrowNode != null) diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java b/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java index 925e575..0b9a8ed 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java @@ -84,12 +84,18 @@ public class ContentController { // ────────────────────────────────────────────────────────────────────────── + private Runnable onEnterContentMode; + private GripEditorController gripEditorController; + public ContentController(ToolContext ctx) { this.ctx = ctx; + this.gripEditorController = new GripEditorController(ctx); } // ── Öffentliche API ──────────────────────────────────────────────────────── + public void setOnEnterContentMode(Runnable r) { this.onEnterContentMode = r; } + public void setDualWindowMenuItem(CheckMenuItem item) { this.dualWindowMenuItem = item; } @@ -206,6 +212,7 @@ public class ContentController { /** 1-Fenster-Content-Modus aktivieren. */ public void switchToContentEditor() { + if (onEnterContentMode != null) onEnterContentMode.run(); ctx.setCurrentTool().accept("content"); if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(true); ctx.setMainToolbar().accept(buildSingleWindowContentToolBar()); @@ -328,6 +335,7 @@ public class ContentController { if (contentItemBtn != null) contentItemBtn.setSelected(true); de.blight.editor.ui.ItemEditorView view = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); + wireItemView(view); setContentCenter(view); setContentRight(null); if (selectItemId != null) view.selectItem(selectItemId); @@ -338,6 +346,7 @@ public class ContentController { ctx.setMainToolbar().accept(tb); de.blight.editor.ui.ItemEditorView view = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); + wireItemView(view); ctx.setCenterView().accept(view); ctx.setRightPanel().accept(null); if (selectItemId != null) view.selectItem(selectItemId); @@ -949,9 +958,26 @@ public class ContentController { 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() { collapseAllContentSections(); activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); + wireItemView(activeItemView); setContentCenter(activeItemView); setContentRight(null); setContentBottom(buildItemLegend()); diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/content/GripEditorController.java b/blight-editor/src/main/java/de/blight/editor/toolset/content/GripEditorController.java new file mode 100644 index 0000000..e970bde --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/content/GripEditorController.java @@ -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 posXSpin, posYSpin, posZSpin; + private Spinner rotXSpin, rotYSpin, rotZSpin; + + private Consumer 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 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 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 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 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 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; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java index ca2190f..332035f 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java @@ -4,6 +4,7 @@ import de.blight.common.model.CharacterStat; import de.blight.common.model.ConsumableEffect; import de.blight.common.model.Item; import de.blight.common.model.ItemCategory; +import de.blight.common.model.ItemGrip; import de.blight.common.model.ItemCategoryManager; import de.blight.common.model.ItemIO; import de.blight.common.model.ItemSubCategory; @@ -42,14 +43,23 @@ public class ItemEditorView extends BorderPane { private TextField idField; private ComboBox catCombo; private ComboBox subCatCombo; - private TextField nameField; - private TextField descField; - private Spinner goldSpinner; - private TextField modelRefField; - private VBox formContainer; - private CheckBox consumableCheck; - private VBox consumablesSection; - private VBox effectsRows; + private Label nameLabel; + private Label descLabel; + private Spinner goldSpinner; + private Spinner damageSpinner; + private VBox weaponSection; + private TextField modelRefField; + private VBox formContainer; + private CheckBox consumableCheck; + private VBox consumablesSection; + private VBox effectsRows; + + public interface GripEditorLauncher { + void open(String modelPath, ItemGrip currentGrip, java.util.function.Consumer onSave); + } + + private Runnable onIdChanged; + private GripEditorLauncher gripEditorLauncher; private final PauseTransition savePause = new PauseTransition(Duration.millis(600)); @@ -59,7 +69,7 @@ public class ItemEditorView extends BorderPane { public ItemEditorView(Path itemDir) { this.itemDir = itemDir; - this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot + this.assetRoot = itemDir.getParent(); setStyle("-fx-background-color: #1e1e2e;"); reload(); @@ -77,6 +87,10 @@ public class ItemEditorView extends BorderPane { // ── Ö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) { if (itemId == null) return; 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 ───────────────────────────────────────────────────── private VBox buildForm() { @@ -196,7 +195,6 @@ public class ItemEditorView extends BorderPane { } }); - // Wenn Kategorie wechselt: SubKategorie-Liste aktualisieren catCombo.valueProperty().addListener((obs, oldCat, newCat) -> { ItemSubCategory prevSub = subCatCombo.getValue(); subCatCombo.getItems().setAll( @@ -207,21 +205,47 @@ public class ItemEditorView extends BorderPane { } else { subCatCombo.setValue(null); } + updateWeaponSectionVisibility(); + scheduleSave(); + }); + subCatCombo.valueProperty().addListener((obs, o, n) -> { + updateWeaponSectionVisibility(); scheduleSave(); }); - subCatCombo.valueProperty().addListener((obs, o, n) -> scheduleSave()); - nameField = new TextField(); - nameField.setPromptText("TextReference-Schlüssel"); - nameField.textProperty().addListener((obs, o, n) -> scheduleSave()); - descField = new TextField(); - descField.setPromptText("TextReference-Schlüssel"); - descField.textProperty().addListener((obs, o, n) -> scheduleSave()); + // Name und Beschreibung: read-only (auto-generierte TextReference-Schlüssel) + nameLabel = new Label(); + nameLabel.setStyle("-fx-text-fill: #777; -fx-font-style: italic;"); + nameLabel.setMaxWidth(Double.MAX_VALUE); + descLabel = new Label(); + descLabel.setStyle("-fx-text-fill: #777; -fx-font-style: italic;"); + descLabel.setMaxWidth(Double.MAX_VALUE); + goldSpinner = new Spinner<>(0, 999999, 0); goldSpinner.setEditable(true); goldSpinner.setMaxWidth(Double.MAX_VALUE); 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.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)"); modelRefField.setEditable(false); @@ -231,10 +255,6 @@ public class ItemEditorView extends BorderPane { pickModelBtn.setTooltip(new Tooltip("Modell auswählen")); 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("✕"); clearModelBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;"); clearModelBtn.setTooltip(new Tooltip("Modell-Referenz entfernen")); @@ -273,12 +293,13 @@ public class ItemEditorView extends BorderPane { row("SubKategorie:", subCatCombo), new Separator(), sectionTitle("Texte"), - row("Name:", nameField), - row("Beschreibung:", descField), + row("Name:", nameLabel), + row("Beschreibung:", descLabel), new Separator(), sectionTitle("Werte"), row("Wert (Gold):", goldSpinner), row("Modell:", modelFullRow), + weaponSection, new Separator(), consumableCheck, consumablesSection @@ -286,6 +307,24 @@ public class ItemEditorView extends BorderPane { 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 ──────────────────────────────────────────────────────── private void pickModel() { @@ -296,8 +335,10 @@ public class ItemEditorView extends BorderPane { private void onIdCommitted() { String id = idField.getText().trim(); if (id.isBlank()) return; - if (nameField.getText().isBlank()) nameField.setText(id + ".name"); - if (descField.getText().isBlank()) descField.setText(id + ".description"); + nameLabel.setText(id + ".name"); + descLabel.setText(id + ".description"); + scheduleSave(); + if (onIdChanged != null) onIdChanged.run(); } // ── Auto-save ───────────────────────────────────────────────────────────── @@ -338,11 +379,13 @@ public class ItemEditorView extends BorderPane { private void loadFormFromItem(Item item) { idField.setText(safe(item.getItemId())); - catCombo.setValue(item.getCategory()); // löst Listener aus → subCatCombo befüllt + catCombo.setValue(item.getCategory()); subCatCombo.setValue(item.getSubCategory()); - nameField.setText(item.getName() != null ? item.getName().id() : ""); - descField.setText(item.getDescription() != null ? item.getDescription().id() : ""); + String id = safe(item.getItemId()); + 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()); + damageSpinner.getValueFactory().setValue(item.getDamage()); ObjectReference ref = item.getModelRef(); modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : ""); consumableCheck.setSelected(item.isConsumable()); @@ -352,15 +395,19 @@ public class ItemEditorView extends BorderPane { addEffectRow(effect.getStat(), effect.getValue()); } } + updateWeaponSectionVisibility(); } private void saveFormToItem(Item item) { item.setItemId(idField.getText().trim()); item.setCategory(catCombo.getValue()); item.setSubCategory(subCatCombo.getValue()); - item.setName(ref(nameField)); - item.setDescription(ref(descField)); + String nameKey = nameLabel.getText().trim(); + 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.setDamage(damageSpinner.getValue()); String mr = modelRefField.getText().trim(); item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr)); item.setConsumable(consumableCheck.isSelected()); @@ -378,14 +425,16 @@ public class ItemEditorView extends BorderPane { private void clearForm() { idField.clear(); - catCombo.setValue(null); // setzt subCatCombo via Listener zurück + catCombo.setValue(null); subCatCombo.setValue(null); - nameField.clear(); - descField.clear(); + nameLabel.setText(""); + descLabel.setText(""); goldSpinner.getValueFactory().setValue(0); + damageSpinner.getValueFactory().setValue(0); modelRefField.clear(); consumableCheck.setSelected(false); effectsRows.getChildren().clear(); + updateWeaponSectionVisibility(); } // ── List operations ─────────────────────────────────────────────────────── @@ -453,9 +502,4 @@ public class ItemEditorView extends BorderPane { } 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); - } } diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java index f8f48d9..2eb589c 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationAction.java @@ -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)"); }; } } diff --git a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java index 2d0debe..ec68382 100644 --- a/blight-game/src/main/java/de/blight/game/config/KeyBindings.java +++ b/blight-game/src/main/java/de/blight/game/config/KeyBindings.java @@ -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) { diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index eefe054..d988474 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -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 query) { this.footstepSystem = fs; diff --git a/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java b/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java new file mode 100644 index 0000000..9ac6b9f --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java @@ -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) {} +} diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 8f88d9d..d1ef144 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -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); diff --git a/blight-game/src/main/java/de/blight/game/state/CharacterState.java b/blight-game/src/main/java/de/blight/game/state/CharacterState.java index 7761225..a94aa52 100644 --- a/blight-game/src/main/java/de/blight/game/state/CharacterState.java +++ b/blight-game/src/main/java/de/blight/game/state/CharacterState.java @@ -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 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 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 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); diff --git a/blight-game/src/main/java/de/blight/game/state/InventoryState.java b/blight-game/src/main/java/de/blight/game/state/InventoryState.java index 3b8f11d..f1f7ba6 100644 --- a/blight-game/src/main/java/de/blight/game/state/InventoryState.java +++ b/blight-game/src/main/java/de/blight/game/state/InventoryState.java @@ -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"; diff --git a/blight-game/src/main/java/de/blight/game/state/SaveGameState.java b/blight-game/src/main/java/de/blight/game/state/SaveGameState.java index 7a4b6da..2e686cd 100644 --- a/blight-game/src/main/java/de/blight/game/state/SaveGameState.java +++ b/blight-game/src/main/java/de/blight/game/state/SaveGameState.java @@ -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) { diff --git a/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java b/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java index b0aa4d4..59bf9e7 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java @@ -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(); diff --git a/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java b/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java index 4086495..f0ef829 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java @@ -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; diff --git a/blight-lang/src/main/resources/lang/text_keys.txt b/blight-lang/src/main/resources/lang/text_keys.txt index 93ceb40..09feced 100644 --- a/blight-lang/src/main/resources/lang/text_keys.txt +++ b/blight-lang/src/main/resources/lang/text_keys.txt @@ -147,6 +147,8 @@ quest.kuehles_bier.success quest.magnetiterz.description quest.magnetiterz.name quest.magnetiterz.success +rostiges_kurzschwert.description +rostiges_kurzschwert.name silas.name strandgut.description strandgut.name diff --git a/blight-map/src/main/map/blight_placed_items.bpi b/blight-map/src/main/map/blight_placed_items.bpi new file mode 100644 index 0000000..b9ce95d --- /dev/null +++ b/blight-map/src/main/map/blight_placed_items.bpi @@ -0,0 +1,2 @@ +# uuid itemId x y z +fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693