Commit vor Voxel Update für die Klippen
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import de.blight.common.BlightHome;
|
||||
import de.blight.editor.tool.ChoiceToolParameter;
|
||||
import de.blight.editor.tool.EditorTool;
|
||||
import de.blight.editor.tool.GrassTool;
|
||||
import de.blight.editor.tool.ToolParameter;
|
||||
import de.blight.editor.tree.PalmOptions;
|
||||
import de.blight.editor.tree.TreeParams;
|
||||
import de.blight.editor.ui.MapObjectsView;
|
||||
import de.blight.editor.ui.TextureChooser;
|
||||
import de.blight.eztree.Billboard;
|
||||
import de.blight.eztree.TreeOptions;
|
||||
@@ -62,6 +64,7 @@ public class EditorApp extends Application {
|
||||
private java.nio.file.attribute.FileTime lastLivePosTime =
|
||||
java.nio.file.attribute.FileTime.fromMillis(0);
|
||||
private VBox assetPanel;
|
||||
private MapObjectsView mapObjectsView;
|
||||
private StackPane worldViewport;
|
||||
private VBox topBar; // MenuBar + aktuelle Toolbar
|
||||
private ToolBar worldToolBar; // Welt-Editor-Toolbar (Layer-Buttons)
|
||||
@@ -229,6 +232,9 @@ public class EditorApp extends Application {
|
||||
private ToggleButton areaBtn;
|
||||
private ToggleButton locationZoneBtn;
|
||||
private ToggleButton playToolBtn;
|
||||
private ToggleButton voxelBtn;
|
||||
private ToggleButton camOrbitBtn;
|
||||
private ToggleButton camFreeBtn;
|
||||
|
||||
// "Objekt"-Button in der Selektionsleiste (zum Zurückschalten bei importierten Objekten)
|
||||
private ToggleButton selModeObjectBtn;
|
||||
@@ -324,6 +330,16 @@ public class EditorApp extends Application {
|
||||
|
||||
primaryStage = stage;
|
||||
input.scanSkeletalRequested = true; // Skelett-Scan beim Start anstoßen
|
||||
|
||||
// Alle ungefangenen Exceptions auf dem FX-Thread loggen – hilft beim Finden des Zahnrad-Auslösers
|
||||
Thread.currentThread().setUncaughtExceptionHandler((t, ex) ->
|
||||
org.slf4j.LoggerFactory.getLogger(EditorApp.class)
|
||||
.error("[FX-Thread] Ungefangene Exception", ex));
|
||||
// Auch andere Threads absichern (JME3-Thread, Worker-Threads)
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, ex) ->
|
||||
org.slf4j.LoggerFactory.getLogger(EditorApp.class)
|
||||
.error("[Thread '{}'] Ungefangene Exception", t.getName(), ex));
|
||||
|
||||
stage.setTitle("Blight World Editor");
|
||||
try (var is = getClass().getResourceAsStream("/icon_editor.png")) {
|
||||
if (is != null) stage.getIcons().add(new Image(is));
|
||||
@@ -932,6 +948,9 @@ public class EditorApp extends Application {
|
||||
areaBtn = new ToggleButton("🗺 Bereiche");
|
||||
locationZoneBtn = new ToggleButton("📍 Locations");
|
||||
playToolBtn = new ToggleButton("🎮 Spielen");
|
||||
voxelBtn = new ToggleButton("⬡ Voxel");
|
||||
camOrbitBtn = new ToggleButton("⊙ Orbit");
|
||||
camFreeBtn = new ToggleButton("✈ FreeFly");
|
||||
baseBtn.setStyle("-fx-font-weight:bold;");
|
||||
grassBtn.setStyle("-fx-font-weight:bold;");
|
||||
grassVertexBtn.setStyle("-fx-font-weight:bold;");
|
||||
@@ -946,6 +965,9 @@ public class EditorApp extends Application {
|
||||
areaBtn.setStyle("-fx-font-weight:bold;");
|
||||
locationZoneBtn.setStyle("-fx-font-weight:bold;");
|
||||
playToolBtn.setStyle("-fx-font-weight:bold;");
|
||||
voxelBtn.setStyle("-fx-font-weight:bold;");
|
||||
camOrbitBtn.setStyle("-fx-font-weight:bold;");
|
||||
camFreeBtn.setStyle("-fx-font-weight:bold;");
|
||||
|
||||
ToggleGroup layerGroup = new ToggleGroup();
|
||||
baseBtn.setToggleGroup(layerGroup);
|
||||
@@ -962,6 +984,7 @@ public class EditorApp extends Application {
|
||||
areaBtn.setToggleGroup(layerGroup);
|
||||
locationZoneBtn.setToggleGroup(layerGroup);
|
||||
playToolBtn.setToggleGroup(layerGroup);
|
||||
voxelBtn.setToggleGroup(layerGroup);
|
||||
baseBtn.setSelected(true);
|
||||
|
||||
baseBtn.setOnAction(e -> {
|
||||
@@ -1030,6 +1053,19 @@ public class EditorApp extends Application {
|
||||
input.activeLayer = SharedInput.LAYER_PLAY_TOOL;
|
||||
root.setRight(buildPlayToolPanel());
|
||||
});
|
||||
voxelBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_VOXEL;
|
||||
input.activeTool = input.voxelTool;
|
||||
root.setRight(toolPanel);
|
||||
showToolParameters(toolPanel, input.activeTool);
|
||||
});
|
||||
|
||||
ToggleGroup camModeGroup = new ToggleGroup();
|
||||
camOrbitBtn.setToggleGroup(camModeGroup);
|
||||
camFreeBtn.setToggleGroup(camModeGroup);
|
||||
camOrbitBtn.setSelected(true);
|
||||
camOrbitBtn.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT);
|
||||
camFreeBtn.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY);
|
||||
|
||||
Label hint = new Label("WASD/QE: Kamera | Mitte-Drag / L+R-Drag: Drehen | L-Klick: hoch | R-Klick: tief");
|
||||
hint.setStyle("-fx-text-fill: #555;");
|
||||
@@ -1042,6 +1078,8 @@ public class EditorApp extends Application {
|
||||
new Separator(Orientation.VERTICAL), riverBtn,
|
||||
new Separator(Orientation.VERTICAL), soundAreaBtn, areaBtn, locationZoneBtn,
|
||||
new Separator(Orientation.VERTICAL), playToolBtn,
|
||||
new Separator(Orientation.VERTICAL), voxelBtn,
|
||||
new Separator(Orientation.VERTICAL), camOrbitBtn, camFreeBtn,
|
||||
new Separator(Orientation.VERTICAL), hint);
|
||||
|
||||
worldToolBar = toolBar;
|
||||
@@ -2775,6 +2813,12 @@ public class EditorApp extends Application {
|
||||
if (tool instanceof de.blight.editor.tool.TextureTool
|
||||
&& param == ((de.blight.editor.tool.TextureTool) tool).textureIndex) {
|
||||
panel.getChildren().addAll(nameLabel, buildTextureChoiceUI(param));
|
||||
} else if (tool instanceof de.blight.editor.tool.VoxelTool vt
|
||||
&& param == vt.textureSlot) {
|
||||
panel.getChildren().addAll(nameLabel, buildVoxelTextureChoiceUI(param));
|
||||
} else if (tool instanceof de.blight.editor.tool.VoxelTool vt2
|
||||
&& param == vt2.mode) {
|
||||
panel.getChildren().addAll(nameLabel, buildVoxelModeUI(param));
|
||||
} else if (param.getImagePaths() != null) {
|
||||
String[] paths = param.getImagePaths();
|
||||
String[] labels = param.getChoices();
|
||||
@@ -3045,6 +3089,132 @@ public class EditorApp extends Application {
|
||||
return tile;
|
||||
}
|
||||
|
||||
private javafx.scene.Node buildVoxelModeUI(ChoiceToolParameter param) {
|
||||
String[] labels = param.getChoices();
|
||||
String[] imgPaths = param.getImagePaths();
|
||||
ToggleGroup tg = new ToggleGroup();
|
||||
javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane();
|
||||
tile.setHgap(4);
|
||||
tile.setVgap(4);
|
||||
tile.setPrefColumns(6);
|
||||
|
||||
for (int j = 0; j < labels.length; j++) {
|
||||
final int idx = j;
|
||||
String path = (imgPaths != null && j < imgPaths.length) ? imgPaths[j] : null;
|
||||
|
||||
ToggleButton btn = new ToggleButton();
|
||||
btn.setToggleGroup(tg);
|
||||
btn.setPrefSize(44, 56);
|
||||
btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
||||
btn.setTooltip(new Tooltip(labels[j]));
|
||||
|
||||
VBox content = new VBox(2);
|
||||
content.setAlignment(Pos.CENTER);
|
||||
|
||||
boolean hasImage = false;
|
||||
if (path != null) {
|
||||
URL resUrl = EditorApp.class.getResource("/" + path);
|
||||
String imgUrl = resUrl != null ? resUrl.toString() : null;
|
||||
if (imgUrl == null) {
|
||||
File f = new File(path).getAbsoluteFile();
|
||||
if (f.exists()) imgUrl = f.toURI().toString();
|
||||
}
|
||||
if (imgUrl != null) {
|
||||
Image img = new Image(imgUrl, 32, 32, true, true);
|
||||
if (!img.isError()) {
|
||||
ImageView iv = new ImageView(img);
|
||||
iv.setFitWidth(32); iv.setFitHeight(32);
|
||||
content.getChildren().add(iv);
|
||||
hasImage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasImage) {
|
||||
boolean isRemove = labels[j].equals("Entfernen");
|
||||
Label icon = new Label(isRemove ? "✕" : "+");
|
||||
icon.setStyle("-fx-font-size: 16; -fx-text-fill: " + (isRemove ? "#cc2222" : "#228822") + ";");
|
||||
content.getChildren().add(icon);
|
||||
}
|
||||
Label lbl = new Label(labels[j]);
|
||||
lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;");
|
||||
content.getChildren().add(lbl);
|
||||
btn.setGraphic(content);
|
||||
|
||||
boolean initially = (j == param.getSelectedIndex());
|
||||
btn.setSelected(initially);
|
||||
applyModeButtonStyle(btn, initially);
|
||||
btn.selectedProperty().addListener((obs, was, isNow) -> {
|
||||
applyModeButtonStyle(btn, isNow);
|
||||
if (isNow) param.setSelectedIndex(idx);
|
||||
});
|
||||
tile.getChildren().add(btn);
|
||||
}
|
||||
tg.selectedToggleProperty().addListener((obs, oldT, newT) -> {
|
||||
if (newT == null) tg.selectToggle(oldT);
|
||||
});
|
||||
return tile;
|
||||
}
|
||||
|
||||
private javafx.scene.Node buildVoxelTextureChoiceUI(ChoiceToolParameter param) {
|
||||
ToggleGroup tg = new ToggleGroup();
|
||||
javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane();
|
||||
tile.setHgap(4);
|
||||
tile.setVgap(4);
|
||||
tile.setPrefColumns(4);
|
||||
|
||||
for (int j = 0; j < 4; j++) {
|
||||
final int idx = j;
|
||||
String path = (j < input.terrainTexturePaths.length
|
||||
&& input.terrainTexturePaths[j] != null
|
||||
&& !input.terrainTexturePaths[j].isEmpty())
|
||||
? input.terrainTexturePaths[j] : "";
|
||||
|
||||
ToggleButton btn = new ToggleButton();
|
||||
btn.setToggleGroup(tg);
|
||||
btn.setPrefSize(56, 76);
|
||||
btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
||||
btn.setTooltip(new Tooltip("Slot " + (j + 1) + ": " + (path.isEmpty() ? "(leer)" : path)));
|
||||
|
||||
VBox content = new VBox(2);
|
||||
content.setAlignment(Pos.CENTER);
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
String imgUrl = resolveImageUrl(path);
|
||||
if (imgUrl != null) {
|
||||
Image img = new Image(imgUrl, 44, 44, true, true);
|
||||
ImageView iv = new ImageView(img.isError() ? null : img);
|
||||
iv.setFitWidth(44);
|
||||
iv.setFitHeight(44);
|
||||
content.getChildren().add(img.isError() ? emptySlotPlaceholder(44) : iv);
|
||||
} else {
|
||||
content.getChildren().add(emptySlotPlaceholder(44));
|
||||
}
|
||||
} else {
|
||||
content.getChildren().add(emptySlotPlaceholder(44));
|
||||
}
|
||||
|
||||
Label lbl = new Label("S" + (j + 1) + " " + labelFromPath(path));
|
||||
lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;");
|
||||
lbl.setMaxWidth(54);
|
||||
content.getChildren().add(lbl);
|
||||
|
||||
btn.setGraphic(content);
|
||||
boolean initially = (j == param.getSelectedIndex());
|
||||
btn.setSelected(initially);
|
||||
applyModeButtonStyle(btn, initially);
|
||||
btn.selectedProperty().addListener((obs, was, isNow) -> {
|
||||
applyModeButtonStyle(btn, isNow);
|
||||
if (isNow) param.setSelectedIndex(idx);
|
||||
});
|
||||
tile.getChildren().add(btn);
|
||||
}
|
||||
|
||||
tg.selectedToggleProperty().addListener((obs, oldT, newT) -> {
|
||||
if (newT == null) tg.selectToggle(oldT);
|
||||
});
|
||||
return tile;
|
||||
}
|
||||
|
||||
private javafx.scene.layout.Region emptySlotPlaceholder(double size) {
|
||||
javafx.scene.layout.Region r = new javafx.scene.layout.Region();
|
||||
r.setPrefSize(size, size);
|
||||
@@ -3172,10 +3342,10 @@ public class EditorApp extends Application {
|
||||
// ── Linke Seite: Asset-Panel (Welteneditor) ──────────────────────────────
|
||||
|
||||
private VBox buildAssetPanel() {
|
||||
VBox panel = new VBox(6);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setPrefWidth(420);
|
||||
panel.setStyle("-fx-background-color: #f0f0f0;");
|
||||
// ── Tab 1: Asset-Baum ─────────────────────────────────────────────────
|
||||
VBox assetContent = new VBox(6);
|
||||
assetContent.setPadding(new Insets(8));
|
||||
assetContent.setStyle("-fx-background-color: #f0f0f0;");
|
||||
|
||||
Label title = new Label("Assets");
|
||||
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
@@ -3283,7 +3453,106 @@ public class EditorApp extends Application {
|
||||
HBox.setHgrow(importBtn, Priority.ALWAYS);
|
||||
bottomBar.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
panel.getChildren().addAll(titleBar, tree, bottomBar);
|
||||
assetContent.getChildren().addAll(titleBar, tree, bottomBar);
|
||||
|
||||
// ── Tab 2: Karte (platzierte Objekte) ─────────────────────────────────
|
||||
mapObjectsView = new MapObjectsView(
|
||||
(x, y, z, toolHint) -> {
|
||||
input.pendingGotoX = x;
|
||||
input.pendingGotoY = y;
|
||||
input.pendingGotoZ = z;
|
||||
input.pendingGotoPitch = (float) (-Math.PI / 3);
|
||||
switch (toolHint) {
|
||||
case "model" -> { input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; root.setRight(buildObjectEditPanel()); }
|
||||
case "item" -> { input.activeLayer = SharedInput.LAYER_ITEMS; root.setRight(buildItemPlacePanel(null)); }
|
||||
case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; root.setRight(buildLightPanel()); }
|
||||
case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; root.setRight(buildEmitterPanel()); }
|
||||
case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; root.setRight(buildWaterPanel()); }
|
||||
case "soundarea" -> { input.activeLayer = SharedInput.LAYER_SOUND_AREAS; root.setRight(buildSoundAreaPanel()); }
|
||||
case "area" -> { input.activeLayer = SharedInput.LAYER_AREAS; root.setRight(buildAreaPanel()); }
|
||||
case "locationzone" -> { input.activeLayer = SharedInput.LAYER_LOCATION_ZONES; root.setRight(buildLocationZonePanel()); }
|
||||
case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; root.setRight(buildWaterfallPanel()); }
|
||||
}
|
||||
},
|
||||
(obj, index, toolHint) -> {
|
||||
switch (toolHint) {
|
||||
case "model" -> {
|
||||
java.util.List<de.blight.common.PlacedModel> list = new java.util.ArrayList<>(de.blight.common.PlacedModelIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.PlacedModelIO.save(list);
|
||||
input.reloadPlacedModels = true;
|
||||
}
|
||||
case "item" -> {
|
||||
de.blight.common.PlacedItem pi = (de.blight.common.PlacedItem) obj;
|
||||
java.util.List<de.blight.common.PlacedItem> list = new java.util.ArrayList<>(de.blight.common.PlacedItemIO.load());
|
||||
list.removeIf(it -> it.uuid().equals(pi.uuid()));
|
||||
de.blight.common.PlacedItemIO.save(list);
|
||||
input.reloadPlacedItems = true;
|
||||
}
|
||||
case "light" -> {
|
||||
java.util.List<de.blight.common.PlacedLight> list = new java.util.ArrayList<>(de.blight.common.LightIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.LightIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "emitter" -> {
|
||||
java.util.List<de.blight.common.PlacedEmitter> list = new java.util.ArrayList<>(de.blight.common.EmitterIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.EmitterIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "water" -> {
|
||||
java.util.List<de.blight.common.PlacedWater> list = new java.util.ArrayList<>(de.blight.common.WaterBodyIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.WaterBodyIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "soundarea" -> {
|
||||
java.util.List<de.blight.common.PlacedSoundArea> list = new java.util.ArrayList<>(de.blight.common.SoundAreaIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.SoundAreaIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "area" -> {
|
||||
java.util.List<de.blight.common.PlacedArea> list = new java.util.ArrayList<>(de.blight.common.AreaIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.AreaIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "locationzone" -> {
|
||||
java.util.List<de.blight.common.PlacedLocationZone> list = new java.util.ArrayList<>(de.blight.common.LocationZoneIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.LocationZoneIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
case "waterfall" -> {
|
||||
java.util.List<java.util.List<de.blight.common.RiverPoint>> list = new java.util.ArrayList<>(de.blight.common.RiverIO.load());
|
||||
if (index >= 0 && index < list.size()) list.remove(index);
|
||||
de.blight.common.RiverIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ── TabPane ───────────────────────────────────────────────────────────
|
||||
Tab assetsTab = new Tab("Assets", assetContent);
|
||||
assetsTab.setClosable(false);
|
||||
|
||||
Tab karteTab = new Tab("Karte", mapObjectsView);
|
||||
karteTab.setClosable(false);
|
||||
karteTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
if (isSelected) mapObjectsView.refresh();
|
||||
});
|
||||
|
||||
TabPane tabPane = new TabPane(assetsTab, karteTab);
|
||||
tabPane.setStyle("-fx-background-color: #e8e8e8;");
|
||||
VBox.setVgrow(tabPane, Priority.ALWAYS);
|
||||
|
||||
VBox panel = new VBox(tabPane);
|
||||
panel.setPrefWidth(420);
|
||||
panel.setStyle("-fx-background-color: #e8e8e8;");
|
||||
VBox.setVgrow(tabPane, Priority.ALWAYS);
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -4082,10 +4351,10 @@ public class EditorApp extends Application {
|
||||
new FileChooser.ExtensionFilter("Alle unterstützten Dateien",
|
||||
"*.j3o","*.obj","*.gltf","*.glb",
|
||||
"*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds",
|
||||
"*.ogg","*.wav"),
|
||||
"*.ogg","*.wav","*.mp3"),
|
||||
new FileChooser.ExtensionFilter("Modelle", "*.j3o","*.obj","*.gltf","*.glb"),
|
||||
new FileChooser.ExtensionFilter("Texturen", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"),
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV)", "*.ogg","*.wav")
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg","*.wav","*.mp3")
|
||||
);
|
||||
var files = fc.showOpenMultipleDialog(owner);
|
||||
if (files == null) return;
|
||||
@@ -4094,7 +4363,7 @@ public class EditorApp extends Application {
|
||||
String name = file.getName().toLowerCase();
|
||||
boolean isNativeModel = name.matches(".*\\.(obj|fbx|gltf|glb)");
|
||||
boolean isJ3o = name.endsWith(".j3o");
|
||||
boolean isAudio = name.matches(".*\\.(ogg|wav)");
|
||||
boolean isAudio = name.matches(".*\\.(ogg|wav|mp3)");
|
||||
boolean isModel = isNativeModel || isJ3o;
|
||||
|
||||
String subDir = isModel ? "Models" : isAudio ? "audio" : "Textures";
|
||||
@@ -4743,6 +5012,7 @@ public class EditorApp extends Application {
|
||||
|
||||
viewport.setOnScroll(e -> {
|
||||
int steps = (int) Math.signum(e.getDeltaY());
|
||||
if (e.isControlDown()) steps *= 10;
|
||||
input.scrollAccum.addAndGet(steps);
|
||||
});
|
||||
|
||||
@@ -4796,6 +5066,8 @@ public class EditorApp extends Application {
|
||||
if (action > 0) // only left-click sets spawn
|
||||
input.playToolClickQueue.offer(new SharedInput.PlayToolClick((float) x, (float) y));
|
||||
}
|
||||
case SharedInput.LAYER_VOXEL ->
|
||||
input.voxelEditQueue.offer(new SharedInput.VoxelEdit((float) x, (float) y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4839,12 +5111,24 @@ public class EditorApp extends Application {
|
||||
String libPath = System.getProperty("java.library.path", "");
|
||||
String projRoot = ProjectRoot.PATH.toString();
|
||||
|
||||
// Karte in run/session-<ts>/ kopieren – dort gitignored, aber im Projekt sichtbar.
|
||||
// Spielaktionen (Item-Aufheben etc.) verändern so nie die Editor-Karte.
|
||||
Path srcMapDir = ProjectRoot.PATH.resolve(
|
||||
Paths.get("blight-map", "src", "main", "map"));
|
||||
Path sessionDir = ProjectRoot.PATH.resolve("run")
|
||||
.resolve("session-" + System.currentTimeMillis());
|
||||
copyDirectory(srcMapDir, sessionDir);
|
||||
String sessionMapPath = sessionDir.resolve("blight_map.blm").toString();
|
||||
|
||||
java.util.List<String> cmd = new java.util.ArrayList<>(List.of(
|
||||
javaExe,
|
||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
|
||||
"-Djava.library.path=" + libPath,
|
||||
"-Dblight.project.root=" + projRoot));
|
||||
"-Dblight.project.root=" + projRoot,
|
||||
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,
|
||||
// Vom Editor gestartet: Hauptmenü überspringen, letzten Stand fortsetzen
|
||||
"-Dblight.autostart=true"));
|
||||
|
||||
if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) {
|
||||
cmd.add("-Dblight.temp.spawn.x=" + input.tempSpawnX);
|
||||
@@ -4896,6 +5180,19 @@ public class EditorApp extends Application {
|
||||
}, "game-launcher").start();
|
||||
}
|
||||
|
||||
private static void copyDirectory(Path src, Path dst) throws IOException {
|
||||
try (java.util.stream.Stream<Path> walk = java.nio.file.Files.walk(src)) {
|
||||
for (Path s : (Iterable<Path>) walk::iterator) {
|
||||
Path d = dst.resolve(src.relativize(s));
|
||||
if (java.nio.file.Files.isDirectory(s)) {
|
||||
java.nio.file.Files.createDirectories(d);
|
||||
} else {
|
||||
java.nio.file.Files.copy(s, d, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void openGameConsole() {
|
||||
if (gameConsoleStage == null) {
|
||||
gameConsoleArea = new TextArea();
|
||||
@@ -4962,8 +5259,8 @@ public class EditorApp extends Application {
|
||||
p.setProperty("cam.yaw", String.valueOf(input.camYaw));
|
||||
p.setProperty("cam.pitch", String.valueOf(input.camPitch));
|
||||
try {
|
||||
Files.createDirectories(ProjectRoot.resolve("config"));
|
||||
try (java.io.Writer w = Files.newBufferedWriter(ProjectRoot.resolve("config", "editor.prefs"))) {
|
||||
Files.createDirectories(BlightHome.resolve("config"));
|
||||
try (java.io.Writer w = Files.newBufferedWriter(BlightHome.resolve("config", "editor.prefs"))) {
|
||||
p.store(w, "Blight Editor – Kamera-Einstellungen");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -5131,7 +5428,7 @@ public class EditorApp extends Application {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Sound-Datei wählen");
|
||||
fc.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV)", "*.ogg", "*.wav"));
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg", "*.wav", "*.mp3"));
|
||||
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
if (java.nio.file.Files.isDirectory(assetRoot))
|
||||
fc.setInitialDirectory(assetRoot.toFile());
|
||||
@@ -5268,7 +5565,7 @@ public class EditorApp extends Application {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle(trackLabels[tIdx] + " wählen");
|
||||
fc.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV)", "*.ogg", "*.wav"));
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg", "*.wav", "*.mp3"));
|
||||
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
if (java.nio.file.Files.isDirectory(assetRoot))
|
||||
fc.setInitialDirectory(assetRoot.toFile());
|
||||
@@ -5873,17 +6170,7 @@ public class EditorApp extends Application {
|
||||
animSetClipListView.getItems().addAll(animSet.getClips());
|
||||
animSetClipListView.setPrefHeight(180);
|
||||
|
||||
// alle verfügbaren Clips aus animations/clips/ (unabhängig ob bereits im Set)
|
||||
java.util.List<String> availableClips = new java.util.ArrayList<>();
|
||||
Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips");
|
||||
if (java.nio.file.Files.isDirectory(clipsDir)) {
|
||||
try (var walk = java.nio.file.Files.walk(clipsDir, 1)) {
|
||||
walk.filter(cp -> cp.toString().endsWith(".j3o"))
|
||||
.map(cp -> cp.getFileName().toString().replaceFirst("\\.j3o$", ""))
|
||||
.sorted()
|
||||
.forEach(availableClips::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
Path animRootDir = ASSET_ROOT.resolve("animations");
|
||||
|
||||
Button addClipBtn = new Button("+ Hinzufügen…");
|
||||
Button removeClipBtn = new Button("- Entfernen");
|
||||
@@ -5895,26 +6182,72 @@ public class EditorApp extends Application {
|
||||
.addListener((obs, ov, nv) -> removeClipBtn.setDisable(nv == null));
|
||||
|
||||
addClipBtn.setOnAction(e -> {
|
||||
ComboBox<String> combo = new ComboBox<>();
|
||||
// alle verfügbaren Clips anzeigen, nicht nur unbenutzte
|
||||
combo.getItems().addAll(availableClips);
|
||||
combo.setEditable(true);
|
||||
combo.setMaxWidth(Double.MAX_VALUE);
|
||||
combo.getSelectionModel().selectFirst();
|
||||
org.slf4j.Logger _log = org.slf4j.LoggerFactory.getLogger("AnimSetClipScan");
|
||||
_log.debug("[ClipScan] animRootDir={} exists={}", animRootDir, java.nio.file.Files.isDirectory(animRootDir));
|
||||
|
||||
javafx.scene.control.Dialog<String> dlg = new javafx.scene.control.Dialog<>();
|
||||
dlg.setTitle("Clip hinzufügen");
|
||||
dlg.setHeaderText("Clip aus animations/clips/ wählen:");
|
||||
dlg.getDialogPane().setContent(combo);
|
||||
// Scan bei jedem Klick frisch – gesamter animations/-Ordner, sets/ ausgenommen
|
||||
java.util.List<String> allClips = new java.util.ArrayList<>();
|
||||
if (java.nio.file.Files.isDirectory(animRootDir)) {
|
||||
try (var walk = java.nio.file.Files.walk(animRootDir)) {
|
||||
walk.filter(cp -> {
|
||||
String name = cp.getFileName().toString();
|
||||
boolean isAnim = name.endsWith(".j3o") || name.endsWith(".glb")
|
||||
|| name.endsWith(".gltf") || name.endsWith(".fbx");
|
||||
boolean notSets = !animRootDir.relativize(cp).startsWith("sets");
|
||||
_log.debug("[ClipScan] {} | isAnim={} notSets={}", cp, isAnim, notSets);
|
||||
return isAnim && notSets;
|
||||
})
|
||||
.map(cp -> cp.getFileName().toString().replaceFirst("\\.[^.]+$", ""))
|
||||
.distinct()
|
||||
.sorted()
|
||||
.forEach(allClips::add);
|
||||
} catch (IOException ex) {
|
||||
_log.error("[ClipScan] Walk fehlgeschlagen", ex);
|
||||
allClips.clear();
|
||||
}
|
||||
}
|
||||
_log.debug("[ClipScan] allClips={}", allClips);
|
||||
_log.debug("[ClipScan] bereits im Set={}", animSetClipListView.getItems());
|
||||
|
||||
// Nur Clips anbieten, die noch nicht im Set enthalten sind
|
||||
java.util.List<String> notYetAdded = allClips.stream()
|
||||
.filter(c -> !animSetClipListView.getItems().contains(c))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
_log.debug("[ClipScan] notYetAdded={}", notYetAdded);
|
||||
|
||||
if (notYetAdded.isEmpty()) {
|
||||
javafx.scene.control.Alert info = new javafx.scene.control.Alert(
|
||||
javafx.scene.control.Alert.AlertType.INFORMATION);
|
||||
info.setTitle("Keine Clips verfügbar");
|
||||
info.setHeaderText(null);
|
||||
info.setContentText("Alle verfügbaren Clips sind bereits im Set enthalten.");
|
||||
info.showAndWait();
|
||||
return;
|
||||
}
|
||||
|
||||
ListView<String> list = new ListView<>();
|
||||
list.getItems().addAll(notYetAdded);
|
||||
list.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
||||
list.setPrefHeight(Math.min(notYetAdded.size() * 26 + 4, 320));
|
||||
list.getSelectionModel().selectFirst();
|
||||
|
||||
javafx.scene.control.Dialog<java.util.List<String>> dlg = new javafx.scene.control.Dialog<>();
|
||||
dlg.setTitle("Animation(en) hinzufügen");
|
||||
dlg.setHeaderText("Verfügbare Clips (noch nicht im Set) — Mehrfachauswahl möglich:");
|
||||
dlg.getDialogPane().setContent(list);
|
||||
dlg.getDialogPane().setPrefWidth(360);
|
||||
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Hinzufügen",
|
||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL);
|
||||
dlg.setResultConverter(bt -> bt == ok ? combo.getValue() : null);
|
||||
dlg.showAndWait().ifPresent(clip -> {
|
||||
if (clip != null && !clip.isBlank()
|
||||
&& !animSetClipListView.getItems().contains(clip)) {
|
||||
animSetClipListView.getItems().add(clip);
|
||||
animSetDirty = true;
|
||||
dlg.setResultConverter(bt -> bt == ok
|
||||
? new java.util.ArrayList<>(list.getSelectionModel().getSelectedItems())
|
||||
: null);
|
||||
dlg.showAndWait().ifPresent(selected -> {
|
||||
for (String clip : selected) {
|
||||
if (!animSetClipListView.getItems().contains(clip)) {
|
||||
animSetClipListView.getItems().add(clip);
|
||||
animSetDirty = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -6026,9 +6359,17 @@ public class EditorApp extends Application {
|
||||
if (animSetClipListView == null) return;
|
||||
String clip = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||
if (clip == null) return;
|
||||
if (animCurrentModelPath == null || animCurrentModelPath.isBlank()) {
|
||||
if (animPreviewStatusLabel != null)
|
||||
animPreviewStatusLabel.setText("Erst ein Charakter-Modell laden.");
|
||||
return;
|
||||
}
|
||||
animSetPendingPlayClip = clip;
|
||||
input.animPreviewLoadPath = "animations/clips/" + clip + ".j3o";
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade " + clip + "…");
|
||||
// Clip zur aktuell geladenen Figur hinzufügen (nicht als Modell laden).
|
||||
// Nach Abschluss setzt AnimPreviewState animPreviewClips, das den
|
||||
// animSetPendingPlayClip-Trigger auslöst und den Clip abspielt.
|
||||
input.animPreviewAddAnimPath = "animations/clips/" + clip + ".j3o";
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade Clip " + clip + "…");
|
||||
}
|
||||
|
||||
private void showAddActionToSetDialog() {
|
||||
|
||||
@@ -24,6 +24,7 @@ import de.blight.editor.state.PalmGeneratorState;
|
||||
import de.blight.editor.state.SceneObjectState;
|
||||
import de.blight.editor.state.TerrainEditorState;
|
||||
import de.blight.editor.state.TreeGeneratorState;
|
||||
import de.blight.editor.state.VoxelEditorState;
|
||||
import de.blight.game.console.JmeConsole;
|
||||
import de.blight.game.state.DayNightState;
|
||||
import javafx.scene.image.WritableImage;
|
||||
@@ -105,6 +106,7 @@ public class JmeEditorApp extends SimpleApplication {
|
||||
stateManager.attach(new AnimPreviewState(input));
|
||||
stateManager.attach(new ModelEditorState(input));
|
||||
stateManager.attach(new ItemPlacementState(input));
|
||||
stateManager.attach(new VoxelEditorState(input));
|
||||
|
||||
input.loadingStatus = "Initialisiere Konsole...";
|
||||
jmeConsole = new JmeConsole(false);
|
||||
|
||||
@@ -7,6 +7,7 @@ import de.blight.editor.tool.HeightTool;
|
||||
import de.blight.editor.tool.HoleTool;
|
||||
import de.blight.editor.tool.TextureTool;
|
||||
import de.blight.editor.tool.UpperHeightTool;
|
||||
import de.blight.editor.tool.VoxelTool;
|
||||
import de.blight.editor.tree.PalmOptions;
|
||||
import de.blight.editor.tree.TreeParams;
|
||||
import javafx.scene.image.WritableImage;
|
||||
@@ -25,6 +26,7 @@ public class SharedInput {
|
||||
public final GrassVertexTool grassVertexTool = new GrassVertexTool();
|
||||
public final TextureTool textureTool = new TextureTool();
|
||||
public final HoleTool holeTool = new HoleTool();
|
||||
public final VoxelTool voxelTool = new VoxelTool();
|
||||
public volatile EditorTool activeTool = heightTool;
|
||||
|
||||
// ── Initialisierungs-Status ───────────────────────────────────────────────
|
||||
@@ -37,6 +39,12 @@ public class SharedInput {
|
||||
// ── Upper-Layer-Sichtbarkeit ─────────────────────────────────────────────
|
||||
public volatile boolean upperLayerVisible = true;
|
||||
|
||||
// ── Kameramodus ───────────────────────────────────────────────────────────
|
||||
/** 0 = Orbit-Terrain-Kamera (Standard), 1 = FreeFly. */
|
||||
public volatile int camMode = 0;
|
||||
public static final int CAM_ORBIT = 0;
|
||||
public static final int CAM_FREEFLY = 1;
|
||||
|
||||
// ── Kamerabewegung (WASD + QE) ──────────────────────────────────────────
|
||||
public volatile boolean forward, backward, left, right, up, down;
|
||||
|
||||
@@ -277,6 +285,19 @@ public class SharedInput {
|
||||
|
||||
// ── Kamera-Info (JME3-Thread schreibt, JavaFX-Thread liest) ─────────────
|
||||
public volatile float camX = 0f, camY = 0f, camZ = 0f;
|
||||
|
||||
// ── Kamera-Teleport (JavaFX schreibt, JME3-Thread liest und setzt zurück) ─
|
||||
/** NaN = kein Teleport angefordert. Y=NaN → Terrain-Höhe + Offset verwenden. */
|
||||
public volatile float pendingGotoX = Float.NaN;
|
||||
public volatile float pendingGotoY = Float.NaN;
|
||||
public volatile float pendingGotoZ = Float.NaN;
|
||||
/** NaN = Kamera-Pitch unverändert lassen. */
|
||||
public volatile float pendingGotoPitch = Float.NaN;
|
||||
|
||||
// ── Reload-Signale nach Löschung aus MapObjectsView ──────────────────────
|
||||
public volatile boolean reloadPlacedModels = false;
|
||||
public volatile boolean reloadPlacedItems = false;
|
||||
public volatile boolean reloadPlacedOther = false; // Lichter, Emitter, Wasser, Bereiche, Zonen
|
||||
/** Yaw in Grad: 0° = Süden (−Z), 90° = Westen (−X), ±180° = Norden (+Z). */
|
||||
public volatile float camYaw = 0f;
|
||||
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
|
||||
@@ -602,6 +623,14 @@ public class SharedInput {
|
||||
public volatile String modelEditorLod2Path = "";
|
||||
public volatile boolean modelEditorLodChanged = false;
|
||||
|
||||
// ── Voxel-Werkzeug ────────────────────────────────────────────────────────
|
||||
/** activeLayer==16 → Voxel-Klippen/Höhlen bearbeiten */
|
||||
public static final int LAYER_VOXEL = 16;
|
||||
|
||||
/** Klick/Drag im Viewport im Voxel-Modus. */
|
||||
public record VoxelEdit(float screenX, float screenY) {}
|
||||
public final ConcurrentLinkedQueue<VoxelEdit> voxelEditQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── Item-Platzierung ──────────────────────────────────────────────────────
|
||||
/** activeLayer==21 → Item-Pickup auf die Karte platzieren */
|
||||
public static final int LAYER_ITEMS = 21;
|
||||
|
||||
@@ -82,25 +82,26 @@ public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
reloadFromDisk();
|
||||
rootNode.attachChild(itemRoot);
|
||||
rootNode.attachChild(previewNode);
|
||||
}
|
||||
|
||||
public void reloadFromDisk() {
|
||||
items.clear();
|
||||
nodes.clear();
|
||||
itemRoot.detachAllChildren();
|
||||
|
||||
try {
|
||||
items.addAll(PlacedItemIO.load());
|
||||
} catch (Exception e) {
|
||||
log.warn("[ItemPlacement] Laden fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
|
||||
for (PlacedItem pi : items) {
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
n.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
|
||||
itemRoot.attachChild(n);
|
||||
nodes.add(n);
|
||||
}
|
||||
|
||||
rootNode.attachChild(itemRoot);
|
||||
rootNode.attachChild(previewNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -116,6 +117,10 @@ public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (input.reloadPlacedItems) {
|
||||
input.reloadPlacedItems = false;
|
||||
reloadFromDisk();
|
||||
}
|
||||
updatePreview();
|
||||
|
||||
SharedInput.ObjectClick click;
|
||||
@@ -161,7 +166,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
if (hits.size() == 0) return;
|
||||
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
PlacedItem pi = new PlacedItem(input.pendingItemId, pt.x, pt.y, pt.z);
|
||||
PlacedItem pi = PlacedItem.create(input.pendingItemId, pt.x, pt.y, pt.z);
|
||||
items.add(pi);
|
||||
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
|
||||
@@ -317,6 +317,11 @@ public class SceneObjectState extends BaseAppState {
|
||||
deleteSelected();
|
||||
}
|
||||
|
||||
if (input.reloadPlacedModels) {
|
||||
input.reloadPlacedModels = false;
|
||||
try { loadPlacedModels(de.blight.common.PlacedModelIO.load()); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// Zusammenfassen
|
||||
if (input.mergeSelectedRequested) {
|
||||
input.mergeSelectedRequested = false;
|
||||
|
||||
@@ -41,6 +41,7 @@ import de.blight.common.PlacedWater;
|
||||
import de.blight.common.RiverPoint;
|
||||
import de.blight.common.SoundAreaIO;
|
||||
import de.blight.common.WaterBodyIO;
|
||||
import de.blight.common.BlightHome;
|
||||
import de.blight.common.MapData;
|
||||
import de.blight.common.MapIO;
|
||||
import de.blight.common.PlacedModelIO;
|
||||
@@ -130,7 +131,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private Texture2D upperSplatTex;
|
||||
|
||||
// ── Kameraposition ────────────────────────────────────────────────────────
|
||||
private static final Path EDITOR_PREFS = de.blight.editor.ProjectRoot.resolve("config", "editor.prefs");
|
||||
private static final Path EDITOR_PREFS = BlightHome.resolve("config", "editor.prefs");
|
||||
private static final float DEFAULT_CAM_Y = 50f;
|
||||
private static final float DEFAULT_PITCH = (float) (-Math.PI / 4); // -45°
|
||||
|
||||
@@ -695,6 +696,22 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
float pgx = input.pendingGotoX;
|
||||
if (!Float.isNaN(pgx)) {
|
||||
input.pendingGotoX = Float.NaN;
|
||||
float pgz = input.pendingGotoZ;
|
||||
float pgy = input.pendingGotoY;
|
||||
if (Float.isNaN(pgy)) {
|
||||
Float h = terrain != null ? terrain.getHeight(new com.jme3.math.Vector2f(pgx, pgz)) : null;
|
||||
pgy = h != null ? h + 20f : camPos.y;
|
||||
}
|
||||
camPos.set(pgx, pgy, pgz);
|
||||
float pp = input.pendingGotoPitch;
|
||||
if (!Float.isNaN(pp)) {
|
||||
input.pendingGotoPitch = Float.NaN;
|
||||
camPitch = FastMath.clamp(pp, -FastMath.HALF_PI + 0.05f, FastMath.HALF_PI - 0.05f);
|
||||
}
|
||||
}
|
||||
updateCamera(tpf);
|
||||
processEdits();
|
||||
processTextureEdits();
|
||||
@@ -729,6 +746,17 @@ public class TerrainEditorState extends BaseAppState {
|
||||
terrain.setMaterial(buildTerrainMaterial());
|
||||
}
|
||||
|
||||
if (input.reloadPlacedOther) {
|
||||
input.reloadPlacedOther = false;
|
||||
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
|
||||
try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {}
|
||||
try { if (waterBodyState != null) waterBodyState.loadPlacedBodies(de.blight.common.WaterBodyIO.load()); } catch (Exception ignored) {}
|
||||
try { if (soundAreaState != null) soundAreaState.loadAreas(de.blight.common.SoundAreaIO.load()); } catch (Exception ignored) {}
|
||||
try { if (areaState != null) areaState.loadAreas(de.blight.common.AreaIO.load()); } catch (Exception ignored) {}
|
||||
try { if (locationZoneState != null) locationZoneState.loadZones(de.blight.common.LocationZoneIO.load()); } catch (Exception ignored) {}
|
||||
try { if (riverEditorState != null) riverEditorState.loadPlacedRivers(de.blight.common.RiverIO.load()); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (input.saveRequested) {
|
||||
input.saveRequested = false;
|
||||
performSave();
|
||||
@@ -814,6 +842,9 @@ public class TerrainEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
/** Gibt den TerrainQuad-Node zurück (z.B. für Voxel-Raycasts). */
|
||||
public TerrainQuad getTerrainNode() { return terrain; }
|
||||
|
||||
/** Gibt die Terrain-Höhe (Welt-Y) an der angegebenen Welt-XZ-Position zurück. */
|
||||
public float getTerrainHeightAt(float worldX, float worldZ) {
|
||||
if (terrain == null) return 0f;
|
||||
@@ -859,7 +890,19 @@ public class TerrainEditorState extends BaseAppState {
|
||||
final List<PlacedArea> areas = areaState != null ? areaState.getPlacedAreas() : null;
|
||||
final List<PlacedLocationZone> locationZones = locationZoneState != null ? locationZoneState.getPlacedZones() : null;
|
||||
|
||||
// ── Schwere Arbeit (Upsample + Datei-I/O) auf Hintergrund-Thread ─────
|
||||
// ── Platzierte Objekte synchron speichern (kleine Textdateien) ──────────
|
||||
// Muss synchron im JME-Thread erfolgen, damit kein Race mit asynchronen
|
||||
// Löschoperationen aus dem JavaFX-Thread entsteht.
|
||||
try { if (models != null) PlacedModelIO.save(models); } catch (IOException e) { log.error("Modelle speichern", e); }
|
||||
try { if (lights != null) LightIO.save(lights); } catch (IOException e) { log.error("Lichter speichern", e); }
|
||||
try { if (emitters != null) EmitterIO.save(emitters); } catch (IOException e) { log.error("Emitter speichern", e); }
|
||||
try { if (waters != null) WaterBodyIO.save(waters); } catch (IOException e) { log.error("Wasser speichern", e); }
|
||||
try { if (rivers != null) de.blight.common.RiverIO.save(rivers); } catch (IOException e) { log.error("Flüsse speichern", e); }
|
||||
try { if (soundAreas != null) SoundAreaIO.save(soundAreas); } catch (IOException e) { log.error("Soundbereiche speichern", e); }
|
||||
try { if (areas != null) AreaIO.save(areas); } catch (IOException e) { log.error("Bereiche speichern", e); }
|
||||
try { if (locationZones != null) LocationZoneIO.save(locationZones); } catch (IOException e) { log.error("Zonen speichern", e); }
|
||||
|
||||
// ── Schwere Arbeit (Terrain-Upsample + Datei-I/O) auf Hintergrund-Thread ─
|
||||
saveExecutor.submit(() -> {
|
||||
try {
|
||||
MapData data = new MapData();
|
||||
@@ -905,14 +948,6 @@ public class TerrainEditorState extends BaseAppState {
|
||||
log.error("Chunk-Export fehlgeschlagen", e);
|
||||
}
|
||||
}
|
||||
if (models != null) PlacedModelIO.save(models);
|
||||
if (lights != null) LightIO.save(lights);
|
||||
if (emitters != null) EmitterIO.save(emitters);
|
||||
if (waters != null) WaterBodyIO.save(waters);
|
||||
if (rivers != null) de.blight.common.RiverIO.save(rivers);
|
||||
if (soundAreas != null) SoundAreaIO.save(soundAreas);
|
||||
if (areas != null) AreaIO.save(areas);
|
||||
if (locationZones != null) LocationZoneIO.save(locationZones);
|
||||
|
||||
input.saveStatusMsg = "Gespeichert: " + MapIO.getMapPath();
|
||||
log.info("{}", input.saveStatusMsg);
|
||||
@@ -941,7 +976,8 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|| layer == SharedInput.LAYER_WATER
|
||||
|| layer == SharedInput.LAYER_SOUND_AREAS || layer == SharedInput.LAYER_AREAS
|
||||
|| layer == SharedInput.LAYER_LOCATION_ZONES
|
||||
|| layer == SharedInput.LAYER_PLAY_TOOL || mx < 0) {
|
||||
|| layer == SharedInput.LAYER_PLAY_TOOL
|
||||
|| layer == SharedInput.LAYER_VOXEL || mx < 0) {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
@@ -1235,18 +1271,29 @@ public class TerrainEditorState extends BaseAppState {
|
||||
float terrainDist = terrainDistBelow();
|
||||
float speed = FastMath.clamp(terrainDist, 5f, CAM_SPEED) * tpf;
|
||||
|
||||
float hFwdX = -FastMath.sin(camYaw);
|
||||
float hFwdZ = -FastMath.cos(camYaw);
|
||||
if (input.forward) { camPos.x -= hFwdX * speed; camPos.z -= hFwdZ * speed; }
|
||||
if (input.backward) { camPos.x += hFwdX * speed; camPos.z += hFwdZ * speed; }
|
||||
if (input.camMode == SharedInput.CAM_FREEFLY) {
|
||||
Vector3f fwd = cam.getDirection().clone();
|
||||
Vector3f lft = cam.getLeft().clone();
|
||||
if (input.forward) camPos.addLocal(fwd.mult(speed));
|
||||
if (input.backward) camPos.subtractLocal(fwd.mult(speed));
|
||||
if (input.left) camPos.addLocal(lft.mult(speed));
|
||||
if (input.right) camPos.subtractLocal(lft.mult(speed));
|
||||
if (input.up) camPos.y += speed;
|
||||
if (input.down) camPos.y -= speed;
|
||||
} else {
|
||||
float hFwdX = -FastMath.sin(camYaw);
|
||||
float hFwdZ = -FastMath.cos(camYaw);
|
||||
if (input.forward) { camPos.x -= hFwdX * speed; camPos.z -= hFwdZ * speed; }
|
||||
if (input.backward) { camPos.x += hFwdX * speed; camPos.z += hFwdZ * speed; }
|
||||
|
||||
Vector3f lft = cam.getLeft().clone().setY(0);
|
||||
if (lft.lengthSquared() > 0.001f) lft.normalizeLocal();
|
||||
if (input.left) camPos.addLocal(lft.mult(speed));
|
||||
if (input.right) camPos.subtractLocal(lft.mult(speed));
|
||||
Vector3f lft = cam.getLeft().clone().setY(0);
|
||||
if (lft.lengthSquared() > 0.001f) lft.normalizeLocal();
|
||||
if (input.left) camPos.addLocal(lft.mult(speed));
|
||||
if (input.right) camPos.subtractLocal(lft.mult(speed));
|
||||
|
||||
if (input.up) orbitAroundTerrain( ORBIT_SPEED * tpf);
|
||||
if (input.down) orbitAroundTerrain(-ORBIT_SPEED * tpf);
|
||||
if (input.up) orbitAroundTerrain( ORBIT_SPEED * tpf);
|
||||
if (input.down) orbitAroundTerrain(-ORBIT_SPEED * tpf);
|
||||
}
|
||||
|
||||
int scroll = input.scrollAccum.getAndSet(0);
|
||||
if (scroll != 0)
|
||||
|
||||
@@ -0,0 +1,603 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.collision.CollisionResult;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.FastMath;
|
||||
import com.jme3.math.Ray;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.texture.Texture;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.VoxelChunk;
|
||||
import de.blight.common.VoxelChunkIO;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.state.TerrainEditorState;
|
||||
import de.blight.game.state.MarchingCubes;
|
||||
import de.blight.game.state.VoxelChunkNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* Editor-AppState für das Voxel-Werkzeug.
|
||||
*
|
||||
* Verwaltet alle im Editor geladenen VoxelChunks (in-memory HashMap +
|
||||
* gerenderte VoxelChunkNodes), verarbeitet Edit-Events aus der SharedInput-Queue
|
||||
* und kümmert sich um Auto-Save und Hintergrund-LOD-Berechnung.
|
||||
*/
|
||||
public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(VoxelEditorState.class);
|
||||
|
||||
/** Maximale Edits pro Frame. */
|
||||
private static final int MAX_EDITS_PER_FRAME = 4;
|
||||
/** Idle-Zeit in Sekunden, nach der LOD1/2 im Hintergrund gebaut werden. */
|
||||
private static final float LOD_REBUILD_IDLE_S = 0.5f;
|
||||
/** Idle-Zeit in Sekunden für Auto-Save. */
|
||||
private static final float AUTO_SAVE_IDLE_S = 3.0f;
|
||||
|
||||
// ── JME-Zustand ──────────────────────────────────────────────────────────
|
||||
|
||||
private SimpleApplication app;
|
||||
private AssetManager assets;
|
||||
private Camera cam;
|
||||
|
||||
/** Root-Node für alle VoxelChunkNodes. */
|
||||
private Node voxelRoot;
|
||||
/** Wird von TerrainEditorState gesetzt; wird als Raycast-Ziel genutzt. */
|
||||
private Node terrainNode;
|
||||
private TerrainQuad terrainQuad;
|
||||
|
||||
private final SharedInput input;
|
||||
|
||||
// ── Chunk-Verwaltung ─────────────────────────────────────────────────────
|
||||
|
||||
/** key → VoxelChunk (in-memory, ggf. dirty). */
|
||||
private final Map<Long, VoxelChunk> chunks = new HashMap<>();
|
||||
/** key → zugehöriger VoxelChunkNode in der Szene. */
|
||||
private final Map<Long, VoxelChunkNode> nodes = new HashMap<>();
|
||||
|
||||
// ── Timers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Sekunden seit letztem Edit. */
|
||||
private float idleSinceEdit = 0f;
|
||||
/** Sekunden seit letztem Save. */
|
||||
private float idleSinceSave = 0f;
|
||||
/** LOD-Rebuild ist für diesen Frame angefordert. */
|
||||
private boolean lodRebuildPending = false;
|
||||
|
||||
// ── Hintergrund-Executor ─────────────────────────────────────────────────
|
||||
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "VoxelEditor-BG");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
// ── Material ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Material voxelMaterial;
|
||||
|
||||
// ── Brush-Indikator ───────────────────────────────────────────────────────
|
||||
|
||||
private Geometry brushIndicator;
|
||||
|
||||
// ── LOD-Rebuild-Queue ─────────────────────────────────────────────────────
|
||||
|
||||
/** Chunks, die LOD1/2 neu brauchen. Wird im Hintergrund-Thread abgearbeitet. */
|
||||
private final ConcurrentLinkedQueue<Long> lodRebuildQueue = new ConcurrentLinkedQueue<>();
|
||||
/** Chunks mit fertigen LOD1/2-Meshes, die im JME-Thread übernommen werden. */
|
||||
private final ConcurrentLinkedQueue<Runnable> lodResultQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public VoxelEditorState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
// ── AppState-Lifecycle ────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
this.app = (SimpleApplication) application;
|
||||
this.assets = app.getAssetManager();
|
||||
this.cam = app.getCamera();
|
||||
|
||||
voxelRoot = new Node("voxelEditorRoot");
|
||||
app.getRootNode().attachChild(voxelRoot);
|
||||
|
||||
// TerrainEditorState wurde vor uns attached und ist zu diesem Zeitpunkt initialisiert
|
||||
TerrainEditorState tes = app.getStateManager().getState(TerrainEditorState.class);
|
||||
if (tes != null) {
|
||||
terrainNode = tes.getTerrainNode();
|
||||
terrainQuad = terrainNode instanceof TerrainQuad tq ? tq : null;
|
||||
}
|
||||
|
||||
voxelMaterial = buildMaterial();
|
||||
|
||||
brushIndicator = buildBrushIndicator();
|
||||
app.getRootNode().attachChild(brushIndicator);
|
||||
|
||||
// Alle vorhandenen .blvc-Dateien laden
|
||||
List<VoxelChunk> loaded = VoxelChunkIO.loadAll();
|
||||
for (VoxelChunk chunk : loaded) {
|
||||
long key = chunkKey(chunk.cx, chunk.cy, chunk.cz);
|
||||
chunks.put(key, chunk);
|
||||
addNodeForChunk(key, chunk);
|
||||
}
|
||||
log.info("VoxelEditorState: {} Chunks geladen.", loaded.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
executor.shutdownNow();
|
||||
voxelRoot.removeFromParent();
|
||||
if (brushIndicator != null) brushIndicator.removeFromParent();
|
||||
nodes.clear();
|
||||
chunks.clear();
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
// LOD-Ergebnisse aus dem Hintergrund-Thread übernehmen
|
||||
Runnable r;
|
||||
while ((r = lodResultQueue.poll()) != null) r.run();
|
||||
|
||||
// Brush-Indikator immer aktualisieren (zeigen/verstecken je nach Layer)
|
||||
updateBrushIndicator();
|
||||
|
||||
// Nur aktiv wenn LAYER_VOXEL gesetzt
|
||||
if (input.activeLayer != SharedInput.LAYER_VOXEL) {
|
||||
idleSinceEdit = 0f;
|
||||
idleSinceSave += tpf;
|
||||
checkAutoSave();
|
||||
return;
|
||||
}
|
||||
|
||||
// Edit-Queue verarbeiten (max. MAX_EDITS_PER_FRAME)
|
||||
int processed = 0;
|
||||
SharedInput.VoxelEdit edit;
|
||||
while (processed < MAX_EDITS_PER_FRAME
|
||||
&& (edit = input.voxelEditQueue.poll()) != null) {
|
||||
handleEdit(edit);
|
||||
processed++;
|
||||
idleSinceEdit = 0f;
|
||||
idleSinceSave = 0f;
|
||||
}
|
||||
|
||||
if (processed == 0) {
|
||||
idleSinceEdit += tpf;
|
||||
idleSinceSave += tpf;
|
||||
}
|
||||
|
||||
// LOD1/2 nach Idle-Zeit im Hintergrund bauen
|
||||
if (idleSinceEdit >= LOD_REBUILD_IDLE_S && !lodRebuildPending) {
|
||||
lodRebuildPending = true;
|
||||
scheduleLodRebuild();
|
||||
}
|
||||
|
||||
checkAutoSave();
|
||||
}
|
||||
|
||||
// ── Öffentliche API ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Wird von TerrainEditorState gesetzt, damit VoxelEditorState Raycasts
|
||||
* gegen das Terrain durchführen kann.
|
||||
*/
|
||||
public void setTerrainNode(Node terrain) {
|
||||
this.terrainNode = terrain;
|
||||
this.terrainQuad = terrain instanceof TerrainQuad tq ? tq : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert alle dirty Chunks synchron.
|
||||
* Kann von außen aufgerufen werden (z.B. beim globalen Speichern).
|
||||
*/
|
||||
public void saveAll() {
|
||||
for (VoxelChunk chunk : chunks.values()) {
|
||||
if (!chunk.dirty) continue;
|
||||
try {
|
||||
VoxelChunkIO.save(chunk);
|
||||
} catch (IOException e) {
|
||||
log.error("Fehler beim Speichern von VoxelChunk ({},{},{}): {}",
|
||||
chunk.cx, chunk.cy, chunk.cz, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Intern: Edit verarbeiten ───────────────────────────────────────────────
|
||||
|
||||
private void handleEdit(SharedInput.VoxelEdit edit) {
|
||||
float jmeX = edit.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - edit.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f worldPos = raycast(jmeX, jmeY);
|
||||
if (worldPos == null) return;
|
||||
applyEdit(worldPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen Raycast durch und gibt den nächstgelegenen Treffpunkt zurück,
|
||||
* oder null wenn kein Treffer.
|
||||
*/
|
||||
private Vector3f raycast(float screenX, float screenY) {
|
||||
Vector2f click2d = new Vector2f(screenX, screenY);
|
||||
Vector3f origin = cam.getWorldCoordinates(click2d, 0f);
|
||||
Vector3f target = cam.getWorldCoordinates(click2d, 1f);
|
||||
Vector3f dir = target.subtract(origin).normalizeLocal();
|
||||
Ray ray = new Ray(origin, dir);
|
||||
|
||||
CollisionResults results = new CollisionResults();
|
||||
float bestDist = Float.MAX_VALUE;
|
||||
Vector3f best = null;
|
||||
|
||||
// Terrain
|
||||
if (terrainNode != null) {
|
||||
terrainNode.collideWith(ray, results);
|
||||
if (results.size() > 0) {
|
||||
CollisionResult cr = results.getClosestCollision();
|
||||
if (cr.getDistance() < bestDist) {
|
||||
bestDist = cr.getDistance();
|
||||
best = cr.getContactPoint();
|
||||
}
|
||||
results.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Voxel-Geometrie
|
||||
voxelRoot.collideWith(ray, results);
|
||||
if (results.size() > 0) {
|
||||
CollisionResult cr = results.getClosestCollision();
|
||||
if (cr.getDistance() < bestDist) {
|
||||
best = cr.getContactPoint();
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wendet den Pinsel auf alle betroffenen Chunks an.
|
||||
*
|
||||
* Modi 0-3: Solid-Voxel hinzufügen (verschiedene Falloff-Kurven), danach alle Voxel
|
||||
* unterhalb der Terrain-Oberfläche in der Pinselfläche entfernen.
|
||||
* Modus 4 (Entfernen): Voxel löschen – für Höhlen unterhalb des Terrains.
|
||||
*/
|
||||
private void applyEdit(Vector3f worldPos) {
|
||||
float radius = (float) input.voxelTool.brushRadius.getValue();
|
||||
float strength = (float) input.voxelTool.brushStrength.getValue();
|
||||
int modeIdx = input.voxelTool.mode.getSelectedIndex();
|
||||
int texSlot = input.voxelTool.textureSlot.getSelectedIndex();
|
||||
byte matId = (byte)(texSlot & 3);
|
||||
boolean remove = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_REMOVE);
|
||||
boolean addFlat = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_ADD);
|
||||
|
||||
float wx = worldPos.x, wy = worldPos.y, wz = worldPos.z;
|
||||
|
||||
int cxMin = VoxelChunk.worldXToCx(wx - radius);
|
||||
int cxMax = VoxelChunk.worldXToCx(wx + radius);
|
||||
int czMin = VoxelChunk.worldZToCz(wz - radius);
|
||||
int czMax = VoxelChunk.worldZToCz(wz + radius);
|
||||
// Spalten-Modi bauen nach oben bis zu strength Voxel → Y-Range entsprechend erweitern
|
||||
int cyMin = VoxelChunk.worldYToCy(wy - radius);
|
||||
int cyMax = (!remove && !addFlat)
|
||||
? VoxelChunk.worldYToCy(wy + strength)
|
||||
: VoxelChunk.worldYToCy(wy + radius);
|
||||
|
||||
for (int cz = czMin; cz <= czMax; cz++) {
|
||||
for (int cx = cxMin; cx <= cxMax; cx++) {
|
||||
for (int cy = cyMin; cy <= cyMax; cy++) {
|
||||
VoxelChunkNode node = getOrCreateNode(cx, cy, cz);
|
||||
VoxelChunk chunk = node.getChunk();
|
||||
|
||||
float lx = VoxelChunk.worldXToLocal(wx, cx);
|
||||
float ly = VoxelChunk.worldYToLocal(wy, cy);
|
||||
float lz = VoxelChunk.worldZToLocal(wz, cz);
|
||||
|
||||
if (remove) {
|
||||
chunk.applyBrush(lx, ly, lz, radius, Byte.MIN_VALUE, (byte)0);
|
||||
} else if (addFlat) {
|
||||
byte d = (byte) Math.min(127, (int) strength);
|
||||
chunk.applyBrush(lx, ly, lz, radius, d, matId);
|
||||
} else {
|
||||
applyAddBrush(chunk, lx, ly, lz, radius, strength, modeIdx, matId);
|
||||
clearVoxelsBelowTerrain(chunk, cx, cy, cz, wx, wz, radius);
|
||||
}
|
||||
|
||||
node.rebuildMesh(0);
|
||||
node.setActiveLod(0);
|
||||
|
||||
long key = chunkKey(cx, cy, cz);
|
||||
if (!lodRebuildQueue.contains(key)) lodRebuildQueue.add(key);
|
||||
lodRebuildPending = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spalten-Pinsel: Für jede XZ-Position im Pinselradius wird eine Voxel-Säule
|
||||
* nach oben aufgebaut. Die Säulenhöhe ist proportional zum mode-spezifischen Falloff:
|
||||
* Spike → steile Spitze in der Mitte
|
||||
* Plateau → gleichmäßige flache Erhöhung
|
||||
* Sinus → sanfte Kuppel
|
||||
* Smooth → weiche raised-cosine Kuppel
|
||||
*/
|
||||
private void applyAddBrush(VoxelChunk chunk,
|
||||
float lx, float ly, float lz,
|
||||
float radius, float strength,
|
||||
int mode, byte matId) {
|
||||
int x0 = Math.max(0, (int)(lx - radius));
|
||||
int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lx + radius));
|
||||
int z0 = Math.max(0, (int)(lz - radius));
|
||||
int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lz + radius));
|
||||
float r2 = radius * radius;
|
||||
byte d = (byte) Math.min(127, (int) strength);
|
||||
// Basiszeile innerhalb dieses Chunks (kann negativ sein für höhere Chunks)
|
||||
int baseY = (int) ly;
|
||||
|
||||
for (int z = z0; z <= z1; z++) {
|
||||
float dz = z - lz;
|
||||
for (int x = x0; x <= x1; x++) {
|
||||
float dx = x - lx;
|
||||
float d2 = dx*dx + dz*dz;
|
||||
if (d2 > r2) continue;
|
||||
|
||||
float t = (float) Math.sqrt(d2) / radius; // 0=Mitte, 1=Rand
|
||||
float falloff;
|
||||
switch (mode) {
|
||||
case de.blight.editor.tool.VoxelTool.MODE_SINUS ->
|
||||
falloff = (float) Math.cos(t * Math.PI / 2);
|
||||
case de.blight.editor.tool.VoxelTool.MODE_SPIKE ->
|
||||
falloff = (1f - t) * (1f - t);
|
||||
case de.blight.editor.tool.VoxelTool.MODE_SMOOTH ->
|
||||
falloff = (float)(0.5 * (1 + Math.cos(t * Math.PI)));
|
||||
default -> // PLATEAU
|
||||
falloff = 1f;
|
||||
}
|
||||
|
||||
int colHeight = (int)(strength * falloff);
|
||||
if (colHeight < 1) continue;
|
||||
|
||||
// Säule von baseY bis baseY+colHeight, geclampt auf Chunk-Grenzen
|
||||
int yBottom = Math.max(0, baseY);
|
||||
int yTop = Math.min(VoxelChunk.SIZE - 1, baseY + colHeight);
|
||||
for (int y = yBottom; y <= yTop; y++) {
|
||||
if (chunk.getDensity(x, y, z) <= 0) {
|
||||
chunk.setDensity(x, y, z, d);
|
||||
chunk.setMaterial(x, y, z, matId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt alle Solid-Voxel in der X/Z-Fußfläche des Pinsels, die unterhalb der
|
||||
* aktuellen Terrain-Oberfläche liegen.
|
||||
*/
|
||||
private void clearVoxelsBelowTerrain(VoxelChunk chunk, int cx, int cy, int cz,
|
||||
float brushWx, float brushWz, float radius) {
|
||||
if (terrainQuad == null || chunk.isEmpty()) return;
|
||||
|
||||
float lxC = VoxelChunk.worldXToLocal(brushWx, cx);
|
||||
float lzC = VoxelChunk.worldZToLocal(brushWz, cz);
|
||||
int x0 = Math.max(0, (int)(lxC - radius));
|
||||
int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lxC + radius));
|
||||
int z0 = Math.max(0, (int)(lzC - radius));
|
||||
int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius));
|
||||
|
||||
for (int lz = z0; lz <= z1; lz++) {
|
||||
float worldZ = VoxelChunk.toWorldZ(cz, lz);
|
||||
for (int lx = x0; lx <= x1; lx++) {
|
||||
float worldX = VoxelChunk.toWorldX(cx, lx);
|
||||
Float h = terrainQuad.getHeight(new Vector2f(worldX, worldZ));
|
||||
if (h == null) continue;
|
||||
|
||||
for (int ly = 0; ly < VoxelChunk.SIZE; ly++) {
|
||||
if (chunk.getDensity(lx, ly, lz) <= 0) continue;
|
||||
if (VoxelChunk.toWorldY(cy, ly) <= h) {
|
||||
chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Intern: Chunk/Node-Verwaltung ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Gibt den VoxelChunkNode für (cx,cy,cz) zurück.
|
||||
* Legt neuen Chunk und Node an, falls noch nicht vorhanden.
|
||||
*/
|
||||
private VoxelChunkNode getOrCreateNode(int cx, int cy, int cz) {
|
||||
long key = chunkKey(cx, cy, cz);
|
||||
VoxelChunkNode node = nodes.get(key);
|
||||
if (node != null) return node;
|
||||
|
||||
// Ggf. aus Datei laden
|
||||
VoxelChunk chunk;
|
||||
if (VoxelChunkIO.exists(cx, cy, cz)) {
|
||||
try {
|
||||
chunk = VoxelChunkIO.load(cx, cy, cz);
|
||||
} catch (IOException e) {
|
||||
log.warn("Chunk ({},{},{}) laden fehlgeschlagen, neu erstellt: {}",
|
||||
cx, cy, cz, e.getMessage());
|
||||
chunk = new VoxelChunk(cx, cy, cz);
|
||||
}
|
||||
} else {
|
||||
chunk = new VoxelChunk(cx, cy, cz);
|
||||
}
|
||||
|
||||
chunks.put(key, chunk);
|
||||
node = addNodeForChunk(key, chunk);
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Erstellt den VoxelChunkNode und hängt ihn in den Szenen-Graph ein. */
|
||||
private VoxelChunkNode addNodeForChunk(long key, VoxelChunk chunk) {
|
||||
VoxelChunkNode node = new VoxelChunkNode(chunk, voxelMaterial);
|
||||
// LOD0 bauen
|
||||
node.rebuildMesh(0);
|
||||
node.setActiveLod(0);
|
||||
voxelRoot.attachChild(node);
|
||||
nodes.put(key, node);
|
||||
// LOD1/2 für Hintergrund vormerken
|
||||
lodRebuildQueue.add(key);
|
||||
return node;
|
||||
}
|
||||
|
||||
// ── Intern: Hintergrund-LOD ───────────────────────────────────────────────
|
||||
|
||||
private void scheduleLodRebuild() {
|
||||
executor.submit(() -> {
|
||||
Long key;
|
||||
while ((key = lodRebuildQueue.poll()) != null) {
|
||||
VoxelChunk chunk = chunks.get(key);
|
||||
if (chunk == null) continue;
|
||||
// LOD1 und LOD2 berechnen (kein JME-Context nötig – nur reine Berechnungen)
|
||||
var mesh1 = MarchingCubes.build(chunk, 4);
|
||||
var mesh2 = MarchingCubes.build(chunk, 16);
|
||||
final Long fKey = key;
|
||||
// Fertige Meshes an JME-Thread übergeben (kein zweites Build!)
|
||||
lodResultQueue.add(() -> {
|
||||
VoxelChunkNode node = nodes.get(fKey);
|
||||
if (node == null) return;
|
||||
if (mesh1 != null) node.setLodMesh(1, mesh1);
|
||||
if (mesh2 != null) node.setLodMesh(2, mesh2);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Intern: Auto-Save ─────────────────────────────────────────────────────
|
||||
|
||||
private void checkAutoSave() {
|
||||
if (idleSinceSave < AUTO_SAVE_IDLE_S) return;
|
||||
boolean anyDirty = false;
|
||||
for (VoxelChunk c : chunks.values()) { if (c.dirty) { anyDirty = true; break; } }
|
||||
if (!anyDirty) return;
|
||||
|
||||
idleSinceSave = 0f;
|
||||
executor.submit(() -> {
|
||||
for (VoxelChunk chunk : chunks.values()) {
|
||||
if (!chunk.dirty) continue;
|
||||
try {
|
||||
VoxelChunkIO.save(chunk);
|
||||
} catch (IOException e) {
|
||||
log.error("Auto-Save VoxelChunk ({},{},{}): {}",
|
||||
chunk.cx, chunk.cy, chunk.cz, e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Intern: Material ─────────────────────────────────────────────────────
|
||||
|
||||
private Material buildMaterial() {
|
||||
Material mat = new Material(assets, "MatDefs/Voxel.j3md");
|
||||
mat.setFloat("TexScale", 4f);
|
||||
|
||||
String[] texPaths = input.terrainTexturePaths;
|
||||
String[] slotNames = {"Tex0", "Tex1", "Tex2", "Tex3"};
|
||||
for (int i = 0; i < slotNames.length; i++) {
|
||||
String path = (i < texPaths.length && texPaths[i] != null && !texPaths[i].isEmpty())
|
||||
? texPaths[i] : "Common/Textures/MissingTexture.png";
|
||||
try {
|
||||
Texture t = assets.loadTexture(path);
|
||||
t.setWrap(Texture.WrapMode.Repeat);
|
||||
mat.setTexture(slotNames[i], t);
|
||||
} catch (Exception e) {
|
||||
log.warn("VoxelEditorState: Textur {} ({}) nicht ladbar: {}", slotNames[i], path, e.getMessage());
|
||||
}
|
||||
}
|
||||
return mat;
|
||||
}
|
||||
|
||||
// ── Intern: Brush-Indikator ───────────────────────────────────────────────
|
||||
|
||||
private void updateBrushIndicator() {
|
||||
if (brushIndicator == null) return;
|
||||
if (input.activeLayer != SharedInput.LAYER_VOXEL) {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
float mx = input.mouseScreenX;
|
||||
float my = input.mouseScreenY;
|
||||
if (mx < 0) {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
float jmeX = mx * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - my * (float) input.viewportScaleY;
|
||||
Vector3f pos = raycast(jmeX, jmeY);
|
||||
if (pos != null) {
|
||||
float r = (float) input.voxelTool.brushRadius.getValue();
|
||||
brushIndicator.setLocalTranslation(pos.x, pos.y + 0.3f, pos.z);
|
||||
brushIndicator.setLocalScale(r, 1f, r);
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Inherit);
|
||||
} else {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
private Geometry buildBrushIndicator() {
|
||||
int segments = 32;
|
||||
FloatBuffer pos = BufferUtils.createFloatBuffer((segments + 1) * 3);
|
||||
pos.put(0f).put(0f).put(0f);
|
||||
for (int i = 0; i < segments; i++) {
|
||||
float a = FastMath.TWO_PI * i / segments;
|
||||
pos.put(FastMath.cos(a)).put(0f).put(FastMath.sin(a));
|
||||
}
|
||||
IntBuffer idx = BufferUtils.createIntBuffer(segments * 3);
|
||||
for (int i = 0; i < segments; i++) {
|
||||
idx.put(0);
|
||||
idx.put(1 + i);
|
||||
idx.put(1 + (i + 1) % segments);
|
||||
}
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
|
||||
mesh.updateBound();
|
||||
|
||||
Geometry geo = new Geometry("voxelBrushIndicator", mesh);
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.2f, 0.6f, 1f, 0.4f));
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
geo.setMaterial(mat);
|
||||
geo.setCullHint(Spatial.CullHint.Always);
|
||||
return geo;
|
||||
}
|
||||
|
||||
// ── Hilfsmethode ─────────────────────────────────────────────────────────
|
||||
|
||||
private static long chunkKey(int cx, int cy, int cz) {
|
||||
return ((long)(cx & 0xFFFF)) | (((long)(cy & 0xFFFF)) << 16) | (((long)(cz & 0xFFFF)) << 32);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package de.blight.editor.tool;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Voxel-Klippen/Höhlen-Werkzeug.
|
||||
*
|
||||
* Modi 0-3 fügen Solid-Voxel oberhalb des Terrains hinzu (gleiche Pinselformen wie das Höhen-Tool).
|
||||
* Voxel, die unterhalb der Terrain-Oberfläche landen, werden automatisch entfernt.
|
||||
* Modus 4 entfernt Voxel – zum Aushöhlen von Höhlen unterhalb des Terrains.
|
||||
*/
|
||||
public class VoxelTool extends EditorTool {
|
||||
|
||||
public static final int MODE_SINUS = 0;
|
||||
public static final int MODE_SPIKE = 1;
|
||||
public static final int MODE_PLATEAU = 2;
|
||||
public static final int MODE_SMOOTH = 3;
|
||||
public static final int MODE_REMOVE = 4;
|
||||
/** Einfaches Hinzufügen: gleichmäßige Dichte überall im Pinsel, kein Terrain-Check. */
|
||||
public static final int MODE_ADD = 5;
|
||||
|
||||
public final ChoiceToolParameter mode = new ChoiceToolParameter(
|
||||
"Modus",
|
||||
new String[]{"Sinus", "Spike", "Plateau", "Smooth", "Entfernen", "Hinzufügen"},
|
||||
MODE_SPIKE,
|
||||
new String[]{
|
||||
"img/editor/terraintool_sinus.png",
|
||||
"img/editor/terraintool_spike.png",
|
||||
"img/editor/terraintool_plateau.png",
|
||||
"img/editor/terraintool_smooth.png",
|
||||
null,
|
||||
null
|
||||
}
|
||||
);
|
||||
|
||||
public final ChoiceToolParameter textureSlot = new ChoiceToolParameter(
|
||||
"Textur", new String[]{"S1", "S2", "S3", "S4"}, 0
|
||||
);
|
||||
|
||||
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 0.5, 30.0);
|
||||
public final ToolParameter brushStrength = new ToolParameter("Stärke", 20.0, 1.0, 80.0);
|
||||
|
||||
@Override public String getName() { return "Voxel"; }
|
||||
|
||||
@Override
|
||||
public List<ChoiceToolParameter> getChoiceParameters() {
|
||||
return List.of(mode, textureSlot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ToolParameter> getParameters() {
|
||||
return List.of(brushRadius, brushStrength);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.CharacterStat;
|
||||
import de.blight.common.model.ConsumableEffect;
|
||||
import de.blight.common.model.Item;
|
||||
import de.blight.common.model.ItemCategory;
|
||||
import de.blight.common.model.ItemCategoryManager;
|
||||
import de.blight.common.model.ItemIO;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.common.model.ObjectReference;
|
||||
import de.blight.common.model.TextReference;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -17,6 +21,7 @@ import javafx.scene.paint.Color;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -33,14 +38,18 @@ public class ItemEditorView extends BorderPane {
|
||||
private Item current = null;
|
||||
|
||||
// Form-Felder
|
||||
private TextField idField;
|
||||
private ComboBox<ItemCategory> catCombo;
|
||||
private TextField idField;
|
||||
private ComboBox<ItemCategory> catCombo;
|
||||
private ComboBox<ItemSubCategory> subCatCombo;
|
||||
private TextField nameField;
|
||||
private TextField descField;
|
||||
private Spinner<Integer> goldSpinner;
|
||||
private TextField modelRefField;
|
||||
private VBox formContainer;
|
||||
private Button deleteBtn;
|
||||
private CheckBox consumableCheck;
|
||||
private VBox consumablesSection;
|
||||
private VBox effectsRows;
|
||||
|
||||
public ItemEditorView(Path itemDir) {
|
||||
this.itemDir = itemDir;
|
||||
@@ -146,8 +155,8 @@ public class ItemEditorView extends BorderPane {
|
||||
case GEAR -> "#5588cc";
|
||||
case CONSUMABLES -> "#55aa55";
|
||||
case QUEST_ITEMS -> "#ccaa33";
|
||||
case USABLES -> "#aa55cc";
|
||||
case MISC -> "#778899";
|
||||
case CRAFTING_ITEMS -> "#aa55cc";
|
||||
case MISC_ITEMS -> "#778899";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -200,6 +209,36 @@ public class ItemEditorView extends BorderPane {
|
||||
}
|
||||
});
|
||||
|
||||
subCatCombo = new ComboBox<>();
|
||||
subCatCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
subCatCombo.setDisable(true);
|
||||
subCatCombo.setPromptText("Erst Kategorie wählen");
|
||||
subCatCombo.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(ItemSubCategory sub, boolean empty) {
|
||||
super.updateItem(sub, empty);
|
||||
setText(empty || sub == null ? null : sub.name());
|
||||
}
|
||||
});
|
||||
subCatCombo.setButtonCell(new ListCell<>() {
|
||||
@Override protected void updateItem(ItemSubCategory sub, boolean empty) {
|
||||
super.updateItem(sub, empty);
|
||||
setText(empty || sub == null ? null : sub.name());
|
||||
}
|
||||
});
|
||||
|
||||
// Wenn Kategorie wechselt: SubKategorie-Liste aktualisieren
|
||||
catCombo.valueProperty().addListener((obs, oldCat, newCat) -> {
|
||||
ItemSubCategory prevSub = subCatCombo.getValue();
|
||||
subCatCombo.getItems().setAll(
|
||||
newCat != null ? ItemCategoryManager.getSubCategories(newCat) : java.util.List.of());
|
||||
subCatCombo.setDisable(newCat == null || subCatCombo.getItems().isEmpty());
|
||||
if (prevSub != null && subCatCombo.getItems().contains(prevSub)) {
|
||||
subCatCombo.setValue(prevSub);
|
||||
} else {
|
||||
subCatCombo.setValue(null);
|
||||
}
|
||||
});
|
||||
|
||||
nameField = new TextField();
|
||||
nameField.setPromptText("TextReference-Schlüssel");
|
||||
descField = new TextField();
|
||||
@@ -230,6 +269,26 @@ public class ItemEditorView extends BorderPane {
|
||||
HBox.setHgrow(modelRefField, Priority.ALWAYS);
|
||||
modelFullRow.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
// ── Consumables-Sektion ───────────────────────────────────────────────
|
||||
consumableCheck = new CheckBox("Konsumierbar");
|
||||
consumableCheck.setStyle("-fx-text-fill: #aaa;");
|
||||
|
||||
effectsRows = new VBox(4);
|
||||
Button addEffectBtn = new Button("+ Effekt hinzufügen");
|
||||
addEffectBtn.setStyle("-fx-background-color: #2a4a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand;");
|
||||
addEffectBtn.setOnAction(e -> addEffectRow(null, 0));
|
||||
consumablesSection = new VBox(6,
|
||||
sectionTitle("Effekte beim Benutzen"),
|
||||
effectsRows,
|
||||
addEffectBtn
|
||||
);
|
||||
consumablesSection.setVisible(false);
|
||||
consumablesSection.setManaged(false);
|
||||
consumableCheck.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
consumablesSection.setVisible(isSelected);
|
||||
consumablesSection.setManaged(isSelected);
|
||||
});
|
||||
|
||||
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;");
|
||||
@@ -240,6 +299,7 @@ public class ItemEditorView extends BorderPane {
|
||||
new Separator(),
|
||||
row("Item-ID:", idField),
|
||||
row("Kategorie:", catCombo),
|
||||
row("SubKategorie:", subCatCombo),
|
||||
new Separator(),
|
||||
sectionTitle("Texte"),
|
||||
row("Name:", nameField),
|
||||
@@ -249,6 +309,9 @@ public class ItemEditorView extends BorderPane {
|
||||
row("Wert (Gold):", goldSpinner),
|
||||
row("Modell:", modelFullRow),
|
||||
new Separator(),
|
||||
consumableCheck,
|
||||
consumablesSection,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
@@ -285,31 +348,54 @@ public class ItemEditorView extends BorderPane {
|
||||
|
||||
private void loadFormFromItem(Item item) {
|
||||
idField.setText(safe(item.getItemId()));
|
||||
catCombo.setValue(item.getCategory());
|
||||
catCombo.setValue(item.getCategory()); // löst Listener aus → subCatCombo befüllt
|
||||
subCatCombo.setValue(item.getSubCategory());
|
||||
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() : "");
|
||||
consumableCheck.setSelected(item.isConsumable());
|
||||
effectsRows.getChildren().clear();
|
||||
if (item.getEffects() != null) {
|
||||
for (ConsumableEffect effect : item.getEffects()) {
|
||||
addEffectRow(effect.getStat(), effect.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveFormToItem(Item item) {
|
||||
item.setItemId(idField.getText().trim());
|
||||
item.setCategory(catCombo.getValue());
|
||||
item.setSubCategory(subCatCombo.getValue());
|
||||
item.setName(ref(nameField));
|
||||
item.setDescription(ref(descField));
|
||||
item.setWorthGold(goldSpinner.getValue());
|
||||
String mr = modelRefField.getText().trim();
|
||||
item.setModelRef(mr.isBlank() ? null : new ObjectReference(mr));
|
||||
item.setConsumable(consumableCheck.isSelected());
|
||||
List<ConsumableEffect> effects = new ArrayList<>();
|
||||
for (Node node : effectsRows.getChildren()) {
|
||||
if (!(node instanceof HBox row)) continue;
|
||||
@SuppressWarnings("unchecked")
|
||||
ComboBox<CharacterStat> sc = (ComboBox<CharacterStat>) row.getChildren().get(0);
|
||||
@SuppressWarnings("unchecked")
|
||||
Spinner<Integer> sp = (Spinner<Integer>) row.getChildren().get(1);
|
||||
if (sc.getValue() != null) effects.add(new ConsumableEffect(sc.getValue(), sp.getValue()));
|
||||
}
|
||||
item.setEffects(effects.isEmpty() ? null : effects);
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
catCombo.setValue(null);
|
||||
catCombo.setValue(null); // setzt subCatCombo via Listener zurück
|
||||
subCatCombo.setValue(null);
|
||||
nameField.clear();
|
||||
descField.clear();
|
||||
goldSpinner.getValueFactory().setValue(0);
|
||||
modelRefField.clear();
|
||||
consumableCheck.setSelected(false);
|
||||
effectsRows.getChildren().clear();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
@@ -317,7 +403,7 @@ public class ItemEditorView extends BorderPane {
|
||||
private void createItem() {
|
||||
Item item = new Item();
|
||||
item.setItemId("neues_item_" + System.currentTimeMillis());
|
||||
item.setCategory(ItemCategory.MISC);
|
||||
item.setCategory(ItemCategory.MISC_ITEMS);
|
||||
items.add(item);
|
||||
listView.getSelectionModel().select(item);
|
||||
}
|
||||
@@ -356,6 +442,28 @@ public class ItemEditorView extends BorderPane {
|
||||
selectItem(savedId);
|
||||
}
|
||||
|
||||
// ── Effect rows ───────────────────────────────────────────────────────────
|
||||
|
||||
private void addEffectRow(CharacterStat stat, int value) {
|
||||
ComboBox<CharacterStat> statCombo = new ComboBox<>();
|
||||
statCombo.getItems().addAll(CharacterStat.values());
|
||||
statCombo.setValue(stat);
|
||||
statCombo.setMinWidth(210);
|
||||
statCombo.setPromptText("Stat wählen");
|
||||
|
||||
Spinner<Integer> valSpinner = new Spinner<>(-99999, 99999, value);
|
||||
valSpinner.setEditable(true);
|
||||
valSpinner.setPrefWidth(100);
|
||||
|
||||
Button removeBtn = new Button("✕");
|
||||
removeBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #cc4444; -fx-cursor: hand;");
|
||||
|
||||
HBox row = new HBox(6, statCombo, valSpinner, removeBtn);
|
||||
row.setAlignment(Pos.CENTER_LEFT);
|
||||
removeBtn.setOnAction(e -> effectsRows.getChildren().remove(row));
|
||||
effectsRows.getChildren().add(row);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static Label sectionTitle(String text) {
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.*;
|
||||
import de.blight.common.RiverPoint;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Übersicht aller auf der Karte platzierten Objekte.
|
||||
* Doppelklick teleportiert die Kamera 5 m über das Objekt und aktiviert das passende Werkzeug.
|
||||
* Entf-Taste löscht den markierten Eintrag nach Sicherheitsabfrage.
|
||||
*/
|
||||
public class MapObjectsView extends VBox {
|
||||
|
||||
@FunctionalInterface
|
||||
public interface JumpAction {
|
||||
void execute(float x, float y, float z, String toolHint);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DeleteAction {
|
||||
void execute(Object placedObj, int index, String toolHint) throws Exception;
|
||||
}
|
||||
|
||||
private record Entry(float x, float y, float z, String toolHint, Object placedObj, int index) {}
|
||||
|
||||
private final JumpAction onJump;
|
||||
private final DeleteAction onDelete;
|
||||
private final TreeItem<String> treeRoot = new TreeItem<>("Karte");
|
||||
private final TreeView<String> tree;
|
||||
private final Map<TreeItem<String>, Entry> entryMap = new HashMap<>();
|
||||
|
||||
public MapObjectsView(JumpAction onJump, DeleteAction onDelete) {
|
||||
this.onJump = onJump;
|
||||
this.onDelete = onDelete;
|
||||
setStyle("-fx-background-color: #f0f0f0;");
|
||||
setPadding(new Insets(8));
|
||||
setSpacing(6);
|
||||
|
||||
Label titleLbl = new Label("Platzierte Objekte");
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #222;");
|
||||
|
||||
Button refreshBtn = new Button("↺");
|
||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #555; -fx-cursor: hand;");
|
||||
refreshBtn.setTooltip(new Tooltip("Neu laden"));
|
||||
refreshBtn.setOnAction(e -> refresh());
|
||||
|
||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
treeRoot.setExpanded(true);
|
||||
tree = new TreeView<>(treeRoot);
|
||||
tree.setShowRoot(false);
|
||||
tree.setStyle("-fx-background-color: #fafafa;");
|
||||
VBox.setVgrow(tree, Priority.ALWAYS);
|
||||
|
||||
tree.setCellFactory(tv -> new TreeCell<>() {
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) { setText(null); setStyle(""); return; }
|
||||
setText(item);
|
||||
boolean isLeaf = getTreeItem() != null && getTreeItem().isLeaf();
|
||||
setStyle(isLeaf
|
||||
? "-fx-text-fill: #333; -fx-padding: 1 4 1 4;"
|
||||
: "-fx-text-fill: #1a4a8a; -fx-font-weight: bold; -fx-padding: 2 4 2 0;");
|
||||
}
|
||||
});
|
||||
|
||||
tree.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() != 2) return;
|
||||
TreeItem<String> sel = tree.getSelectionModel().getSelectedItem();
|
||||
if (sel == null || !sel.isLeaf()) return;
|
||||
Entry entry = entryMap.get(sel);
|
||||
if (entry != null) onJump.execute(entry.x(), entry.y(), entry.z(), entry.toolHint());
|
||||
});
|
||||
|
||||
tree.setOnKeyPressed(e -> {
|
||||
if (e.getCode() != KeyCode.DELETE) return;
|
||||
TreeItem<String> sel = tree.getSelectionModel().getSelectedItem();
|
||||
if (sel == null || !sel.isLeaf()) return;
|
||||
Entry entry = entryMap.get(sel);
|
||||
if (entry == null) return;
|
||||
confirmAndDelete(sel.getValue(), entry);
|
||||
e.consume();
|
||||
});
|
||||
|
||||
getChildren().addAll(header, tree);
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
treeRoot.getChildren().clear();
|
||||
entryMap.clear();
|
||||
|
||||
loadModels();
|
||||
loadItems();
|
||||
loadLights();
|
||||
loadEmitters();
|
||||
loadWaterBodies();
|
||||
loadRivers();
|
||||
loadSoundAreas();
|
||||
loadAreas();
|
||||
loadLocationZones();
|
||||
}
|
||||
|
||||
// ── Löschen ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void confirmAndDelete(String label, Entry entry) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle("Objekt löschen");
|
||||
alert.setHeaderText("Objekt wirklich löschen?");
|
||||
alert.setContentText(label);
|
||||
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
|
||||
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
if (result.isEmpty() || result.get() != ButtonType.YES) return;
|
||||
|
||||
try {
|
||||
onDelete.execute(entry.placedObj(), entry.index(), entry.toolHint());
|
||||
refresh();
|
||||
} catch (Exception ex) {
|
||||
Alert err = new Alert(Alert.AlertType.ERROR, "Fehler: " + ex.getMessage(), ButtonType.OK);
|
||||
err.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Loader ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void loadModels() {
|
||||
try {
|
||||
List<PlacedModel> list = PlacedModelIO.load();
|
||||
TreeItem<String> group = group("Modelle", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedModel m = list.get(idx);
|
||||
TreeItem<String> item = leaf(lastName(m.modelPath()) + " " + pos(m.x(), m.y(), m.z()));
|
||||
entryMap.put(item, new Entry(m.x(), m.y() + 5f, m.z(), "model", m, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadItems() {
|
||||
try {
|
||||
List<PlacedItem> list = PlacedItemIO.load();
|
||||
TreeItem<String> group = group("Items", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedItem i = list.get(idx);
|
||||
TreeItem<String> item = leaf(i.itemId() + " " + pos(i.x(), i.y(), i.z()));
|
||||
entryMap.put(item, new Entry(i.x(), i.y() + 5f, i.z(), "item", i, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadLights() {
|
||||
try {
|
||||
List<PlacedLight> list = LightIO.load();
|
||||
TreeItem<String> group = group("Lichter", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedLight l = list.get(idx);
|
||||
TreeItem<String> item = leaf("Licht " + pos(l.x(), l.y(), l.z()));
|
||||
entryMap.put(item, new Entry(l.x(), l.y() + 5f, l.z(), "light", l, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadEmitters() {
|
||||
try {
|
||||
List<PlacedEmitter> list = EmitterIO.load();
|
||||
TreeItem<String> group = group("Emitter", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedEmitter e = list.get(idx);
|
||||
TreeItem<String> item = leaf(lastName(e.texturePath()) + " " + pos(e.x(), e.y(), e.z()));
|
||||
entryMap.put(item, new Entry(e.x(), e.y() + 5f, e.z(), "emitter", e, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadWaterBodies() {
|
||||
try {
|
||||
List<PlacedWater> list = WaterBodyIO.load();
|
||||
TreeItem<String> group = group("Wasser", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedWater w = list.get(idx);
|
||||
float cx = centroid(w.pointsX()), cz = centroid(w.pointsZ());
|
||||
TreeItem<String> item = leaf("Wasser #" + (idx + 1) + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "water", w, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadRivers() {
|
||||
try {
|
||||
List<List<RiverPoint>> list = RiverIO.load();
|
||||
TreeItem<String> group = group("Wasserfälle", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
List<RiverPoint> river = list.get(idx);
|
||||
float cx = 0f, cy = 0f, cz = 0f;
|
||||
for (RiverPoint pt : river) { cx += pt.x(); cy += pt.y(); cz += pt.z(); }
|
||||
if (!river.isEmpty()) { cx /= river.size(); cy /= river.size(); cz /= river.size(); }
|
||||
TreeItem<String> item = leaf("Wasserfall #" + (idx + 1) + " " + pos(cx, cy, cz));
|
||||
entryMap.put(item, new Entry(cx, cy + 5f, cz, "waterfall", river, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadSoundAreas() {
|
||||
try {
|
||||
List<PlacedSoundArea> list = SoundAreaIO.load();
|
||||
TreeItem<String> group = group("Soundbereiche", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedSoundArea s = list.get(idx);
|
||||
float cx = centroid(s.pointsX()), cz = centroid(s.pointsZ());
|
||||
String label = s.soundPath() != null && !s.soundPath().isBlank()
|
||||
? lastName(s.soundPath()) : "Soundbereich #" + (idx + 1);
|
||||
TreeItem<String> item = leaf(label + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "soundarea", s, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadAreas() {
|
||||
try {
|
||||
List<PlacedArea> list = AreaIO.load();
|
||||
TreeItem<String> group = group("Bereiche", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedArea a = list.get(idx);
|
||||
float cx = centroid(a.pointsX()), cz = centroid(a.pointsZ());
|
||||
String label = a.nameId() != null && !a.nameId().isBlank()
|
||||
? a.nameId() : "Bereich #" + (idx + 1);
|
||||
TreeItem<String> item = leaf(label + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "area", a, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void loadLocationZones() {
|
||||
try {
|
||||
List<PlacedLocationZone> list = LocationZoneIO.load();
|
||||
TreeItem<String> group = group("Zonen", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedLocationZone z = list.get(idx);
|
||||
float cx = centroid(z.pointsX()), cz = centroid(z.pointsZ());
|
||||
String label = z.nameId() != null && !z.nameId().isBlank()
|
||||
? z.nameId() : "Zone #" + (idx + 1);
|
||||
TreeItem<String> item = leaf(label + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "locationzone", z, idx));
|
||||
group.getChildren().add(item);
|
||||
}
|
||||
treeRoot.getChildren().add(group);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static TreeItem<String> group(String name, int count) {
|
||||
TreeItem<String> g = new TreeItem<>(name + " (" + count + ")");
|
||||
g.setExpanded(true);
|
||||
return g;
|
||||
}
|
||||
|
||||
private static TreeItem<String> leaf(String label) {
|
||||
return new TreeItem<>(label);
|
||||
}
|
||||
|
||||
private static String pos(float x, float y, float z) {
|
||||
return String.format("(%.0f, %.0f, %.0f)", x, y, z);
|
||||
}
|
||||
|
||||
private static String posXZ(float x, float z) {
|
||||
return String.format("(%.0f, ?, %.0f)", x, z);
|
||||
}
|
||||
|
||||
private static float centroid(float[] arr) {
|
||||
if (arr == null || arr.length == 0) return 0f;
|
||||
float sum = 0f;
|
||||
for (float v : arr) sum += v;
|
||||
return sum / arr.length;
|
||||
}
|
||||
|
||||
private static String lastName(String path) {
|
||||
if (path == null || path.isBlank()) return "—";
|
||||
int slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||
return slash >= 0 ? path.substring(slash + 1) : path;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user