diff --git a/assets/imported/animations/Sword And Shield Attack Counterattack.fbx b/assets/imported/animations/Sword And Shield Attack Counterattack.fbx new file mode 100644 index 0000000..d310dc7 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Attack Counterattack.fbx differ diff --git a/assets/imported/animations/Sword And Shield Block Idle.fbx b/assets/imported/animations/Sword And Shield Block Idle.fbx new file mode 100644 index 0000000..c5464cd Binary files /dev/null and b/assets/imported/animations/Sword And Shield Block Idle.fbx differ diff --git a/assets/imported/animations/Sword And Shield Block Impact.fbx b/assets/imported/animations/Sword And Shield Block Impact.fbx new file mode 100644 index 0000000..713c925 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Block Impact.fbx differ diff --git a/assets/imported/animations/Sword And Shield Block.fbx b/assets/imported/animations/Sword And Shield Block.fbx new file mode 100644 index 0000000..8a05bde Binary files /dev/null and b/assets/imported/animations/Sword And Shield Block.fbx differ diff --git a/assets/imported/animations/Sword And Shield Impact.fbx b/assets/imported/animations/Sword And Shield Impact.fbx new file mode 100644 index 0000000..a90dbd0 Binary files /dev/null and b/assets/imported/animations/Sword And Shield Impact.fbx differ diff --git a/assets/imported/models/medieval round shield 3d model.glb b/assets/imported/models/medieval round shield 3d model.glb new file mode 100644 index 0000000..ad6f926 Binary files /dev/null and b/assets/imported/models/medieval round shield 3d model.glb differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/imported/medieval round shield 3d model.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/imported/medieval round shield 3d model.j3o.thumb.png new file mode 100644 index 0000000..55142ac Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/imported/medieval round shield 3d model.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Attack Counterattack.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Attack Counterattack.j3o new file mode 100644 index 0000000..534e551 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Attack Counterattack.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Block Idle.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Block Idle.j3o new file mode 100644 index 0000000..dc02004 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Block Idle.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Block Impact.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Block Impact.j3o new file mode 100644 index 0000000..b148cd2 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Block Impact.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Block.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Block.j3o new file mode 100644 index 0000000..5671164 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Block.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sword And Shield Impact.j3o b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Impact.j3o new file mode 100644 index 0000000..ee20e9f Binary files /dev/null and b/blight-assets/src/main/resources/Characters/clips/Sword And Shield Impact.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 1974614..fb36aa0 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -14,7 +14,12 @@ "Sword And Shield Run", "Sword And Shield Walk", "Sword And Shield Attack", - "Sword And Shield Slash 2" + "Sword And Shield Slash 2", + "Sword And Shield Attack Counterattack", + "Sword And Shield Block", + "Sword And Shield Block Idle", + "Sword And Shield Block Impact", + "Sword And Shield Impact" ], "actionMap": { "DEFAULT": "Idle", @@ -31,7 +36,12 @@ "SWORD_WALK": "Sword And Shield Walk", "SWORD_RUN": "Sword And Shield Run", "SWORD_ATTACK_3": "Sword And Shield Attack", - "SWORD_ATTACK_2": "Sword And Shield Slash 2" + "SWORD_ATTACK_2": "Sword And Shield Slash 2", + "SWORD_BLOCK": "Sword And Shield Block Idle", + "SWORD_IDLE_TO_BLOCK": "Sword And Shield Block", + "SWORD_BLOCK_IMPACT": "Sword And Shield Block Impact", + "SWORD_IDLE_IMPACT": "Sword And Shield Impact", + "SWORD_COUNTERATTACK": "Sword And Shield Attack Counterattack" }, "previewModelPath": "Characters/Models/hero.j3o", "animOffsets": { diff --git a/blight-assets/src/main/resources/Models/imported/medieval round shield 3d model.j3o.meta b/blight-assets/src/main/resources/Models/imported/medieval round shield 3d model.j3o.meta new file mode 100644 index 0000000..1d19397 --- /dev/null +++ b/blight-assets/src/main/resources/Models/imported/medieval round shield 3d model.j3o.meta @@ -0,0 +1,30 @@ +#Sun Sep 20 18:14:13 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=medieval round shield 3d model +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/schilder/einfacher schild.j3o b/blight-assets/src/main/resources/Models/waffen/schilder/einfacher schild.j3o new file mode 100644 index 0000000..c7b6f25 Binary files /dev/null and b/blight-assets/src/main/resources/Models/waffen/schilder/einfacher schild.j3o differ diff --git a/blight-assets/src/main/resources/items/crafting_items/blutagave.item b/blight-assets/src/main/resources/items/crafting_items/blutagave.item index 1f96014..50f5857 100644 --- a/blight-assets/src/main/resources/items/crafting_items/blutagave.item +++ b/blight-assets/src/main/resources/items/crafting_items/blutagave.item @@ -9,6 +9,7 @@ "id": "blutagave.description" }, "worthGold": 25, + "damage": 0, "modelRef": { "path": "Models/plants/usable/blutagave.j3o" }, diff --git a/blight-assets/src/main/resources/items/crafting_items/erzmoss.item b/blight-assets/src/main/resources/items/crafting_items/erzmoss.item index 329dffc..aa2ead5 100644 --- a/blight-assets/src/main/resources/items/crafting_items/erzmoss.item +++ b/blight-assets/src/main/resources/items/crafting_items/erzmoss.item @@ -9,6 +9,7 @@ "id": "erzmoss.description" }, "worthGold": 100, + "damage": 0, "modelRef": { "path": "Models/plants/usable/erzmoss.j3o" }, diff --git a/blight-assets/src/main/resources/items/weapon/einfacher_schild.item b/blight-assets/src/main/resources/items/weapon/einfacher_schild.item new file mode 100644 index 0000000..e215526 --- /dev/null +++ b/blight-assets/src/main/resources/items/weapon/einfacher_schild.item @@ -0,0 +1,25 @@ +{ + "itemId": "einfacher_schild", + "category": "WEAPON", + "subCategory": "SHIELD", + "name": { + "id": "einfacher_schild.name" + }, + "description": { + "id": "einfacher_schild.description" + }, + "worthGold": 0, + "damage": 0, + "grip": { + "posX": -0.12, + "posY": 0.4, + "posZ": -0.13, + "rotX": 90.0, + "rotY": 0.0, + "rotZ": 90.0 + }, + "modelRef": { + "path": "Models/waffen/schilder/einfacher schild.j3o" + }, + "consumable": false +} \ No newline at end of file diff --git a/blight-common/src/main/java/de/blight/common/PlacedItem.java b/blight-common/src/main/java/de/blight/common/PlacedItem.java index 37c5c5b..6c0dcba 100644 --- a/blight-common/src/main/java/de/blight/common/PlacedItem.java +++ b/blight-common/src/main/java/de/blight/common/PlacedItem.java @@ -3,10 +3,15 @@ package de.blight.common; import java.util.UUID; /** Ein Item-Pickup das auf der Spielwelt liegt (Kartendaten). */ -public record PlacedItem(String uuid, String itemId, float x, float y, float z) { +public record PlacedItem(String uuid, String itemId, float x, float y, float z, float rotX, float rotY, float rotZ) { - /** Erzeugt ein neues PlacedItem mit automatisch generierter UUID. */ + /** Erzeugt ein neues PlacedItem mit automatisch generierter UUID (alle Rotationen = 0). */ public static PlacedItem create(String itemId, float x, float y, float z) { - return new PlacedItem(UUID.randomUUID().toString(), itemId, x, y, z); + return new PlacedItem(UUID.randomUUID().toString(), itemId, x, y, z, 0f, 0f, 0f); + } + + /** Gibt eine Kopie mit geänderter Position/Rotation zurück. */ + public PlacedItem withTransform(float nx, float ny, float nz, float nRotX, float nRotY, float nRotZ) { + return new PlacedItem(uuid, itemId, nx, ny, nz, nRotX, nRotY, nRotZ); } } diff --git a/blight-common/src/main/java/de/blight/common/PlacedItemIO.java b/blight-common/src/main/java/de/blight/common/PlacedItemIO.java index 4922b46..85a1299 100644 --- a/blight-common/src/main/java/de/blight/common/PlacedItemIO.java +++ b/blight-common/src/main/java/de/blight/common/PlacedItemIO.java @@ -25,11 +25,11 @@ public final class PlacedItemIO { Path p = getPath(); Files.createDirectories(p.getParent()); try (BufferedWriter w = Files.newBufferedWriter(p)) { - w.write("# uuid\titemId\tx\ty\tz"); + w.write("# uuid\titemId\tx\ty\tz\trotX\trotY\trotZ"); w.newLine(); for (PlacedItem it : items) { - w.write(String.format(Locale.ROOT, "%s\t%s\t%.5f\t%.5f\t%.5f%n", - it.uuid(), it.itemId(), it.x(), it.y(), it.z())); + w.write(String.format(Locale.ROOT, "%s\t%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f%n", + it.uuid(), it.itemId(), it.x(), it.y(), it.z(), it.rotX(), it.rotY(), it.rotZ())); } } } @@ -43,17 +43,29 @@ public final class PlacedItemIO { if (line.isEmpty() || line.startsWith("#")) continue; String[] f = line.split("\t", -1); try { - if (f.length >= 5) { - // Aktuelles Format: uuid itemId x y z + if (f.length >= 8) { + // Aktuelles Format: uuid itemId x y z rotX rotY rotZ list.add(new PlacedItem(f[0], f[1], - Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]))); + Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]), + Float.parseFloat(f[5]), Float.parseFloat(f[6]), Float.parseFloat(f[7]))); + } else if (f.length >= 6) { + // Vorherige Version: uuid itemId x y z rotY (kein rotX/rotZ) + list.add(new PlacedItem(f[0], f[1], + Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]), + 0f, Float.parseFloat(f[5]), 0f)); + } else if (f.length >= 5) { + // uuid itemId x y z (kein rot) + list.add(new PlacedItem(f[0], f[1], + Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]), + 0f, 0f, 0f)); } else if (f.length == 4) { // Altes Format: itemId x y z → UUID deterministisch aus Inhalt ableiten String deterministicId = UUID.nameUUIDFromBytes( ("legacy:" + f[0] + ":" + f[1] + ":" + f[2] + ":" + f[3]) .getBytes(StandardCharsets.UTF_8)).toString(); list.add(new PlacedItem(deterministicId, f[0], - Float.parseFloat(f[1]), Float.parseFloat(f[2]), Float.parseFloat(f[3]))); + Float.parseFloat(f[1]), Float.parseFloat(f[2]), Float.parseFloat(f[3]), + 0f, 0f, 0f)); } } catch (NumberFormatException ignored) {} } 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 759e17b..d063d0a 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 @@ -39,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, BOW, CROSSBOW, MAGIC_CRYSTAL)); + mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, BOW, CROSSBOW, MAGIC_CRYSTAL, SHIELD)); 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-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index c412507..ed2d2fa 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -204,19 +204,10 @@ public class EditorApp extends Application { toolPanel = buildToolPanel(); - topBar = topBarController.buildTop(toolCtx, new de.blight.editor.toolset.topbar.TopBarController.Deps( - toolPanel, - modelEditorController, assetController, animSetController, - contentController, worldPanelController, voxelController, - grassController, atmosphereController, waterController, - gameController, terrainController, objectController, - this::switchToVegetationGenerator, this::switchToEzTree, - this::switchToTripo, this::openCharacterEditor, - this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings - )); - root.setTop(topBar); - root.setRight(worldPanelController.buildAuswahlPanel()); - + // assetController muss VOR buildTop() erstellt werden, damit es korrekt in Deps landet. + // Alle Abhängigkeiten (objectController, animPreviewController etc.) sind bereits verfügbar. + // Die Buttons (atmosphaereBtn, wasserMergedBtn, zonenBtn) sind im TopBarController + // als final-Felder vordefiniert und unabhängig von buildTop(). assetController = new de.blight.editor.toolset.asset.AssetTreeController(toolCtx, new de.blight.editor.toolset.asset.AssetTreeController.Deps( objectController, animPreviewController, animSetController, @@ -230,6 +221,20 @@ public class EditorApp extends Application { terrainController::loadTexMeta, terrainController::saveTexMeta, () -> currentTool, r -> pendingAfterSave = r )); + + topBar = topBarController.buildTop(toolCtx, new de.blight.editor.toolset.topbar.TopBarController.Deps( + toolPanel, + modelEditorController, assetController, animSetController, + contentController, worldPanelController, voxelController, + grassController, atmosphereController, waterController, + gameController, terrainController, objectController, + this::switchToVegetationGenerator, this::switchToEzTree, + this::switchToTripo, this::openCharacterEditor, + this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings + )); + root.setTop(topBar); + root.setRight(worldPanelController.buildAuswahlPanel()); + assetController.buildPanel(); assetController.setupOverlay(centerStack); @@ -430,8 +435,25 @@ public class EditorApp extends Application { worldPanelController.updateLocationZonePanel(input.selectedLocationZoneInfo); } + // Items haben höchste Priorität: ein selektiertes Item verdrängt + // alle konkurrierenden Auswahl-Treffer desselben Klicks. + if (input.itemSelectionChanged && input.selectedItemInfo != null + && (input.activeLayer == SharedInput.LAYER_AUSWAHL + || input.activeLayer == SharedInput.LAYER_ITEMS_EDIT)) { + input.itemSelectionChanged = false; + input.auswahlZoneSelected = false; + input.auswahlLightSelected = false; + input.auswahlEmitterSelected = false; + input.auswahlSoundSelected = false; + input.auswahlWaterSelected = false; + input.auswahlWaterfallSelected = false; + log.info("[Auswahl→Tool] Item ausgewaehlt → Info='{}'", input.selectedItemInfo); + root.setRight(worldPanelController.buildAuswahlItemEditPanel(input.selectedItemInfo)); + } + if (input.auswahlZoneSelected) { input.auswahlZoneSelected = false; + log.info("[Auswahl→Tool] Zone ausgewaehlt → LAYER_ZONEN"); input.activeLayer = SharedInput.LAYER_ZONEN; topBarController.getZonenBtn().setSelected(true); root.setRight(worldPanelController.buildZonenPanel()); @@ -439,6 +461,7 @@ public class EditorApp extends Application { if (input.auswahlLightSelected) { input.auswahlLightSelected = false; + log.info("[Auswahl→Tool] Licht ausgewaehlt → Atmo-Tab, Info='{}'", input.selectedLightInfo); topBarController.getAtmosphaereBtn().setSelected(true); root.setRight(atmosphereController.buildPanel()); atmosphereController.updateLightPanel(input.selectedLightInfo); @@ -446,6 +469,7 @@ public class EditorApp extends Application { if (input.auswahlEmitterSelected) { input.auswahlEmitterSelected = false; + log.info("[Auswahl→Tool] Emitter ausgewaehlt → Atmo-Tab/LAYER_EMITTERS, Info='{}'", input.selectedEmitterInfo); topBarController.getAtmosphaereBtn().setSelected(true); input.activeLayer = SharedInput.LAYER_EMITTERS; root.setRight(atmosphereController.buildPanel()); @@ -454,6 +478,7 @@ public class EditorApp extends Application { if (input.auswahlSoundSelected) { input.auswahlSoundSelected = false; + log.info("[Auswahl→Tool] Sound ausgewaehlt → Atmo-Tab/LAYER_PLACED_SOUNDS, Info='{}'", input.selectedSoundInfo); topBarController.getAtmosphaereBtn().setSelected(true); input.activeLayer = SharedInput.LAYER_PLACED_SOUNDS; root.setRight(atmosphereController.buildPanel()); @@ -462,6 +487,7 @@ public class EditorApp extends Application { if (input.auswahlWaterSelected) { input.auswahlWaterSelected = false; + log.info("[Auswahl→Tool] Wasser ausgewaehlt → LAYER_WATER"); topBarController.getWasserMergedBtn().setSelected(true); input.activeLayer = SharedInput.LAYER_WATER; root.setRight(waterController.buildPanel()); @@ -470,15 +496,22 @@ public class EditorApp extends Application { if (input.auswahlWaterfallSelected) { input.auswahlWaterfallSelected = false; + log.info("[Auswahl→Tool] Wasserfall ausgewaehlt → LAYER_WATERFALL"); topBarController.getWasserMergedBtn().setSelected(true); input.activeLayer = SharedInput.LAYER_WATERFALL; root.setRight(waterController.buildPanel()); waterController.updateWaterfallPanel(input.selectedWaterfallInfo); } + // itemSelectionChanged mit null = Item abgewählt → Auswahl-Panel wiederherstellen, + // aber nur wenn noch im Auswahl-Kontext (kein anderes Tool geöffnet) if (input.itemSelectionChanged) { input.itemSelectionChanged = false; - worldPanelController.updateAuswahlItemPanel(input.selectedItemInfo); + if (input.activeLayer == SharedInput.LAYER_AUSWAHL + || input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) { + log.info("[Auswahl→Tool] Item abgewaehlt → Auswahl-Panel"); + root.setRight(worldPanelController.buildAuswahlPanel()); + } } if (input.zonenSelectionChanged) { @@ -952,7 +985,9 @@ public class EditorApp extends Application { input.deleteEmitterRequested = true; else if (input.activeLayer == SharedInput.LAYER_PLACED_SOUNDS && input.selectedSoundInfo != null) input.deleteSoundRequested = true; - else if (input.activeLayer == SharedInput.LAYER_ITEMS && input.selectedItemInfo != null) + else if ((input.activeLayer == SharedInput.LAYER_ITEMS + || input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) + && input.selectedItemInfo != null) input.deleteSelectedItemRequested = true; else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) { if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; 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 af2b992..51c4e6c 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -970,6 +970,9 @@ public class SharedInput { /** activeLayer==21 → Item-Pickup auf die Karte platzieren */ public static final int LAYER_ITEMS = 21; + /** activeLayer==36 → ausgewähltes Item per Gizmo verschieben/rotieren */ + public static final int LAYER_ITEMS_EDIT = 36; + /** JFX → JME: itemId des zu platzierenden Items (aus .item-Datei). */ public volatile String pendingItemId = null; @@ -1178,6 +1181,8 @@ public class SharedInput { public volatile float gripEditorRotZ = 0f; /** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */ public volatile boolean gripEditorGizmoDirty = false; + /** true → Gizmo ist ein 28cm-Unterarm-Pfeil (Schild); false → 15cm-Waffen-Pfeil. */ + public volatile boolean gripEditorIsShield = false; /** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */ public record SculptEdit(float screenX, float screenY, int action) {} @@ -1246,11 +1251,19 @@ public class SharedInput { public volatile boolean deselectAllOnAuswahl = false; /** JME3 → JavaFX: Zone wurde im Auswahl-Modus selektiert → Zonen-Werkzeug aktivieren. */ public volatile boolean auswahlZoneSelected = false; - /** JME3 → JavaFX: Item-Spawn im Auswahl-Modus selektiert. Format "idx|itemId|x|y|z" oder null. */ + /** JME3 → JavaFX: Item-Spawn im Auswahl-Modus selektiert. Format "idx|itemId|x|y|z|rotX|rotY|rotZ" oder null. */ public volatile String selectedItemInfo = null; public volatile boolean itemSelectionChanged = false; /** JavaFX → JME3: Selektierten Item-Spawn löschen. */ public volatile boolean deleteSelectedItemRequested = false; + /** JavaFX → JME3: Position/Rotation des selektierten Items ändern. */ + public volatile boolean itemEditRequest = false; + public volatile float itemEditX = 0f; + public volatile float itemEditY = 0f; + public volatile float itemEditZ = 0f; + public volatile float itemEditRotX = 0f; + public volatile float itemEditRotY = 0f; + public volatile float itemEditRotZ = 0f; /** JME3-intern: Auswahl-Klick für ItemPlacementState (kein Objekt/keine Zone getroffen). */ public final ConcurrentLinkedQueue auswahlClickQueue = new ConcurrentLinkedQueue<>(); /** JME3-intern: Auswahl-Klick für LightState (kein Objekt/keine Zone getroffen). */ diff --git a/blight-editor/src/main/java/de/blight/editor/ToolRegistry.java b/blight-editor/src/main/java/de/blight/editor/ToolRegistry.java index 71eafc1..5efc4e9 100644 --- a/blight-editor/src/main/java/de/blight/editor/ToolRegistry.java +++ b/blight-editor/src/main/java/de/blight/editor/ToolRegistry.java @@ -1,5 +1,8 @@ package de.blight.editor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Verwaltet das aktive Werkzeug und leitet den AnimationTimer-Frame weiter. * @@ -8,6 +11,8 @@ package de.blight.editor; */ public class ToolRegistry { + private static final Logger log = LoggerFactory.getLogger(ToolRegistry.class); + private ToolController active = null; /** @@ -19,10 +24,12 @@ public class ToolRegistry { if (tool == active) { return; } + String prev = active != null ? active.toolId() : "–"; if (active != null) { active.deactivate(); } active = tool; + log.info("[Tool] {} → {}", prev, tool.toolId()); tool.activate(); } diff --git a/blight-editor/src/main/java/de/blight/editor/state/AuswahlMarkerState.java b/blight-editor/src/main/java/de/blight/editor/state/AuswahlMarkerState.java new file mode 100644 index 0000000..58a94cb --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/AuswahlMarkerState.java @@ -0,0 +1,86 @@ +package de.blight.editor.state; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.asset.AssetManager; +import com.jme3.collision.CollisionResults; +import com.jme3.math.Ray; +import com.jme3.math.Vector2f; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.scene.Node; +import com.jme3.terrain.geomipmap.TerrainQuad; +import de.blight.editor.SharedInput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Gemeinsame Basis für Zustandsklassen die Marker-Knoten verwalten und + * Auswahl-Klicks via {@code auswahlXxxClickQueue} verarbeiten + * (EmitterState, LightState, PlacedSoundState). + * + * Subklassen überschreiben {@link #onMarkerSelected(int)} um die eigene + * Selektion auszuführen und das zugehörige SharedInput-Flag zu setzen. + */ +public abstract class AuswahlMarkerState extends BaseAppState { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + protected final SharedInput input; + protected SimpleApplication app; + protected Camera cam; + protected AssetManager assets; + protected Node rootNode; + protected TerrainQuad terrain; + + protected final List markers = new ArrayList<>(); + + protected AuswahlMarkerState(SharedInput input) { + this.input = input; + } + + @Override + protected void initialize(Application application) { + app = (SimpleApplication) application; + cam = app.getCamera(); + assets = app.getAssetManager(); + rootNode = app.getRootNode(); + } + + public void setTerrain(TerrainQuad terrain) { + this.terrain = terrain; + } + + protected final int pickMarker(Ray ray) { + for (int i = 0; i < markers.size(); i++) { + CollisionResults res = new CollisionResults(); + markers.get(i).collideWith(ray, res); + if (res.size() > 0) return i; + } + return -1; + } + + protected final void handleAuswahlClick(SharedInput.ObjectClick click) { + float jmeX = click.screenX() * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; + Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); + Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); + Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); + int hit = pickMarker(ray); + log.info("[Auswahl] {}: Klick ({},{}), Marker getroffen: {}, Marker gesamt: {}", + getClass().getSimpleName(), (int) click.screenX(), (int) click.screenY(), + hit, markers.size()); + if (hit >= 0) { + onMarkerSelected(hit); + } + } + + /** + * Wird aufgerufen wenn ein Marker-Treffer gefunden wurde. + * Subklasse selektiert den Eintrag und setzt das zugehörige auswahl*Selected-Flag. + */ + protected abstract void onMarkerSelected(int idx); +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java b/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java index 169ce88..6803472 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/EmitterState.java @@ -18,15 +18,10 @@ import com.jme3.terrain.geomipmap.TerrainQuad; import de.blight.common.PlacedEmitter; import de.blight.editor.SharedInput; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.ArrayList; import java.util.List; -public class EmitterState extends BaseAppState { - - private static final Logger log = LoggerFactory.getLogger(EmitterState.class); +public class EmitterState extends AuswahlMarkerState { private static final float MARKER_RADIUS = 0.4f; private static final float ACTIVATION_ALPHA = 0.07f; @@ -35,16 +30,8 @@ public class EmitterState extends BaseAppState { private static final String GEO_MARKER = "emitter_marker"; private static final String GEO_ACTRADIUS = "activation_sphere"; - private final SharedInput input; - private SimpleApplication app; - private Camera cam; - private AssetManager assets; - private Node rootNode; - private TerrainQuad terrain; - // parallel lists: emitters[i] <-> markers[i] <-> particles[i] private final List emitters = new ArrayList<>(); - private final List markers = new ArrayList<>(); private final List particles = new ArrayList<>(); private int selectedIdx = -1; @@ -52,19 +39,11 @@ public class EmitterState extends BaseAppState { private List pendingEmitters = null; public EmitterState(SharedInput input) { - this.input = input; + super(input); } // ── Lifecycle ───────────────────────────────────────────────────────────── - @Override - protected void initialize(Application application) { - app = (SimpleApplication) application; - cam = app.getCamera(); - assets = app.getAssetManager(); - rootNode = app.getRootNode(); - } - @Override protected void cleanup(Application application) { clearAll(); } @Override @@ -77,7 +56,11 @@ public class EmitterState extends BaseAppState { @Override protected void onDisable() {} - public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; } + @Override + protected void onMarkerSelected(int idx) { + selectEmitter(idx); + input.auswahlEmitterSelected = true; + } // ── Update ──────────────────────────────────────────────────────────────── @@ -180,28 +163,6 @@ public class EmitterState extends BaseAppState { }; } - private void handleAuswahlClick(SharedInput.ObjectClick click) { - float jmeX = click.screenX() * (float) input.viewportScaleX; - float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; - Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); - Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); - Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); - int hit = pickMarker(ray); - if (hit >= 0) { - selectEmitter(hit); - input.auswahlEmitterSelected = true; - } - } - - private int pickMarker(Ray ray) { - for (int i = 0; i < markers.size(); i++) { - CollisionResults res = new CollisionResults(); - markers.get(i).collideWith(ray, res); - if (res.size() > 0) return i; - } - return -1; - } - // ── Selection ───────────────────────────────────────────────────────────── private void selectEmitter(int idx) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java b/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java index b03b2c2..bba6eb4 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java @@ -4,12 +4,18 @@ import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; +import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingSphere; +import com.jme3.bounding.BoundingVolume; import com.jme3.collision.CollisionResults; import com.jme3.material.Material; import com.jme3.math.*; import com.jme3.renderer.Camera; +import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.*; import com.jme3.scene.shape.Box; +import com.jme3.scene.shape.Cylinder; +import com.jme3.scene.shape.Torus; import com.jme3.terrain.geomipmap.TerrainQuad; import de.blight.common.PlacedItem; import de.blight.common.PlacedItemIO; @@ -21,6 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -29,11 +36,31 @@ import java.util.Map; /** * Verwaltet die Platzierung von Items (Pickups) auf der Karte im Editor. * Aktiv wenn {@code input.activeLayer == LAYER_ITEMS}. + * Auswahl-Modus (LAYER_AUSWAHL): Item auswählen → wechselt zu LAYER_ITEMS_EDIT. + * LAYER_ITEMS_EDIT: Gizmo (Pfeile + Ringe) zum Verschieben/Rotieren. */ public class ItemPlacementState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(ItemPlacementState.class); + // ── Gizmo-Konstanten (identisch mit SceneObjectState) ──────────────────── + private static final ColorRGBA COL_X = new ColorRGBA(0.92f, 0.12f, 0.12f, 1f); + private static final ColorRGBA COL_Y = new ColorRGBA(0.12f, 0.88f, 0.12f, 1f); + private static final ColorRGBA COL_Z = new ColorRGBA(0.12f, 0.35f, 1.00f, 1f); + + private static final float SHAFT_LEN = 3.2f; + private static final float HEAD_LEN = 0.9f; + private static final float SHAFT_RADIUS = 0.06f; + private static final float HEAD_RADIUS = 0.22f; + private static final float PX_PER_WE = 0.15f; + private static final float RING_RADIUS = 4.5f; + private static final float RING_TUBE = 0.25f; + private static final float DEG_PER_PX = 0.8f; + + private static final String HIGHLIGHT_NAME = "selectionHighlight"; + private static final ColorRGBA HIGHLIGHT_COLOR = new ColorRGBA(0.3f, 0.8f, 1.0f, 1f); + + // ── Felder ──────────────────────────────────────────────────────────────── private final SharedInput input; private SimpleApplication app; private Camera cam; @@ -46,9 +73,16 @@ public class ItemPlacementState extends BaseAppState { private final Map itemDefs = new HashMap<>(); private Node itemRoot; private Node previewNode; - private int selectedItemIdx = -1; + private int selectedItemIdx = -1; private Node selectedItemNode = null; + // Gizmo + private Node gizmoNode; + private Node arrowX, arrowY, arrowZ; + private Node ringX, ringY, ringZ; + private int activeGizmo = -1; + private int activeRing = -1; + public ItemPlacementState(SharedInput input) { this.input = input; } @@ -66,7 +100,7 @@ public class ItemPlacementState extends BaseAppState { this.assets = app.getAssetManager(); this.rootNode = this.app.getRootNode(); - java.nio.file.Path itemDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("items"); + Path itemDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("items"); for (Item it : ItemIO.loadAll(itemDir)) { if (it.getItemId() != null) itemDefs.put(it.getItemId(), it); } @@ -80,6 +114,11 @@ public class ItemPlacementState extends BaseAppState { prev.setMaterial(pm); prev.getMesh().setMode(com.jme3.scene.Mesh.Mode.Lines); previewNode.attachChild(prev); + + gizmoNode = new Node("itemGizmo"); + buildGizmo(); + gizmoNode.setCullHint(Spatial.CullHint.Always); + rootNode.attachChild(gizmoNode); } @Override @@ -101,6 +140,7 @@ public class ItemPlacementState extends BaseAppState { for (PlacedItem pi : items) { Node n = buildItemNode(pi.itemId()); n.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z()); + applyRot(n, pi.rotX(), pi.rotY(), pi.rotZ()); itemRoot.attachChild(n); nodes.add(n); } @@ -113,7 +153,9 @@ public class ItemPlacementState extends BaseAppState { } @Override - protected void cleanup(Application app) {} + protected void cleanup(Application app) { + if (gizmoNode != null) gizmoNode.removeFromParent(); + } // ── Update ──────────────────────────────────────────────────────────────── @@ -125,11 +167,15 @@ public class ItemPlacementState extends BaseAppState { reloadFromDisk(); } - // Highlight entfernen sobald wir nicht mehr im Auswahl-Modus sind - if (selectedItemIdx >= 0 && input.activeLayer != SharedInput.LAYER_AUSWAHL) { + // Highlight entfernen sobald wir weder im Auswahl- noch im Edit-Modus sind + if (selectedItemIdx >= 0 + && input.activeLayer != SharedInput.LAYER_AUSWAHL + && input.activeLayer != SharedInput.LAYER_ITEMS_EDIT) { deselectItem(); } + updateGizmoVisibility(); + updatePreview(); SharedInput.ObjectClick click; @@ -142,6 +188,26 @@ public class ItemPlacementState extends BaseAppState { } } + // LAYER_ITEMS_EDIT: Gizmo-Klicks und Drags + if (input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) { + SharedInput.ObjectClick objClick; + while ((objClick = input.objectClickQueue.poll()) != null) { + handleItemGizmoClick(objClick); + } + boolean hadDrags = false; + SharedInput.ObjectDrag drag; + while ((drag = input.objectDragQueue.poll()) != null) { + handleItemGizmoDrag(drag); + hadDrags = true; + } + if (hadDrags) { + saveItems(); + } + if (selectedItemIdx >= 0) { + updateGizmoPosition(); + } + } + SharedInput.ObjectClick auswahlClick; while ((auswahlClick = input.auswahlClickQueue.poll()) != null) { handleAuswahlClick(auswahlClick); @@ -155,8 +221,19 @@ public class ItemPlacementState extends BaseAppState { input.onObjectDeleted.run(); } } + + if (input.itemEditRequest) { + input.itemEditRequest = false; + if (selectedItemIdx >= 0) { + applyItemEdit(selectedItemIdx, + input.itemEditX, input.itemEditY, input.itemEditZ, + input.itemEditRotX, input.itemEditRotY, input.itemEditRotZ); + } + } } + // ── Vorschau ────────────────────────────────────────────────────────────── + private void updatePreview() { if (input.activeLayer != SharedInput.LAYER_ITEMS || input.pendingItemId == null @@ -179,6 +256,8 @@ public class ItemPlacementState extends BaseAppState { previewNode.setCullHint(Spatial.CullHint.Inherit); } + // ── Platzierung ─────────────────────────────────────────────────────────── + private void handlePlace(SharedInput.ObjectClick click) { if (terrain == null || input.pendingItemId == null) return; float jx = click.screenX() * (float) input.viewportScaleX; @@ -188,30 +267,34 @@ public class ItemPlacementState extends BaseAppState { terrain.collideWith(ray, hits); if (hits.size() == 0) return; - Vector3f pt = hits.getClosestCollision().getContactPoint(); + Vector3f pt = hits.getClosestCollision().getContactPoint(); PlacedItem pi = PlacedItem.create(input.pendingItemId, pt.x, pt.y, pt.z); items.add(pi); Node n = buildItemNode(pi.itemId()); n.setLocalTranslation(pt.x, pt.y + 0.25f, pt.z); + applyRot(n, 0f, 0f, 0f); itemRoot.attachChild(n); nodes.add(n); try { PlacedItemIO.save(items); - log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", - pi.itemId(), pt.x, pt.y, pt.z); + log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", pi.itemId(), pt.x, pt.y, pt.z); } catch (IOException e) { log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage()); } } + // ── Auswahl-Klick (LAYER_AUSWAHL → Item-Picking) ───────────────────────── + private void handleAuswahlClick(SharedInput.ObjectClick click) { float jmeX = click.screenX() * (float) input.viewportScaleX; float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; Ray ray = screenToRay(jmeX, jmeY); CollisionResults hits = new CollisionResults(); itemRoot.collideWith(ray, hits); + log.info("[Auswahl] ItemPlacementState: Klick ({},{}), Treffer: {}, Items gesamt: {}", + (int) click.screenX(), (int) click.screenY(), hits.size(), items.size()); if (hits.size() > 0) { Spatial geo = hits.getClosestCollision().getGeometry(); Node parent = geo.getParent(); @@ -219,44 +302,362 @@ public class ItemPlacementState extends BaseAppState { parent = parent.getParent(); } if (parent != null) { - selectItem(nodes.indexOf(parent)); + int idx = nodes.indexOf(parent); + log.info("[Auswahl] ItemPlacementState: Item {} ausgewaehlt (idx={})", items.get(idx).itemId(), idx); + selectItem(idx); return; } } deselectItem(); } + // ── Gizmo-Klick (LAYER_ITEMS_EDIT) ─────────────────────────────────────── + + private void handleItemGizmoClick(SharedInput.ObjectClick click) { + if (selectedItemIdx < 0) return; + float jmeX = click.screenX() * (float) input.viewportScaleX; + float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; + Ray ray = screenToRay(jmeX, jmeY); + + if (input.objectEditTool == SharedInput.EDIT_TOOL_MOVE) { + int hit = pickGizmo(ray); + if (hit >= 0) { + activeGizmo = hit; + activeRing = -1; + return; + } + } else if (input.objectEditTool == SharedInput.EDIT_TOOL_ROTATE) { + int hit = pickRing(ray); + if (hit >= 0) { + activeRing = hit; + activeGizmo = -1; + return; + } + } + activeGizmo = -1; + activeRing = -1; + + // Klick außerhalb Gizmo: anderes Item oder leere Fläche? + CollisionResults hits = new CollisionResults(); + itemRoot.collideWith(ray, hits); + if (hits.size() > 0) { + Spatial geo = hits.getClosestCollision().getGeometry(); + // Highlight-Klone ignorieren + if (HIGHLIGHT_NAME.equals(geo.getParent() != null ? geo.getParent().getName() : "")) { + return; + } + Node parent = geo.getParent(); + while (parent != null && !nodes.contains(parent)) { + parent = parent.getParent(); + } + if (parent != null) { + int idx = nodes.indexOf(parent); + if (idx != selectedItemIdx) { + selectItem(idx); + } + return; + } + } + deselectItem(); + } + + // ── Gizmo-Drag (LAYER_ITEMS_EDIT) ──────────────────────────────────────── + + private void handleItemGizmoDrag(SharedInput.ObjectDrag drag) { + if (selectedItemIdx < 0) return; + switch (input.objectEditTool) { + case SharedInput.EDIT_TOOL_MOVE -> handleMoveDrag(drag); + case SharedInput.EDIT_TOOL_ROTATE -> handleRotateDrag(drag); + } + } + + private void handleMoveDrag(SharedInput.ObjectDrag drag) { + if (activeGizmo < 0 || selectedItemIdx < 0) return; + Vector3f arrowDir = getArrowWorldDir(activeGizmo); + Vector3f origin = gizmoNode.getWorldTranslation(); + Vector3f scrO = cam.getScreenCoordinates(origin); + Vector3f scrT = cam.getScreenCoordinates(origin.add(arrowDir)); + float sdx = scrT.x - scrO.x; + float sdy = -(scrT.y - scrO.y); + float screenLen2 = sdx * sdx + sdy * sdy; + float magnitude; + if (screenLen2 < 0.01f) { + magnitude = -drag.dy() * PX_PER_WE; + } else { + magnitude = (drag.dx() * sdx + drag.dy() * sdy) / screenLen2; + } + Vector3f delta = arrowDir.mult(magnitude); + + Node n = nodes.get(selectedItemIdx); + n.move(delta.x, delta.y, delta.z); + + PlacedItem pi = items.get(selectedItemIdx); + PlacedItem updated = pi.withTransform( + pi.x() + delta.x, pi.y() + delta.y, pi.z() + delta.z, + pi.rotX(), pi.rotY(), pi.rotZ()); + items.set(selectedItemIdx, updated); + publishSelection(selectedItemIdx); + updateGizmoPosition(); + } + + private void handleRotateDrag(SharedInput.ObjectDrag drag) { + if (activeRing < 0 || selectedItemIdx < 0) return; + + PlacedItem pi = items.get(selectedItemIdx); + Quaternion qObjY = new Quaternion().fromAngleAxis(pi.rotY(), Vector3f.UNIT_Y); + Vector3f rotAxis = switch (activeRing) { + case 0 -> qObjY.mult(new Vector3f(1, 0, 0)); + case 1 -> new Vector3f(0, 1, 0); + default -> qObjY.mult(new Vector3f(0, 0, 1)); + }; + + Vector3f center = gizmoNode.getWorldTranslation(); + Vector3f toCam = cam.getLocation().subtract(center).normalizeLocal(); + Vector3f tangent = rotAxis.cross(toCam); + float tLen = tangent.length(); + float angle; + if (tLen < 1e-4f) { + angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD; + } else { + tangent.divideLocal(tLen); + Vector3f scrC = cam.getScreenCoordinates(center); + Vector3f scrT = cam.getScreenCoordinates(center.add(tangent)); + float sdx = scrT.x - scrC.x; + float sdy = -(scrT.y - scrC.y); + float sl2 = sdx * sdx + sdy * sdy; + if (sl2 < 0.5f) { + angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD; + } else { + float proj = (drag.dx() * sdx + drag.dy() * sdy) / sl2; + angle = proj / RING_RADIUS; + } + } + + Node n = nodes.get(selectedItemIdx); + Quaternion deltaRot = new Quaternion().fromAngleAxis(angle, rotAxis); + n.setLocalRotation(deltaRot.mult(n.getLocalRotation())); + + float[] angles = n.getLocalRotation().toAngles(null); + PlacedItem updated = pi.withTransform(pi.x(), pi.y(), pi.z(), angles[0], angles[1], angles[2]); + items.set(selectedItemIdx, updated); + publishSelection(selectedItemIdx); + } + + // ── Selektion ───────────────────────────────────────────────────────────── + private void selectItem(int idx) { deselectItem(); selectedItemIdx = idx; selectedItemNode = nodes.get(idx); - Geometry highlight = new Geometry("item_sel", - new com.jme3.scene.shape.Box(0.28f, 0.28f, 0.28f)); - Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", new com.jme3.math.ColorRGBA(1f, 0.6f, 0.1f, 1f)); - mat.getAdditionalRenderState().setWireframe(true); - mat.getAdditionalRenderState().setDepthTest(false); - highlight.setMaterial(mat); - highlight.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent); - selectedItemNode.attachChild(highlight); - PlacedItem pi = items.get(idx); - input.selectedItemInfo = idx + "|" + pi.itemId() + "|" + pi.x() + "|" + pi.y() + "|" + pi.z(); - input.itemSelectionChanged = true; + addSelectionHighlight(idx); + publishSelection(idx); + input.activeLayer = SharedInput.LAYER_ITEMS_EDIT; + updateGizmoPosition(); + updateGizmoVisibility(); } private void deselectItem() { if (selectedItemNode != null) { - Spatial h = selectedItemNode.getChild("item_sel"); - if (h != null) h.removeFromParent(); + removeSelectionHighlight(selectedItemIdx); selectedItemNode = null; } + activeGizmo = -1; + activeRing = -1; if (selectedItemIdx >= 0) { selectedItemIdx = -1; input.selectedItemInfo = null; input.itemSelectionChanged = true; } + if (input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) { + input.activeLayer = SharedInput.LAYER_AUSWAHL; + } + updateGizmoVisibility(); } + private void publishSelection(int idx) { + PlacedItem pi = items.get(idx); + input.selectedItemInfo = idx + "|" + pi.itemId() + + "|" + pi.x() + "|" + pi.y() + "|" + pi.z() + + "|" + pi.rotX() + "|" + pi.rotY() + "|" + pi.rotZ(); + input.itemSelectionChanged = true; + } + + // ── Highlight ───────────────────────────────────────────────────────────── + + private void addSelectionHighlight(int idx) { + Node itemNode = nodes.get(idx); + removeSelectionHighlight(idx); + Node highlight = new Node(HIGHLIGHT_NAME); + for (Spatial child : new ArrayList<>(itemNode.getChildren())) { + if (child == gizmoNode) continue; + Spatial clone = child.clone(); + applySelectionMaterial(clone); + highlight.attachChild(clone); + } + itemNode.attachChild(highlight); + } + + private void removeSelectionHighlight(int idx) { + if (idx < 0 || idx >= nodes.size()) return; + Spatial h = nodes.get(idx).getChild(HIGHLIGHT_NAME); + if (h != null) h.removeFromParent(); + } + + private void applySelectionMaterial(Spatial s) { + if (s instanceof Geometry geo) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", HIGHLIGHT_COLOR); + mat.getAdditionalRenderState().setWireframe(true); + mat.getAdditionalRenderState().setDepthTest(false); + geo.setMaterial(mat); + geo.setQueueBucket(RenderQueue.Bucket.Transparent); + } else if (s instanceof Node n) { + for (Spatial child : new ArrayList<>(n.getChildren())) { + applySelectionMaterial(child); + } + } + } + + // ── Gizmo-Bau ───────────────────────────────────────────────────────────── + + private void buildGizmo() { + gizmoNode.detachAllChildren(); + + arrowX = buildArrowNode(COL_X, "arrowX", + new Vector3f(1, 0, 0), + new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y)); + arrowY = buildArrowNode(COL_Y, "arrowY", + new Vector3f(0, 1, 0), + new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X)); + arrowZ = buildArrowNode(COL_Z, "arrowZ", + new Vector3f(0, 0, -1), + new Quaternion()); + gizmoNode.attachChild(arrowX); + gizmoNode.attachChild(arrowY); + gizmoNode.attachChild(arrowZ); + + ringX = buildRingNode(COL_X, "ringX", + new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y)); + ringY = buildRingNode(COL_Y, "ringY", + new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X)); + ringZ = buildRingNode(COL_Z, "ringZ", + new Quaternion()); + gizmoNode.attachChild(ringX); + gizmoNode.attachChild(ringY); + gizmoNode.attachChild(ringZ); + } + + private Node buildArrowNode(ColorRGBA color, String name, Vector3f dir, Quaternion geomRot) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + mat.getAdditionalRenderState().setDepthTest(false); + + Geometry shaft = new Geometry(name + "_shaft", + new Cylinder(2, 10, SHAFT_RADIUS, SHAFT_LEN, false)); + shaft.setLocalRotation(geomRot); + shaft.setLocalTranslation(dir.mult(SHAFT_LEN * 0.5f)); + shaft.setMaterial(mat); + shaft.setQueueBucket(RenderQueue.Bucket.Translucent); + + Geometry head = new Geometry(name + "_head", + new Cylinder(2, 10, HEAD_RADIUS, HEAD_LEN, true)); + head.setLocalRotation(geomRot); + head.setLocalTranslation(dir.mult(SHAFT_LEN + HEAD_LEN * 0.5f)); + head.setMaterial(mat); + head.setQueueBucket(RenderQueue.Bucket.Translucent); + + Node node = new Node(name); + node.attachChild(shaft); + node.attachChild(head); + return node; + } + + private Node buildRingNode(ColorRGBA color, String name, Quaternion nodeRot) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + mat.getAdditionalRenderState().setDepthTest(false); + + Geometry ring = new Geometry(name + "_ring", + new Torus(48, 10, RING_TUBE, RING_RADIUS)); + ring.setMaterial(mat); + ring.setQueueBucket(RenderQueue.Bucket.Translucent); + + Node node = new Node(name); + node.setLocalRotation(nodeRot); + node.attachChild(ring); + return node; + } + + // ── Gizmo-Position / -Sichtbarkeit ──────────────────────────────────────── + + private void updateGizmoPosition() { + if (selectedItemIdx < 0) return; + Node n = nodes.get(selectedItemIdx); + n.updateGeometricState(); + BoundingVolume bv = n.getWorldBound(); + Vector3f center; + float radius; + if (bv instanceof BoundingSphere bs) { + center = bs.getCenter().clone(); + radius = bs.getRadius(); + } else if (bv instanceof BoundingBox bb) { + center = bb.getCenter().clone(); + radius = bb.getExtent(null).length(); + } else { + center = n.getWorldTranslation().clone(); + radius = 0.5f; + } + gizmoNode.setLocalTranslation(center); + gizmoNode.setLocalScale(Math.max(0.01f, radius / RING_RADIUS)); + + float objRotY = items.get(selectedItemIdx).rotY(); + Quaternion qObjY = new Quaternion().fromAngleAxis(objRotY, Vector3f.UNIT_Y); + ringX.setLocalRotation(qObjY.mult(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y))); + ringY.setLocalRotation(qObjY.mult(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X))); + ringZ.setLocalRotation(qObjY.mult(new Quaternion())); + } + + private void updateGizmoVisibility() { + if (gizmoNode == null) return; + boolean sel = selectedItemIdx >= 0 && input.activeLayer == SharedInput.LAYER_ITEMS_EDIT; + boolean move = sel && input.objectEditTool == SharedInput.EDIT_TOOL_MOVE; + boolean rotate = sel && input.objectEditTool == SharedInput.EDIT_TOOL_ROTATE; + + Spatial.CullHint showArrows = move ? Spatial.CullHint.Inherit : Spatial.CullHint.Always; + Spatial.CullHint showRings = rotate ? Spatial.CullHint.Inherit : Spatial.CullHint.Always; + arrowX.setCullHint(showArrows); arrowY.setCullHint(showArrows); arrowZ.setCullHint(showArrows); + ringX.setCullHint(showRings); ringY.setCullHint(showRings); ringZ.setCullHint(showRings); + gizmoNode.setCullHint((move || rotate) ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + } + + // ── Gizmo-Picking ───────────────────────────────────────────────────────── + + private int pickGizmo(Ray ray) { + CollisionResults hits = new CollisionResults(); + arrowX.collideWith(ray, hits); if (hits.size() > 0) return 0; hits.clear(); + arrowY.collideWith(ray, hits); if (hits.size() > 0) return 1; hits.clear(); + arrowZ.collideWith(ray, hits); if (hits.size() > 0) return 2; + return -1; + } + + private int pickRing(Ray ray) { + CollisionResults hits = new CollisionResults(); + ringX.collideWith(ray, hits); if (hits.size() > 0) return 0; hits.clear(); + ringY.collideWith(ray, hits); if (hits.size() > 0) return 1; hits.clear(); + ringZ.collideWith(ray, hits); if (hits.size() > 0) return 2; + return -1; + } + + private Vector3f getArrowWorldDir(int idx) { + return switch (idx) { + case 0 -> new Vector3f(1, 0, 0); + case 1 -> new Vector3f(0, 1, 0); + default -> new Vector3f(0, 0, -1); + }; + } + + // ── Edit / Löschen ──────────────────────────────────────────────────────── + private void removeItem(int idx) { itemRoot.detachChild(nodes.get(idx)); items.remove(idx); @@ -268,8 +669,39 @@ public class ItemPlacementState extends BaseAppState { } } + private void applyItemEdit(int idx, float x, float y, float z, float rotX, float rotY, float rotZ) { + PlacedItem updated = items.get(idx).withTransform(x, y, z, rotX, rotY, rotZ); + items.set(idx, updated); + + Node n = nodes.get(idx); + n.setLocalTranslation(x, y + 0.25f, z); + applyRot(n, rotX, rotY, rotZ); + + publishSelection(idx); + + try { + PlacedItemIO.save(items); + } catch (IOException e) { + log.warn("[ItemPlacement] Speichern nach Edit fehlgeschlagen: {}", e.getMessage()); + } + } + + private void saveItems() { + try { + PlacedItemIO.save(items); + } catch (IOException e) { + log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage()); + } + } + // ── Hilfsmethoden ──────────────────────────────────────────────────────── + private static void applyRot(Node n, float rotX, float rotY, float rotZ) { + Quaternion q = new Quaternion(); + q.fromAngles(rotX, rotY, rotZ); + n.setLocalRotation(q); + } + private Node buildItemNode(String itemId) { Node n = new Node("item_" + itemId); n.setUserData("itemId", itemId); diff --git a/blight-editor/src/main/java/de/blight/editor/state/LightState.java b/blight-editor/src/main/java/de/blight/editor/state/LightState.java index cd1ce89..0a8b089 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/LightState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/LightState.java @@ -21,7 +21,7 @@ import de.blight.editor.SharedInput; import java.util.ArrayList; import java.util.List; -public class LightState extends BaseAppState { +public class LightState extends AuswahlMarkerState { // ── Glühbirnen-Geometrie ────────────────────────────────────────────────── private static final float BULB_RADIUS = 0.38f; @@ -37,35 +37,19 @@ public class LightState extends BaseAppState { private static final ColorRGBA SEL_COLOR = new ColorRGBA(1f, 1f, 0f, 1f); private static final ColorRGBA SOCKET_COLOR = new ColorRGBA(0.22f, 0.22f, 0.22f, 1f); - private final SharedInput input; - private SimpleApplication app; - private Camera cam; - private AssetManager assets; - private Node rootNode; - private TerrainQuad terrain; - // parallel lists: lights[i] <-> markers[i] <-> pointLights[i] private final List lights = new ArrayList<>(); - private final List markers = new ArrayList<>(); private final List pointLights = new ArrayList<>(); private int selectedIdx = -1; private List pendingLights = null; public LightState(SharedInput input) { - this.input = input; + super(input); } // ── Lifecycle ───────────────────────────────────────────────────────────── - @Override - protected void initialize(Application application) { - app = (SimpleApplication) application; - cam = app.getCamera(); - assets = app.getAssetManager(); - rootNode = app.getRootNode(); - } - @Override protected void cleanup(Application application) { clearAll(); @@ -80,7 +64,11 @@ public class LightState extends BaseAppState { } @Override protected void onDisable() {} - public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; } + @Override + protected void onMarkerSelected(int idx) { + selectLight(idx); + input.auswahlLightSelected = true; + } // ── Update ──────────────────────────────────────────────────────────────── @@ -153,29 +141,6 @@ public class LightState extends BaseAppState { selectLight(lights.size() - 1); } - private void handleAuswahlClick(SharedInput.ObjectClick click) { - float jmeX = click.screenX() * (float) input.viewportScaleX; - float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; - Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); - Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); - Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); - int hit = pickMarker(ray); - if (hit >= 0) { - selectLight(hit); - input.auswahlLightSelected = true; - } - } - - /** Trifft den Glow-Halo oder den Bulb irgendeines Markers. */ - private int pickMarker(Ray ray) { - for (int i = 0; i < markers.size(); i++) { - CollisionResults res = new CollisionResults(); - markers.get(i).collideWith(ray, res); - if (res.size() > 0) return i; - } - return -1; - } - // ── Selection ───────────────────────────────────────────────────────────── private void selectLight(int idx) { 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 b535723..7fc4b9d 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 @@ -15,6 +15,7 @@ import com.jme3.light.DirectionalLight; import com.jme3.light.LightProbe; import com.jme3.light.PointLight; import com.jme3.material.Material; +import com.jme3.material.MatParam; import com.jme3.math.*; import com.jme3.renderer.Camera; import com.jme3.renderer.RenderManager; @@ -31,6 +32,7 @@ import com.jme3.anim.SkinningControl; import com.jme3.effect.ParticleEmitter; import com.jme3.effect.ParticleMesh; import com.jme3.material.RenderState; +import com.jme3.texture.Texture; import com.jme3.util.BufferUtils; import de.blight.editor.SharedInput; import de.blight.editor.util.ImpostorUtil; @@ -1093,6 +1095,17 @@ public class ModelEditorState extends BaseAppState { VertexBuffer vb = mesh.getBuffer(type); if (vb != null) vb.setUpdateNeeded(); } + // Texturen als update-needed markieren – verhindert schwarzes Rendering + // wenn die GPU-Textur-Zustände durch vorherige Thumbnail-Renders beschädigt wurden. + Material mat = g.getMaterial(); + if (mat != null) { + for (MatParam param : mat.getParams()) { + Object val = param.getValue(); + if (val instanceof Texture tex && tex.getImage() != null) { + tex.getImage().setUpdateNeeded(); + } + } + } } else if (s instanceof Node n) { for (Spatial child : n.getChildren()) markBuffersUpdateNeeded(child); } @@ -1520,20 +1533,33 @@ public class ModelEditorState extends BaseAppState { input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); } - /** Zeichnet / aktualisiert den 15cm-Grip-Gizmo (orangefarbener Pfeil). */ + /** Zeichnet / aktualisiert den Grip-Gizmo: 15cm orange (Waffe) oder 28cm cyan (Schild/Unterarm). */ 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 + boolean shieldMode = input.gripEditorIsShield; + + // Waffe: 15cm orange | Schild: 28cm cyan + float shaftLen = shieldMode ? 0.24f : 0.12f; + float shaftRad = shieldMode ? 0.010f : 0.008f; + float headRad = shieldMode ? 0.026f : 0.022f; + float headLen = shieldMode ? 0.04f : 0.03f; + + ColorRGBA shaftColor = shieldMode + ? new ColorRGBA(0f, 0.75f, 0.9f, 1f) + : new ColorRGBA(1f, 0.55f, 0f, 1f); + ColorRGBA headColor = shieldMode + ? new ColorRGBA(0f, 0.5f, 0.75f, 1f) + : new ColorRGBA(1f, 0.3f, 0f, 1f); + ColorRGBA axisColor = shieldMode + ? new ColorRGBA(0.6f, 1f, 0.6f, 1f) + : new ColorRGBA(1f, 0.8f, 0.1f, 1f); Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", new ColorRGBA(1f, 0.55f, 0f, 1f)); + mat.setColor("Color", shaftColor); Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - matHead.setColor("Color", new ColorRGBA(1f, 0.3f, 0f, 1f)); + matHead.setColor("Color", headColor); Node group = new Node("gripGroup"); group.setLocalTranslation(gripPosX, gripPosY, gripPosZ); @@ -1545,8 +1571,8 @@ public class ModelEditorState extends BaseAppState { 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)); + float markerSize = shieldMode ? 0.018f : 0.015f; + Geometry marker = new Geometry("gMarker", new Box(markerSize, markerSize, markerSize)); marker.setMaterial(mat); // Schaft entlang +Z @@ -1562,14 +1588,12 @@ public class ModelEditorState extends BaseAppState { headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X); head.setLocalRotation(headRot); - // Griffachsen-Zylinder: runder Zylinder senkrecht zum Schaft (+Y), - // zeigt die Roll-Richtung (ersetzt die frühere flache Klingenflosse). - // JME3-Cylinder liegt standardmäßig auf +Z → -90° um X dreht ihn nach +Y. - Material matFin = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - matFin.setColor("Color", new ColorRGBA(1f, 0.8f, 0.1f, 1f)); - float axisLen = 0.06f; + // Querachsen-Zylinder entlang +Y: zeigt Roll-/Orientierungsachse + Material matAxis = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + matAxis.setColor("Color", axisColor); + float axisLen = shieldMode ? 0.08f : 0.06f; Geometry gripAxis = new Geometry("gGripAxis", new Cylinder(4, 8, 0.006f, axisLen, true)); - gripAxis.setMaterial(matFin); + gripAxis.setMaterial(matAxis); Quaternion axisRot = new Quaternion(); axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X); gripAxis.setLocalRotation(axisRot); diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedSoundState.java b/blight-editor/src/main/java/de/blight/editor/state/PlacedSoundState.java index 0bf2f43..65e2219 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PlacedSoundState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PlacedSoundState.java @@ -19,7 +19,7 @@ import de.blight.editor.SharedInput; import java.util.ArrayList; import java.util.List; -public class PlacedSoundState extends BaseAppState { +public class PlacedSoundState extends AuswahlMarkerState { private static final float MARKER_RADIUS = 0.30f; private static final float GLOW_RADIUS = MARKER_RADIUS * 2.4f; @@ -31,34 +31,18 @@ public class PlacedSoundState extends BaseAppState { private static final float POS_EPSILON = 0.05f; - private final SharedInput input; - private SimpleApplication app; - private Camera cam; - private AssetManager assets; - private Node rootNode; - private TerrainQuad terrain; - // parallel lists: sounds[i] <-> markers[i] - private final List sounds = new ArrayList<>(); - private final List markers = new ArrayList<>(); + private final List sounds = new ArrayList<>(); private int selectedIdx = -1; private List pendingSounds = null; public PlacedSoundState(SharedInput input) { - this.input = input; + super(input); } // ── Lifecycle ───────────────────────────────────────────────────────────── - @Override - protected void initialize(Application application) { - app = (SimpleApplication) application; - cam = app.getCamera(); - assets = app.getAssetManager(); - rootNode = app.getRootNode(); - } - @Override protected void cleanup(Application application) { clearAll(); @@ -74,7 +58,11 @@ public class PlacedSoundState extends BaseAppState { @Override protected void onDisable() {} - public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; } + @Override + protected void onMarkerSelected(int idx) { + selectSound(idx); + input.auswahlSoundSelected = true; + } // ── Update ──────────────────────────────────────────────────────────────── @@ -145,28 +133,6 @@ public class PlacedSoundState extends BaseAppState { selectSound(sounds.size() - 1); } - private void handleAuswahlClick(SharedInput.ObjectClick click) { - float jmeX = click.screenX() * (float) input.viewportScaleX; - float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; - Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); - Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); - Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); - int hit = pickMarker(ray); - if (hit >= 0) { - selectSound(hit); - input.auswahlSoundSelected = true; - } - } - - private int pickMarker(Ray ray) { - for (int i = 0; i < markers.size(); i++) { - CollisionResults res = new CollisionResults(); - markers.get(i).collideWith(ray, res); - if (res.size() > 0) return i; - } - return -1; - } - // ── Selection ───────────────────────────────────────────────────────────── private void selectSound(int idx) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java index 22eb173..76e4526 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java @@ -707,6 +707,9 @@ public class SceneObjectState extends BaseAppState { } if (closest == null) { deselectAll(); return; } int idx = findObjectIndexByNode(closest.getGeometry()); + log.info("[SceneObject] Klick ({},{}): Treffer='{}', idx={}, layer={}", + (int)click.screenX(), (int)click.screenY(), + closest.getGeometry().getName(), idx, input.activeLayer); if (idx >= 0) { selectObject(idx, click.shift()); // Vorgefertigte Objekte (externe Modelle + Gruppen) können nicht sub-selektiert werden. @@ -723,6 +726,11 @@ public class SceneObjectState extends BaseAppState { } return; } + // Ray traf etwas in objectRoot, aber kein bekanntes Objekt — nicht an + // andere Layer-Queues weiterleiten (verhindert versehentliches Emitter-Select). + log.info("[SceneObject] Geometrie '{}' in objectRoot aber nicht in objNodes — Klick ignoriert", + closest.getGeometry().getName()); + return; } // 3. Terrain-Treffer – im Edit-Modus: zurück zu Place-Modus, deselektieren @@ -733,6 +741,8 @@ public class SceneObjectState extends BaseAppState { } // Auswahl-Modus: kein Objekt angeklickt → deselektieren + Zonen/Items prüfen lassen if (input.activeLayer == SharedInput.LAYER_AUSWAHL) { + log.info("[SceneObject] LAYER_AUSWAHL-Broadcast bei Klick ({},{}): kein SceneObject getroffen, {} Objekte im objectRoot", + (int)click.screenX(), (int)click.screenY(), objNodes.size()); deselectAll(); float sx = click.screenX(), sy = click.screenY(); input.areaClickQueue.offer(new SharedInput.AreaClick(sx, sy, false)); 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 0b9a8ed..af82d97 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 @@ -344,6 +344,7 @@ public class ContentController { ctx.setCurrentTool().accept("itemEditor"); ToolBar tb = makeSingleWindowToolBar("Item-Manager"); ctx.setMainToolbar().accept(tb); + ctx.setLeftPanel().accept(buildContentTreePanel()); de.blight.editor.ui.ItemEditorView view = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); wireItemView(view); @@ -960,7 +961,7 @@ public class ContentController { private void wireItemView(de.blight.editor.ui.ItemEditorView view) { view.setOnIdChanged(this::refreshContentItemTree); - view.setGripEditorLauncher((modelPath, currentGrip, onSave) -> { + view.setGripEditorLauncher((modelPath, currentGrip, isShield, onSave) -> { String itemId = view.getCurrentItemId(); gripEditorController.setOnReturn(() -> { if (dualWindowMode) { @@ -970,7 +971,7 @@ public class ContentController { switchToItemEditor(itemId); } }); - gripEditorController.open(modelPath, currentGrip, onSave); + gripEditorController.open(modelPath, currentGrip, isShield, onSave); }); } 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 index e970bde..08664be 100644 --- 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 @@ -27,6 +27,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { private Consumer pendingOnSave; private Runnable onReturn; private boolean suppressSpinnerEvents = false; + private boolean shieldMode = false; public void setOnReturn(Runnable r) { this.onReturn = r; } @@ -37,11 +38,13 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { // ── GripEditorLauncher ──────────────────────────────────────────────────── @Override - public void open(String modelPath, ItemGrip currentGrip, Consumer onSave) { + public void open(String modelPath, ItemGrip currentGrip, boolean isShield, Consumer onSave) { pendingOnSave = onSave; + shieldMode = isShield; SharedInput input = ctx.input(); - input.gripEditorActive = true; + input.gripEditorActive = true; + input.gripEditorIsShield = isShield; // Grip-Werte aus Item übernehmen (oder Defaults) ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip(); @@ -63,7 +66,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { ctx.setCenterView().accept(ctx.viewportSupplier().get()); ctx.setCurrentTool().accept("gripEditor"); ctx.setMainToolbar().accept(buildToolbar()); - ctx.setRightPanel().accept(buildPanel(g)); + ctx.setRightPanel().accept(buildPanel(g, isShield)); ctx.setLeftPanel().accept(null); ctx.setBottomPanel().accept(null); } @@ -82,7 +85,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { // ── Panel ───────────────────────────────────────────────────────────────── - private VBox buildPanel(ItemGrip g) { + private VBox buildPanel(ItemGrip g, boolean isShield) { 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"); @@ -123,7 +126,9 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { row("Y:", rotYSpin), row("Z:", rotZSpin), new Separator(), - styledHint("Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."), + styledHint(isShield + ? "Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge." + : "Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."), new Separator(), saveBtn, cancelBtn @@ -156,7 +161,9 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { grip.setRotY(rotYSpin.getValue().floatValue()); grip.setRotZ(rotZSpin.getValue().floatValue()); - ctx.input().gripEditorActive = false; + SharedInput input = ctx.input(); + input.gripEditorActive = false; + input.gripEditorIsShield = false; Consumer save = pendingOnSave; Runnable ret = onReturn; @@ -168,7 +175,8 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { } private void onCancel() { - ctx.input().gripEditorActive = false; + ctx.input().gripEditorActive = false; + ctx.input().gripEditorIsShield = false; Runnable ret = onReturn; pendingOnSave = null; diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java index 37b8fbc..7afec11 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java @@ -386,7 +386,8 @@ public class ViewportController { private boolean isObjectMode() { return input.activeLayer == SharedInput.LAYER_OBJECTS || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT - || input.activeLayer == SharedInput.LAYER_AUSWAHL; + || input.activeLayer == SharedInput.LAYER_AUSWAHL + || input.activeLayer == SharedInput.LAYER_ITEMS_EDIT; } private void startEditTimer() { diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/world/WorldPanelController.java b/blight-editor/src/main/java/de/blight/editor/toolset/world/WorldPanelController.java index efd13f5..1f31112 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/world/WorldPanelController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/world/WorldPanelController.java @@ -5,8 +5,10 @@ import de.blight.editor.SharedInput; import de.blight.editor.ToolContext; import de.blight.editor.toolset.EditorUiHelper; import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.control.*; import javafx.scene.layout.*; +import javafx.util.StringConverter; /** * Owns the world-zone side panels: @@ -41,15 +43,96 @@ public class WorldPanelController { auswahlDynamicContent.getChildren().add(noSel); return; } + // Format: "idx|itemId|x|y|z|rotX|rotY|rotZ" String[] parts = info.split("\\|"); String itemId = parts.length > 1 ? parts[1] : "?"; + float cx = parts.length > 2 ? parseF(parts[2]) : 0f; + float cy = parts.length > 3 ? parseF(parts[3]) : 0f; + float cz = parts.length > 4 ? parseF(parts[4]) : 0f; + float cRotY = parts.length > 6 ? parseF(parts[6]) : 0f; + Label idLabel = new Label("Item: " + itemId); idLabel.setStyle("-fx-font-weight: bold;"); + + Spinner xSpin = posSpin(cx); + Spinner ySpin = posSpin(cy); + Spinner zSpin = posSpin(cz); + Spinner rotYSpin = rotSpin(cRotY); + + SharedInput input = ctx.input(); + Runnable sendEdit = () -> { + input.itemEditX = xSpin.getValue().floatValue(); + input.itemEditY = ySpin.getValue().floatValue(); + input.itemEditZ = zSpin.getValue().floatValue(); + input.itemEditRotY = rotYSpin.getValue().floatValue(); + input.itemEditRequest = true; + }; + xSpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + ySpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + zSpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + rotYSpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + Button deleteBtn = new Button("Löschen"); deleteBtn.setMaxWidth(Double.MAX_VALUE); deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;"); - deleteBtn.setOnAction(e -> ctx.input().deleteSelectedItemRequested = true); - auswahlDynamicContent.getChildren().addAll(idLabel, deleteBtn); + deleteBtn.setOnAction(e -> input.deleteSelectedItemRequested = true); + + auswahlDynamicContent.getChildren().addAll( + idLabel, + new Separator(), + EditorUiHelper.sectionTitle("Position"), + spinRow("X:", xSpin), spinRow("Y:", ySpin), spinRow("Z:", zSpin), + new Separator(), + EditorUiHelper.sectionTitle("Rotation"), + spinRow("Y°:", rotYSpin), + new Separator(), + deleteBtn + ); + } + + private static float parseF(String s) { + try { return Float.parseFloat(s); } catch (NumberFormatException e) { return 0f; } + } + + private static Spinner posSpin(float initial) { + SpinnerValueFactory.DoubleSpinnerValueFactory vf = + new SpinnerValueFactory.DoubleSpinnerValueFactory(-10000, 10000, initial, 0.5); + vf.setConverter(new StringConverter<>() { + @Override public String toString(Double v) { return v == null ? "0" : String.format("%.2f", v); } + @Override public Double fromString(String s) { + try { return Double.parseDouble(s.replace(",", ".")); } + catch (NumberFormatException e) { return vf.getValue(); } + } + }); + Spinner sp = new Spinner<>(vf); + sp.setEditable(true); + sp.setMaxWidth(Double.MAX_VALUE); + return sp; + } + + private static Spinner rotSpin(float initial) { + SpinnerValueFactory.DoubleSpinnerValueFactory vf = + new SpinnerValueFactory.DoubleSpinnerValueFactory(-360, 360, initial, 5.0); + vf.setConverter(new StringConverter<>() { + @Override public String toString(Double v) { return v == null ? "0" : String.format("%.1f°", v); } + @Override public Double fromString(String s) { + try { return Double.parseDouble(s.replace("°", "").replace(",", ".")); } + catch (NumberFormatException e) { return vf.getValue(); } + } + }); + Spinner sp = new Spinner<>(vf); + sp.setEditable(true); + sp.setMaxWidth(Double.MAX_VALUE); + return sp; + } + + private static HBox spinRow(String labelText, Spinner spinner) { + Label lbl = new Label(labelText); + lbl.setMinWidth(28); + HBox.setHgrow(spinner, Priority.ALWAYS); + HBox row = new HBox(6, lbl, spinner); + row.setAlignment(Pos.CENTER_LEFT); + return row; } public void updateZonenPanel(String kind) { @@ -321,6 +404,108 @@ public class WorldPanelController { return panel; } + public VBox buildAuswahlItemEditPanel(String info) { + String[] parts = info != null ? info.split("\\|") : new String[0]; + String itemId = parts.length > 1 ? parts[1] : "?"; + float cx = parts.length > 2 ? parseF(parts[2]) : 0f; + float cy = parts.length > 3 ? parseF(parts[3]) : 0f; + float cz = parts.length > 4 ? parseF(parts[4]) : 0f; + // Format: idx|itemId|x|y|z|rotX|rotY|rotZ (rotX/rotZ in radians → convert to degrees) + float cRotX = parts.length > 5 ? (float) Math.toDegrees(parseF(parts[5])) : 0f; + float cRotY = parts.length > 6 ? (float) Math.toDegrees(parseF(parts[6])) : 0f; + float cRotZ = parts.length > 7 ? (float) Math.toDegrees(parseF(parts[7])) : 0f; + + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + Label titleLbl = new Label("Item-Pickup"); + titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + Label idLbl = new Label(itemId); + idLbl.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); + + // Werkzeug-Umschalter Verschieben / Rotieren + SharedInput input = ctx.input(); + Button moveBtn = new Button("Verschieben"); + Button rotateBtn = new Button("Rotieren"); + moveBtn.setMaxWidth(Double.MAX_VALUE); + rotateBtn.setMaxWidth(Double.MAX_VALUE); + final String TOOL_ON = "-fx-background-color: #2980b9; -fx-text-fill: white; -fx-font-weight: bold;"; + final String TOOL_OFF = "-fx-background-color: #bdc3c7; -fx-text-fill: #555;"; + Runnable updateToolStyle = () -> { + boolean mv = input.objectEditTool == SharedInput.EDIT_TOOL_MOVE; + moveBtn.setStyle(mv ? TOOL_ON : TOOL_OFF); + rotateBtn.setStyle(mv ? TOOL_OFF : TOOL_ON); + }; + updateToolStyle.run(); + moveBtn.setOnAction(e -> { + input.objectEditTool = SharedInput.EDIT_TOOL_MOVE; + updateToolStyle.run(); + }); + rotateBtn.setOnAction(e -> { + input.objectEditTool = SharedInput.EDIT_TOOL_ROTATE; + updateToolStyle.run(); + }); + HBox toolRow = new HBox(6, moveBtn, rotateBtn); + toolRow.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(moveBtn, Priority.ALWAYS); + HBox.setHgrow(rotateBtn, Priority.ALWAYS); + + Spinner xSpin = posSpin(cx); + Spinner ySpin = posSpin(cy); + Spinner zSpin = posSpin(cz); + Spinner rotXSpin = rotSpin(cRotX); + Spinner rotYSpin = rotSpin(cRotY); + Spinner rotZSpin = rotSpin(cRotZ); + + Runnable sendEdit = () -> { + input.itemEditX = xSpin.getValue().floatValue(); + input.itemEditY = ySpin.getValue().floatValue(); + input.itemEditZ = zSpin.getValue().floatValue(); + input.itemEditRotX = (float) Math.toRadians(rotXSpin.getValue()); + input.itemEditRotY = (float) Math.toRadians(rotYSpin.getValue()); + input.itemEditRotZ = (float) Math.toRadians(rotZSpin.getValue()); + input.itemEditRequest = true; + }; + xSpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + ySpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + zSpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + rotXSpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + rotYSpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + rotZSpin.valueProperty().addListener((o, a, b) -> sendEdit.run()); + + Button deleteBtn = new Button("Loeschen"); + deleteBtn.setMaxWidth(Double.MAX_VALUE); + deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white; -fx-font-weight: bold;"); + deleteBtn.setOnAction(e -> input.deleteSelectedItemRequested = true); + + inner.getChildren().addAll( + titleLbl, idLbl, new Separator(), + toolRow, + new Separator(), + EditorUiHelper.sectionTitle("Position"), + spinRow("X:", xSpin), spinRow("Y:", ySpin), spinRow("Z:", zSpin), + new Separator(), + EditorUiHelper.sectionTitle("Rotation"), + spinRow("X°:", rotXSpin), spinRow("Y°:", rotYSpin), spinRow("Z°:", rotZSpin), + new Separator(), + deleteBtn, + new Separator(), + EditorUiHelper.styledHint("Klick auf leere Flaeche"), + EditorUiHelper.styledHint(" → Abwaehlen") + ); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + public VBox buildWegePanel() { VBox inner = new VBox(8); inner.setPadding(new Insets(10)); diff --git a/blight-editor/src/main/java/de/blight/editor/ui/AreaEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/AreaEditorView.java index b518d34..4b58b8e 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/AreaEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/AreaEditorView.java @@ -14,7 +14,6 @@ import javafx.scene.layout.*; import javafx.util.Duration; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.*; public class AreaEditorView extends BorderPane { @@ -234,50 +233,11 @@ public class AreaEditorView extends BorderPane { // ── Localisation defaults ────────────────────────────────────────────────── - private void writeDefaultLocalisation(String fullId) { + private static void writeDefaultLocalisation(String fullId) { if (fullId == null || fullId.isBlank()) return; String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId; if (shortId.isBlank()) return; - String key = fullId + ".name"; - String defaultName = EditorNaming.toDisplayName(shortId); - - Path langSrc = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang"); - Path[] langMirrors = { - ProjectRoot.resolve("blight-lang", "bin", "main", "lang"), - ProjectRoot.resolve("blight-lang", "build", "resources", "main", "lang"), - }; - for (String locale : new String[]{"de", "en"}) { - Path src = langSrc.resolve("messages_" + locale + ".properties"); - try { - writeOrUpdateKey(src, key, defaultName); - for (Path mirrorDir : langMirrors) { - Path mirror = mirrorDir.resolve("messages_" + locale + ".properties"); - if (Files.exists(mirror)) { - Files.copy(src, mirror, StandardCopyOption.REPLACE_EXISTING); - } - } - } catch (IOException e) { - // non-critical - } - } - } - - private static void writeOrUpdateKey(Path file, String key, String value) throws IOException { - if (!Files.exists(file)) return; - String needle = key + "="; - String needleSpace = key + " ="; - java.util.List lines = new java.util.ArrayList<>(Files.readAllLines(file, StandardCharsets.UTF_8)); - boolean found = false; - for (int i = 0; i < lines.size(); i++) { - if (lines.get(i).stripLeading().startsWith(needle) || lines.get(i).stripLeading().startsWith(needleSpace)) { - lines.set(i, key + "=" + value); - found = true; - break; - } - } - if (!found) lines.add(key + "=" + value); - Files.writeString(file, String.join("\n", lines) + "\n", - StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + LangDefaults.writeOrUpdate(fullId + ".name", EditorNaming.toDisplayName(shortId)); } // ── Helpers ─────────────────────────────────────────────────────────────── 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 332035f..2ca9101 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 @@ -48,6 +48,7 @@ public class ItemEditorView extends BorderPane { private Spinner goldSpinner; private Spinner damageSpinner; private VBox weaponSection; + private VBox shieldSection; private TextField modelRefField; private VBox formContainer; private CheckBox consumableCheck; @@ -55,7 +56,7 @@ public class ItemEditorView extends BorderPane { private VBox effectsRows; public interface GripEditorLauncher { - void open(String modelPath, ItemGrip currentGrip, java.util.function.Consumer onSave); + void open(String modelPath, ItemGrip currentGrip, boolean isShield, java.util.function.Consumer onSave); } private Runnable onIdChanged; @@ -156,7 +157,6 @@ public class ItemEditorView extends BorderPane { if (wasFocused && !isFocused) onIdCommitted(); }); idField.setOnAction(e -> onIdCommitted()); - idField.textProperty().addListener((obs, o, n) -> scheduleSave()); catCombo = new ComboBox<>(); catCombo.getItems().addAll(ItemCategory.values()); @@ -246,6 +246,20 @@ public class ItemEditorView extends BorderPane { weaponSection.setVisible(false); weaponSection.setManaged(false); + // ── Schild-Sektion ──────────────────────────────────────────────────── + Button shieldGripBtn = new Button("Unterarm-Griff definieren..."); + shieldGripBtn.setMaxWidth(Double.MAX_VALUE); + shieldGripBtn.setStyle("-fx-background-color: #2a4a5a; -fx-text-fill: #aaddff; -fx-cursor: hand;"); + shieldGripBtn.setOnAction(e -> openShieldGripEditor()); + + shieldSection = new VBox(6, + new Separator(), + sectionTitle("Schild-Griff"), + shieldGripBtn + ); + shieldSection.setVisible(false); + shieldSection.setManaged(false); + modelRefField = new TextField(); modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)"); modelRefField.setEditable(false); @@ -300,6 +314,7 @@ public class ItemEditorView extends BorderPane { row("Wert (Gold):", goldSpinner), row("Modell:", modelFullRow), weaponSection, + shieldSection, new Separator(), consumableCheck, consumablesSection @@ -310,7 +325,16 @@ public class ItemEditorView extends BorderPane { private void openGripEditor() { if (current == null || gripEditorLauncher == null) return; String modelPath = modelRefField.getText().trim(); - gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), grip -> { + gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), false, grip -> { + current.setGrip(grip); + persistCurrent(); + }); + } + + private void openShieldGripEditor() { + if (current == null || gripEditorLauncher == null) return; + String modelPath = modelRefField.getText().trim(); + gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), true, grip -> { current.setGrip(grip); persistCurrent(); }); @@ -319,10 +343,12 @@ public class ItemEditorView extends BorderPane { private void updateWeaponSectionVisibility() { ItemCategory cat = catCombo.getValue(); ItemSubCategory sub = subCatCombo.getValue(); - boolean isWeapon = cat == ItemCategory.WEAPON - && sub != ItemSubCategory.SHIELD; + boolean isShield = cat == ItemCategory.WEAPON && sub == ItemSubCategory.SHIELD; + boolean isWeapon = cat == ItemCategory.WEAPON && !isShield; weaponSection.setVisible(isWeapon); weaponSection.setManaged(isWeapon); + shieldSection.setVisible(isShield); + shieldSection.setManaged(isShield); } // ── Modell-Auswahl ──────────────────────────────────────────────────────── @@ -333,14 +359,29 @@ public class ItemEditorView extends BorderPane { } private void onIdCommitted() { - String id = idField.getText().trim(); - if (id.isBlank()) return; - nameLabel.setText(id + ".name"); - descLabel.setText(id + ".description"); - scheduleSave(); + String newId = idField.getText().trim(); + if (newId.isBlank()) return; + nameLabel.setText(newId + ".name"); + descLabel.setText(newId + ".description"); + // Alte Datei löschen, wenn die ID geändert wurde + if (current != null) { + String oldId = current.getItemId(); + if (oldId != null && !oldId.isBlank() && !oldId.equals(newId)) { + try { ItemIO.delete(oldId, itemDir); } catch (IOException ignored) {} + } + } + persistCurrent(); + writeDefaultLocalisation(newId); if (onIdChanged != null) onIdChanged.run(); } + // ── Localisation defaults ───────────────────────────────────────────────── + + private static void writeDefaultLocalisation(String itemId) { + if (itemId == null || itemId.isBlank()) return; + LangDefaults.writeIfAbsent(itemId + ".name", EditorNaming.toDisplayName(itemId)); + } + // ── Auto-save ───────────────────────────────────────────────────────────── private void scheduleSave() { diff --git a/blight-editor/src/main/java/de/blight/editor/ui/LangDefaults.java b/blight-editor/src/main/java/de/blight/editor/ui/LangDefaults.java new file mode 100644 index 0000000..5be45d1 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/ui/LangDefaults.java @@ -0,0 +1,80 @@ +package de.blight.editor.ui; + +import de.blight.editor.ProjectRoot; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Schreibt Default-Einträge in die Sprachdateien (messages_de/en.properties) + * inklusive der Build-Mirror-Verzeichnisse. + */ +public final class LangDefaults { + + private static final Path LANG_SRC = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang"); + private static final Path[] MIRRORS = { + ProjectRoot.resolve("blight-lang", "bin", "main", "lang"), + ProjectRoot.resolve("blight-lang", "build", "resources", "main", "lang"), + }; + private static final String[] LOCALES = {"de", "en"}; + + private LangDefaults() {} + + /** + * Fügt {@code key=value} in alle Locale-Dateien ein, sofern der Schlüssel + * dort noch nicht vorhanden ist. Bestehende Übersetzungen werden nicht + * überschrieben. + */ + public static void writeIfAbsent(String key, String value) { + forEach(key, value, false); + } + + /** + * Schreibt {@code key=value} in alle Locale-Dateien; überschreibt einen + * vorhandenen Eintrag oder hängt ihn ans Ende, falls er fehlt. + */ + public static void writeOrUpdate(String key, String value) { + forEach(key, value, true); + } + + // ── intern ──────────────────────────────────────────────────────────────── + + private static void forEach(String key, String value, boolean overwrite) { + for (String locale : LOCALES) { + Path src = LANG_SRC.resolve("messages_" + locale + ".properties"); + try { + writeToFile(src, key, value, overwrite); + for (Path mirrorDir : MIRRORS) { + Path mirror = mirrorDir.resolve("messages_" + locale + ".properties"); + if (Files.exists(mirror)) { + Files.copy(src, mirror, StandardCopyOption.REPLACE_EXISTING); + } + } + } catch (IOException ignored) {} + } + } + + private static void writeToFile(Path file, String key, String value, boolean overwrite) + throws IOException { + if (!Files.exists(file)) return; + String needle = key + "="; + String needleSpace = key + " ="; + List lines = new ArrayList<>(Files.readAllLines(file, StandardCharsets.UTF_8)); + for (int i = 0; i < lines.size(); i++) { + String stripped = lines.get(i).stripLeading(); + if (stripped.startsWith(needle) || stripped.startsWith(needleSpace)) { + if (!overwrite) return; + lines.set(i, key + "=" + value); + Files.writeString(file, String.join("\n", lines) + "\n", + StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + return; + } + } + lines.add(key + "=" + value); + Files.writeString(file, String.join("\n", lines) + "\n", + StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/ui/LocationEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/LocationEditorView.java index df84130..50be843 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/LocationEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/LocationEditorView.java @@ -3,7 +3,6 @@ package de.blight.editor.ui; import de.blight.common.LocationIO; import de.blight.common.model.Location; import de.blight.common.model.TextReference; -import de.blight.editor.ProjectRoot; import javafx.animation.PauseTransition; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -15,8 +14,6 @@ import javafx.scene.layout.*; import javafx.util.Duration; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; import java.util.List; public class LocationEditorView extends BorderPane { @@ -194,50 +191,11 @@ public class LocationEditorView extends BorderPane { // ── Localisation defaults ────────────────────────────────────────────────── - private void writeDefaultLocalisation(String fullId) { + private static void writeDefaultLocalisation(String fullId) { if (fullId == null || fullId.isBlank()) return; String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId; if (shortId.isBlank()) return; - String key = fullId + ".name"; - String defaultName = EditorNaming.toDisplayName(shortId); - - Path langSrc = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang"); - Path[] langMirrors = { - ProjectRoot.resolve("blight-lang", "bin", "main", "lang"), - ProjectRoot.resolve("blight-lang", "build", "resources", "main", "lang"), - }; - for (String locale : new String[]{"de", "en"}) { - Path src = langSrc.resolve("messages_" + locale + ".properties"); - try { - writeOrUpdateKey(src, key, defaultName); - for (Path mirrorDir : langMirrors) { - Path mirror = mirrorDir.resolve("messages_" + locale + ".properties"); - if (Files.exists(mirror)) { - Files.copy(src, mirror, StandardCopyOption.REPLACE_EXISTING); - } - } - } catch (IOException e) { - // non-critical - } - } - } - - private static void writeOrUpdateKey(Path file, String key, String value) throws IOException { - if (!Files.exists(file)) return; - String needle = key + "="; - String needleSpace = key + " ="; - java.util.List lines = new java.util.ArrayList<>(Files.readAllLines(file, StandardCharsets.UTF_8)); - boolean found = false; - for (int i = 0; i < lines.size(); i++) { - if (lines.get(i).stripLeading().startsWith(needle) || lines.get(i).stripLeading().startsWith(needleSpace)) { - lines.set(i, key + "=" + value); - found = true; - break; - } - } - if (!found) lines.add(key + "=" + value); - Files.writeString(file, String.join("\n", lines) + "\n", - StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + LangDefaults.writeOrUpdate(fullId + ".name", EditorNaming.toDisplayName(shortId)); } // ── Helpers ─────────────────────────────────────────────────────────────── diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimPlayer.java b/blight-game/src/main/java/de/blight/game/animation/AnimPlayer.java index 173dc67..1cba064 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimPlayer.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimPlayer.java @@ -41,12 +41,23 @@ public class AnimPlayer { private float blendDuration; /** Einsprungzeit: Clip startet bei diesem Zeitwert statt bei 0 (überspringt z.B. das Ausholen). */ private float pendingStartOffset; + /** Wenn true: Clip rückwärts abspielen (speed=-1, Start am Ende). */ + private boolean pendingReversed; public AnimPlayer(AnimComposer composer, SkinningControl skinning) { this.composer = composer; this.skinning = skinning; } + /** + * Spielt einen Clip rückwärts ab (speed=-1, startet am Ende). + * Geeignet für IDLE_TO_BLOCK rückwärts beim Loslassen der Blocktaste. + */ + public boolean playReversed(String clipName, float transition) { + this.pendingReversed = true; + return play(clipName, transition); + } + /** * Spielt einen Clip ab, optional ab einem Zeitversatz (z.B. Ausholen überspringen). * @@ -167,10 +178,15 @@ public class AnimPlayer { pendingClip = null; blendSpace = null; double len = currentAction != null ? currentAction.getLength() : -1; - if (pendingStartOffset > 0f && currentAction != null && len > 0.01) { + if (pendingReversed && currentAction != null && len > 0.01) { + currentAction.setSpeed(-1f); + composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len); + pendingReversed = false; + } else if (pendingStartOffset > 0f && currentAction != null && len > 0.01) { composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(pendingStartOffset % (float) len); } pendingStartOffset = 0f; + pendingReversed = false; log.info("[AnimPlayer] direktPlay → '{}' length={}s", clipName, len); return currentAction != null; } catch (Exception e) { @@ -192,13 +208,17 @@ public class AnimPlayer { currentAction = composer.setCurrentAction(clip); currentClip = clip; double len = currentAction != null ? currentAction.getLength() : 0.0; - // AnimLayer.setTime: kein extra controlUpdate, kein loop=false-Seiteneffekt. - // Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung). - if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) { + if (pendingReversed && currentAction != null && len > 0.01) { + currentAction.setSpeed(-1f); + composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len); + pendingReversed = false; + } else if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) { + // Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung). float t = (pendingStartOffset + elapsed) % (float) len; composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t); pendingStartOffset = 0f; } + pendingReversed = false; log.debug("[AnimPlayer] finishBlend → '{}'", clip); } catch (Exception e) { log.warn("[AnimPlayer] finishBlend('{}') fehlgeschlagen: {}", clip, e.getMessage()); 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 b8e8ac1..351112d 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 @@ -34,18 +34,21 @@ public enum AnimationAction { // ── Schwert ─────────────────────────────────────────────────────────────── SWORD_ATTACK_1, - SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN, + SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK, + SWORD_IDLE_TO_BLOCK, SWORD_COUNTERATTACK, SWORD_BLOCK_IMPACT, SWORD_IDLE_IMPACT, SWORD_IDLE, SWORD_WALK, SWORD_RUN, // ── Axt ─────────────────────────────────────────────────────────────────── - AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_HIT_TAKEN, + AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_ATTACK_1, AXE_ATTACK_2, AXE_ATTACK_3, AXE_HEAVY_ATTACK, + AXE_IDLE_TO_BLOCK, AXE_COUNTERATTACK, AXE_BLOCK_IMPACT, AXE_IDLE_IMPACT, AXE_IDLE, AXE_WALK, AXE_RUN, // ── Zweihänder ──────────────────────────────────────────────────────────── - TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_HIT_TAKEN, + TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK, + TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_COUNTERATTACK, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT, TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN, // ── Bogen ───────────────────────────────────────────────────────────────── @@ -96,35 +99,44 @@ public enum AnimationAction { case SWORD_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Schwert)"); case SWORD_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Schwert)"); case SWORD_BLOCK -> TextRegistry.resolve(null, key, "Block (Schwert)"); - case SWORD_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Schwert)"); case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)"); - case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)"); - case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)"); - case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)"); + case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)"); + case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)"); + case SWORD_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Schwert)"); + case SWORD_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Schwert)"); + case SWORD_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Schwert)"); + case SWORD_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Schwert)"); + case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)"); case SWORD_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)"); case SWORD_RUN -> TextRegistry.resolve(null, key, "Laufen (Schwert)"); case AXE_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Axt)"); case AXE_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Axt)"); case AXE_BLOCK -> TextRegistry.resolve(null, key, "Block (Axt)"); - case AXE_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Axt)"); case AXE_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Axt)"); case AXE_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Axt)"); - case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)"); - case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)"); - case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)"); + case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)"); + case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)"); + case AXE_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Axt)"); + case AXE_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Axt)"); + case AXE_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Axt)"); + case AXE_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Axt)"); + case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)"); case AXE_WALK -> TextRegistry.resolve(null, key, "Gehen (Axt)"); case AXE_RUN -> TextRegistry.resolve(null, key, "Laufen (Axt)"); case TWO_HAND_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Zweihänder)"); case TWO_HAND_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Zweihänder)"); case TWO_HAND_BLOCK -> TextRegistry.resolve(null, key, "Block (Zweihänder)"); - case TWO_HAND_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)"); case TWO_HAND_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Zweihänder)"); case TWO_HAND_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Zweihänder)"); case TWO_HAND_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Zweihänder)"); - case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)"); - case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)"); + case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)"); + case TWO_HAND_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Zweihänder)"); + case TWO_HAND_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Zweihänder)"); + case TWO_HAND_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Zweihänder)"); + case TWO_HAND_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)"); + case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)"); case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)"); case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)"); 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 44e93f6..e7a8878 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 @@ -42,6 +42,8 @@ public class PlayerInputControl { private static final float COMBAT_BLEND = 0.05f; /** Zeit in Sekunden nach einem Angriff, in der der nächste Kombo-Schlag gedrückt werden kann. */ private static final float COMBO_WINDOW = 0.6f; + /** Maximale Haltezeit für einen Kurz-Klick (Konterangriff); darüber = Block-Eingabe. */ + private static final float BLOCK_TAP_MAX = 0.20f; // Combo-Overlap-Utility: MELEE_COMBO_ACTIVE + COMBO_ATK2_OFFSET hier eintragen, // sobald die Animationen in Blender angeglichen wurden (passende Übergangsposen). // AnimPlayer.play(clip, transition, startOffset) und nextStartOffset in tryPlay() sind @@ -67,15 +69,20 @@ public class PlayerInputControl { private boolean forward, backward, left, right, sprint, walk; private boolean paused = false; + private enum BlockPhase { NONE, WAITING, ENTERING, HOLDING, EXITING } + // ── Kampf-Zustand ───────────────────────────────────────────────────────── private MainCharacter mainCharacter = null; private boolean weaponDrawn = false; private ItemSubCategory drawnWeaponType = null; private boolean isAiming = false; private boolean rightMouseHeld = false; - private boolean combatAnimActive = false; - private float combatAnimRemaining = 0f; - private Runnable combatAnimCallback = null; + private BlockPhase blockPhase = BlockPhase.NONE; + private float blockPhaseTimer = 0f; + private boolean combatAnimActive = false; + private float combatAnimRemaining = 0f; + private Runnable combatAnimCallback = null; + private boolean combatAnimSkipComboWindow = false; /** Aktueller Schritt in der Kombo-Sequenz (0–3): 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK3-kritisch. */ private int comboStep = 0; /** Zeitfenster nach einem Angriff, in dem der nächste Kombo-Schlag akzeptiert wird. */ @@ -136,6 +143,7 @@ public class PlayerInputControl { private int groundGraceFrames = 0; private double nextTransitionLength = 0.0; private float nextStartOffset = 0f; + private boolean nextPlayReversed = false; private boolean pickupActive = false; private float pickupRemaining = 0f; @@ -269,6 +277,7 @@ public class PlayerInputControl { if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); stopWading(); rightMouseHeld = false; + blockPhase = BlockPhase.NONE; combatAnimActive = false; combatAnimCallback = null; comboStep = 0; @@ -682,8 +691,10 @@ public class PlayerInputControl { nextTransitionLength = COMBAT_BLEND; Runnable cb = combatAnimCallback; combatAnimCallback = null; + boolean skipWindow = combatAnimSkipComboWindow; + combatAnimSkipComboWindow = false; if (cb != null) cb.run(); - if (!combatAnimActive && isMeleeType(drawnWeaponType)) { + if (!skipWindow && !combatAnimActive && isMeleeType(drawnWeaponType)) { comboWindowActive = true; comboWindowRemaining = COMBO_WINDOW; log.info("[Combat] Kombo-Fenster geöffnet (step={} next={})", comboStep, comboStep); @@ -704,13 +715,14 @@ public class PlayerInputControl { } } + updateBlockPhase(tpf); + // Animations-Auswahl if (jumpFrames > 0) jumpFrames--; if (groundGraceFrames > 0) groundGraceFrames--; - // Während des Kombo-Fensters läuft die letzte Angriffs-Animation weiter — - // kein Wechsel zu IDLE/WALK, sonst entsteht ein sichtbares Zucken beim nächsten Schlag. - if (!combatAnimActive && !comboWindowActive) { + // Block-Phase verwaltet ihre eigenen Animationen. + if (!combatAnimActive && !isBlocking()) { AnimationAction target; if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) { target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP; @@ -724,10 +736,6 @@ public class PlayerInputControl { target = resolveWeaponAction(target); - if (weaponDrawn && rightMouseHeld && isMeleeType(drawnWeaponType)) { - target = getMeleeBlockAction(drawnWeaponType); - } - if (target != currentAnim) { playAction(target); currentAnim = target; @@ -796,6 +804,7 @@ public class PlayerInputControl { if (drawnWeaponType == null) { weaponDrawn = false; return; } isAiming = false; rightMouseHeld = false; + blockPhase = BlockPhase.NONE; AnimationAction sheathAnim = getSheathAction(drawnWeaponType); float dur = resolveClipLength(sheathAnim, 0.5f); requestAnimation(sheathAnim, dur, () -> { @@ -806,9 +815,9 @@ public class PlayerInputControl { } private void handleCombatAttack() { - if (!weaponDrawn || blockingAnimActive || combatAnimActive) { - log.info("[Combat] BLOCKIERT: weaponDrawn={} blockingAnim={} combatAnim={}", - weaponDrawn, blockingAnimActive, combatAnimActive); + if (!weaponDrawn || blockingAnimActive || combatAnimActive || isBlocking()) { + log.info("[Combat] BLOCKIERT: weaponDrawn={} blockingAnim={} combatAnim={} blocking={}", + weaponDrawn, blockingAnimActive, combatAnimActive, isBlocking()); return; } if (isMeleeType(drawnWeaponType)) { @@ -853,12 +862,103 @@ public class PlayerInputControl { private void handleCombatAlt(boolean isPressed) { if (!weaponDrawn || blockingAnimActive) return; if (isMeleeType(drawnWeaponType)) { - rightMouseHeld = isPressed; + if (isPressed) { + if (blockPhase == BlockPhase.NONE) { + blockPhase = BlockPhase.WAITING; + blockPhaseTimer = 0f; + } + } else { + switch (blockPhase) { + case WAITING -> { + blockPhase = BlockPhase.NONE; + playCounterAttack(); + } + case ENTERING, HOLDING -> startBlockExit(); + default -> {} + } + } } else if (isRangedType(drawnWeaponType) && isPressed) { isAiming = !isAiming; } } + private void updateBlockPhase(float tpf) { + switch (blockPhase) { + case WAITING -> { + blockPhaseTimer += tpf; + if (blockPhaseTimer >= BLOCK_TAP_MAX) { + if (hasShieldEquipped()) { + startBlockEnter(); + } else { + blockPhase = BlockPhase.NONE; + } + } + } + case ENTERING -> { + blockPhaseTimer -= tpf; + if (blockPhaseTimer <= 0f) { + blockPhase = BlockPhase.HOLDING; + AnimationAction blockAct = getMeleeBlockAction(drawnWeaponType); + nextTransitionLength = COMBAT_BLEND; + playAction(blockAct); + currentAnim = blockAct; + } + } + case EXITING -> { + blockPhaseTimer -= tpf; + if (blockPhaseTimer <= 0f) { + blockPhase = BlockPhase.NONE; + currentAnim = null; + } + } + default -> {} + } + } + + private void startBlockEnter() { + blockPhase = BlockPhase.ENTERING; + AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType); + float dur = resolveClipLength(idleToBlock, 0.5f); + blockPhaseTimer = dur; + nextTransitionLength = COMBAT_BLEND; + playAction(idleToBlock); + currentAnim = idleToBlock; + } + + private void startBlockExit() { + blockPhase = BlockPhase.EXITING; + AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType); + float dur = resolveClipLength(idleToBlock, 0.5f); + blockPhaseTimer = dur; + nextTransitionLength = COMBAT_BLEND; + nextPlayReversed = true; + playAction(idleToBlock); + currentAnim = idleToBlock; + } + + private void playCounterAttack() { + if (combatAnimActive) return; + AnimationAction counterAct = getMeleeCounterAttackAction(drawnWeaponType); + float dur = resolveClipLength(counterAct, 0.7f); + nextTransitionLength = COMBAT_BLEND; + combatAnimActive = true; + combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND); + combatAnimCallback = null; + combatAnimSkipComboWindow = true; + playAction(counterAct); + currentAnim = counterAct; + } + + public boolean isBlocking() { + return blockPhase == BlockPhase.ENTERING + || blockPhase == BlockPhase.HOLDING + || blockPhase == BlockPhase.EXITING; + } + + private boolean hasShieldEquipped() { + return mainCharacter != null && mainCharacter.getEquippedShield() != null; + } + private AnimationAction resolveWeaponAction(AnimationAction base) { if (!weaponDrawn || drawnWeaponType == null) return base; return switch (base) { @@ -986,6 +1086,38 @@ public class PlayerInputControl { }; } + private static AnimationAction getMeleeIdleToBlockAction(ItemSubCategory s) { + return switch (s) { + case AXE -> AnimationAction.AXE_IDLE_TO_BLOCK; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_TO_BLOCK; + default -> AnimationAction.SWORD_IDLE_TO_BLOCK; + }; + } + + private static AnimationAction getMeleeCounterAttackAction(ItemSubCategory s) { + return switch (s) { + case AXE -> AnimationAction.AXE_COUNTERATTACK; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_COUNTERATTACK; + default -> AnimationAction.SWORD_COUNTERATTACK; + }; + } + + public static AnimationAction getMeleeBlockImpactAction(ItemSubCategory s) { + return switch (s) { + case AXE -> AnimationAction.AXE_BLOCK_IMPACT; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT; + default -> AnimationAction.SWORD_BLOCK_IMPACT; + }; + } + + public static AnimationAction getMeleeIdleImpactAction(ItemSubCategory s) { + return switch (s) { + case AXE -> AnimationAction.AXE_IDLE_IMPACT; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT; + default -> AnimationAction.SWORD_IDLE_IMPACT; + }; + } + private static AnimationAction getRangedDrawAction(ItemSubCategory s) { return switch (s) { case CROSSBOW -> AnimationAction.CROSSBOW_DRAW; @@ -1254,12 +1386,17 @@ public class PlayerInputControl { if (animPlayer == null) { animPlayer = new AnimPlayer(animComposer, skinningControl); } - float transLen = (float) nextTransitionLength; - float startOff = nextStartOffset; + float transLen = (float) nextTransitionLength; + float startOff = nextStartOffset; + boolean reversed = nextPlayReversed; nextTransitionLength = 0.0; nextStartOffset = 0f; - boolean ok = animPlayer.play(clip, transLen, startOff); - log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED"); + nextPlayReversed = false; + boolean ok = reversed + ? animPlayer.playReversed(clip, transLen) + : animPlayer.play(clip, transLen, startOff); + log.info("[Anim] AnimPlayer.{}('{}') transLen={} → {}", reversed ? "playReversed" : "play", + clip, transLen, ok ? "OK" : "FAILED"); if (ok) { runningClip = animPlayer.getCurrentClip(); applyAnimOffset(clip); 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 index 87ec5cb..129d182 100644 --- a/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java +++ b/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java @@ -27,9 +27,10 @@ public class WeaponAttachmentControl extends AbstractControl { private static final Logger log = LoggerFactory.getLogger(WeaponAttachmentControl.class); - private static final String BONE_SPINE = "mixamorig:Spine2"; - private static final String BONE_HAND = "mixamorig:RightHand"; - private static final String BONE_PALM = "mixamorig:RightHandMiddle1"; + private static final String BONE_SPINE = "mixamorig:Spine2"; + private static final String BONE_HAND = "mixamorig:RightHand"; + private static final String BONE_PALM = "mixamorig:RightHandMiddle1"; + private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand"; private static final float DEG_TO_RAD = (float) Math.PI / 180f; private static final float HALF_PI = (float) Math.PI * 0.5f; @@ -37,6 +38,18 @@ public class WeaponAttachmentControl extends AbstractControl { /** Zusatz-Versatz beim gezückten Schwert in lokalem Waffen-Space (X=entlang Klinge, Y=entlang Griff, Z=senkrecht zur Schneide). */ private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.0f, 0.05f); + /** Schild am Rücken: Griff unterhalb der Schultern, parallel zum Boden, Schild über dem Schwert. */ + private static final Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f); + + // Schild am Arm (gezückt): Rotation relativ zur Bind-Pose des Unterarm-Knochens. + // Werte: 0f = keine Drehung, HALF_PI = +90°, -HALF_PI = -90° + private static final float SHIELD_ARM_ROT_X = -HALF_PI; + private static final float SHIELD_ARM_ROT_Y = 0f; + private static final float SHIELD_ARM_ROT_Z = 0f; + + // Verschiebung entlang des Unterarms Richtung Ellbogen (lokale −Y des LeftHand-Bones). + private static final float SHIELD_ARM_ELBOW_OFFSET = 0.25f; + private final AssetManager assetManager; private final PlayerInputControl playerInput; private final MainCharacter mc; @@ -57,6 +70,12 @@ public class WeaponAttachmentControl extends AbstractControl { private Spatial weaponSpatial; private boolean gripDiagLogged = false; + private Item lastEquippedShield; + private Spatial shieldSpatial; + + private Node leftForeArmNode; + private Quaternion leftForeArmBindRotInChar; + public WeaponAttachmentControl(AssetManager assetManager, PlayerInputControl playerInput, MainCharacter mc) { @@ -87,6 +106,12 @@ public class WeaponAttachmentControl extends AbstractControl { } catch (Exception e) { log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage()); } + try { + leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM); + } catch (Exception e) { + log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage()); + } + leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM); log.info("[WeaponAttach] handBindRotInChar={}", handBindRotInChar); } @@ -100,6 +125,7 @@ public class WeaponAttachmentControl extends AbstractControl { } Item current = mc.getEquippedMeleeWeapon(); + Item shield = mc.getEquippedShield(); boolean drawn = playerInput.isWeaponDrawn(); if (current != lastEquippedWeapon) { @@ -112,6 +138,16 @@ public class WeaponAttachmentControl extends AbstractControl { lastDrawnState = drawn; updateWeaponTransform(drawn, current != null ? current.getGrip() : null); } + + if (shield != lastEquippedShield) { + detachShield(); + lastEquippedShield = shield; + if (shield != null) loadShield(shield); + } + + if (shieldSpatial != null) { + updateShieldTransform(drawn, shield); + } } // ── Laden ──────────────────────────────────────────────────────────────── @@ -245,6 +281,73 @@ public class WeaponAttachmentControl extends AbstractControl { weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos))); } + // ── Schild ──────────────────────────────────────────────────────────────── + + private void loadShield(Item item) { + if (item.getModelRef() == null || item.getModelRef().getPath() == null) { + log.warn("[WeaponAttach] Kein modelRef an Schild '{}'.", item.getItemId()); + return; + } + try { + shieldSpatial = assetManager.loadModel(item.getModelRef().getPath()); + ((Node) spatial).attachChild(shieldSpatial); + log.info("[WeaponAttach] Schild geladen: {}", item.getModelRef().getPath()); + } catch (Exception e) { + log.warn("[WeaponAttach] Schildmodell nicht ladbar ({}): {}", + item.getModelRef().getPath(), e.getMessage()); + shieldSpatial = null; + } + } + + private void updateShieldTransform(boolean drawn, Item shield) { + Quaternion charWorldRotInv = spatial.getWorldRotation().inverse(); + Vector3f charWorldPos = spatial.getWorldTranslation(); + ItemGrip grip = shield != null ? shield.getGrip() : null; + + if (drawn && leftForeArmNode != null) { + Vector3f boneInChar = charWorldRotInv.mult( + leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); + Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation()); + applyShieldArmTransform(boneInChar, boneRotInChar, grip); + shieldSpatial.setCullHint(Spatial.CullHint.Inherit); + } else if (!drawn && backNode != null) { + Vector3f boneInChar = charWorldRotInv.mult( + backNode.getWorldTranslation().subtract(charWorldPos)); + Quaternion boneRotInChar = charWorldRotInv.mult(backNode.getWorldRotation()); + applyShieldBackTransform(boneInChar, boneRotInChar, grip); + shieldSpatial.setCullHint(Spatial.CullHint.Inherit); + } else { + shieldSpatial.setCullHint(Spatial.CullHint.Always); + } + } + + private void applyShieldArmTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { + Quaternion animDelta = (leftForeArmBindRotInChar != null) + ? boneRotInChar.mult(leftForeArmBindRotInChar.inverse()) + : boneRotInChar; + Quaternion armRot = new Quaternion().fromAngles(SHIELD_ARM_ROT_X, SHIELD_ARM_ROT_Y, SHIELD_ARM_ROT_Z); + Quaternion shieldRot = animDelta.mult(armRot); + Vector3f gripPos = (grip != null) + ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) + : Vector3f.ZERO; + // Ankerpunkt Richtung Ellbogen verschieben (lokale −Y des LeftHand-Bones) + Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -SHIELD_ARM_ELBOW_OFFSET, 0f))); + shieldSpatial.setLocalRotation(shieldRot); + shieldSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos))); + } + + private void applyShieldBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { + // 180° um Y: Modell-+X → Spine-X (Griff nach links), Modell-+Z → Spine-−Z (flach am Rücken) + Quaternion backRot = new Quaternion().fromAngles(0f, (float) Math.PI, 0f); + Quaternion shieldRot = boneRotInChar.mult(backRot); + Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(SHIELD_BACK_OFFSET)); + Vector3f gripPos = (grip != null) + ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) + : Vector3f.ZERO; + shieldSpatial.setLocalRotation(shieldRot); + shieldSpatial.setLocalTranslation(gripTarget.subtract(shieldRot.mult(gripPos))); + } + // ── Detach ──────────────────────────────────────────────────────────────── private void detachWeapon() { @@ -255,6 +358,13 @@ public class WeaponAttachmentControl extends AbstractControl { lastDrawnState = false; } + private void detachShield() { + if (shieldSpatial != null) { + shieldSpatial.removeFromParent(); + shieldSpatial = null; + } + } + @Override protected void controlRender(RenderManager rm, ViewPort vp) {} } 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 59bf9e7..562e315 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 @@ -186,8 +186,7 @@ public class WorldInteractableState extends BaseAppState { private final ActionListener interactListener = (name, isPressed, tpf) -> { if (!isPressed || phase != Phase.IDLE) return; - int idx = findNearestInRange(); - if (idx >= 0) startApproach(idx); + if (hoveredIdx >= 0) startApproach(hoveredIdx); }; private final ActionListener getUpListener = (name, isPressed, tpf) -> { @@ -247,22 +246,6 @@ public class WorldInteractableState extends BaseAppState { return new Vector3f(e.worldX(), e.worldY(), e.worldZ()); } - // ── Suche nächstes Interactable ─────────────────────────────────────────── - - private int findNearestInRange() { - Vector3f pos = physicsChar.getPhysicsLocation(); - int bestIdx = -1; - float bestDist = Float.MAX_VALUE; - for (int i = 0; i < entries.size(); i++) { - InteractableEntry e = entries.get(i); - float dx = e.worldX() - pos.x; - float dz = e.worldZ() - pos.z; - float d = (float) Math.sqrt(dx * dx + dz * dz); - float range = (e.type() == InteractableType.BENCH) ? BENCH_RANGE : BED_RANGE; - if (d < range && d < bestDist) { bestDist = d; bestIdx = i; } - } - return bestIdx; - } // ── Annäherung ──────────────────────────────────────────────────────────── 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 f0ef829..e3e0613 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 @@ -153,6 +153,9 @@ public class WorldItemsState extends BaseAppState { for (PlacedItem pi : items) { Spatial s = buildVisual(pi); s.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z()); + com.jme3.math.Quaternion q = new com.jme3.math.Quaternion(); + q.fromAngles(pi.rotX(), pi.rotY(), pi.rotZ()); + s.setLocalRotation(q); s.setUserData("itemId", pi.itemId()); itemsRoot.attachChild(s); visuals.add(s); diff --git a/blight-lang/src/main/resources/lang/messages_de.properties b/blight-lang/src/main/resources/lang/messages_de.properties index 85ea0dd..95e8515 100644 --- a/blight-lang/src/main/resources/lang/messages_de.properties +++ b/blight-lang/src/main/resources/lang/messages_de.properties @@ -243,3 +243,7 @@ location.silas.name=Silas location.silas_.name=Silas location.silas_haus.name=Silas Haus location.silas_heim.name=Silas Heim +einfacher_schild.name=Einfacher Schild +rostiges_kurzschwert.name=Rostiges Kurzschwert +blutagave.name=Blutagave +erzmoss.name=Erzmoss diff --git a/blight-lang/src/main/resources/lang/messages_en.properties b/blight-lang/src/main/resources/lang/messages_en.properties index 94bbee9..5a93401 100644 --- a/blight-lang/src/main/resources/lang/messages_en.properties +++ b/blight-lang/src/main/resources/lang/messages_en.properties @@ -98,3 +98,7 @@ location.silas.name=Silas location.silas_.name=Silas location.silas_haus.name=Silas Haus location.silas_heim.name=Silas Heim +einfacher_schild.name=Simple Shield +rostiges_kurzschwert.name=Rusty Short Sword +blutagave.name=Blood Agave +erzmoss.name=Ore Moss diff --git a/blight-lang/src/main/resources/lang/text_keys.txt b/blight-lang/src/main/resources/lang/text_keys.txt index 09feced..be4e092 100644 --- a/blight-lang/src/main/resources/lang/text_keys.txt +++ b/blight-lang/src/main/resources/lang/text_keys.txt @@ -126,6 +126,8 @@ dialog.silas.wer_bist_du.textnpc dialog.silas.wer_seid_ihr.description dialog.silas.wer_seid_ihr.textmainchar dialog.silas.wer_seid_ihr.textnpc +einfacher_schild.description +einfacher_schild.name erzmoss.description erzmoss.name hero.name @@ -138,6 +140,10 @@ location.silas_ location.silas_haus location.silas_heim location.test +neues_item_1789920920760.description +neues_item_1789920920760.name +neues_item_1789920asdf920760.description +neues_item_1789920asdf920760.name quest.blight_wuehler.description quest.blight_wuehler.name quest.blight_wuehler.success diff --git a/blight-map/src/main/map/blight_placed_items.bpi b/blight-map/src/main/map/blight_placed_items.bpi index b9ce95d..6209e86 100644 --- a/blight-map/src/main/map/blight_placed_items.bpi +++ b/blight-map/src/main/map/blight_placed_items.bpi @@ -1,2 +1,3 @@ -# uuid itemId x y z -fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693 +# uuid itemId x y z rotX rotY rotZ +fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693 0.00000 0.00000 0.00000 +feebed68-92b8-4458-9c7c-b1b781c1eba5 einfacher_schild -4.45856 1.72895 -3.67467 -0.14178 0.64812 0.00000