diff --git a/blight-assets/src/main/resources/Models/custom_mesh_0.j3o b/blight-assets/src/main/resources/Models/custom_mesh_0.j3o index d6fc94a..3523f34 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_0.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_0.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_1.j3o b/blight-assets/src/main/resources/Models/custom_mesh_1.j3o index ef03a16..cc0a6e7 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_1.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_1.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_2.j3o b/blight-assets/src/main/resources/Models/custom_mesh_2.j3o index e3cae4a..5113003 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_2.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_2.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_3.j3o b/blight-assets/src/main/resources/Models/custom_mesh_3.j3o index 321f38e..d614e41 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_3.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_3.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_4.j3o b/blight-assets/src/main/resources/Models/custom_mesh_4.j3o index 130f135..9d3f8e0 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_4.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_4.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_5.j3o b/blight-assets/src/main/resources/Models/custom_mesh_5.j3o index eacc7a6..f238959 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_5.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_5.j3o differ diff --git a/blight-assets/src/main/resources/Models/custom_mesh_6.j3o b/blight-assets/src/main/resources/Models/custom_mesh_6.j3o index 5458389..30d415e 100644 Binary files a/blight-assets/src/main/resources/Models/custom_mesh_6.j3o and b/blight-assets/src/main/resources/Models/custom_mesh_6.j3o differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o b/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o index 11d1c95..0e36ae9 100644 Binary files a/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o and b/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o.meta b/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o.meta new file mode 100644 index 0000000..0b3ef65 --- /dev/null +++ b/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o.meta @@ -0,0 +1,20 @@ +#Tue Jun 09 10:41:26 CEST 2026 +castShadow=true +category= +cullDistance=120.0 +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +name=blutagave +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=0.3 +scaleY=0.3 +scaleZ=0.3 +solid=false +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o.thumb.png b/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o.thumb.png new file mode 100644 index 0000000..13a8c50 Binary files /dev/null and b/blight-assets/src/main/resources/Models/plants/usable/blutagave.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o b/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o index 49894dc..759eeea 100644 Binary files a/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o and b/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o.meta b/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o.meta new file mode 100644 index 0000000..022ca76 --- /dev/null +++ b/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o.meta @@ -0,0 +1,20 @@ +#Tue Jun 09 09:59:57 CEST 2026 +castShadow=true +category= +cullDistance=120.0 +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +name=erzmoss +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=0.33 +scaleY=0.33 +scaleZ=0.33 +solid=false +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o.thumb.png b/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o.thumb.png new file mode 100644 index 0000000..1a52afd Binary files /dev/null and b/blight-assets/src/main/resources/Models/plants/usable/erzmoss.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o b/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o index d6a70ba..a42a88c 100644 Binary files a/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o and b/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o.meta b/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o.meta new file mode 100644 index 0000000..951bd54 --- /dev/null +++ b/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o.meta @@ -0,0 +1,20 @@ +#Tue Jun 09 10:38:00 CEST 2026 +castShadow=true +category= +cullDistance=120.0 +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +name=geisterfarn +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=0.3 +scaleY=0.3 +scaleZ=0.3 +solid=true +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o.thumb.png b/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o.thumb.png new file mode 100644 index 0000000..279e21f Binary files /dev/null and b/blight-assets/src/main/resources/Models/plants/usable/geisterfarn.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o b/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o index 5a221f1..6b39b33 100644 Binary files a/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o and b/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o.meta b/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o.meta new file mode 100644 index 0000000..abc0a41 --- /dev/null +++ b/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o.meta @@ -0,0 +1,20 @@ +#Tue Jun 09 10:42:24 CEST 2026 +castShadow=true +category= +cullDistance=120.0 +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +name=quelllilie +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=0.3 +scaleY=0.3 +scaleZ=0.3 +solid=false +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o.thumb.png b/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o.thumb.png new file mode 100644 index 0000000..3c5fbc3 Binary files /dev/null and b/blight-assets/src/main/resources/Models/plants/usable/quelllilie.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o b/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o index 389977d..7e28d21 100644 Binary files a/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o and b/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o.meta b/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o.meta new file mode 100644 index 0000000..0f3bea1 --- /dev/null +++ b/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o.meta @@ -0,0 +1,20 @@ +#Tue Jun 09 10:39:27 CEST 2026 +castShadow=true +category= +cullDistance=120.0 +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +name=sonnenherz +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=0.3 +scaleY=0.3 +scaleZ=0.3 +solid=false +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o.thumb.png b/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o.thumb.png new file mode 100644 index 0000000..0b4a62f Binary files /dev/null and b/blight-assets/src/main/resources/Models/plants/usable/sonnenherz.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o b/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o index 6c5de89..e4ddfae 100644 Binary files a/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o and b/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o differ diff --git a/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o.meta b/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o.meta new file mode 100644 index 0000000..47ca57c --- /dev/null +++ b/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o.meta @@ -0,0 +1,20 @@ +#Tue Jun 09 10:40:05 CEST 2026 +castShadow=true +category= +cullDistance=120.0 +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +name=windschiff +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=0.3 +scaleY=0.3 +scaleZ=0.3 +solid=false +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o.thumb.png b/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o.thumb.png new file mode 100644 index 0000000..5e8c580 Binary files /dev/null and b/blight-assets/src/main/resources/Models/plants/usable/windschiff.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_Color.jpg b/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_Color.jpg new file mode 100644 index 0000000..64ebdb3 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_Color.jpg differ diff --git a/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_NormalGL.jpg b/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_NormalGL.jpg new file mode 100644 index 0000000..e1237cc Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/Ground068_1K-JPG_NormalGL.jpg differ diff --git a/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_Color.jpg b/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_Color.jpg new file mode 100644 index 0000000..4584efb Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_Color.jpg differ diff --git a/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_NormalDX.jpg b/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_NormalDX.jpg new file mode 100644 index 0000000..e4d12e0 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/Rocks006_1K-JPG_NormalDX.jpg differ diff --git a/blight-assets/src/main/resources/items/misc/blutagave.item b/blight-assets/src/main/resources/items/misc/blutagave.item new file mode 100644 index 0000000..4f6c24b --- /dev/null +++ b/blight-assets/src/main/resources/items/misc/blutagave.item @@ -0,0 +1,14 @@ +{ + "itemId": "blutagave", + "category": "MISC", + "name": { + "id": "blutagave.name" + }, + "description": { + "id": "blutagave.description" + }, + "worthGold": 25, + "modelRef": { + "path": "Models/plants/usable/blutagave.j3o" + } +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/items/misc/erzmoss.item b/blight-assets/src/main/resources/items/misc/erzmoss.item new file mode 100644 index 0000000..f28f5b3 --- /dev/null +++ b/blight-assets/src/main/resources/items/misc/erzmoss.item @@ -0,0 +1,14 @@ +{ + "itemId": "erzmoss", + "category": "MISC", + "name": { + "id": "erzmoss.name" + }, + "description": { + "id": "erzmoss.description" + }, + "worthGold": 100, + "modelRef": { + "path": "Models/plants/usable/erzmoss.j3o" + } +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/items/neues_item_1780949443027.item b/blight-assets/src/main/resources/items/neues_item_1780949443027.item deleted file mode 100644 index 1ed9243..0000000 --- a/blight-assets/src/main/resources/items/neues_item_1780949443027.item +++ /dev/null @@ -1,11 +0,0 @@ -{ - "itemId": "neues_item_1780949443027", - "category": "CONSUMABLES", - "name": { - "id": "bloddagave.name" - }, - "description": { - "id": "bloddagave.description" - }, - "worthGold": 50 -} \ No newline at end of file diff --git a/blight-common/src/main/java/de/blight/common/MapData.java b/blight-common/src/main/java/de/blight/common/MapData.java index 06aeccd..c22a7cb 100644 --- a/blight-common/src/main/java/de/blight/common/MapData.java +++ b/blight-common/src/main/java/de/blight/common/MapData.java @@ -53,7 +53,9 @@ public final class MapData { public final byte[] splatA; /** Texturpfade für Basis-Terrain (4 Slots, "" = Standard-Textur). */ - public final String[] terrainTextures = new String[]{"", "", "", ""}; + public final String[] terrainTextures = new String[]{"", "", "", ""}; + /** Normal-Map-Pfade für Basis-Terrain (4 Slots, "" = keine Normal-Map). */ + public final String[] terrainNormalMaps = new String[]{"", "", "", ""}; /** Splatmap Rot-Kanal Gebirge: Tex1-Helligkeit, immer 255 [SPLAT_SIZE²]. */ public final byte[] upperSplatR; @@ -65,7 +67,9 @@ public final class MapData { public final byte[] upperSplatA; /** Texturpfade für Gebirge (4 Slots, "" = Standard-Textur). */ - public final String[] upperTextures = new String[]{"", "", "", ""}; + public final String[] upperTextures = new String[]{"", "", "", ""}; + /** Normal-Map-Pfade für Gebirge (4 Slots, "" = keine Normal-Map). */ + public final String[] upperNormalMaps = new String[]{"", "", "", ""}; /** Gras-Dichte [SPLAT_SIZE²], Bytes 0–255 (0=kein Gras, 255=max Dichte). */ public final byte[] grassDensity; diff --git a/blight-common/src/main/java/de/blight/common/MapIO.java b/blight-common/src/main/java/de/blight/common/MapIO.java index df7fc94..51fc656 100644 --- a/blight-common/src/main/java/de/blight/common/MapIO.java +++ b/blight-common/src/main/java/de/blight/common/MapIO.java @@ -55,7 +55,7 @@ public final class MapIO { } private static final int MAGIC = 0x424C4947; // "BLIG" - private static final int VERSION = 10; + private static final int VERSION = 11; // Größen älterer Saves (v≤9) – für Migrations-Upsampling private static final int OLD_TERRAIN_VERTS = 4097; @@ -127,6 +127,9 @@ public final class MapIO { out.writeInt(slotEnd); for (int i = 0; i < slotEnd; i++) out.writeUTF(slots[i] != null ? slots[i] : ""); out.write(data.grassTextureMap); + // v11: normal-map-pfade + writeStrings(out, data.terrainNormalMaps); + writeStrings(out, data.upperNormalMaps); } // Atomares Umbenennen: erst wenn die Datei vollständig ist ersetzen wir die alte. // ATOMIC_MOVE schlägt auf manchen Systemen cross-device fehl → Fallback auf REPLACE_EXISTING. @@ -246,6 +249,10 @@ public final class MapIO { upsampleBytes(old, OLD_SPLAT_SIZE, data.grassTextureMap, MapData.SPLAT_SIZE); } } + if (version >= 11) { + readStrings(in, data.terrainNormalMaps); + readStrings(in, data.upperNormalMaps); + } } return data; } diff --git a/blight-common/src/main/java/de/blight/common/model/ItemIO.java b/blight-common/src/main/java/de/blight/common/model/ItemIO.java index 8a9f172..01d94df 100644 --- a/blight-common/src/main/java/de/blight/common/model/ItemIO.java +++ b/blight-common/src/main/java/de/blight/common/model/ItemIO.java @@ -12,11 +12,12 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; import java.util.stream.Stream; /** * Lädt und speichert {@link Item}-Instanzen als JSON. - * Dateiformat: {@code .item} im items/-Verzeichnis. + * Dateiformat: {@code items//.item}. * Liste wird nach Kategorie-Ordinal, dann nach Name (TextReference-ID) sortiert. */ public final class ItemIO { @@ -36,9 +37,17 @@ public final class ItemIO { public static void save(Item item, Path itemDir) throws IOException { if (item.getItemId() == null || item.getItemId().isBlank()) throw new IllegalArgumentException("itemId darf nicht leer sein"); - Files.createDirectories(itemDir); - Files.writeString(itemDir.resolve(item.getItemId() + EXTENSION), - GSON.toJson(item), StandardCharsets.UTF_8); + Path targetDir = categoryDir(item, itemDir); + Files.createDirectories(targetDir); + Path newPath = targetDir.resolve(item.getItemId() + EXTENSION); + Files.writeString(newPath, GSON.toJson(item), StandardCharsets.UTF_8); + // Entferne veraltete Kopien an anderen Orten (z.B. nach Kategorie-Wechsel) + try (Stream walk = Files.walk(itemDir)) { + walk.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().equals(item.getItemId() + EXTENSION)) + .filter(p -> !p.equals(newPath)) + .forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException ignored) {} }); + } log.debug("[ItemIO] Gespeichert: {}", item.getItemId()); } @@ -49,8 +58,9 @@ public final class ItemIO { public static List loadAll(Path itemDir) { List result = new ArrayList<>(); if (!Files.isDirectory(itemDir)) return result; - try (Stream walk = Files.list(itemDir)) { - walk.filter(p -> p.toString().endsWith(EXTENSION)) + try (Stream walk = Files.walk(itemDir)) { + walk.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(EXTENSION)) .sorted() .forEach(p -> { try { result.add(load(p)); } @@ -64,6 +74,19 @@ public final class ItemIO { } public static void delete(String itemId, Path itemDir) throws IOException { - Files.deleteIfExists(itemDir.resolve(itemId + EXTENSION)); + if (!Files.isDirectory(itemDir)) return; + try (Stream walk = Files.walk(itemDir)) { + List found = walk.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().equals(itemId + EXTENSION)) + .collect(Collectors.toList()); + for (Path p : found) Files.deleteIfExists(p); + } + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private static Path categoryDir(Item item, Path itemDir) { + if (item.getCategory() == null) return itemDir; + return itemDir.resolve(item.getCategory().name().toLowerCase()); } } 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 aeb6700..eab2ddd 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -3523,7 +3523,12 @@ public class EditorApp extends Application { if (pStr.endsWith(".item")) { MenuItem placeItem = new MenuItem("📍 Platzieren"); placeItem.setOnAction(ev -> activateItemPlacement(p)); - ctx.getItems().addAll(placeItem, new SeparatorMenuItem()); + MenuItem editItem = new MenuItem("✏ Im Item-Editor öffnen"); + editItem.setOnAction(ev -> { + String itemId = p.getFileName().toString().replace(".item", ""); + switchToItemEditor(itemId); + }); + ctx.getItems().addAll(placeItem, editItem, new SeparatorMenuItem()); } MenuItem reveal = new MenuItem("Im Dateisystem anzeigen"); @@ -4535,6 +4540,14 @@ public class EditorApp extends Application { // Asset-Tree aktualisieren input.refreshAssets = true; + + // Thumbnail generieren (JME3-Thread liest das Flag und rendert) + java.nio.file.Path finalJ3o = category.isEmpty() ? absolutePath + : ASSET_ROOT.resolve("Models") + .resolve(java.nio.file.Path.of(category.replace('/', java.io.File.separatorChar))) + .resolve(name.isEmpty() ? absolutePath.getFileName().toString() + : name.replaceAll("[\\\\/:*?\"<>|]", "_") + ".j3o"); + input.modelEditorThumbnailRequest = finalJ3o; } private void importLodFile(int slot) { @@ -6610,6 +6623,10 @@ public class EditorApp extends Application { } private void switchToItemEditor() { + switchToItemEditor(null); + } + + private void switchToItemEditor(String selectItemId) { onF5 = null; currentTool = "itemEditor"; ToolBar tb = new ToolBar(); @@ -6619,8 +6636,11 @@ public class EditorApp extends Application { label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); topBar.getChildren().set(1, tb); - root.setCenter(new de.blight.editor.ui.ItemEditorView(ASSET_ROOT.resolve("items"))); + de.blight.editor.ui.ItemEditorView view = + new de.blight.editor.ui.ItemEditorView(ASSET_ROOT.resolve("items")); + root.setCenter(view); root.setRight(null); + if (selectItemId != null) view.selectItem(selectItemId); } private void switchToQuestEditor() { 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 800da04..79ecc35 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -587,6 +587,12 @@ public class SharedInput { /** JFX → JME: Model-Editor schließen. */ public volatile boolean modelEditorCloseRequest = false; + /** + * JFX → JME: Thumbnail für das Modell unter diesem absoluten Pfad generieren. + * Wird nach dem Speichern im ModelEditor gesetzt; JME3 rendert + speichert Sidecar + UserData. + */ + public volatile java.nio.file.Path modelEditorThumbnailRequest = null; + /** JME → JFX: true wenn das geladene Modell eingebettete LOD-Kinder hat (kein separater Pfad nötig). */ public volatile boolean modelEditorHasEmbeddedLods = false; 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 5a85c0e..36c0e6e 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 @@ -13,13 +13,18 @@ import com.jme3.scene.shape.Box; import com.jme3.terrain.geomipmap.TerrainQuad; import de.blight.common.PlacedItem; import de.blight.common.PlacedItemIO; +import de.blight.common.model.Item; +import de.blight.common.model.ItemIO; +import de.blight.editor.ProjectRoot; import de.blight.editor.SharedInput; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Verwaltet die Platzierung von Items (Pickups) auf der Karte im Editor. @@ -36,10 +41,11 @@ public class ItemPlacementState extends BaseAppState { private Node rootNode; private TerrainQuad terrain; - private final List items = new ArrayList<>(); - private final List nodes = new ArrayList<>(); - private Node itemRoot; - private Node previewNode; + private final List items = new ArrayList<>(); + private final List nodes = new ArrayList<>(); + private final Map itemDefs = new HashMap<>(); + private Node itemRoot; + private Node previewNode; public ItemPlacementState(SharedInput input) { this.input = input; @@ -58,6 +64,11 @@ 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"); + for (Item it : ItemIO.loadAll(itemDir)) { + if (it.getItemId() != null) itemDefs.put(it.getItemId(), it); + } + itemRoot = new Node("itemRoot"); previewNode = new Node("itemPreview"); previewNode.setCullHint(Spatial.CullHint.Always); @@ -171,12 +182,28 @@ public class ItemPlacementState extends BaseAppState { private Node buildItemNode(String itemId) { Node n = new Node("item_" + itemId); + n.setUserData("itemId", itemId); + + Item def = itemDefs.get(itemId); + if (def != null && def.getModelRef() != null + && def.getModelRef().getPath() != null + && !def.getModelRef().getPath().isBlank()) { + try { + Spatial model = assets.loadModel(def.getModelRef().getPath()); + model.setName("itemModel_" + itemId); + n.attachChild(model); + return n; + } catch (Exception e) { + log.warn("[ItemPlacement] Modell für '{}' nicht ladbar: {}", itemId, e.getMessage()); + } + } + + // Fallback: goldener Würfel Geometry g = new Geometry("itemGeo_" + itemId, new Box(0.15f, 0.15f, 0.15f)); Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", new ColorRGBA(1f, 0.82f, 0.1f, 1f)); g.setMaterial(mat); n.attachChild(g); - n.setUserData("itemId", itemId); return n; } 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 7f98c9c..dd89689 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 @@ -4,6 +4,9 @@ import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.bounding.BoundingBox; +import com.jme3.export.Savable; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.export.binary.BinaryImporter; import com.jme3.light.AmbientLight; import com.jme3.light.DirectionalLight; import com.jme3.material.Material; @@ -15,6 +18,7 @@ import com.jme3.util.BufferUtils; import de.blight.editor.SharedInput; import java.nio.FloatBuffer; +import java.nio.file.Path; /** * Modell-Editor-Modus: zeigt ein einzelnes Modell in isolierter Vorschau @@ -148,6 +152,13 @@ public class ModelEditorState extends BaseAppState { applyPivot(input.modelEditorPivotY); } + // Thumbnail auf Anforderung generieren + Path thumbReq = input.modelEditorThumbnailRequest; + if (thumbReq != null && modelWrapper != null) { + input.modelEditorThumbnailRequest = null; + generateThumbnail(thumbReq); + } + // Orbit-Kamera: Maus-Delta (Middle-Button zieht Kamera, Right-Button dreht Orbit) int[] delta = input.consumeMouseDelta(); if (delta[0] != 0 || delta[1] != 0) { @@ -401,5 +412,35 @@ public class ModelEditorState extends BaseAppState { if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls); } + // ── Thumbnail ───────────────────────────────────────────────────────────── + + private void generateThumbnail(Path j3oPath) { + if (modelWrapper == null) return; + try { + // Klon des aktuellen Modells (inkl. angewandter Skalierung) + Spatial modelClone = modelWrapper.clone(); + byte[] thumb = ThumbnailRenderer.render(modelClone, app.getRenderManager(), app.getRenderer()); + if (thumb == null) return; + ThumbnailRenderer.saveSidecar(thumb, j3oPath); + embedThumbnail(thumb, j3oPath); + } catch (Exception e) { + System.err.println("[ModelEditor] Thumbnail-Fehler: " + e.getMessage()); + } + } + + private void embedThumbnail(byte[] pngBytes, Path j3oPath) { + try { + BinaryImporter importer = BinaryImporter.getInstance(); + importer.setAssetManager(app.getAssetManager()); + Savable savable = importer.load(j3oPath.toFile()); + if (savable instanceof Spatial root) { + ThumbnailRenderer.embed(root, pngBytes); + BinaryExporter.getInstance().save(root, j3oPath.toFile()); + } + } catch (Exception e) { + System.err.println("[ModelEditor] j3o-Embed fehlgeschlagen: " + e.getMessage()); + } + } + public String getCurrentPath() { return currentPath; } } 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 3d0089b..0e4a58b 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 @@ -1243,8 +1243,24 @@ public class SceneObjectState extends BaseAppState { model.setLocalRotation(rot); } - Files.createDirectories(req.destJ3o().getParent()); - BinaryExporter.getInstance().save(model, req.destJ3o().toFile()); + // Thumbnail generieren und in j3o einbetten + try { + byte[] thumb = ThumbnailRenderer.render(model, app.getRenderManager(), app.getRenderer()); + if (thumb != null) { + ThumbnailRenderer.embed(model, thumb); + Files.createDirectories(req.destJ3o().getParent()); + BinaryExporter.getInstance().save(model, req.destJ3o().toFile()); + ThumbnailRenderer.saveSidecar(thumb, req.destJ3o()); + } else { + Files.createDirectories(req.destJ3o().getParent()); + BinaryExporter.getInstance().save(model, req.destJ3o().toFile()); + } + } catch (Exception thumbEx) { + System.err.println("[SceneObject] Thumbnail-Fehler: " + thumbEx.getMessage()); + Files.createDirectories(req.destJ3o().getParent()); + BinaryExporter.getInstance().save(model, req.destJ3o().toFile()); + } + if (req.srcToDelete() != null) Files.deleteIfExists(req.srcToDelete()); setStatus("Konvertiert: " + req.destJ3o().getFileName()); input.refreshAssets = true; diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index d7c04cd..a7bc5e8 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -408,15 +408,19 @@ public class TerrainEditorState extends BaseAppState { for (byte b : splatR) { if (b != 0) { rAllZero = false; break; } } if (rAllZero) Arrays.fill(splatR, (byte) 255); // Gespeicherte Texturpfade in SharedInput übernehmen - System.arraycopy(loadedMapData.terrainTextures, 0, - input.terrainTexturePaths, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(loadedMapData.terrainTextures, 0, + input.terrainTexturePaths, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(loadedMapData.terrainNormalMaps, 0, + input.terrainNormalMapPaths, 0, MapData.TEXTURE_SLOTS); // Zweite Splatmap (Slots 5-8) upperSplatR = loadedMapData.upperSplatR.clone(); upperSplatG = loadedMapData.upperSplatG.clone(); upperSplatB = loadedMapData.upperSplatB.clone(); upperSplatA = loadedMapData.upperSplatA.clone(); - System.arraycopy(loadedMapData.upperTextures, 0, - input.upperTexturePaths, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(loadedMapData.upperTextures, 0, + input.upperTexturePaths, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(loadedMapData.upperNormalMaps, 0, + input.upperNormalMapPaths, 0, MapData.TEXTURE_SLOTS); // Alte Gebirge-Splatmap-Migration: R=255 überall war der Gebirge-Standard. // Im neuen 1-Terrain-System bedeutet das: Slot-5-Textur deckt alles ab → auf 0 setzen. boolean upperRAllMax = true; @@ -837,8 +841,10 @@ public class TerrainEditorState extends BaseAppState { final byte[] upG = upperSplatG != null ? upperSplatG.clone() : null; final byte[] upB = upperSplatB != null ? upperSplatB.clone() : null; final byte[] upA = upperSplatA != null ? upperSplatA.clone() : null; - final String[] texPaths = input.terrainTexturePaths.clone(); - final String[] upperPaths = input.upperTexturePaths.clone(); + final String[] texPaths = input.terrainTexturePaths.clone(); + final String[] normalPaths = input.terrainNormalMapPaths.clone(); + final String[] upperPaths = input.upperTexturePaths.clone(); + final String[] upperNormPaths = input.upperNormalMapPaths.clone(); final GrassTuftIO.GrassData grassData = placedObjectState != null ? new GrassTuftIO.GrassData(placedObjectState.getSlotPaths(), placedObjectState.getAllTufts()) @@ -870,14 +876,16 @@ public class TerrainEditorState extends BaseAppState { System.arraycopy(snapG, 0, data.splatG, 0, data.splatG.length); System.arraycopy(snapB, 0, data.splatB, 0, data.splatB.length); System.arraycopy(snapA, 0, data.splatA, 0, data.splatA.length); - System.arraycopy(texPaths, 0, data.terrainTextures, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(texPaths, 0, data.terrainTextures, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(normalPaths, 0, data.terrainNormalMaps, 0, MapData.TEXTURE_SLOTS); } if (upR != null) { System.arraycopy(upR, 0, data.upperSplatR, 0, data.upperSplatR.length); System.arraycopy(upG, 0, data.upperSplatG, 0, data.upperSplatG.length); System.arraycopy(upB, 0, data.upperSplatB, 0, data.upperSplatB.length); System.arraycopy(upA, 0, data.upperSplatA, 0, data.upperSplatA.length); - System.arraycopy(upperPaths, 0, data.upperTextures, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(upperPaths, 0, data.upperTextures, 0, MapData.TEXTURE_SLOTS); + System.arraycopy(upperNormPaths, 0, data.upperNormalMaps, 0, MapData.TEXTURE_SLOTS); } if (grassData != null) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java b/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java new file mode 100644 index 0000000..4a85369 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/ThumbnailRenderer.java @@ -0,0 +1,200 @@ +package de.blight.editor.state; + +import com.jme3.bounding.BoundingBox; +import com.jme3.bounding.BoundingVolume; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.Renderer; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import com.jme3.texture.Texture2D; +import com.jme3.util.BufferUtils; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; + +/** + * Offscreen-Thumbnail-Rendering für j3o-Modelle (128×128 PNG, transparenter Hintergrund). + * + * Kamera: 45° von oben, 45° Yaw von der X-Achse (entspricht Orbit yaw=45°, pitch=45°). + * Zoom: Die Bounding-Box wird auf die Kamera-Bildebene projiziert; der Abstand wird so + * gewählt, dass das Modell die volle Breite oder Höhe des Thumbnails ausfüllt. + * Licht: ein Hauptlicht genau aus Blickrichtung + leichtes Umgebungslicht. + * + * Alle Methoden müssen vom JME3-Render-Thread aufgerufen werden. + */ +public final class ThumbnailRenderer { + + public static final int SIZE = 128; + + // Kamerawinkel: yaw = 45° (zwischen X- und Z-Achse), pitch = 45° (von oben) + private static final float YAW_RAD = FastMath.DEG_TO_RAD * 45f; + private static final float PITCH_RAD = FastMath.DEG_TO_RAD * 45f; + + // Einheits-Versatzvektor Kamera→Zentrum (Orbit-Formel mit yaw=45°, pitch=45°): + // x = cos(pitch)*sin(yaw) = 0.5 + // y = sin(pitch) = √2/2 + // z = cos(pitch)*cos(yaw) = 0.5 → Länge = 1.0 (bereits normiert) + private static final Vector3f CAM_OFFSET = new Vector3f( + FastMath.cos(PITCH_RAD) * FastMath.sin(YAW_RAD), + FastMath.sin(PITCH_RAD), + FastMath.cos(PITCH_RAD) * FastMath.cos(YAW_RAD)); + + // Kamera-Basisvektoren (JME3-lookAt von CAM_OFFSET in Richtung Ursprung, up=Y): + // forward = -CAM_OFFSET = (-0.5, -0.7071, -0.5) + // left = UNIT_Y × forward / |...| = (-0.7071, 0, 0.7071) + // screen_right = -left = ( 0.7071, 0, -0.7071) + // screen_up = forward × left = (-0.5, 0.7071, -0.5) + private static final Vector3f SCREEN_RIGHT = new Vector3f( 0.7071f, 0f, -0.7071f); + private static final Vector3f SCREEN_UP = new Vector3f(-0.5f, 0.7071f, -0.5f); + + // FOV des Thumbnails (45°, quadratisch → aspect = 1) + private static final float FOV_DEG = 45f; + private static final float HALF_FOV = FastMath.DEG_TO_RAD * (FOV_DEG * 0.5f); + + // Rand: 2 % Abstand zum Frame-Rand (verhindert Clipping) + private static final float MARGIN = 0.98f; + + private static final String USERDATA_KEY = "thumbnail"; + + private ThumbnailRenderer() {} + + // ── Öffentliche API ─────────────────────────────────────────────────────── + + /** + * Rendert {@code model} zu einem 128×128 ARGB-PNG mit transparentem Hintergrund. + * {@code model} darf beim Aufruf nicht in einem anderen Scene-Graph hängen. + */ + public static byte[] render(Spatial model, RenderManager rm, Renderer renderer) { + model.updateGeometricState(); + BoundingVolume bv = model.getWorldBound(); + + Vector3f center = (bv instanceof BoundingBox bb) + ? bb.getCenter().clone() : Vector3f.ZERO.clone(); + float ex = (bv instanceof BoundingBox bb) ? bb.getXExtent() : 1f; + float ey = (bv instanceof BoundingBox bb) ? bb.getYExtent() : 1f; + float ez = (bv instanceof BoundingBox bb) ? bb.getZExtent() : 1f; + + // Maximale Ausdehnung der Bounding-Box auf die Kamera-Bildebene projizieren + float maxR = ex * Math.abs(SCREEN_RIGHT.x) + ey * Math.abs(SCREEN_RIGHT.y) + ez * Math.abs(SCREEN_RIGHT.z); + float maxU = ex * Math.abs(SCREEN_UP.x) + ey * Math.abs(SCREEN_UP.y) + ez * Math.abs(SCREEN_UP.z); + float screenRadius = Math.max(maxR, maxU); + if (screenRadius < 1e-4f) screenRadius = 1f; + + // Kameradistanz: Modell füllt Thumbnail (mit MARGIN Spielraum) + float dist = screenRadius / (MARGIN * FastMath.tan(HALF_FOV)); + float near = Math.max(dist * 0.01f, 0.001f); + float far = dist * 10f; + + Camera cam = new Camera(SIZE, SIZE); + cam.setFrustumPerspective(FOV_DEG, 1f, near, far); + cam.setLocation(center.add(CAM_OFFSET.mult(dist))); + cam.lookAt(center, Vector3f.UNIT_Y); + + // Licht genau aus Blickrichtung (= Richtung von Kamera zum Modell) + Vector3f lightDir = CAM_OFFSET.negate(); // von Kamera → Zentrum + + Node scene = new Node("_thumb_scene"); + scene.addLight(new DirectionalLight(lightDir, new ColorRGBA(1.6f, 1.6f, 1.6f, 1f))); + scene.addLight(new AmbientLight(new ColorRGBA(0.25f, 0.25f, 0.25f, 1f))); + scene.attachChild(model); + scene.updateGeometricState(); + + return renderToFramebuffer(scene, cam, rm, renderer); + } + + /** + * Setzt die UserData {@code "thumbnail"} als Base64-PNG auf dem Modell. + * Vor dem BinaryExporter.save() aufrufen. + */ + public static void embed(Spatial model, byte[] pngBytes) { + if (pngBytes == null) return; + model.setUserData(USERDATA_KEY, Base64.getEncoder().encodeToString(pngBytes)); + } + + public static String getUserDataKey() { return USERDATA_KEY; } + + /** Speichert PNG-Bytes als {@code .thumb.png}. */ + public static void saveSidecar(byte[] pngBytes, Path j3oPath) { + if (pngBytes == null) return; + try { + Files.write(sidecarPath(j3oPath), pngBytes); + } catch (IOException e) { + System.err.println("[ThumbnailRenderer] Sidecar-Fehler: " + e.getMessage()); + } + } + + /** Pfad der Sidecar-Datei: {@code .thumb.png}. */ + public static Path sidecarPath(Path j3oPath) { + return j3oPath.resolveSibling(j3oPath.getFileName() + ".thumb.png"); + } + + // ── Intern ─────────────────────────────────────────────────────────────── + + private static byte[] renderToFramebuffer(Node scene, Camera cam, + RenderManager rm, Renderer renderer) { + Texture2D colorTex = new Texture2D(SIZE, SIZE, Image.Format.RGBA8); + FrameBuffer fb = new FrameBuffer(SIZE, SIZE, 1); + fb.setDepthBuffer(Image.Format.Depth); + fb.setColorTexture(colorTex); + + ViewPort vp = rm.createPreView("_blight_thumb", cam); + vp.setClearFlags(true, true, true); + vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f)); // transparent + vp.setOutputFrameBuffer(fb); + vp.attachScene(scene); + + try { + rm.renderViewPort(vp, 0.016f); + ByteBuffer buf = BufferUtils.createByteBuffer(SIZE * SIZE * 4); + renderer.readFrameBuffer(fb, buf); + return pngFromRgba(buf); + } catch (Exception e) { + System.err.println("[ThumbnailRenderer] Render-Fehler: " + e.getMessage()); + return null; + } finally { + rm.removePreView(vp); + scene.detachAllChildren(); // model-Node löst sich aus _thumb_scene + fb.dispose(); + } + } + + /** Konvertiert RGBA-Rohdaten (OpenGL, Y=0 unten) in ein ARGB-PNG mit Alphakanal. */ + private static byte[] pngFromRgba(ByteBuffer buf) { + buf.rewind(); + byte[] raw = new byte[SIZE * SIZE * 4]; + buf.get(raw); + BufferedImage img = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < SIZE; y++) { + for (int x = 0; x < SIZE; x++) { + int srcY = SIZE - 1 - y; // Y-Flip: OpenGL ist bottom-up + int i = (srcY * SIZE + x) * 4; + int r = raw[i] & 0xFF; + int g = raw[i + 1] & 0xFF; + int b = raw[i + 2] & 0xFF; + int a = raw[i + 3] & 0xFF; + img.setRGB(x, y, (a << 24) | (r << 16) | (g << 8) | b); + } + } + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + ImageIO.write(img, "PNG", baos); + return baos.toByteArray(); + } catch (IOException e) { + return null; + } + } +} 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 e13c20a..1e324c3 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 @@ -9,7 +9,6 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.geometry.Insets; -import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.*; @@ -21,31 +20,121 @@ import java.nio.file.Path; import java.util.List; /** - * Item-Verwaltung: zwei identische ItemPanel-Instanzen nebeneinander. - * Liste sortiert nach Kategorie, dann nach Name. + * Item-Manager: Liste links (volle Höhe), Bearbeitungsformular rechts. */ public class ItemEditorView extends BorderPane { - private final ObservableList sharedItems = FXCollections.observableArrayList(); + private final ObservableList items = FXCollections.observableArrayList(); private final Path itemDir; + private final Path assetRoot; + + private final SortedList sortedItems; + private final ListView listView; + private Item current = null; + + // Form-Felder + private TextField idField; + private ComboBox catCombo; + private TextField nameField; + private TextField descField; + private Spinner goldSpinner; + private TextField modelRefField; + private VBox formContainer; + private Button deleteBtn; public ItemEditorView(Path itemDir) { - this.itemDir = itemDir; + this.itemDir = itemDir; + this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot setStyle("-fx-background-color: #1e1e2e;"); reload(); - ItemPanel left = new ItemPanel("Liste 1", sharedItems, itemDir, this::reload); - ItemPanel right = new ItemPanel("Liste 2", sharedItems, itemDir, this::reload); + // ── Linke Seite: Liste ──────────────────────────────────────────────── + sortedItems = new SortedList<>(items, ItemIO.SORT_ORDER); + listView = new ListView<>(sortedItems); + listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;"); + listView.setCellFactory(lv -> new ListCell<>() { + @Override protected void updateItem(Item item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { setText(null); setStyle(""); return; } + String catName = item.getCategory() != null ? item.getCategory().name() : "—"; + String id = item.getItemId() != null ? item.getItemId() : "—"; + setText(id); + String color = categoryColor(item.getCategory()); + setStyle("-fx-text-fill: #dddddd;" + + " -fx-border-color: transparent transparent transparent " + color + ";" + + " -fx-border-width: 0 0 0 3;" + + " -fx-padding: 3 6 3 8;"); + setTooltip(new Tooltip("[" + catName + "] " + id)); + } + }); + listView.getSelectionModel().selectedItemProperty() + .addListener((obs, old, nw) -> onItemSelected(old, nw)); - HBox panels = new HBox(1, left, right); - HBox.setHgrow(left, Priority.ALWAYS); - HBox.setHgrow(right, Priority.ALWAYS); - setCenter(panels); + Button refreshBtn = new Button("↺"); + refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;"); + refreshBtn.setOnAction(e -> reload()); + Label titleLbl = new Label("Items"); + titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;"); + HBox header = new HBox(8, titleLbl, refreshBtn); + header.setPadding(new Insets(8, 10, 8, 10)); + header.setAlignment(Pos.CENTER_LEFT); + header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;"); + + HBox legend = buildLegend(); + legend.setPadding(new Insets(4, 8, 4, 8)); + legend.setStyle("-fx-background-color: #1a1a2a;"); + + Button newBtn = new Button("Neues Item"); + newBtn.setMaxWidth(Double.MAX_VALUE); + newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;"); + newBtn.setOnAction(e -> createItem()); + + deleteBtn = new Button("Löschen"); + deleteBtn.setMaxWidth(Double.MAX_VALUE); + deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;"); + deleteBtn.setDisable(true); + deleteBtn.setOnAction(e -> deleteSelected()); + + HBox listButtons = new HBox(6, newBtn, deleteBtn); + listButtons.setPadding(new Insets(6, 8, 6, 8)); + HBox.setHgrow(newBtn, Priority.ALWAYS); + HBox.setHgrow(deleteBtn, Priority.ALWAYS); + + VBox leftPane = new VBox(header, listView, legend, listButtons); + leftPane.setPrefWidth(440); + leftPane.setMinWidth(300); + leftPane.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;"); + VBox.setVgrow(listView, Priority.ALWAYS); + + // ── Rechte Seite: Formular ──────────────────────────────────────────── + formContainer = buildForm(); + formContainer.setDisable(true); + + ScrollPane formScroll = new ScrollPane(formContainer); + formScroll.setFitToWidth(true); + formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;"); + + setLeft(leftPane); + setCenter(formScroll); } - public void reload() { + // ── Öffentliche API ─────────────────────────────────────────────────────── + + public void selectItem(String itemId) { + if (itemId == null) return; + items.stream() + .filter(i -> itemId.equals(i.getItemId())) + .findFirst() + .ifPresent(listView.getSelectionModel()::select); + } + + // ── Reload ──────────────────────────────────────────────────────────────── + + private void reload() { + String selectedId = current != null ? current.getItemId() : null; List loaded = ItemIO.loadAll(itemDir); - sharedItems.setAll(loaded); + items.setAll(loaded); + if (selectedId != null) selectItem(selectedId); } // ── Category colors ─────────────────────────────────────────────────────── @@ -62,306 +151,233 @@ public class ItemEditorView extends BorderPane { }; } - // ── Single panel ────────────────────────────────────────────────────────── + // ── Legend ──────────────────────────────────────────────────────────────── - static class ItemPanel extends VBox { + private HBox buildLegend() { + HBox box = new HBox(8); + box.setAlignment(Pos.CENTER_LEFT); + for (ItemCategory cat : ItemCategory.values()) { + Label dot = new Label("■"); + dot.setStyle("-fx-text-fill: " + categoryColor(cat) + "; -fx-font-size: 10;"); + Label lbl = new Label(cat.name()); + lbl.setStyle("-fx-text-fill: #888; -fx-font-size: 10;"); + box.getChildren().addAll(dot, lbl); + } + return box; + } - private final ObservableList items; - private final Path itemDir; - private final Runnable onSaved; + // ── Form construction ───────────────────────────────────────────────────── - private final SortedList sortedItems; - private final ListView listView; - private Item current = null; + private VBox buildForm() { + VBox form = new VBox(6); + form.setPadding(new Insets(14)); + form.setStyle("-fx-background-color: #252535;"); - // Form fields - private TextField idField; - private ComboBox catCombo; - private TextField nameField; - private TextField descField; - private Spinner goldSpinner; - private TextField modelRefField; + idField = new TextField(); + idField.setPromptText("eindeutige ID"); + idField.focusedProperty().addListener((obs, wasFocused, isFocused) -> { + if (wasFocused && !isFocused) onIdCommitted(); + }); + idField.setOnAction(e -> onIdCommitted()); - // Form container - private VBox formContainer; - private Button deleteBtn; + catCombo = new ComboBox<>(); + catCombo.getItems().addAll(ItemCategory.values()); + catCombo.setMaxWidth(Double.MAX_VALUE); + catCombo.setCellFactory(lv -> new ListCell<>() { + @Override protected void updateItem(ItemCategory cat, boolean empty) { + super.updateItem(cat, empty); + if (empty || cat == null) { setText(null); return; } + setText(cat.name()); + setStyle("-fx-text-fill: " + categoryColor(cat) + ";"); + } + }); + catCombo.setButtonCell(new ListCell<>() { + @Override protected void updateItem(ItemCategory cat, boolean empty) { + super.updateItem(cat, empty); + if (empty || cat == null) { setText(null); return; } + setText(cat.name()); + setStyle("-fx-text-fill: " + categoryColor(cat) + ";"); + } + }); - ItemPanel(String title, ObservableList items, Path itemDir, Runnable onSaved) { - this.items = items; - this.itemDir = itemDir; - this.onSaved = onSaved; + nameField = new TextField(); + nameField.setPromptText("TextReference-Schlüssel"); + descField = new TextField(); + descField.setPromptText("TextReference-Schlüssel"); + goldSpinner = new Spinner<>(0, 999999, 0); + goldSpinner.setEditable(true); + goldSpinner.setMaxWidth(Double.MAX_VALUE); - setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;"); - setSpacing(0); + modelRefField = new TextField(); + modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)"); + modelRefField.setEditable(false); + modelRefField.setStyle("-fx-background-color: #1e1e2e; -fx-text-fill: #ccc;"); - // ── Header ──────────────────────────────────────────────────────── - Label titleLbl = new Label(title); - titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;"); - Button refreshBtn = new Button("↺"); - refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;"); - refreshBtn.setOnAction(e -> onSaved.run()); - HBox header = new HBox(8, titleLbl, refreshBtn); - header.setPadding(new Insets(8, 10, 8, 10)); - header.setAlignment(Pos.CENTER_LEFT); - header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;" - + " -fx-border-width: 0 0 1 0;"); + Button pickModelBtn = new Button("…"); + pickModelBtn.setTooltip(new Tooltip("Modell auswählen")); + pickModelBtn.setOnAction(e -> pickModel()); - // ── Item list (sorted) ───────────────────────────────────────────── - sortedItems = new SortedList<>(items, ItemIO.SORT_ORDER); - listView = new ListView<>(sortedItems); - listView.setPrefHeight(200); - listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;"); - listView.setCellFactory(lv -> new ListCell<>() { - @Override protected void updateItem(Item item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { setText(null); setStyle(""); return; } + HBox modelRow = new HBox(4, modelRefField, pickModelBtn); + HBox.setHgrow(modelRefField, Priority.ALWAYS); + modelRow.setAlignment(Pos.CENTER_LEFT); - String catName = item.getCategory() != null ? item.getCategory().name() : "—"; - String name = item.getName() != null ? item.getName().id() - : (item.getItemId() != null ? item.getItemId() : "—"); - setText(name); - String color = categoryColor(item.getCategory()); - setStyle("-fx-text-fill: #dddddd;" - + " -fx-border-color: transparent transparent transparent " + color + ";" - + " -fx-border-width: 0 0 0 3;" - + " -fx-padding: 3 6 3 8;"); - setTooltip(new Tooltip("[" + catName + "] " + item.getItemId())); - } - }); - listView.getSelectionModel().selectedItemProperty() - .addListener((obs, old, nw) -> onItemSelected(old, nw)); + Button clearModelBtn = new Button("✕"); + clearModelBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;"); + clearModelBtn.setTooltip(new Tooltip("Modell-Referenz entfernen")); + clearModelBtn.setOnAction(e -> modelRefField.clear()); - // Category legend - HBox legend = buildLegend(); - legend.setPadding(new Insets(4, 8, 4, 8)); - legend.setStyle("-fx-background-color: #1a1a2a;"); + HBox modelFullRow = new HBox(4, modelRefField, pickModelBtn, clearModelBtn); + HBox.setHgrow(modelRefField, Priority.ALWAYS); + modelFullRow.setAlignment(Pos.CENTER_LEFT); - Button newBtn = new Button("Neues Item"); - newBtn.setMaxWidth(Double.MAX_VALUE); - newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;"); - newBtn.setOnAction(e -> createItem()); + Button saveBtn = new Button("Item speichern"); + saveBtn.setMaxWidth(Double.MAX_VALUE); + saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;"); + saveBtn.setOnAction(e -> saveCurrentItem()); - deleteBtn = new Button("Löschen"); - deleteBtn.setMaxWidth(Double.MAX_VALUE); - deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;"); - deleteBtn.setDisable(true); - deleteBtn.setOnAction(e -> deleteSelected()); + form.getChildren().addAll( + sectionTitle("Item"), + new Separator(), + row("Item-ID:", idField), + row("Kategorie:", catCombo), + new Separator(), + sectionTitle("Texte"), + row("Name:", nameField), + row("Beschreibung:", descField), + new Separator(), + sectionTitle("Werte"), + row("Wert (Gold):", goldSpinner), + row("Modell:", modelFullRow), + new Separator(), + saveBtn + ); + return form; + } - HBox listButtons = new HBox(6, newBtn, deleteBtn); - listButtons.setPadding(new Insets(6, 8, 6, 8)); - HBox.setHgrow(newBtn, Priority.ALWAYS); - HBox.setHgrow(deleteBtn, Priority.ALWAYS); + // ── Modell-Auswahl ──────────────────────────────────────────────────────── - VBox listSection = new VBox(listView, legend, listButtons); - listSection.setStyle("-fx-background-color: #1a1a2a;"); + private void pickModel() { + ModelChooser chooser = new ModelChooser(assetRoot); + chooser.showAndWait().ifPresent(path -> modelRefField.setText(path)); + } - // ── Form ────────────────────────────────────────────────────────── - formContainer = buildForm(); + private void onIdCommitted() { + String id = idField.getText().trim(); + if (id.isBlank()) return; + if (nameField.getText().isBlank()) nameField.setText(id + ".name"); + if (descField.getText().isBlank()) descField.setText(id + ".description"); + } + + // ── Form load / save ────────────────────────────────────────────────────── + + private void onItemSelected(Item old, Item nw) { + if (old != null) saveFormToItem(old); + current = nw; + deleteBtn.setDisable(nw == null); + if (nw != null) { + formContainer.setDisable(false); + loadFormFromItem(nw); + } else { formContainer.setDisable(true); - - ScrollPane formScroll = new ScrollPane(formContainer); - formScroll.setFitToWidth(true); - formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;"); - VBox.setVgrow(formScroll, Priority.ALWAYS); - - getChildren().addAll(header, listSection, new Separator(), formScroll); - } - - // ── Legend ──────────────────────────────────────────────────────────── - - private HBox buildLegend() { - HBox box = new HBox(10); - box.setAlignment(Pos.CENTER_LEFT); - for (ItemCategory cat : ItemCategory.values()) { - Label dot = new Label("■"); - dot.setStyle("-fx-text-fill: " + categoryColor(cat) + "; -fx-font-size: 10;"); - Label lbl = new Label(cat.name()); - lbl.setStyle("-fx-text-fill: #888; -fx-font-size: 10;"); - box.getChildren().addAll(dot, lbl); - } - return box; - } - - // ── Form construction ───────────────────────────────────────────────── - - private VBox buildForm() { - VBox form = new VBox(6); - form.setPadding(new Insets(10)); - form.setStyle("-fx-background-color: #252535;"); - - idField = new TextField(); - idField.setPromptText("eindeutige ID"); - - catCombo = new ComboBox<>(); - catCombo.getItems().addAll(ItemCategory.values()); - catCombo.setMaxWidth(Double.MAX_VALUE); - catCombo.setCellFactory(lv -> new ListCell<>() { - @Override protected void updateItem(ItemCategory cat, boolean empty) { - super.updateItem(cat, empty); - if (empty || cat == null) { setText(null); return; } - setText(cat.name()); - setStyle("-fx-text-fill: " + categoryColor(cat) + ";"); - } - }); - catCombo.setButtonCell(new ListCell<>() { - @Override protected void updateItem(ItemCategory cat, boolean empty) { - super.updateItem(cat, empty); - if (empty || cat == null) { setText(null); return; } - setText(cat.name()); - setStyle("-fx-text-fill: " + categoryColor(cat) + ";"); - } - }); - - nameField = new TextField(); - nameField.setPromptText("TextReference-Schlüssel"); - descField = new TextField(); - descField.setPromptText("TextReference-Schlüssel"); - goldSpinner = new Spinner<>(0, 999999, 0); - goldSpinner.setEditable(true); - goldSpinner.setMaxWidth(Double.MAX_VALUE); - modelRefField = new TextField(); - modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)"); - - Button saveBtn = new Button("Item speichern"); - saveBtn.setMaxWidth(Double.MAX_VALUE); - saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;"); - saveBtn.setOnAction(e -> saveCurrentItem()); - - form.getChildren().addAll( - sectionTitle("Item"), - new Separator(), - row("Item-ID:", idField), - row("Kategorie:", catCombo), - new Separator(), - sectionTitle("Texte"), - row("Name:", nameField), - row("Beschreibung:", descField), - new Separator(), - sectionTitle("Werte"), - row("Wert (Gold):", goldSpinner), - row("Modell:", modelRefField), - new Separator(), - saveBtn - ); - return form; - } - - // ── Form load / save ────────────────────────────────────────────────── - - private void onItemSelected(Item old, Item nw) { - if (old != null) saveFormToItem(old); - current = nw; - deleteBtn.setDisable(nw == null); - if (nw != null) { - formContainer.setDisable(false); - loadFormFromItem(nw); - } else { - formContainer.setDisable(true); - clearForm(); - } - } - - private void loadFormFromItem(Item item) { - idField.setText(safe(item.getItemId())); - catCombo.setValue(item.getCategory()); - nameField.setText(item.getName() != null ? item.getName().id() : ""); - descField.setText(item.getDescription() != null ? item.getDescription().id() : ""); - goldSpinner.getValueFactory().setValue(item.getWorthGold()); - ObjectReference ref = item.getModelRef(); - modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : ""); - } - - private void saveFormToItem(Item item) { - item.setItemId(idField.getText().trim()); - item.setCategory(catCombo.getValue()); - item.setName(ref(nameField)); - item.setDescription(ref(descField)); - item.setWorthGold(goldSpinner.getValue()); - String mr = modelRefField.getText().trim(); - item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr)); - } - - private void clearForm() { - idField.clear(); - catCombo.setValue(null); - nameField.clear(); - descField.clear(); - goldSpinner.getValueFactory().setValue(0); - modelRefField.clear(); - } - - // ── List operations ─────────────────────────────────────────────────── - - private void createItem() { - Item item = new Item(); - item.setItemId("neues_item_" + System.currentTimeMillis()); - item.setCategory(ItemCategory.MISC); - items.add(item); - // Select the new item in the sorted view - listView.getSelectionModel().select(item); - } - - private void deleteSelected() { - Item sel = listView.getSelectionModel().getSelectedItem(); - if (sel == null) return; - String iId = sel.getItemId(); - items.remove(sel); - if (iId != null && !iId.isBlank()) { - try { ItemIO.delete(iId, itemDir); } - catch (IOException e) { /* ignore */ } - } - current = null; clearForm(); - formContainer.setDisable(true); - deleteBtn.setDisable(true); - onSaved.run(); - } - - private void saveCurrentItem() { - if (current == null) return; - saveFormToItem(current); - if (current.getItemId() == null || current.getItemId().isBlank()) { - new Alert(Alert.AlertType.ERROR, "Item-ID darf nicht leer sein.", ButtonType.OK).showAndWait(); - return; - } - try { - ItemIO.save(current, itemDir); - } catch (IOException e) { - new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); - return; - } - onSaved.run(); - // Re-select after reload (list re-sorts) - String savedId = current.getItemId(); - items.stream() - .filter(i -> savedId.equals(i.getItemId())) - .findFirst() - .ifPresent(listView.getSelectionModel()::select); - } - - // ── Helpers ─────────────────────────────────────────────────────────── - - private static Label sectionTitle(String text) { - Label l = new Label(text); - l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;"); - return l; - } - - private static HBox row(String labelText, Node control) { - Label lbl = new Label(labelText); - lbl.setMinWidth(120); - lbl.setStyle("-fx-text-fill: #aaa;"); - HBox.setHgrow(control, Priority.ALWAYS); - HBox box = new HBox(8, lbl, control); - box.setAlignment(Pos.CENTER_LEFT); - return box; - } - - private static String safe(String s) { return s != null ? s : ""; } - - private static TextReference ref(TextField f) { - String s = f.getText().trim(); - return s.isBlank() ? null : new TextReference(s); } } + + private void loadFormFromItem(Item item) { + idField.setText(safe(item.getItemId())); + catCombo.setValue(item.getCategory()); + nameField.setText(item.getName() != null ? item.getName().id() : ""); + descField.setText(item.getDescription() != null ? item.getDescription().id() : ""); + goldSpinner.getValueFactory().setValue(item.getWorthGold()); + ObjectReference ref = item.getModelRef(); + modelRefField.setText(ref != null && ref.getPath() != null ? ref.getPath() : ""); + } + + private void saveFormToItem(Item item) { + item.setItemId(idField.getText().trim()); + item.setCategory(catCombo.getValue()); + item.setName(ref(nameField)); + item.setDescription(ref(descField)); + item.setWorthGold(goldSpinner.getValue()); + String mr = modelRefField.getText().trim(); + item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr)); + } + + private void clearForm() { + idField.clear(); + catCombo.setValue(null); + nameField.clear(); + descField.clear(); + goldSpinner.getValueFactory().setValue(0); + modelRefField.clear(); + } + + // ── List operations ─────────────────────────────────────────────────────── + + private void createItem() { + Item item = new Item(); + item.setItemId("neues_item_" + System.currentTimeMillis()); + item.setCategory(ItemCategory.MISC); + items.add(item); + listView.getSelectionModel().select(item); + } + + private void deleteSelected() { + Item sel = listView.getSelectionModel().getSelectedItem(); + if (sel == null) return; + String iId = sel.getItemId(); + items.remove(sel); + if (iId != null && !iId.isBlank()) { + try { ItemIO.delete(iId, itemDir); } + catch (IOException e) { /* ignore */ } + } + current = null; + clearForm(); + formContainer.setDisable(true); + deleteBtn.setDisable(true); + reload(); + } + + private void saveCurrentItem() { + if (current == null) return; + saveFormToItem(current); + if (current.getItemId() == null || current.getItemId().isBlank()) { + new Alert(Alert.AlertType.ERROR, "Item-ID darf nicht leer sein.", ButtonType.OK).showAndWait(); + return; + } + try { + ItemIO.save(current, itemDir); + } catch (IOException e) { + new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); + return; + } + String savedId = current.getItemId(); + reload(); + selectItem(savedId); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private static Label sectionTitle(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;"); + return l; + } + + private static HBox row(String labelText, Node control) { + Label lbl = new Label(labelText); + lbl.setMinWidth(100); + lbl.setStyle("-fx-text-fill: #aaa;"); + HBox.setHgrow(control, Priority.ALWAYS); + HBox box = new HBox(8, lbl, control); + box.setAlignment(Pos.CENTER_LEFT); + return box; + } + + private static String safe(String s) { return s != null ? s : ""; } + + private static TextReference ref(TextField f) { + String s = f.getText().trim(); + return s.isBlank() ? null : new TextReference(s); + } } diff --git a/blight-editor/src/main/java/de/blight/editor/ui/ModelChooser.java b/blight-editor/src/main/java/de/blight/editor/ui/ModelChooser.java new file mode 100644 index 0000000..e90f82f --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/ui/ModelChooser.java @@ -0,0 +1,196 @@ +package de.blight.editor.ui; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.stage.Modality; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * Dialog zum Auswählen eines .j3o-Modells. + * Zeigt Karten mit 128×128-Thumbnail wenn eine {@code .j3o.thumb.png} Sidecar-Datei vorhanden ist. + * Gibt den relativen Asset-Pfad (z.B. "Models/Items/sword.j3o") zurück oder {@code null} bei Abbruch. + */ +public class ModelChooser extends Dialog { + + private static final int THUMB_SIZE = 96; + private static final int CARD_W = THUMB_SIZE + 16; + private static final int CARD_H = THUMB_SIZE + 38; + + private final ToggleGroup toggleGroup = new ToggleGroup(); + private final List allCards = new ArrayList<>(); + private final VBox contentBox = new VBox(16); + private final TextField filterField = new TextField(); + private final Path assetRoot; + + private String selectedPath; + + public ModelChooser(Path assetRoot) { + this.assetRoot = assetRoot; + setTitle("Modell auswählen"); + initModality(Modality.APPLICATION_MODAL); + setResizable(true); + + contentBox.setPadding(new Insets(4)); + + Path modelsDir = assetRoot.resolve("Models"); + if (Files.isDirectory(modelsDir)) { + // Modelle nach Unterordner gruppieren + buildCards(modelsDir); + } + + filterField.setPromptText("Filtern…"); + filterField.textProperty().addListener( + (obs, o, n) -> applyFilter(n == null ? "" : n.toLowerCase())); + + ScrollPane scroll = new ScrollPane(contentBox); + scroll.setFitToWidth(true); + scroll.setPrefSize(720, 500); + scroll.setStyle("-fx-background-color: transparent;"); + + VBox root = new VBox(8, filterField, scroll); + root.setPadding(new Insets(10)); + + getDialogPane().setContent(root); + getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK); + okBtn.setText("Auswählen"); + okBtn.setDisable(true); + + toggleGroup.selectedToggleProperty().addListener((obs, o, n) -> { + okBtn.setDisable(n == null); + selectedPath = (n instanceof ToggleButton tb) ? (String) tb.getUserData() : null; + }); + + setResultConverter(btn -> btn == ButtonType.OK ? selectedPath : null); + } + + // ── Karten bauen ───────────────────────────────────────────────────────── + + private void buildCards(Path modelsDir) { + // Alle .j3o-Dateien sammeln, nach Verzeichnis gruppiert + List j3oFiles = new ArrayList<>(); + try (Stream walk = Files.walk(modelsDir)) { + walk.filter(Files::isRegularFile) + .filter(p -> p.getFileName().toString().toLowerCase().endsWith(".j3o")) + .sorted() + .forEach(j3oFiles::add); + } catch (IOException ignored) {} + + if (j3oFiles.isEmpty()) return; + + // Nach Unterordner gruppieren + String currentGroup = null; + FlowPane currentPane = null; + + for (Path j3o : j3oFiles) { + String relToModels = modelsDir.relativize(j3o.getParent()).toString().replace('\\', '/'); + if (relToModels.isEmpty()) relToModels = "(Wurzel)"; + + if (!relToModels.equals(currentGroup)) { + currentGroup = relToModels; + currentPane = newFlowPane(); + contentBox.getChildren().add(section(currentGroup, currentPane)); + } + + String assetPath = assetRoot.relativize(j3o).toString().replace('\\', '/'); + Image thumb = loadThumb(j3o); + ToggleButton card = buildCard(j3o.getFileName().toString(), assetPath, thumb); + currentPane.getChildren().add(card); + allCards.add(card); + } + } + + private Image loadThumb(Path j3o) { + // Sidecar: .thumb.png + Path sidecar = j3o.resolveSibling(j3o.getFileName() + ".thumb.png"); + if (Files.isRegularFile(sidecar)) { + try (InputStream is = Files.newInputStream(sidecar)) { + return new Image(is, THUMB_SIZE, THUMB_SIZE, true, true); + } catch (IOException ignored) {} + } + return null; + } + + // ── Karte ──────────────────────────────────────────────────────────────── + + private ToggleButton buildCard(String displayName, String assetPath, Image thumb) { + ImageView iv; + if (thumb != null) { + iv = new ImageView(thumb); + } else { + // Platzhalter: Gitterbox-Symbol + iv = new ImageView(); + iv.setStyle("-fx-background-color: #2a2a3a;"); + } + iv.setFitWidth(THUMB_SIZE); + iv.setFitHeight(THUMB_SIZE); + iv.setPreserveRatio(true); + + String short_ = truncate(displayName.replace(".j3o", ""), 14); + Label lbl = new Label(short_); + lbl.setMaxWidth(CARD_W - 4); + lbl.setAlignment(Pos.CENTER); + lbl.setStyle("-fx-font-size: 10;"); + + VBox graphic = new VBox(4, iv, lbl); + graphic.setAlignment(Pos.TOP_CENTER); + + ToggleButton btn = new ToggleButton(); + btn.setGraphic(graphic); + btn.setUserData(assetPath); + btn.setToggleGroup(toggleGroup); + btn.setPrefSize(CARD_W, CARD_H); + btn.setTooltip(new Tooltip(assetPath)); + + btn.setOnMouseClicked(e -> { + if (e.getClickCount() == 2) { + selectedPath = assetPath; + Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK); + if (okBtn != null) okBtn.fire(); + } + }); + return btn; + } + + // ── Filter ──────────────────────────────────────────────────────────────── + + private void applyFilter(String lower) { + for (ToggleButton card : allCards) { + String path = (String) card.getUserData(); + boolean visible = lower.isBlank() || path.toLowerCase().contains(lower); + card.setVisible(visible); + card.setManaged(visible); + } + } + + // ── Layout-Helfer ───────────────────────────────────────────────────────── + + private static FlowPane newFlowPane() { + FlowPane fp = new FlowPane(8, 8); + fp.setPadding(new Insets(4, 4, 4, 4)); + return fp; + } + + private static VBox section(String title, FlowPane pane) { + Label lbl = new Label(title); + lbl.setStyle("-fx-font-weight: bold; -fx-font-size: 11;"); + return new VBox(4, lbl, new Separator(), pane); + } + + private static String truncate(String s, int max) { + if (s.length() <= max) return s; + return s.substring(0, max - 1) + "…"; + } +} diff --git a/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java b/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java index ab2efd7..a2700ec 100644 --- a/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java +++ b/blight-game/src/main/java/de/blight/game/animation/RetargetingSystem.java @@ -1,16 +1,25 @@ package de.blight.game.animation; -import com.jme3.anim.*; -import com.jme3.math.FastMath; -import com.jme3.math.Quaternion; -import com.jme3.math.Vector3f; -import com.jme3.scene.Node; -import com.jme3.scene.Spatial; -import com.jme3.scene.control.Control; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import com.jme3.anim.AnimClip; +import com.jme3.anim.AnimComposer; +import com.jme3.anim.AnimTrack; +import com.jme3.anim.Armature; +import com.jme3.anim.Joint; +import com.jme3.anim.SkinningControl; +import com.jme3.anim.TransformTrack; +import com.jme3.math.Quaternion; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.control.Control; /** * Model-space retargeting with correct parent-chain propagation. @@ -406,7 +415,6 @@ public final class RetargetingSystem { return findControl(s, SkinningControl.class); } - @SuppressWarnings("unchecked") static T findControl(Spatial s, Class type) { T c = s.getControl(type); if (c != null) return c; diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index bf50921..c3763f3 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -108,6 +108,12 @@ public class WorldScene extends BaseAppState { @Override protected void onEnable() { + try { + assetManager.registerLocator( + AnimationLibrary.findAssetRoot().toAbsolutePath().toString(), + com.jme3.asset.plugins.FileLocator.class); + } catch (Exception ignored) {} + BlightGame.status("Baue Beleuchtung und Himmel..."); buildLighting(); @@ -529,6 +535,7 @@ public class WorldScene extends BaseAppState { String[] mapTex = map.terrainTextures; String[] matParams = {"DiffuseMap","DiffuseMap_1","DiffuseMap_2","DiffuseMap_3"}; String[] scaleP = {"DiffuseMap_0_scale","DiffuseMap_1_scale","DiffuseMap_2_scale","DiffuseMap_3_scale"}; + String[] nmParams = {"NormalMap","NormalMap_1","NormalMap_2","NormalMap_3"}; for (int i = 0; i < 4; i++) { String path = (mapTex[i] != null && !mapTex[i].isEmpty()) ? mapTex[i] : DEF_TEX[i]; if (path == null || path.isEmpty()) continue; @@ -536,6 +543,18 @@ public class WorldScene extends BaseAppState { tex.setWrap(Texture.WrapMode.Repeat); mat.setTexture(matParams[i], tex); mat.setFloat(scaleP[i], 512f); + String nmp = map.terrainNormalMaps[i]; + System.out.println("[WorldScene] Slot " + i + " NormalMap: '" + nmp + "'"); + if (nmp != null && !nmp.isEmpty()) { + try { + Texture nm = assetManager.loadTexture(nmp); + nm.setWrap(Texture.WrapMode.Repeat); + mat.setTexture(nmParams[i], nm); + System.out.println("[WorldScene] Slot " + i + " NormalMap geladen OK"); + } catch (Exception e) { + System.err.println("[WorldScene] NormalMap nicht ladbar: " + nmp + " – " + e.getMessage()); + } + } } // Ältere Maps haben splatR=0 → Gras (Slot 0) wäre unsichtbar; auf 255 setzen. diff --git a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java index ae46365..d3e7436 100644 --- a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java +++ b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java @@ -282,6 +282,7 @@ public class TerrainChunkState extends BaseAppState { float[] positions = new float[vertCount * 3]; float[] normals = new float[vertCount * 3]; + float[] tangents = new float[vertCount * 4]; float[] texCoords = new float[vertCount * 2]; int[] indices = new int[indexCount]; @@ -307,12 +308,23 @@ public class TerrainChunkState extends BaseAppState { if (nLen > 1e-6f) { nx /= nLen; ny /= nLen; nz /= nLen; } normals[pi] = nx; normals[pi+1] = ny; normals[pi+2] = nz; - // Welt-Raum UV (0–1 über 4096 m) für globale Splat-Map + // Tangente: Projektion der +X-Achse auf die Oberfläche (Gram-Schmidt) + // U wächst mit worldX → Tangente zeigt in +X-Richtung + float dotX = nx; // dot((1,0,0), N) + float tx = 1f - dotX * nx; + float ty = -dotX * ny; + float tz = -dotX * nz; + float tLen = (float) Math.sqrt(tx*tx + ty*ty + tz*tz); + if (tLen > 1e-6f) { tx /= tLen; ty /= tLen; tz /= tLen; } + int gi = vi * 4; + tangents[gi] = tx; tangents[gi+1] = ty; tangents[gi+2] = tz; tangents[gi+3] = 1f; + + // Welt-Raum UV für Splat-Map: JME3-TerrainQuad-Konvention V=0 → worldZ=+2048 float worldX = chunkCX + lx; float worldZ = chunkCZ + lz; int ti = vi * 2; texCoords[ti] = (worldX + 2048f) / 4096f; - texCoords[ti+1] = (worldZ + 2048f) / 4096f; + texCoords[ti+1] = 1f - (worldZ + 2048f) / 4096f; } } @@ -331,6 +343,7 @@ public class TerrainChunkState extends BaseAppState { Mesh mesh = new Mesh(); mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(positions)); mesh.setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(normals)); + mesh.setBuffer(VertexBuffer.Type.Tangent, 4, BufferUtils.createFloatBuffer(tangents)); mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoords)); mesh.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(indices)); mesh.updateBound(); 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 043c778..1e7ec24 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 @@ -11,8 +11,6 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.material.Material; import com.jme3.math.ColorRGBA; -import com.jme3.math.FastMath; -import com.jme3.math.Quaternion; import com.jme3.math.Vector3f; import com.jme3.scene.Geometry; import com.jme3.scene.Node; @@ -64,9 +62,6 @@ public class WorldItemsState extends BaseAppState { private final List visuals = new ArrayList<>(); private final Map itemDefs = new HashMap<>(); - private final Quaternion rotQuat = new Quaternion(); - private float rotAccum = 0f; - public WorldItemsState(KeyBindings keyBindings, CharacterControl physicsChar, MainCharacter mainCharacter, PlayerInputControl playerInput) { this.keyBindings = keyBindings; @@ -136,12 +131,7 @@ public class WorldItemsState extends BaseAppState { protected void cleanup(Application app) {} @Override - public void update(float tpf) { - if (visuals.isEmpty()) return; - rotAccum += tpf * 60f; - rotQuat.fromAngles(0f, rotAccum * FastMath.DEG_TO_RAD, 0f); - for (Spatial s : visuals) s.setLocalRotation(rotQuat); - } + public void update(float tpf) {} // ── Interaktion ─────────────────────────────────────────────────────────── diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index 1274c73..5655272 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index c50f5b8..80b8bd3 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index 9a3f07f..bb3c009 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -43,3 +43,4 @@ Models/plants/fern/fern_20260608_165628.j3o 163.74956 0.53243 21.45433 0.00000 1 Models/plants/fern/fern_20260608_165628.j3o 162.27063 0.88450 24.03092 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/plants/misc/heliconia+plant+3d+model.j3o 152.94803 0.96488 8.99865 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/plants/misc/heliconia+plant+3d+model.j3o 155.57500 0.96457 10.04861 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/plants/misc/kaktusfeige.j3o 153.89978 0.98474 38.66549 0.00000 2.50000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 diff --git a/blight-map/src/main/map/blight_placed_items.bpi b/blight-map/src/main/map/blight_placed_items.bpi new file mode 100644 index 0000000..3320a13 --- /dev/null +++ b/blight-map/src/main/map/blight_placed_items.bpi @@ -0,0 +1 @@ +# itemId x y z diff --git a/blight-map/src/main/map/chunks/chunk_17_16.blc b/blight-map/src/main/map/chunks/chunk_17_16.blc index 71dda1a..70f594f 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_16.blc and b/blight-map/src/main/map/chunks/chunk_17_16.blc differ diff --git a/doc/.~lock.Heilung, Ausdauer, Mana.odt# b/doc/.~lock.Heilung, Ausdauer, Mana.odt# deleted file mode 100644 index ea75902..0000000 --- a/doc/.~lock.Heilung, Ausdauer, Mana.odt# +++ /dev/null @@ -1 +0,0 @@ -,mario,mario-mint,08.06.2026 22:14,file:///home/mario/.config/libreoffice/4; \ No newline at end of file