Weiter gebastelt
@@ -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
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 7.6 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
14
blight-assets/src/main/resources/items/misc/blutagave.item
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
14
blight-assets/src/main/resources/items/misc/erzmoss.item
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"itemId": "neues_item_1780949443027",
|
||||
"category": "CONSUMABLES",
|
||||
"name": {
|
||||
"id": "bloddagave.name"
|
||||
},
|
||||
"description": {
|
||||
"id": "bloddagave.description"
|
||||
},
|
||||
"worthGold": 50
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <itemId>.item} im items/-Verzeichnis.
|
||||
* Dateiformat: {@code items/<category>/<itemId>.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<Path> 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<Item> loadAll(Path itemDir) {
|
||||
List<Item> result = new ArrayList<>();
|
||||
if (!Files.isDirectory(itemDir)) return result;
|
||||
try (Stream<Path> walk = Files.list(itemDir)) {
|
||||
walk.filter(p -> p.toString().endsWith(EXTENSION))
|
||||
try (Stream<Path> 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<Path> walk = Files.walk(itemDir)) {
|
||||
List<Path> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<PlacedItem> items = new ArrayList<>();
|
||||
private final List<Node> nodes = new ArrayList<>();
|
||||
private Node itemRoot;
|
||||
private Node previewNode;
|
||||
private final List<PlacedItem> items = new ArrayList<>();
|
||||
private final List<Node> nodes = new ArrayList<>();
|
||||
private final Map<String, Item> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 <j3oPath>.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 <j3oPath>.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Item> sharedItems = FXCollections.observableArrayList();
|
||||
private final ObservableList<Item> items = FXCollections.observableArrayList();
|
||||
private final Path itemDir;
|
||||
private final Path assetRoot;
|
||||
|
||||
private final SortedList<Item> sortedItems;
|
||||
private final ListView<Item> listView;
|
||||
private Item current = null;
|
||||
|
||||
// Form-Felder
|
||||
private TextField idField;
|
||||
private ComboBox<ItemCategory> catCombo;
|
||||
private TextField nameField;
|
||||
private TextField descField;
|
||||
private Spinner<Integer> 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<Item> 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<Item> items;
|
||||
private final Path itemDir;
|
||||
private final Runnable onSaved;
|
||||
// ── Form construction ─────────────────────────────────────────────────────
|
||||
|
||||
private final SortedList<Item> sortedItems;
|
||||
private final ListView<Item> 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<ItemCategory> catCombo;
|
||||
private TextField nameField;
|
||||
private TextField descField;
|
||||
private Spinner<Integer> 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<Item> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
|
||||
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<ToggleButton> 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<Path> j3oFiles = new ArrayList<>();
|
||||
try (Stream<Path> 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: <model.j3o>.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) + "…";
|
||||
}
|
||||
}
|
||||
@@ -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 extends Control> T findControl(Spatial s, Class<T> type) {
|
||||
T c = s.getControl(type);
|
||||
if (c != null) return c;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Spatial> visuals = new ArrayList<>();
|
||||
private final Map<String, Item> 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 ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
1
blight-map/src/main/map/blight_placed_items.bpi
Normal file
@@ -0,0 +1 @@
|
||||
# itemId x y z
|
||||
@@ -1 +0,0 @@
|
||||
,mario,mario-mint,08.06.2026 22:14,file:///home/mario/.config/libreoffice/4;
|
||||