Voxel Klippen System weiter optimiert, Monolog System implementiert, Compiler Warnungen behoben
This commit is contained in:
@@ -171,8 +171,9 @@ public class EditorApp extends Application {
|
||||
editableSubClips = new java.util.LinkedHashMap<>();
|
||||
|
||||
// Character-Editor-Zustand
|
||||
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
||||
private de.blight.editor.ui.RoutineEditorView routineEditorView;
|
||||
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
||||
private de.blight.editor.ui.RoutineEditorView routineEditorView;
|
||||
private de.blight.editor.ui.MonologueEditorView monologueEditorView;
|
||||
private StackPane routineJmeSlot;
|
||||
private javafx.scene.layout.VBox charEditContainer; // deaktiviert solange kein Character geladen
|
||||
private javafx.scene.layout.VBox charFractionSection; // sichtbar nur für NPCs
|
||||
@@ -212,8 +213,16 @@ public class EditorApp extends Application {
|
||||
private VBox waterDynamicContent;
|
||||
private Label waterCurrentHeightLabel;
|
||||
|
||||
// Ansichts-Umschalter
|
||||
private boolean wireframeActive = false;
|
||||
private CheckMenuItem viewTopologyItem = null;
|
||||
|
||||
// Sound-Bereich-Werkzeug-Zustand
|
||||
private VBox soundAreaDynamicContent;
|
||||
private VBox voxelCliffDynamicContent;
|
||||
private Label voxelCliffStatusLabel;
|
||||
private Button voxelCliffGenerateBtn;
|
||||
private Button voxelCliffDeleteBtn;
|
||||
|
||||
// Bereich-Werkzeug-Zustand
|
||||
private VBox areaDynamicContent;
|
||||
@@ -310,6 +319,7 @@ public class EditorApp extends Application {
|
||||
private ToggleButton locationZoneBtn;
|
||||
private ToggleButton playToolBtn;
|
||||
private ToggleButton voxelBtn;
|
||||
private ToggleButton voxelCliffBtn;
|
||||
private ToggleButton camOrbitBtn;
|
||||
private ToggleButton camFreeBtn;
|
||||
|
||||
@@ -643,6 +653,23 @@ public class EditorApp extends Application {
|
||||
updateLocationZonePanel(input.selectedLocationZoneInfo);
|
||||
}
|
||||
|
||||
if (input.cliffZoneSelectionChanged) {
|
||||
input.cliffZoneSelectionChanged = false;
|
||||
updateVoxelCliffPanel(input.selectedCliffZoneIdx >= 0);
|
||||
}
|
||||
|
||||
String cliffMsg = input.cliffGenStatusMsg;
|
||||
if (cliffMsg != null) {
|
||||
input.cliffGenStatusMsg = null;
|
||||
updateVoxelCliffStatus(cliffMsg);
|
||||
}
|
||||
|
||||
if (input.cliffGenComplete) {
|
||||
input.cliffGenComplete = false;
|
||||
// Zu Voxel-Tool wechseln, damit der User direkt backen kann
|
||||
Platform.runLater(() -> { if (voxelBtn != null) voxelBtn.fire(); });
|
||||
}
|
||||
|
||||
if (input.spawnPickChanged) {
|
||||
input.spawnPickChanged = false;
|
||||
updateSpawnFields(input.pickedSpawnInfo);
|
||||
@@ -1164,14 +1191,21 @@ public class EditorApp extends Application {
|
||||
Menu viewMenu = new Menu("Ansicht");
|
||||
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
|
||||
resetCam.setOnAction(e -> input.addMouseDelta(0, 0));
|
||||
MenuItem viewTexture = new MenuItem("Textur");
|
||||
MenuItem viewWireframe = new MenuItem("Drahtgitter");
|
||||
CheckMenuItem viewTopology = new CheckMenuItem("Topologie-Overlay");
|
||||
viewTexture.setOnAction(e -> input.wireframeRequest = 2);
|
||||
viewWireframe.setOnAction(e -> input.wireframeRequest = 1);
|
||||
viewTopology.setOnAction(e -> input.topologyRequest = viewTopology.isSelected() ? 1 : 2);
|
||||
MenuItem viewTexture = new MenuItem("Textur (Ctrl+G)");
|
||||
MenuItem viewWireframe = new MenuItem("Drahtgitter (Ctrl+G)");
|
||||
viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Ctrl+T)");
|
||||
viewTexture.setOnAction(e -> {
|
||||
wireframeActive = false;
|
||||
input.wireframeRequest = 2;
|
||||
});
|
||||
viewWireframe.setOnAction(e -> {
|
||||
wireframeActive = true;
|
||||
input.wireframeRequest = 1;
|
||||
});
|
||||
viewTopologyItem.setOnAction(e ->
|
||||
input.topologyRequest = viewTopologyItem.isSelected() ? 1 : 2);
|
||||
viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe,
|
||||
new SeparatorMenuItem(), viewTopology);
|
||||
new SeparatorMenuItem(), viewTopologyItem);
|
||||
|
||||
Menu zeitMenu = new Menu("Zeit");
|
||||
ToggleGroup zeitGroup = new ToggleGroup();
|
||||
@@ -1202,6 +1236,7 @@ public class EditorApp extends Application {
|
||||
locationZoneBtn = new ToggleButton("📍 Locations");
|
||||
playToolBtn = new ToggleButton("🎮 Spielen");
|
||||
voxelBtn = new ToggleButton("⬡ Voxel");
|
||||
voxelCliffBtn = new ToggleButton("⛰ Klippe");
|
||||
stoneBtn = new ToggleButton("🪨 Steine");
|
||||
camOrbitBtn = new ToggleButton("⊙ Orbit");
|
||||
camFreeBtn = new ToggleButton("✈ FreeFly");
|
||||
@@ -1220,6 +1255,7 @@ public class EditorApp extends Application {
|
||||
locationZoneBtn.setStyle("-fx-font-weight:bold;");
|
||||
playToolBtn.setStyle("-fx-font-weight:bold;");
|
||||
voxelBtn.setStyle("-fx-font-weight:bold;");
|
||||
voxelCliffBtn.setStyle("-fx-font-weight:bold;");
|
||||
stoneBtn.setStyle("-fx-font-weight:bold;");
|
||||
camOrbitBtn.setStyle("-fx-font-weight:bold;");
|
||||
camFreeBtn.setStyle("-fx-font-weight:bold;");
|
||||
@@ -1240,6 +1276,7 @@ public class EditorApp extends Application {
|
||||
locationZoneBtn.setToggleGroup(layerGroup);
|
||||
playToolBtn.setToggleGroup(layerGroup);
|
||||
voxelBtn.setToggleGroup(layerGroup);
|
||||
voxelCliffBtn.setToggleGroup(layerGroup);
|
||||
stoneBtn.setToggleGroup(layerGroup);
|
||||
baseBtn.setSelected(true);
|
||||
|
||||
@@ -1315,6 +1352,10 @@ public class EditorApp extends Application {
|
||||
root.setRight(toolPanel);
|
||||
showToolParameters(toolPanel, input.activeTool);
|
||||
});
|
||||
voxelCliffBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_VOXEL_CLIFF;
|
||||
root.setRight(buildVoxelCliffPanel());
|
||||
});
|
||||
stoneBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_STONE;
|
||||
input.activeTool = input.stoneTool;
|
||||
@@ -1345,7 +1386,7 @@ 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), voxelBtn, voxelCliffBtn,
|
||||
new Separator(Orientation.VERTICAL), stoneBtn,
|
||||
new Separator(Orientation.VERTICAL), camOrbitBtn, camFreeBtn, camResetBtn,
|
||||
new Separator(Orientation.VERTICAL), hint);
|
||||
@@ -3286,7 +3327,38 @@ public class EditorApp extends Application {
|
||||
poller.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
||||
poller.play();
|
||||
|
||||
panel.getChildren().addAll(new Separator(), bakeBtn, bakeBar, bakeBarLabel, bakeStatus);
|
||||
// ── Markierte Baked-Chunks löschen (Modus 6) ─────────────────────
|
||||
Label markCountLabel = new Label("0 Chunks markiert");
|
||||
markCountLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
|
||||
|
||||
Button deleteMarkedBtn = new Button("Markierte Chunks löschen");
|
||||
deleteMarkedBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteMarkedBtn.setStyle("-fx-background-color: #ba4a4a; -fx-text-fill: white;");
|
||||
deleteMarkedBtn.setDisable(true);
|
||||
deleteMarkedBtn.setOnAction(e -> {
|
||||
input.deleteMarkedBakedRequested = true;
|
||||
deleteMarkedBtn.setDisable(true);
|
||||
});
|
||||
|
||||
Button clearMarkBtn = new Button("Markierung leeren");
|
||||
clearMarkBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
clearMarkBtn.setDisable(true);
|
||||
clearMarkBtn.setOnAction(e -> input.clearMarkedBakedRequested = true);
|
||||
|
||||
// Poller: Anzahl markierter Chunks + Bake-Status aktualisieren
|
||||
javafx.animation.Timeline markPoller = new javafx.animation.Timeline(
|
||||
new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> {
|
||||
int n = input.markedDeleteBakedKeys.size();
|
||||
markCountLabel.setText(n + " Chunk" + (n != 1 ? "s" : "") + " markiert");
|
||||
deleteMarkedBtn.setDisable(n == 0);
|
||||
clearMarkBtn.setDisable(n == 0);
|
||||
})
|
||||
);
|
||||
markPoller.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
||||
markPoller.play();
|
||||
|
||||
panel.getChildren().addAll(new Separator(), bakeBtn, bakeBar, bakeBarLabel, bakeStatus,
|
||||
new Separator(), markCountLabel, deleteMarkedBtn, clearMarkBtn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7485,6 +7557,8 @@ public class EditorApp extends Application {
|
||||
input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0));
|
||||
case SharedInput.LAYER_LOCATION_ZONES ->
|
||||
input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0));
|
||||
case SharedInput.LAYER_VOXEL_CLIFF ->
|
||||
input.voxelCliffClickQueue.offer(new SharedInput.VoxelCliffClick((float) x, (float) y, action < 0));
|
||||
case SharedInput.LAYER_PLAY_TOOL -> {
|
||||
if (action > 0) // only left-click sets spawn
|
||||
input.playToolClickQueue.offer(new SharedInput.PlayToolClick((float) x, (float) y));
|
||||
@@ -7802,11 +7876,19 @@ public class EditorApp extends Application {
|
||||
case E -> input.down = pressed;
|
||||
case SHIFT -> input.shiftHeld = pressed;
|
||||
case CONTROL -> input.ctrlHeld = pressed;
|
||||
case ENTER -> {
|
||||
if (pressed && input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) {
|
||||
input.generateCliffVoxelsRequested = true;
|
||||
if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true);
|
||||
if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText("Generiere…");
|
||||
}
|
||||
}
|
||||
case ESCAPE -> {
|
||||
if (pressed && (input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|
||||
|| input.activeLayer == SharedInput.LAYER_WATER))
|
||||
|| input.activeLayer == SharedInput.LAYER_WATER
|
||||
|| input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF))
|
||||
input.cancelZoneDrawing = true;
|
||||
}
|
||||
case DELETE -> {
|
||||
@@ -7820,6 +7902,8 @@ public class EditorApp extends Application {
|
||||
input.deleteAreaRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES)
|
||||
input.deleteLocationZoneRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF)
|
||||
input.deleteVoxelCliffZoneRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_WATER)
|
||||
input.deleteWaterRequested = true;
|
||||
}
|
||||
@@ -7832,6 +7916,19 @@ public class EditorApp extends Application {
|
||||
if (pressed && input.activeLayer == SharedInput.LAYER_WATER)
|
||||
input.waterSampleHeightRequested = true;
|
||||
}
|
||||
case G -> {
|
||||
if (pressed && input.ctrlHeld) {
|
||||
wireframeActive = !wireframeActive;
|
||||
input.wireframeRequest = wireframeActive ? 1 : 2;
|
||||
}
|
||||
}
|
||||
case T -> {
|
||||
if (pressed && input.ctrlHeld && viewTopologyItem != null) {
|
||||
boolean sel = !viewTopologyItem.isSelected();
|
||||
viewTopologyItem.setSelected(sel);
|
||||
input.topologyRequest = sel ? 1 : 2;
|
||||
}
|
||||
}
|
||||
case F1 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> baseBtn.fire()); }
|
||||
case F2 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> grassBtn.fire()); }
|
||||
case F5 -> {
|
||||
@@ -7860,6 +7957,117 @@ public class EditorApp extends Application {
|
||||
|
||||
// ── Sound-Bereich-Panel ───────────────────────────────────────────────────
|
||||
|
||||
// ── Voxel-Klippen-Zone-Panel ──────────────────────────────────────────────
|
||||
|
||||
private VBox buildVoxelCliffPanel() {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
inner.getChildren().addAll(
|
||||
sectionTitle("Voxel-Klippen-Zonen"),
|
||||
styledHint("L-Klick → Punkt setzen / Startpunkt: Polygon schließen"),
|
||||
styledHint("R-Klick → Letzten Punkt rückgängig"),
|
||||
styledHint("ESC → Abbrechen / Zone löschen"),
|
||||
styledHint("Enter → Generieren + Voxel-Tool öffnen"),
|
||||
styledHint("Entf → Zone löschen"),
|
||||
new Separator(),
|
||||
sectionTitle("Höhen-Parameter"));
|
||||
|
||||
Slider maxHeightSlider = new Slider(0, 30, input.cliffMaxHeight);
|
||||
maxHeightSlider.valueProperty().addListener((o, ov, nv) ->
|
||||
input.cliffMaxHeight = nv.floatValue());
|
||||
inner.getChildren().addAll(new Label("Max. Höhe über Terrain"), withField(maxHeightSlider, "%.1f"));
|
||||
|
||||
Slider noiseScaleSlider = new Slider(0.01, 0.5, input.cliffNoiseScale);
|
||||
noiseScaleSlider.valueProperty().addListener((o, ov, nv) ->
|
||||
input.cliffNoiseScale = nv.floatValue());
|
||||
inner.getChildren().addAll(new Label("Höhen-Noise-Frequenz"), withField(noiseScaleSlider, "%.3f"));
|
||||
|
||||
Slider edgeBlendSlider = new Slider(0, 30, input.cliffEdgeBlend);
|
||||
edgeBlendSlider.valueProperty().addListener((o, ov, nv) ->
|
||||
input.cliffEdgeBlend = nv.floatValue());
|
||||
inner.getChildren().addAll(new Label("Rand-Überblendung"), withField(edgeBlendSlider, "%.1f"));
|
||||
|
||||
inner.getChildren().addAll(
|
||||
new Separator(),
|
||||
sectionTitle("Oberflächen-Rauigkeit"));
|
||||
|
||||
Slider roughAmpSlider = new Slider(0, 20, input.cliffRoughnessAmp);
|
||||
roughAmpSlider.valueProperty().addListener((o, ov, nv) ->
|
||||
input.cliffRoughnessAmp = nv.floatValue());
|
||||
inner.getChildren().addAll(new Label("Oberflächen-Amplitude"), withField(roughAmpSlider, "%.1f"));
|
||||
|
||||
Slider roughFreqSlider = new Slider(0.01, 0.5, input.cliffRoughnessScale);
|
||||
roughFreqSlider.valueProperty().addListener((o, ov, nv) ->
|
||||
input.cliffRoughnessScale = nv.floatValue());
|
||||
inner.getChildren().addAll(new Label("Oberflächen-Frequenz"), withField(roughFreqSlider, "%.3f"));
|
||||
|
||||
inner.getChildren().addAll(
|
||||
new Separator(),
|
||||
sectionTitle("Gewählte Zone"),
|
||||
new Separator());
|
||||
|
||||
voxelCliffDynamicContent = new VBox(6);
|
||||
inner.getChildren().add(voxelCliffDynamicContent);
|
||||
|
||||
voxelCliffStatusLabel = new Label("");
|
||||
voxelCliffStatusLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
|
||||
voxelCliffStatusLabel.setWrapText(true);
|
||||
inner.getChildren().add(voxelCliffStatusLabel);
|
||||
|
||||
// Sofort korrekten Zustand anzeigen (Zone evtl. schon vor Panel-Bau geladen)
|
||||
updateVoxelCliffPanel(input.selectedCliffZoneIdx >= 0);
|
||||
|
||||
ScrollPane scroll = new ScrollPane(inner);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
|
||||
VBox panel = new VBox(scroll);
|
||||
VBox.setVgrow(scroll, Priority.ALWAYS);
|
||||
panel.setPrefWidth(270);
|
||||
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void updateVoxelCliffPanel(boolean hasSelection) {
|
||||
if (voxelCliffDynamicContent == null) return;
|
||||
voxelCliffDynamicContent.getChildren().clear();
|
||||
|
||||
if (!hasSelection) {
|
||||
Label noSel = new Label("Keine Zone ausgewählt");
|
||||
noSel.setStyle("-fx-text-fill: #888;");
|
||||
voxelCliffDynamicContent.getChildren().add(noSel);
|
||||
if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(true);
|
||||
if (voxelCliffDeleteBtn != null) voxelCliffDeleteBtn.setDisable(true);
|
||||
return;
|
||||
}
|
||||
|
||||
voxelCliffGenerateBtn = new Button("⛰ Voxel generieren");
|
||||
voxelCliffGenerateBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
voxelCliffGenerateBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;");
|
||||
voxelCliffGenerateBtn.setOnAction(e -> {
|
||||
input.generateCliffVoxelsRequested = true;
|
||||
voxelCliffGenerateBtn.setDisable(true);
|
||||
if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText("Generiere…");
|
||||
});
|
||||
|
||||
voxelCliffDeleteBtn = new Button("🗑 Zone löschen");
|
||||
voxelCliffDeleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
voxelCliffDeleteBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||
voxelCliffDeleteBtn.setOnAction(e -> input.deleteVoxelCliffZoneRequested = true);
|
||||
|
||||
Label hint = new Label("Hinweis: Generierung legt Voxel über das Terrain. Bestehende Voxel\nim Bereich werden nicht gelöscht (Reset-Brush verwenden).");
|
||||
hint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
||||
hint.setWrapText(true);
|
||||
|
||||
voxelCliffDynamicContent.getChildren().addAll(
|
||||
voxelCliffGenerateBtn, voxelCliffDeleteBtn, new Separator(), hint);
|
||||
}
|
||||
|
||||
private void updateVoxelCliffStatus(String msg) {
|
||||
if (voxelCliffStatusLabel != null) voxelCliffStatusLabel.setText(msg);
|
||||
if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(false);
|
||||
}
|
||||
|
||||
private VBox buildSoundAreaPanel() {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
@@ -9871,6 +10079,7 @@ public class EditorApp extends Application {
|
||||
dialogEditorView.setDisable(true);
|
||||
routineEditorView = new de.blight.editor.ui.RoutineEditorView(input, ASSET_ROOT.resolve("character"));
|
||||
routineEditorView.setDisable(true);
|
||||
monologueEditorView = new de.blight.editor.ui.MonologueEditorView();
|
||||
|
||||
// worldViewport in den Tagesabläufe-Tab verschieben; centerStack bekommt Platzhalter
|
||||
routineJmeSlot = new StackPane();
|
||||
@@ -9889,7 +10098,8 @@ public class EditorApp extends Application {
|
||||
centerTabs.setTabClosingPolicy(javafx.scene.control.TabPane.TabClosingPolicy.UNAVAILABLE);
|
||||
centerTabs.getTabs().addAll(
|
||||
new javafx.scene.control.Tab("Dialog", dialogEditorView),
|
||||
new javafx.scene.control.Tab("Tagesabläufe", routineSplit)
|
||||
new javafx.scene.control.Tab("Tagesabläufe", routineSplit),
|
||||
new javafx.scene.control.Tab("Monologe", monologueEditorView)
|
||||
);
|
||||
root.setCenter(centerTabs);
|
||||
root.setRight(buildCharacterEditorPanel());
|
||||
|
||||
@@ -35,6 +35,7 @@ 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.editor.state.VoxelCliffEditorState;
|
||||
import de.blight.editor.state.SculptedMeshEditorState;
|
||||
import de.blight.editor.state.ModelImportState;
|
||||
import de.blight.editor.state.PathNetworkEditorState;
|
||||
@@ -202,6 +203,7 @@ public class JmeEditorApp extends SimpleApplication {
|
||||
stateManager.attach(new ModelEditorState(input));
|
||||
stateManager.attach(new ItemPlacementState(input));
|
||||
stateManager.attach(new VoxelEditorState(input));
|
||||
stateManager.attach(new VoxelCliffEditorState(input));
|
||||
stateManager.attach(new SculptedMeshEditorState(input));
|
||||
stateManager.attach(new ModelImportState(input));
|
||||
|
||||
|
||||
@@ -768,11 +768,22 @@ public class SharedInput {
|
||||
public volatile int bakeDone = 0;
|
||||
/** JME → JFX: Gesamtzahl der zu backenden Chunks (0 = nicht gestartet). */
|
||||
public volatile int bakeTotal = 0;
|
||||
/** JME → JFX: Status-Meldung nach Abschluss des Backens. */
|
||||
/** JME → JFX: Status-Meldung nach Abschluss des Backens oder Löschens. */
|
||||
public volatile String bakeStatusMsg = null;
|
||||
/** JME → JFX: Aktuell abgeschlossene Blur-Iteration (0-7). */
|
||||
public volatile int blurIterDone = 0;
|
||||
|
||||
/**
|
||||
* Chunks, die per Brush zum Löschen markiert wurden (chunkKey-kodiert).
|
||||
* Thread-sicher; JME schreibt, JFX liest (nur size() für Anzeige).
|
||||
*/
|
||||
public final java.util.Set<Long> markedDeleteBakedKeys =
|
||||
java.util.concurrent.ConcurrentHashMap.newKeySet();
|
||||
/** JFX → JME: alle markierten Chunks löschen. */
|
||||
public volatile boolean deleteMarkedBakedRequested = false;
|
||||
/** JFX → JME: Markierung ohne Löschen leeren. */
|
||||
public volatile boolean clearMarkedBakedRequested = false;
|
||||
|
||||
/** Terrain-Slot (0-7) für flache Voxel-Flächen, -1 = kein Slot. */
|
||||
public volatile int voxelFlatSlot = -1;
|
||||
/** Terrain-Slot (0-7) für steile Wände. */
|
||||
@@ -783,7 +794,7 @@ public class SharedInput {
|
||||
public volatile boolean voxelTexturesChanged = false;
|
||||
|
||||
/** Wenn true, werden beim Aktivieren des Voxel-Layers alle anderen Objekte als Wireframe gerendert. */
|
||||
public volatile boolean voxelWireframeEnabled = true;
|
||||
public volatile boolean voxelWireframeEnabled = false;
|
||||
|
||||
// ── Item-Platzierung ──────────────────────────────────────────────────────
|
||||
/** activeLayer==21 → Item-Pickup auf die Karte platzieren */
|
||||
@@ -999,4 +1010,34 @@ public class SharedInput {
|
||||
public volatile boolean sculptDeleteSelected = false;
|
||||
/** VoxelEditorState setzt true nach Bake-Abschluss → SculptedMeshEditorState scannt neue .j3o-Dateien. */
|
||||
public volatile boolean sculptRescanNeeded = false;
|
||||
|
||||
// ── Voxel-Klippen-Zonen ───────────────────────────────────────────────────
|
||||
/** activeLayer==31 → Voxel-Klippen-Zonen (Polygon) platzieren und Voxel generieren */
|
||||
public static final int LAYER_VOXEL_CLIFF = 31;
|
||||
|
||||
public record VoxelCliffClick(float screenX, float screenY, boolean rightButton) {}
|
||||
public final ConcurrentLinkedQueue<VoxelCliffClick> voxelCliffClickQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** JFX → JME: Voxel für die ausgewählte Klippen-Zone generieren. */
|
||||
public volatile boolean generateCliffVoxelsRequested = false;
|
||||
/** JFX → JME: Ausgewählte Klippen-Zone löschen. */
|
||||
public volatile boolean deleteVoxelCliffZoneRequested = false;
|
||||
/** JME → JFX: Status-Meldung nach Generierung. */
|
||||
public volatile String cliffGenStatusMsg = null;
|
||||
/** JME → JFX: Index der ausgewählten Klippen-Zone (-1 = keine). */
|
||||
public volatile int selectedCliffZoneIdx = -1;
|
||||
/** JME → JFX: Selektion hat sich geändert → Panel aktualisieren. */
|
||||
public volatile boolean cliffZoneSelectionChanged = false;
|
||||
/** JME → JFX: Generierung abgeschlossen → zu LAYER_VOXEL wechseln. */
|
||||
public volatile boolean cliffGenComplete = false;
|
||||
/** Maximale Kliff-Höhe über Terrain in Welt-Einheiten (2D-fBm-Skalierung). */
|
||||
public volatile float cliffMaxHeight = 3.0f;
|
||||
/** 2D-Noise-Frequenz für die Höhenverteilung (kleiner = breiter, größer = lokaler). */
|
||||
public volatile float cliffNoiseScale = 0.15f;
|
||||
/** Rand-Überblendung: Zone läuft über diese Distanz zum Rand hin aus. */
|
||||
public volatile float cliffEdgeBlend = 5.0f;
|
||||
/** Oberflächen-Rauigkeit in Welt-Einheiten (wie stark 3D-Noise die Iso-Fläche verschiebt). */
|
||||
public volatile float cliffRoughnessAmp = 4.0f;
|
||||
/** 3D-Noise-Frequenz für die Kliff-Oberfläche (höher = feiner Detail). */
|
||||
public volatile float cliffRoughnessScale = 0.12f;
|
||||
}
|
||||
|
||||
@@ -284,6 +284,7 @@ public class ModelImportState extends BaseAppState {
|
||||
startImpostorPass(0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void startImpostorPass(int pass) {
|
||||
Texture2D capTex = new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.RGBA8);
|
||||
impostorFB = new FrameBuffer(ImpostorUtil.SIZE, ImpostorUtil.SIZE, 1);
|
||||
|
||||
@@ -207,6 +207,22 @@ public class SculptedMeshEditorState extends BaseAppState {
|
||||
// ── Scan / Laden ──────────────────────────────────────────────────────────
|
||||
|
||||
private void rescanBakedChunks() {
|
||||
// Geladene Meshes entfernen, deren Baked-Dateien nicht mehr existieren
|
||||
Iterator<Map.Entry<Long, EditableMesh>> it = meshes.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<Long, EditableMesh> e = it.next();
|
||||
EditableMesh em = e.getValue();
|
||||
if (!VoxelChunkIO.bakedExists(em.cx, em.cy, em.cz)) {
|
||||
em.node.removeFromParent();
|
||||
if (selectedKey == e.getKey()) {
|
||||
selectedKey = -1L;
|
||||
input.selectedSculptKey = -1L;
|
||||
input.selectedSculptLabel = null;
|
||||
}
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
// Neue Chunks laden
|
||||
for (int[] cxyz : SculptedMeshIO.findAllBakedChunks()) {
|
||||
long key = chunkKey(cxyz[0], cxyz[1], cxyz[2]);
|
||||
if (meshes.containsKey(key)) continue;
|
||||
|
||||
@@ -107,9 +107,10 @@ public class TerrainEditorState extends BaseAppState {
|
||||
private LightState lightState;
|
||||
private EmitterState emitterState;
|
||||
private WaterBodyState waterBodyState;
|
||||
private SoundAreaState soundAreaState;
|
||||
private AreaState areaState;
|
||||
private LocationZoneState locationZoneState;
|
||||
private SoundAreaState soundAreaState;
|
||||
private AreaState areaState;
|
||||
private LocationZoneState locationZoneState;
|
||||
private VoxelCliffEditorState voxelCliffEditorState;
|
||||
private RiverEditorState riverEditorState;
|
||||
private MapData loadedMapData;
|
||||
private Node axesGizmo;
|
||||
@@ -329,6 +330,18 @@ public class TerrainEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
input.loadingStatus = "Lade Voxel-Klippen-Zonen...";
|
||||
voxelCliffEditorState = app.getStateManager().getState(VoxelCliffEditorState.class);
|
||||
if (voxelCliffEditorState != null) {
|
||||
voxelCliffEditorState.setTerrain(terrain);
|
||||
try {
|
||||
var cliffZones = de.blight.common.VoxelCliffZoneIO.load();
|
||||
if (!cliffZones.isEmpty()) voxelCliffEditorState.loadZones(cliffZones);
|
||||
} catch (IOException e) {
|
||||
log.error("Voxel-Klippen-Zonen nicht ladbar", e);
|
||||
}
|
||||
}
|
||||
|
||||
input.loadingStatus = "Lade Bereiche...";
|
||||
areaState = app.getStateManager().getState(AreaState.class);
|
||||
if (areaState != null) {
|
||||
@@ -993,13 +1006,15 @@ public class TerrainEditorState extends BaseAppState {
|
||||
log.debug("[Debug] DebugNoLight = {}", debugNoLight);
|
||||
}
|
||||
|
||||
// Wireframe-Modus setzen
|
||||
// Wireframe-Modus setzen (Ctrl+G aus EditorApp)
|
||||
int wfReq = input.wireframeRequest;
|
||||
if (wfReq != 0) {
|
||||
input.wireframeRequest = 0;
|
||||
wireframeMode = (wfReq == 1);
|
||||
if (terrain != null)
|
||||
terrain.getMaterial().getAdditionalRenderState().setWireframe(wireframeMode);
|
||||
// VoxelEditorState überwacht voxelWireframeEnabled und synchronisiert seine Szene-Wireframe
|
||||
input.voxelWireframeEnabled = wireframeMode;
|
||||
}
|
||||
|
||||
// Topologie-Overlay
|
||||
@@ -1037,6 +1052,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
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) {}
|
||||
try { if (voxelCliffEditorState != null) voxelCliffEditorState.loadZones(de.blight.common.VoxelCliffZoneIO.load()); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (input.saveRequested) {
|
||||
@@ -1194,9 +1210,11 @@ public class TerrainEditorState extends BaseAppState {
|
||||
final List<PlacedEmitter> emitters = emitterState != null ? emitterState.getPlacedEmitters() : null;
|
||||
final List<PlacedWater> waters = waterBodyState != null ? waterBodyState.getPlacedBodies() : null;
|
||||
final List<List<RiverPoint>> rivers = riverEditorState != null ? riverEditorState.getPlacedRivers() : null;
|
||||
final List<PlacedSoundArea> soundAreas = soundAreaState != null ? soundAreaState.getPlacedAreas() : null;
|
||||
final List<PlacedArea> areas = areaState != null ? areaState.getPlacedAreas() : null;
|
||||
final List<PlacedLocationZone> locationZones = locationZoneState != null ? locationZoneState.getPlacedZones() : null;
|
||||
final List<PlacedSoundArea> soundAreas = soundAreaState != null ? soundAreaState.getPlacedAreas() : null;
|
||||
final List<PlacedArea> areas = areaState != null ? areaState.getPlacedAreas() : null;
|
||||
final List<PlacedLocationZone> locationZones = locationZoneState != null ? locationZoneState.getPlacedZones() : null;
|
||||
final List<de.blight.common.PlacedVoxelCliffZone> cliffZones =
|
||||
voxelCliffEditorState != null ? voxelCliffEditorState.getPlacedZones() : null;
|
||||
|
||||
// ── Platzierte Objekte synchron speichern (kleine Textdateien) ──────────
|
||||
// Muss synchron im JME-Thread erfolgen, damit kein Race mit asynchronen
|
||||
@@ -1210,6 +1228,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
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); }
|
||||
try { if (cliffZones != null) de.blight.common.VoxelCliffZoneIO.save(cliffZones); } catch (IOException e) { log.error("Klippen-Zonen speichern", e); }
|
||||
if (stoneEditorState != null) stoneEditorState.saveIfModified();
|
||||
|
||||
// ── Schwere Arbeit (Terrain-Upsample + Datei-I/O) auf Hintergrund-Thread ─
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.PlacedVoxelCliffZone;
|
||||
import de.blight.editor.SharedInput;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class VoxelCliffEditorState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(VoxelCliffEditorState.class);
|
||||
|
||||
private static final float SNAP_DIST = 8f;
|
||||
private static final float LINE_OFFSET_Y = 0.4f;
|
||||
|
||||
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(1.0f, 0.6f, 0.1f, 1f);
|
||||
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 1f, 0.1f, 1f);
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(1f, 0.85f, 0.3f, 1f);
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
private TerrainEditorState terrainEditorState;
|
||||
|
||||
private final List<PlacedVoxelCliffZone> zones = new ArrayList<>();
|
||||
private final List<Geometry> zoneGeos = new ArrayList<>();
|
||||
private int selectedIdx = -1;
|
||||
|
||||
// laufendes Polygon
|
||||
private boolean placing = false;
|
||||
private final List<Float> currX = new ArrayList<>();
|
||||
private final List<Float> currZ = new ArrayList<>();
|
||||
private Geometry inProgGeo = null;
|
||||
private Geometry lastPointMarker = null;
|
||||
|
||||
private List<PlacedVoxelCliffZone> pendingZones = null;
|
||||
|
||||
public VoxelCliffEditorState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
terrainEditorState = app.getStateManager().getState(TerrainEditorState.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) { clearAll(); }
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
if (pendingZones != null) {
|
||||
loadZones(pendingZones);
|
||||
pendingZones = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (input.activeLayer != SharedInput.LAYER_VOXEL_CLIFF) {
|
||||
if (placing) cancelPoly();
|
||||
return;
|
||||
}
|
||||
|
||||
SharedInput.VoxelCliffClick click;
|
||||
while ((click = input.voxelCliffClickQueue.poll()) != null) {
|
||||
handleClick(click);
|
||||
}
|
||||
|
||||
if (input.cancelZoneDrawing) {
|
||||
input.cancelZoneDrawing = false;
|
||||
if (placing) {
|
||||
cancelPoly();
|
||||
} else if (selectedIdx >= 0) {
|
||||
// ESC mit ausgewählter, aber nicht generierter Zone → Zone löschen
|
||||
removeZone(selectedIdx);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.deleteVoxelCliffZoneRequested) {
|
||||
input.deleteVoxelCliffZoneRequested = false;
|
||||
if (selectedIdx >= 0) {
|
||||
removeZone(selectedIdx);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.generateCliffVoxelsRequested) {
|
||||
input.generateCliffVoxelsRequested = false;
|
||||
if (selectedIdx >= 0 && selectedIdx < zones.size()) {
|
||||
generateZoneVoxels(zones.get(selectedIdx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Click-Behandlung ───────────────────────────────────────────────────────
|
||||
|
||||
private void handleClick(SharedInput.VoxelCliffClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
if (click.rightButton()) {
|
||||
if (placing) {
|
||||
if (!currX.isEmpty()) {
|
||||
currX.remove(currX.size() - 1);
|
||||
currZ.remove(currZ.size() - 1);
|
||||
}
|
||||
if (currX.isEmpty()) {
|
||||
cancelPoly();
|
||||
} else {
|
||||
updateInProgressGeo();
|
||||
}
|
||||
} else {
|
||||
deselect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
float hitX = pt.x, hitZ = pt.z;
|
||||
|
||||
if (placing) {
|
||||
// Snap-Close VOR Vertex-Snap prüfen: Klick nahe Startpunkt → Polygon schließen
|
||||
if (currX.size() >= 3) {
|
||||
float dx = hitX - currX.get(0);
|
||||
float dz = hitZ - currZ.get(0);
|
||||
if (dx * dx + dz * dz < SNAP_DIST * SNAP_DIST) {
|
||||
closePoly();
|
||||
return;
|
||||
}
|
||||
}
|
||||
float[] snapped = snapVertex(hitX, hitZ);
|
||||
hitX = snapped[0];
|
||||
hitZ = snapped[1];
|
||||
currX.add(hitX);
|
||||
currZ.add(hitZ);
|
||||
updateInProgressGeo();
|
||||
} else {
|
||||
for (int i = 0; i < zones.size(); i++) {
|
||||
PlacedVoxelCliffZone z = zones.get(i);
|
||||
if (SoundAreaState.pointInPolygon(hitX, hitZ, z.pointsX(), z.pointsZ())) {
|
||||
selectZone(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Nur eine Zone gleichzeitig erlaubt — neue Zone erst nach Löschen/Generieren
|
||||
if (!zones.isEmpty()) return;
|
||||
deselect();
|
||||
placing = true;
|
||||
currX.clear();
|
||||
currZ.clear();
|
||||
currX.add(hitX);
|
||||
currZ.add(hitZ);
|
||||
updateInProgressGeo();
|
||||
}
|
||||
}
|
||||
|
||||
private float[] snapVertex(float x, float z) {
|
||||
float bestDist2 = SNAP_DIST * SNAP_DIST;
|
||||
float bx = x, bz = z;
|
||||
for (PlacedVoxelCliffZone zone : zones) {
|
||||
for (int i = 0; i < zone.pointsX().length; i++) {
|
||||
float dx = x - zone.pointsX()[i];
|
||||
float dz = z - zone.pointsZ()[i];
|
||||
float d2 = dx * dx + dz * dz;
|
||||
if (d2 < bestDist2) { bestDist2 = d2; bx = zone.pointsX()[i]; bz = zone.pointsZ()[i]; }
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < currX.size(); i++) {
|
||||
float dx = x - currX.get(i);
|
||||
float dz = z - currZ.get(i);
|
||||
float d2 = dx * dx + dz * dz;
|
||||
if (d2 < bestDist2) { bestDist2 = d2; bx = currX.get(i); bz = currZ.get(i); }
|
||||
}
|
||||
return new float[]{bx, bz};
|
||||
}
|
||||
|
||||
private void closePoly() {
|
||||
if (currX.size() < 3) {
|
||||
cancelPoly();
|
||||
return;
|
||||
}
|
||||
float[] xs = toArray(currX);
|
||||
float[] zs = toArray(currZ);
|
||||
PlacedVoxelCliffZone zone = new PlacedVoxelCliffZone(
|
||||
xs, zs, 1f, 60f, 0.025f, 4, 0.5f, 15f, 42);
|
||||
addZone(zone);
|
||||
selectZone(zones.size() - 1);
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
private void cancelPoly() {
|
||||
placing = false;
|
||||
currX.clear();
|
||||
currZ.clear();
|
||||
if (inProgGeo != null) { rootNode.detachChild(inProgGeo); inProgGeo = null; }
|
||||
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||
}
|
||||
|
||||
// ── In-Progress-Visualisierung ────────────────────────────────────────────
|
||||
|
||||
private void updateInProgressGeo() {
|
||||
if (inProgGeo != null) rootNode.detachChild(inProgGeo);
|
||||
if (currX.isEmpty()) { inProgGeo = null; updateLastPointMarker(); return; }
|
||||
inProgGeo = buildLineGeo("cliff_inprog", currX, currZ, COLOR_INPROG, Mesh.Mode.LineStrip);
|
||||
rootNode.attachChild(inProgGeo);
|
||||
updateLastPointMarker();
|
||||
}
|
||||
|
||||
private void updateLastPointMarker() {
|
||||
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||
if (currX.isEmpty()) return;
|
||||
|
||||
float x = currX.get(currX.size() - 1);
|
||||
float z = currZ.get(currZ.size() - 1);
|
||||
float y = (terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f) + LINE_OFFSET_Y + 0.05f;
|
||||
float s = 1.5f;
|
||||
|
||||
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||
buf.put(x - s).put(y).put(z - s);
|
||||
buf.put(x + s).put(y).put(z + s);
|
||||
buf.put(x - s).put(y).put(z + s);
|
||||
buf.put(x + s).put(y).put(z - s);
|
||||
buf.flip();
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setMode(Mesh.Mode.Lines);
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, buf);
|
||||
mesh.updateBound();
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(1f, 0.3f, 0.1f, 1f));
|
||||
mat.getAdditionalRenderState().setLineWidth(3f);
|
||||
|
||||
lastPointMarker = new Geometry("cliff_lastpoint", mesh);
|
||||
lastPointMarker.setMaterial(mat);
|
||||
rootNode.attachChild(lastPointMarker);
|
||||
}
|
||||
|
||||
// ── Selektion ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectZone(int idx) {
|
||||
if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) {
|
||||
zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
}
|
||||
selectedIdx = idx;
|
||||
zoneGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
|
||||
input.selectedCliffZoneIdx = idx;
|
||||
input.cliffZoneSelectionChanged = true;
|
||||
}
|
||||
|
||||
private void deselect() {
|
||||
if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) {
|
||||
zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
}
|
||||
selectedIdx = -1;
|
||||
input.selectedCliffZoneIdx = -1;
|
||||
input.cliffZoneSelectionChanged = true;
|
||||
}
|
||||
|
||||
// ── Hinzufügen / Entfernen ────────────────────────────────────────────────
|
||||
|
||||
private void addZone(PlacedVoxelCliffZone zone) {
|
||||
zones.add(zone);
|
||||
List<Float> xs = toList(zone.pointsX());
|
||||
List<Float> zs = toList(zone.pointsZ());
|
||||
Geometry geo = buildLineGeo("cliff_zone_" + (zones.size() - 1),
|
||||
xs, zs, COLOR_NORMAL, Mesh.Mode.LineLoop);
|
||||
rootNode.attachChild(geo);
|
||||
zoneGeos.add(geo);
|
||||
}
|
||||
|
||||
private void removeZone(int idx) {
|
||||
rootNode.detachChild(zoneGeos.get(idx));
|
||||
zones.remove(idx);
|
||||
zoneGeos.remove(idx);
|
||||
selectedIdx = -1;
|
||||
input.selectedCliffZoneIdx = -1;
|
||||
input.cliffZoneSelectionChanged = true;
|
||||
}
|
||||
|
||||
private void clearAll() {
|
||||
for (Geometry g : zoneGeos) rootNode.detachChild(g);
|
||||
zones.clear();
|
||||
zoneGeos.clear();
|
||||
cancelPoly();
|
||||
selectedIdx = -1;
|
||||
}
|
||||
|
||||
// ── Linien-Geometrie ──────────────────────────────────────────────────────
|
||||
|
||||
private Geometry buildLineGeo(String name, List<Float> xs, List<Float> zs,
|
||||
ColorRGBA color, Mesh.Mode mode) {
|
||||
int n = xs.size();
|
||||
FloatBuffer posBuffer = BufferUtils.createFloatBuffer(n * 3);
|
||||
for (int i = 0; i < n; i++) {
|
||||
float hy = terrain != null ? terrain.getHeight(new Vector2f(xs.get(i), zs.get(i))) : 0f;
|
||||
posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i));
|
||||
}
|
||||
posBuffer.flip();
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setMode(mode);
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuffer);
|
||||
mesh.updateBound();
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setLineWidth(2.5f);
|
||||
|
||||
Geometry geo = new Geometry(name, mesh);
|
||||
geo.setMaterial(mat);
|
||||
return geo;
|
||||
}
|
||||
|
||||
// ── Voxel-Generation ─────────────────────────────────────────────────────
|
||||
|
||||
private void generateZoneVoxels(PlacedVoxelCliffZone zone) {
|
||||
VoxelEditorState ves = app.getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves == null) {
|
||||
input.cliffGenStatusMsg = "Fehler: VoxelEditorState nicht verfügbar.";
|
||||
return;
|
||||
}
|
||||
|
||||
float[] xs = zone.pointsX();
|
||||
float[] zs = zone.pointsZ();
|
||||
|
||||
float minX = xs[0], maxX = xs[0], minZ = zs[0], maxZ = zs[0];
|
||||
for (int i = 1; i < xs.length; i++) {
|
||||
if (xs[i] < minX) minX = xs[i];
|
||||
if (xs[i] > maxX) maxX = xs[i];
|
||||
if (zs[i] < minZ) minZ = zs[i];
|
||||
if (zs[i] > maxZ) maxZ = zs[i];
|
||||
}
|
||||
|
||||
// Alle Höhen- und Rauigkeits-Parameter kommen aus SharedInput (live aus dem Panel)
|
||||
float maxHeight = input.cliffMaxHeight;
|
||||
float noiseScale = input.cliffNoiseScale;
|
||||
float edgeBlend = input.cliffEdgeBlend;
|
||||
float roughAmp = input.cliffRoughnessAmp;
|
||||
float roughScale = input.cliffRoughnessScale;
|
||||
|
||||
int count = 0;
|
||||
for (int xi = (int) Math.floor(minX); xi <= (int) Math.ceil(maxX); xi++) {
|
||||
for (int zi = (int) Math.floor(minZ); zi <= (int) Math.ceil(maxZ); zi++) {
|
||||
float wx = xi, wz = zi;
|
||||
if (!SoundAreaState.pointInPolygon(wx, wz, xs, zs)) continue;
|
||||
|
||||
float th = terrainH(wx, wz);
|
||||
|
||||
float[] near = nearestEdgePoint(wx, wz, xs, zs);
|
||||
float edgeDist = near[0];
|
||||
float boundaryTh = terrainH(near[1], near[2]);
|
||||
float baseH = Math.max(boundaryTh, th);
|
||||
|
||||
// Organische Übergangslinie: niederfrequenter Noise verschiebt die
|
||||
// effektive Kantendistanz → keine geometrisch geraden Linien im Tal
|
||||
float lineNoise = fBm(wx * 0.02f, wz * 0.02f, 3, 0.5f, zone.seed() + 54321);
|
||||
float effectiveDist = edgeDist + lineNoise * edgeBlend * 0.35f;
|
||||
|
||||
float edgeFactor = smoothstep(0f, edgeBlend, effectiveDist);
|
||||
float noise2d = fBm(wx * noiseScale, wz * noiseScale,
|
||||
zone.octaves(), zone.persistence(), zone.seed());
|
||||
float fromY = th - 14f;
|
||||
float topY = baseH + noise2d * maxHeight * edgeFactor;
|
||||
|
||||
// Rauigkeit zur Kante hin ausblenden → Talbereich bleibt ruhiger
|
||||
float effRough = roughAmp * edgeFactor;
|
||||
|
||||
if (topY <= fromY) continue;
|
||||
ves.fillColumnRange3D(wx, wz, fromY, topY,
|
||||
effRough, roughScale,
|
||||
zone.octaves(), zone.persistence(), zone.seed() + 7919);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Voxel-Kliff generiert: {} Spalten in Zone {}.", count, selectedIdx);
|
||||
|
||||
// Zone nach der Generierung entfernen (Outline verschwindet, Tool wechselt zu Voxel)
|
||||
int genIdx = selectedIdx;
|
||||
removeZone(genIdx);
|
||||
|
||||
input.cliffGenStatusMsg = "Generiert: " + count + " Spalten.";
|
||||
input.cliffGenComplete = true; // → EditorApp wechselt zu LAYER_VOXEL
|
||||
}
|
||||
|
||||
private float terrainH(float worldX, float worldZ) {
|
||||
if (terrainEditorState != null) {
|
||||
return terrainEditorState.getTerrainHeightFast(worldX, worldZ);
|
||||
}
|
||||
if (terrain != null) {
|
||||
Float h = terrain.getHeight(new Vector2f(worldX, worldZ));
|
||||
return h != null ? h : 0f;
|
||||
}
|
||||
return 0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Least-Squares-Ebene durch die Terrain-Höhen der Polygon-Eckpunkte.
|
||||
* Gibt [a, b, c] zurück, sodass planeH(x,z) = a*x + b*z + c.
|
||||
*/
|
||||
// ── Noise (fBm + Value Noise, deterministisch) ────────────────────────────
|
||||
|
||||
private static float fBm(float x, float z, int octaves, float persistence, int seed) {
|
||||
float value = 0f, amp = 1f, freq = 1f, maxAmp = 0f;
|
||||
for (int i = 0; i < octaves; i++) {
|
||||
value += amp * valueNoise(x * freq, z * freq, seed + i * 1337);
|
||||
maxAmp += amp;
|
||||
amp *= persistence;
|
||||
freq *= 2f;
|
||||
}
|
||||
return maxAmp > 0f ? value / maxAmp : 0f;
|
||||
}
|
||||
|
||||
private static float valueNoise(float x, float z, int seed) {
|
||||
int xi = (int) Math.floor(x), zi = (int) Math.floor(z);
|
||||
float xf = x - xi, zf = z - zi;
|
||||
float ux = xf * xf * (3f - 2f * xf);
|
||||
float uz = zf * zf * (3f - 2f * zf);
|
||||
float v00 = hash2i(xi, zi, seed);
|
||||
float v10 = hash2i(xi + 1, zi, seed);
|
||||
float v01 = hash2i(xi, zi + 1, seed);
|
||||
float v11 = hash2i(xi + 1, zi + 1, seed);
|
||||
return lerp(lerp(v00, v10, ux), lerp(v01, v11, ux), uz);
|
||||
}
|
||||
|
||||
private static float hash2i(int x, int z, int seed) {
|
||||
int h = seed ^ (x * 374761393) ^ (z * 668265263);
|
||||
h = (h ^ (h >>> 13)) * 1274126177;
|
||||
h ^= h >>> 16;
|
||||
return (h & 0x7FFFFFFF) / (float) 0x7FFFFFFF;
|
||||
}
|
||||
|
||||
private static float lerp(float a, float b, float t) { return a + (b - a) * t; }
|
||||
|
||||
private static float smoothstep(float edge0, float edge1, float x) {
|
||||
if (edge1 <= edge0) return 1f;
|
||||
float t = Math.max(0f, Math.min(1f, (x - edge0) / (edge1 - edge0)));
|
||||
return t * t * (3f - 2f * t);
|
||||
}
|
||||
|
||||
private static float distToPolygonEdge(float px, float pz, float[] xs, float[] zs) {
|
||||
return nearestEdgePoint(px, pz, xs, zs)[0];
|
||||
}
|
||||
|
||||
/** Gibt [dist, nearX, nearZ] zurück — Distanz und Koordinaten des nächsten Randpunkts. */
|
||||
private static float[] nearestEdgePoint(float px, float pz, float[] xs, float[] zs) {
|
||||
float minD2 = Float.MAX_VALUE;
|
||||
float nearX = px, nearZ = pz;
|
||||
int n = xs.length;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int j = (i + 1) % n;
|
||||
float ax = xs[i], az = zs[i];
|
||||
float bx = xs[j], bz = zs[j];
|
||||
float dx = bx - ax, dz = bz - az;
|
||||
float lenSq = dx * dx + dz * dz;
|
||||
float cx, cz;
|
||||
if (lenSq < 1e-6f) {
|
||||
cx = ax; cz = az;
|
||||
} else {
|
||||
float t = Math.max(0f, Math.min(1f, ((px - ax) * dx + (pz - az) * dz) / lenSq));
|
||||
cx = ax + t * dx; cz = az + t * dz;
|
||||
}
|
||||
float ex = px - cx, ez = pz - cz;
|
||||
float d2 = ex * ex + ez * ez;
|
||||
if (d2 < minD2) { minD2 = d2; nearX = cx; nearZ = cz; }
|
||||
}
|
||||
return new float[]{(float) Math.sqrt(minD2), nearX, nearZ};
|
||||
}
|
||||
|
||||
// ── Save / Load ───────────────────────────────────────────────────────────
|
||||
|
||||
public List<PlacedVoxelCliffZone> getPlacedZones() {
|
||||
return new ArrayList<>(zones);
|
||||
}
|
||||
|
||||
public void loadZones(List<PlacedVoxelCliffZone> loaded) {
|
||||
if (rootNode == null) {
|
||||
pendingZones = new ArrayList<>(loaded);
|
||||
return;
|
||||
}
|
||||
clearAll();
|
||||
for (PlacedVoxelCliffZone z : loaded) {
|
||||
addZone(z);
|
||||
}
|
||||
if (!zones.isEmpty()) {
|
||||
selectZone(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static float[] toArray(List<Float> list) {
|
||||
float[] a = new float[list.size()];
|
||||
for (int i = 0; i < list.size(); i++) a[i] = list.get(i);
|
||||
return a;
|
||||
}
|
||||
|
||||
private static List<Float> toList(float[] arr) {
|
||||
List<Float> l = new ArrayList<>(arr.length);
|
||||
for (float f : arr) l.add(f);
|
||||
return l;
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,13 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
private Geometry brushIndicator;
|
||||
|
||||
// ── Overlay für markierte Baked-Chunks ────────────────────────────────────
|
||||
|
||||
/** Root-Node für rote Chunk-Markierungen im MODE_DELETE_BAKED. */
|
||||
private Node markedChunkOverlayRoot;
|
||||
/** key → zugehörige Overlay-Geometrie. */
|
||||
private final Map<Long, Geometry> markedOverlays = new HashMap<>();
|
||||
|
||||
// ── Basis-Terrain-Referenzebene (y = -10) ────────────────────────────────
|
||||
|
||||
/** Flache Referenzebene bei Welt-Y = -10; nur im LAYER_VOXEL sichtbar. */
|
||||
@@ -185,6 +192,11 @@ public class VoxelEditorState extends BaseAppState {
|
||||
brushIndicator = buildBrushIndicator();
|
||||
app.getRootNode().attachChild(brushIndicator);
|
||||
|
||||
// Overlay-Root für markierte Baked-Chunks
|
||||
markedChunkOverlayRoot = new Node("markedBakedChunks");
|
||||
markedChunkOverlayRoot.setCullHint(Spatial.CullHint.Always);
|
||||
app.getRootNode().attachChild(markedChunkOverlayRoot);
|
||||
|
||||
// Basis-Terrain-Referenzebene bei y = -10 + Chunk-Gitter
|
||||
basePlaneNode = new Node("voxelBasePlaneNode");
|
||||
basePlane = buildBasePlane();
|
||||
@@ -207,9 +219,10 @@ public class VoxelEditorState extends BaseAppState {
|
||||
protected void cleanup(Application app) {
|
||||
executor.shutdownNow();
|
||||
voxelRoot.removeFromParent();
|
||||
if (brushIndicator != null) brushIndicator.removeFromParent();
|
||||
if (basePlaneNode != null) basePlaneNode.removeFromParent();
|
||||
if (wireframeActive) applyWireframe(false);
|
||||
if (brushIndicator != null) brushIndicator.removeFromParent();
|
||||
if (basePlaneNode != null) basePlaneNode.removeFromParent();
|
||||
if (markedChunkOverlayRoot != null) markedChunkOverlayRoot.removeFromParent();
|
||||
if (wireframeActive) applyWireframe(false);
|
||||
nodes.clear();
|
||||
chunks.clear();
|
||||
}
|
||||
@@ -253,20 +266,44 @@ public class VoxelEditorState extends BaseAppState {
|
||||
});
|
||||
}
|
||||
|
||||
// Markierte Baked-Chunks löschen
|
||||
if (input.deleteMarkedBakedRequested) {
|
||||
input.deleteMarkedBakedRequested = false;
|
||||
Set<Long> toDelete = new HashSet<>(input.markedDeleteBakedKeys);
|
||||
input.markedDeleteBakedKeys.clear();
|
||||
clearMarkedOverlays();
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
deleteMarkedBaked(toDelete);
|
||||
} catch (Throwable e) {
|
||||
log.error("Baked-Löschen fehlgeschlagen: {}", e.getMessage(), e);
|
||||
input.bakeStatusMsg = "FEHLER: " + e.getMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Markierung leeren
|
||||
if (input.clearMarkedBakedRequested) {
|
||||
input.clearMarkedBakedRequested = false;
|
||||
input.markedDeleteBakedKeys.clear();
|
||||
clearMarkedOverlays();
|
||||
}
|
||||
|
||||
// Voxel-Texturen aktualisiert?
|
||||
if (input.voxelTexturesChanged) {
|
||||
input.voxelTexturesChanged = false;
|
||||
applyTextures(voxelMaterial);
|
||||
}
|
||||
|
||||
// Layer-Wechsel erkennen → Referenzebene und Wireframe steuern
|
||||
// Layer-Wechsel erkennen → Referenzebene steuern (kein automatischer Wireframe-Wechsel)
|
||||
boolean isVoxelLayer = input.activeLayer == SharedInput.LAYER_VOXEL;
|
||||
if (isVoxelLayer != prevLayerWasVoxel) {
|
||||
prevLayerWasVoxel = isVoxelLayer;
|
||||
onVoxelLayerChanged(isVoxelLayer);
|
||||
}
|
||||
// Wireframe-Toggle während LAYER_VOXEL (Button-Klick im Panel)
|
||||
if (isVoxelLayer && wireframeActive != input.voxelWireframeEnabled) {
|
||||
// Globaler Wireframe-Toggle (Ctrl+G): voxelWireframeEnabled wird von TerrainEditorState
|
||||
// bei wireframeRequest gesetzt und gilt ebenenunabhängig für die gesamte Szene.
|
||||
if (wireframeActive != input.voxelWireframeEnabled) {
|
||||
applyWireframe(input.voxelWireframeEnabled);
|
||||
}
|
||||
|
||||
@@ -356,6 +393,131 @@ public class VoxelEditorState extends BaseAppState {
|
||||
this.terrainQuad = terrain instanceof TerrainQuad tq ? tq : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Füllt eine Voxel-Spalte von fromWorldY bis toWorldY mit Solid-Material.
|
||||
* Muss im JME-Thread aufgerufen werden (von VoxelCliffEditorState).
|
||||
*/
|
||||
public void fillColumnRange(float worldX, float worldZ, float fromWorldY, float toWorldY) {
|
||||
if (fromWorldY > toWorldY) return;
|
||||
int cx = VoxelChunk.worldXToCx(worldX);
|
||||
int cz = VoxelChunk.worldZToCz(worldZ);
|
||||
int lx = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldXToLocal(worldX, cx)));
|
||||
int lzl = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldZToLocal(worldZ, cz)));
|
||||
int cyMin = VoxelChunk.worldYToCy(fromWorldY);
|
||||
int cyMax = VoxelChunk.worldYToCy(toWorldY);
|
||||
for (int cy = cyMin; cy <= cyMax; cy++) {
|
||||
VoxelChunk chunk = getOrCreateChunk(cx, cy, cz);
|
||||
int startLY = (cy == cyMin)
|
||||
? Math.max(0, (int)(fromWorldY - cy * (float) VoxelChunk.CELLS))
|
||||
: 0;
|
||||
int endLY = (cy == cyMax)
|
||||
? Math.min(VoxelChunk.SIZE - 1, (int)(toWorldY - cy * (float) VoxelChunk.CELLS))
|
||||
: VoxelChunk.SIZE - 1;
|
||||
for (int ly = startLY; ly <= endLY; ly++) {
|
||||
chunk.setDensity(lx, ly, lzl, (byte) 127);
|
||||
}
|
||||
chunk.dirty = true;
|
||||
long key = chunkKey(cx, cy, cz);
|
||||
if (!chunk.isEmpty() && !nodes.containsKey(key)) {
|
||||
addNodeForChunk(key, chunk);
|
||||
}
|
||||
dirtyChunksThisFrame.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Füllt eine Voxel-Spalte mit einem 3D-Dichte-Rauschfeld für Kliff-Oberflächen.
|
||||
* Anders als fillColumnRange (binär solid/air) erzeugt diese Methode einen Dichtegradienten
|
||||
* mit 3D-fBm-Verschiebung, der den 7-Pass-Gaussian-Blur beim Backen überlebt.
|
||||
* Muss im JME-Thread aufgerufen werden.
|
||||
*/
|
||||
public void fillColumnRange3D(float worldX, float worldZ,
|
||||
float fromWorldY, float topWorldY,
|
||||
float roughAmp, float roughFreq,
|
||||
int octaves, float persistence, int seed) {
|
||||
if (fromWorldY > topWorldY) return;
|
||||
int cx = VoxelChunk.worldXToCx(worldX);
|
||||
int cz = VoxelChunk.worldZToCz(worldZ);
|
||||
int lx = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldXToLocal(worldX, cx)));
|
||||
int lzl = Math.max(0, Math.min(VoxelChunk.SIZE - 1, (int) VoxelChunk.worldZToLocal(worldZ, cz)));
|
||||
|
||||
// Gradient: 127 Dichte-Einheiten über roughAmp Welt-Einheiten.
|
||||
// Damit geht die Dichte von 127 (weit unterhalb) über 0 (Oberfläche) zu -128 (weit oberhalb).
|
||||
float gradient = roughAmp > 0.5f ? 127f / roughAmp : 254f;
|
||||
|
||||
// Bereich: von Boden bis topWorldY + roughAmp (Noise kann die Fläche nach oben verschieben)
|
||||
float extendedTop = topWorldY + roughAmp;
|
||||
|
||||
int cyMin = VoxelChunk.worldYToCy(fromWorldY);
|
||||
int cyMax = VoxelChunk.worldYToCy(extendedTop);
|
||||
|
||||
for (int cy = cyMin; cy <= cyMax; cy++) {
|
||||
VoxelChunk chunk = getOrCreateChunk(cx, cy, cz);
|
||||
float chunkYBase = cy * (float) VoxelChunk.CELLS;
|
||||
int startLY = (cy == cyMin)
|
||||
? Math.max(0, (int)(fromWorldY - chunkYBase))
|
||||
: 0;
|
||||
int endLY = (cy == cyMax)
|
||||
? Math.min(VoxelChunk.SIZE - 1, (int)(extendedTop - chunkYBase))
|
||||
: VoxelChunk.SIZE - 1;
|
||||
|
||||
for (int ly = startLY; ly <= endLY; ly++) {
|
||||
float worldY = chunkYBase + ly;
|
||||
byte density;
|
||||
if (worldY <= fromWorldY) {
|
||||
// Unterhalb des Terrain-Ankers: immer solid – Iso-Fläche darf nicht unter das Terrain tauchen
|
||||
density = (byte) 127;
|
||||
} else {
|
||||
// n3d ∈ [-1, 1]: 3D-fBm-Rauschen verschiebt die Iso-Fläche lokal
|
||||
float n3d = noise3DfBm(worldX * roughFreq, worldY * roughFreq, worldZ * roughFreq,
|
||||
octaves, persistence, seed);
|
||||
float d = (topWorldY - worldY) * gradient + n3d * roughAmp * gradient;
|
||||
density = (byte) Math.max(-128, Math.min(127, (int) d));
|
||||
}
|
||||
chunk.setDensity(lx, ly, lzl, density);
|
||||
}
|
||||
chunk.dirty = true;
|
||||
long key = chunkKey(cx, cy, cz);
|
||||
if (!chunk.isEmpty() && !nodes.containsKey(key)) {
|
||||
addNodeForChunk(key, chunk);
|
||||
}
|
||||
dirtyChunksThisFrame.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
private static float noise3DfBm(float x, float y, float z, int octaves, float persistence, int seed) {
|
||||
float value = 0f, amp = 1f, freq = 1f, maxAmp = 0f;
|
||||
for (int i = 0; i < octaves; i++) {
|
||||
value += amp * noise3DValue(x * freq, y * freq, z * freq, seed + i * 1337);
|
||||
maxAmp += amp;
|
||||
amp *= persistence;
|
||||
freq *= 2f;
|
||||
}
|
||||
return maxAmp > 0f ? (value / maxAmp) * 2f - 1f : 0f;
|
||||
}
|
||||
|
||||
private static float noise3DValue(float x, float y, float z, int seed) {
|
||||
int xi = (int) Math.floor(x), yi = (int) Math.floor(y), zi = (int) Math.floor(z);
|
||||
float xf = x - xi, yf = y - yi, zf = z - zi;
|
||||
float ux = xf * xf * (3f - 2f * xf);
|
||||
float uy = yf * yf * (3f - 2f * yf);
|
||||
float uz = zf * zf * (3f - 2f * zf);
|
||||
float x0y0 = nlerp(hash3n(xi, yi, zi, seed), hash3n(xi+1, yi, zi, seed), ux);
|
||||
float x0y1 = nlerp(hash3n(xi, yi+1, zi, seed), hash3n(xi+1, yi+1, zi, seed), ux);
|
||||
float x1y0 = nlerp(hash3n(xi, yi, zi+1, seed), hash3n(xi+1, yi, zi+1, seed), ux);
|
||||
float x1y1 = nlerp(hash3n(xi, yi+1, zi+1, seed), hash3n(xi+1, yi+1, zi+1, seed), ux);
|
||||
return nlerp(nlerp(x0y0, x0y1, uy), nlerp(x1y0, x1y1, uy), uz);
|
||||
}
|
||||
|
||||
private static float nlerp(float a, float b, float t) { return a + (b - a) * t; }
|
||||
|
||||
private static float hash3n(int x, int y, int z, int seed) {
|
||||
int h = seed ^ (x * 374761393) ^ (y * 1013904223) ^ (z * 668265263);
|
||||
h = (h ^ (h >>> 13)) * 1274126177;
|
||||
h ^= h >>> 16;
|
||||
return (h & 0x7FFFFFFF) / (float) 0x7FFFFFFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert alle dirty Chunks synchron.
|
||||
* Kann von außen aufgerufen werden (z.B. beim globalen Speichern).
|
||||
@@ -464,6 +626,12 @@ public class VoxelEditorState extends BaseAppState {
|
||||
int modeIdx = input.voxelTool.mode.getSelectedIndex();
|
||||
boolean isHorizontal = input.voxelTool.horizontal;
|
||||
|
||||
// Baked-Chunk Markierungs-Modus: keine Voxel-Bearbeitung, nur Selektion
|
||||
if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_DELETE_BAKED) {
|
||||
toggleBakedChunkMark(hit.pos.x, hit.pos.z, radius, action >= 0);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isCave = (modeIdx == de.blight.editor.tool.VoxelTool.MODE_REMOVE);
|
||||
boolean isColumn = !isCave;
|
||||
boolean lower = action < 0;
|
||||
@@ -689,20 +857,16 @@ public class VoxelEditorState extends BaseAppState {
|
||||
if (chunk.getDensity(lx, ly, lz) > 0) { currentTop = ly; break; }
|
||||
}
|
||||
if (currentTop < 0) {
|
||||
if (cy == -1) {
|
||||
// Basis-Ebene: Ankerpunkt bei ly=118 (Welt-Y = -10)
|
||||
currentTop = 118;
|
||||
// Terrain-Oberfläche als Ankerpunkt
|
||||
float th = terrainH(wx, wz);
|
||||
int tCy = VoxelChunk.worldYToCy(th);
|
||||
if (cy == tCy) {
|
||||
currentTop = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
|
||||
(int)(th - cy * (float) VoxelChunk.CELLS)));
|
||||
} else {
|
||||
// Nur weiterwachsen wenn der darunterliegende Chunk bis an die
|
||||
// Grenze reicht (ly ≥ SIZE-3), sonst würde ein losgelöster Klumpen entstehen
|
||||
VoxelChunk below = chunks.get(chunkKey(cx, cy - 1, cz));
|
||||
if (below == null) continue;
|
||||
boolean belowAtBoundary = false;
|
||||
for (int bly = VoxelChunk.SIZE - 1; bly >= VoxelChunk.SIZE - 3; bly--) {
|
||||
if (below.getDensity(lx, bly, lz) > 0) { belowAtBoundary = true; break; }
|
||||
}
|
||||
if (!belowAtBoundary) continue;
|
||||
currentTop = 0;
|
||||
// Chunks ober- oder unterhalb des Terrain-Chunks ohne bestehende Voxel: überspringen.
|
||||
// Kein Foundation-Fill – das Terrain-Mesh übernimmt die visuelle Abdeckung.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
int newTop = Math.min(VoxelChunk.SIZE - 1, currentTop + colStep);
|
||||
@@ -767,6 +931,119 @@ public class VoxelEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Baked-Chunk Markierung ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Markiert (mark=true) oder hebt die Markierung (mark=false) aller gebackenen
|
||||
* Chunks auf, deren XZ-Ausdehnung den Pinselkreis schneidet.
|
||||
* Fügt/entfernt gleichzeitig das rote Overlay-Quad im JME-Szenen-Graph.
|
||||
*/
|
||||
private void toggleBakedChunkMark(float brushWX, float brushWZ, float radius, boolean mark) {
|
||||
float r2 = radius * radius;
|
||||
|
||||
int cxMin = VoxelChunk.worldXToCx(brushWX - radius);
|
||||
int cxMax = VoxelChunk.worldXToCx(brushWX + radius);
|
||||
int czMin = VoxelChunk.worldZToCz(brushWZ - radius);
|
||||
int czMax = VoxelChunk.worldZToCz(brushWZ + radius);
|
||||
|
||||
for (int cx = cxMin; cx <= cxMax; cx++) {
|
||||
for (int cz = czMin; cz <= czMax; cz++) {
|
||||
// Nächster Punkt des Chunk-AABB zum Brush-Mittelpunkt
|
||||
float chunkX0 = cx * VoxelChunk.CELLS - 2048f;
|
||||
float chunkZ0 = cz * VoxelChunk.CELLS - 2048f;
|
||||
float nearX = Math.max(chunkX0, Math.min(brushWX, chunkX0 + VoxelChunk.CELLS));
|
||||
float nearZ = Math.max(chunkZ0, Math.min(brushWZ, chunkZ0 + VoxelChunk.CELLS));
|
||||
float dx = brushWX - nearX, dz = brushWZ - nearZ;
|
||||
if (dx*dx + dz*dz > r2) continue;
|
||||
|
||||
// Alle cy-Ebenen prüfen, die ein gebackenes LOD0 haben
|
||||
for (int cy = -2; cy <= 10; cy++) {
|
||||
if (!VoxelChunkIO.bakedExists(cx, cy, cz)) continue;
|
||||
long key = chunkKey(cx, cy, cz);
|
||||
if (mark) {
|
||||
if (input.markedDeleteBakedKeys.add(key)) {
|
||||
addMarkedOverlay(key, cx, cz);
|
||||
}
|
||||
} else {
|
||||
if (input.markedDeleteBakedKeys.remove(key)) {
|
||||
removeMarkedOverlay(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addMarkedOverlay(long key, int cx, int cz) {
|
||||
if (markedOverlays.containsKey(key)) return;
|
||||
float x0 = cx * VoxelChunk.CELLS - 2048f;
|
||||
float z0 = cz * VoxelChunk.CELLS - 2048f;
|
||||
|
||||
com.jme3.scene.shape.Quad q = new com.jme3.scene.shape.Quad(VoxelChunk.CELLS, VoxelChunk.CELLS);
|
||||
Geometry geo = new Geometry("markedBaked_" + key, q);
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(1f, 0.15f, 0.15f, 0.45f));
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
geo.setMaterial(mat);
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
|
||||
// Quad liegt im XY-Raum; -90° um X rotieren → horizontale XZ-Ebene
|
||||
Quaternion rot = new Quaternion();
|
||||
rot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||
geo.setLocalRotation(rot);
|
||||
// Nach Rotation reicht der Quad von (x0, -9, z0) bis (x0+CELLS, -9, z0+CELLS)
|
||||
geo.setLocalTranslation(x0, -9f, z0 + VoxelChunk.CELLS);
|
||||
|
||||
markedChunkOverlayRoot.attachChild(geo);
|
||||
markedOverlays.put(key, geo);
|
||||
}
|
||||
|
||||
private void removeMarkedOverlay(long key) {
|
||||
Geometry geo = markedOverlays.remove(key);
|
||||
if (geo != null) geo.removeFromParent();
|
||||
}
|
||||
|
||||
private void clearMarkedOverlays() {
|
||||
for (Geometry geo : markedOverlays.values()) geo.removeFromParent();
|
||||
markedOverlays.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht die per Brush markierten gebackenen Chunks (alle LODs + Sculpt-Overlays).
|
||||
* Läuft im Hintergrund-Thread.
|
||||
*/
|
||||
private void deleteMarkedBaked(Set<Long> keys) {
|
||||
if (keys.isEmpty()) {
|
||||
input.bakeStatusMsg = "Keine Chunks markiert.";
|
||||
return;
|
||||
}
|
||||
int deleted = 0;
|
||||
for (long key : keys) {
|
||||
int cx = (int)(key & 0xFFFF); if (cx >= 0x8000) cx -= 0x10000;
|
||||
int cy = (int)((key >> 16) & 0xFFFF); if (cy >= 0x8000) cy -= 0x10000;
|
||||
int cz = (int)((key >> 32) & 0xFFFF); if (cz >= 0x8000) cz -= 0x10000;
|
||||
for (int lod = 0; lod < 3; lod++) {
|
||||
try {
|
||||
Files.deleteIfExists(VoxelChunkIO.getBakedPath(cx, cy, cz, lod));
|
||||
} catch (IOException e) {
|
||||
log.warn("Baked LOD{} löschen fehlgeschlagen ({},{},{}): {}",
|
||||
lod, cx, cy, cz, e.getMessage());
|
||||
}
|
||||
}
|
||||
if (de.blight.common.SculptedMeshIO.exists(cx, cy, cz)) {
|
||||
try { de.blight.common.SculptedMeshIO.delete(cx, cy, cz); }
|
||||
catch (Exception e) { log.warn("Sculpt-Overlay löschen fehlgeschlagen: {}", e.getMessage()); }
|
||||
}
|
||||
deleted++;
|
||||
}
|
||||
input.sculptRescanNeeded = true;
|
||||
input.bakeStatusMsg = "Gelöscht: " + deleted + " Chunk" + (deleted != 1 ? "s" : "") + ".";
|
||||
log.info("Markierte Baked-Chunks gelöscht: {}.", deleted);
|
||||
}
|
||||
|
||||
// ── Terrain-Höhe (schneller O(1)-Zugriff) ─────────────────────────────────
|
||||
|
||||
private float terrainH(float worldX, float worldZ) {
|
||||
@@ -782,11 +1059,11 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
/**
|
||||
* Berechnet Slope-Parameter im Pinselradius.
|
||||
* Rückgabe: [highH, lowH, dirX, dirZ, projHigh, projLow]
|
||||
* dirX/Z = Einheitsvektor vom Tiefpunkt zum Hochpunkt
|
||||
* Rückgabe: [highH, lowH, dirX, dirZ, projHigh, projLow, avgH]
|
||||
* dirX/Z = Einheitsvektor vom Tiefpunkt zum Hochpunkt
|
||||
* projHigh = Projektion des Hochpunkts auf die Achse (relativ zur Brush-Mitte)
|
||||
* projLow = Projektion des Tiefpunkts auf die Achse (immer ≤ projHigh)
|
||||
* projHigh - projLow = |maxPos - minPos|, immer ≥ 0.
|
||||
* avgH = Durchschnittshöhe aller Spalten im Pinselbereich (für Smooth-Modus)
|
||||
*/
|
||||
private float[] computeSlopeParams(float brushWX, float brushWZ, float radius) {
|
||||
float r2 = radius * radius;
|
||||
@@ -795,6 +1072,8 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
float maxH = Float.NEGATIVE_INFINITY, minH = Float.POSITIVE_INFINITY;
|
||||
float maxX = brushWX, maxZ = brushWZ, minX = brushWX, minZ = brushWZ;
|
||||
float sum = 0f;
|
||||
int count = 0;
|
||||
|
||||
for (int xi = xMin; xi <= xMax; xi++) {
|
||||
float dx = xi - brushWX;
|
||||
@@ -804,10 +1083,12 @@ public class VoxelEditorState extends BaseAppState {
|
||||
float h = columnTopWorldY(xi, zi);
|
||||
if (h > maxH) { maxH = h; maxX = xi; maxZ = zi; }
|
||||
if (h < minH) { minH = h; minX = xi; minZ = zi; }
|
||||
sum += h;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxH == Float.NEGATIVE_INFINITY) return new float[]{ Float.NaN, Float.NaN, 0, 0, 0, 0 };
|
||||
if (maxH == Float.NEGATIVE_INFINITY) return new float[]{ Float.NaN, Float.NaN, 0, 0, 0, 0, Float.NaN };
|
||||
|
||||
// Richtung vom tiefsten zum höchsten Punkt — garantiert projHigh - projLow = |maxPos - minPos|
|
||||
float ddx = maxX - minX, ddz = maxZ - minZ;
|
||||
@@ -816,7 +1097,8 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
float projHigh = (maxX - brushWX) * ddx + (maxZ - brushWZ) * ddz;
|
||||
float projLow = (minX - brushWX) * ddx + (minZ - brushWZ) * ddz;
|
||||
return new float[]{ maxH, minH, ddx, ddz, projHigh, projLow };
|
||||
float avgH = sum / count;
|
||||
return new float[]{ maxH, minH, ddx, ddz, projHigh, projLow, avgH };
|
||||
}
|
||||
|
||||
/** Welt-Y des höchsten Solid-Voxels an (worldX, worldZ), oder Terrain-Höhe wenn keine Voxel. */
|
||||
@@ -834,7 +1116,7 @@ public class VoxelEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
}
|
||||
return -10f; // Kein Voxel vorhanden → Basis-Niveau
|
||||
return terrainH(worldX, worldZ); // Kein Voxel → Terrain-Oberfläche als Basis
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -876,9 +1158,11 @@ public class VoxelEditorState extends BaseAppState {
|
||||
for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) {
|
||||
if (chunk.getDensity(lx, ly, lz) > 0) { currentTopLY = ly; break; }
|
||||
}
|
||||
// Terrain-Höhe als Fallback wenn noch keine Voxel in dieser Spalte
|
||||
float th = currentTopLY < 0 ? terrainH(wx, wz) : Float.NaN;
|
||||
float currentTopWY = currentTopLY >= 0
|
||||
? VoxelChunk.toWorldY(cy, currentTopLY)
|
||||
: -10f; // Keine Voxel → Basis-Niveau als Referenz
|
||||
: th;
|
||||
|
||||
float diff = targetH - currentTopWY;
|
||||
if (Math.abs(diff) < 0.5f) continue;
|
||||
@@ -888,17 +1172,16 @@ public class VoxelEditorState extends BaseAppState {
|
||||
int startLY;
|
||||
if (currentTopLY >= 0) {
|
||||
startLY = currentTopLY;
|
||||
} else if (cy == -1) {
|
||||
startLY = 118;
|
||||
} else {
|
||||
VoxelChunk below = chunks.get(chunkKey(cx, cy - 1, cz));
|
||||
if (below == null) continue;
|
||||
boolean belowAtBoundary = false;
|
||||
for (int bly = VoxelChunk.SIZE - 1; bly >= VoxelChunk.SIZE - 3; bly--) {
|
||||
if (below.getDensity(lx, bly, lz) > 0) { belowAtBoundary = true; break; }
|
||||
// Terrain-Oberfläche als Ankerpunkt
|
||||
int tCy = VoxelChunk.worldYToCy(th);
|
||||
if (cy == tCy) {
|
||||
startLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
|
||||
(int)(th - cy * (float) VoxelChunk.CELLS)));
|
||||
} else {
|
||||
// Chunk ohne bestehende Voxel und nicht der Terrain-Chunk: überspringen.
|
||||
continue;
|
||||
}
|
||||
if (!belowAtBoundary) continue;
|
||||
startLY = 0;
|
||||
}
|
||||
int newTop = Math.min(VoxelChunk.SIZE - 1, startLY + step);
|
||||
for (int ly = startLY; ly <= newTop; ly++) {
|
||||
@@ -916,25 +1199,19 @@ public class VoxelEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth-Pinsel (Linksklick): gerichteter Slope von lowH nach highH.
|
||||
* Ziel interpoliert zwischen tatsächlicher Projektion von Tief- und Hochpunkt —
|
||||
* der höchste Punkt behält genau highH, der tiefste genau lowH.
|
||||
* Smooth-Pinsel (Linksklick): bewegt alle Spalten im Pinselbereich auf die
|
||||
* Durchschnittshöhe aller Spalten zu (sp[6]).
|
||||
*
|
||||
* Spikes (Spalten über dem Durchschnitt) werden abgebaut, tiefe Stellen
|
||||
* leicht angehoben. Dadurch entsteht eine gleichmäßig geebnte Fläche,
|
||||
* ohne dass neues Material an Stellen aufgebaut wird, die bereits flach sind.
|
||||
*/
|
||||
private void applySmoothColumn(VoxelChunk chunk, int cx, int cy, int cz,
|
||||
float brushWX, float brushWZ,
|
||||
float radius, float strength, float[] sp) {
|
||||
if (sp == null || Float.isNaN(sp[0])) return;
|
||||
final float highH = sp[0], lowH = sp[1], dirX = sp[2], dirZ = sp[3];
|
||||
final float projHigh = sp[4], projLow = sp[5];
|
||||
final float projRange = projHigh - projLow; // = |maxPos − minPos| ≥ 0
|
||||
if (projRange < 0.5f) return;
|
||||
|
||||
applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> {
|
||||
float proj = coord[0] * dirX + coord[1] * dirZ;
|
||||
float t = (proj - projLow) / projRange;
|
||||
t = Math.max(0f, Math.min(1f, t));
|
||||
return lowH + (highH - lowH) * t;
|
||||
});
|
||||
if (sp == null || Float.isNaN(sp[6])) return;
|
||||
final float targetH = sp[6];
|
||||
applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> targetH);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1073,6 +1350,12 @@ public class VoxelEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
input.blurIterDone = 0;
|
||||
// Bilateraler Filter: Nachbarn mit großem Dichtesprung (Klippen, Felskanten)
|
||||
// bekommen fast Null-Gewicht → scharfe Übergänge bleiben erhalten.
|
||||
// Flache Bereiche (geringe Differenz) werden voll geglättet wie bisher.
|
||||
// sigma_r = 30 → diff=20: w≈0.80 (flach, volles Blur);
|
||||
// diff=80: w≈0.04 (Klippe, kaum Blur).
|
||||
final float BILATERAL_INV_2S2 = 1f / (2f * 30f * 30f);
|
||||
for (int iter = 0; iter < 7; iter++) {
|
||||
Map<Long, float[]> nextBufs = new HashMap<>();
|
||||
for (VoxelChunk c : nonEmpty) {
|
||||
@@ -1082,17 +1365,23 @@ public class VoxelEditorState extends BaseAppState {
|
||||
for (int by = 0; by < blurN; by++) {
|
||||
for (int bz = 0; bz < blurN; bz++) {
|
||||
for (int bx = 0; bx < blurN; bx++) {
|
||||
float sum = 0f;
|
||||
float center = cur[c.idx(bx, by, bz)];
|
||||
float wSum = 0f, vSum = 0f;
|
||||
for (int dy = -1; dy <= 1; dy++)
|
||||
for (int dz = -1; dz <= 1; dz++)
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
int sx = bx+dx, sy = by+dy, sz = bz+dz;
|
||||
float nb;
|
||||
if (sx >= 0 && sx < blurN && sy >= 0 && sy < blurN && sz >= 0 && sz < blurN)
|
||||
sum += cur[c.idx(sx, sy, sz)];
|
||||
nb = cur[c.idx(sx, sy, sz)];
|
||||
else
|
||||
sum += getBlurBuf(curBufs, allOriginal, c, sx, sy, sz);
|
||||
nb = getBlurBuf(curBufs, allOriginal, c, sx, sy, sz);
|
||||
float diff = nb - center;
|
||||
float w = (float) Math.exp(-diff * diff * BILATERAL_INV_2S2);
|
||||
vSum += w * nb;
|
||||
wSum += w;
|
||||
}
|
||||
next[c.idx(bx, by, bz)] = sum / 27f;
|
||||
next[c.idx(bx, by, bz)] = vSum / wSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1251,7 +1540,7 @@ public class VoxelEditorState extends BaseAppState {
|
||||
VoxelChunk[] nb = getNeighbors(original.cx, original.cy, original.cz, blurredMap);
|
||||
|
||||
Mesh[] meshes = {
|
||||
MarchingCubes.smooth(MarchingCubes.build(blurred, 1, nb), 4, 0.4f),
|
||||
MarchingCubes.smooth(MarchingCubes.build(blurred, 1, nb), 1, 0.3f),
|
||||
MarchingCubes.smooth(MarchingCubes.build(blurred, 4, nb), 3, 0.4f),
|
||||
MarchingCubes.smooth(MarchingCubes.build(blurred, 16, nb), 2, 0.4f),
|
||||
};
|
||||
@@ -1762,7 +2051,9 @@ public class VoxelEditorState extends BaseAppState {
|
||||
basePlane.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
if (chunkGrid != null)
|
||||
chunkGrid.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
applyWireframe(entered && input.voxelWireframeEnabled);
|
||||
if (markedChunkOverlayRoot != null)
|
||||
markedChunkOverlayRoot.setCullHint(entered ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
// Wireframe-Zustand beim Layer-Wechsel NICHT ändern — wird global via Ctrl+G gesteuert.
|
||||
}
|
||||
|
||||
/** Zeigt/verbirgt die Voxel-Chunk-Nodes (wird vom SculptedMeshEditorState gesteuert). */
|
||||
|
||||
@@ -8,6 +8,8 @@ import java.util.List;
|
||||
* Modi 0-3 (Sinus/Spike/Plateau/Smooth): Säulen nach oben/unten – für Felstürme, Plateaus.
|
||||
* Modus 4 (Aushöhlen): Kugel-Entfernen.
|
||||
* Modus 5 (Zurücksetzen): Setzt Voxel im Bereich auf das Basis-Niveau y=-10 zurück.
|
||||
* Modus 6 (Baked löschen): Linksklick markiert gebackene Chunks im Pinselbereich (rotes
|
||||
* Overlay), Rechtsklick hebt Markierung auf. Ein Panel-Button löscht alle markierten Chunks.
|
||||
*
|
||||
* horizontal=false (Vertikal): Säulen entlang Y-Achse.
|
||||
* horizontal=true (Horizontal): Brush entlang der Flächennormale; bei flacher Fläche kein Effekt.
|
||||
@@ -19,16 +21,17 @@ import java.util.List;
|
||||
*/
|
||||
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;
|
||||
public static final int MODE_RESET = 5;
|
||||
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;
|
||||
public static final int MODE_RESET = 5;
|
||||
public static final int MODE_DELETE_BAKED = 6;
|
||||
|
||||
public final ChoiceToolParameter mode = new ChoiceToolParameter(
|
||||
"Modus",
|
||||
new String[]{"Sinus", "Spike", "Plateau", "Smooth", "Aushöhlen", "Zurücksetzen"},
|
||||
new String[]{"Sinus", "Spike", "Plateau", "Smooth", "Aushöhlen", "Zurücksetzen", "Baked löschen"},
|
||||
MODE_SINUS,
|
||||
new String[]{
|
||||
"img/editor/terraintool_sinus.png",
|
||||
@@ -36,6 +39,7 @@ public class VoxelTool extends EditorTool {
|
||||
"img/editor/terraintool_plateau.png",
|
||||
"img/editor/terraintool_smooth.png",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
}
|
||||
);
|
||||
|
||||
@@ -584,8 +584,9 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
List<de.blight.common.model.DialogStep> heroList = new ArrayList<>();
|
||||
for (int i = 0; i < heroStepsView.getItems().size(); i++) {
|
||||
String heroKey = stepKey(base, ".textmainchar", i);
|
||||
heroList.add(new de.blight.common.model.DialogStep(
|
||||
new TextReference(stepKey(base, ".textmainchar", i)), null));
|
||||
new TextReference(heroKey), new AudioReference(heroKey)));
|
||||
}
|
||||
opt.setHeroSteps(heroList);
|
||||
opt.setTextHero(heroList.isEmpty() ? null
|
||||
@@ -593,8 +594,9 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
List<de.blight.common.model.DialogStep> npcList = new ArrayList<>();
|
||||
for (int i = 0; i < npcStepsView.getItems().size(); i++) {
|
||||
String npcKey = stepKey(base, ".textnpc", i);
|
||||
npcList.add(new de.blight.common.model.DialogStep(
|
||||
new TextReference(stepKey(base, ".textnpc", i)), null));
|
||||
new TextReference(npcKey), new AudioReference(npcKey)));
|
||||
}
|
||||
opt.setNpcSteps(npcList);
|
||||
opt.setTextNpc(npcList.isEmpty() ? null
|
||||
@@ -708,9 +710,9 @@ public class DialogEditorView extends BorderPane {
|
||||
opt.setTextHero(new TextReference(base + ".textmainchar"));
|
||||
opt.setTextNpc(new TextReference(base + ".textnpc"));
|
||||
opt.setHeroSteps(new ArrayList<>(List.of(new de.blight.common.model.DialogStep(
|
||||
new TextReference(base + ".textmainchar"), null))));
|
||||
new TextReference(base + ".textmainchar"), new AudioReference(base + ".textmainchar")))));
|
||||
opt.setNpcSteps(new ArrayList<>(List.of(new de.blight.common.model.DialogStep(
|
||||
new TextReference(base + ".textnpc"), null))));
|
||||
new TextReference(base + ".textnpc"), new AudioReference(base + ".textnpc")))));
|
||||
allOptions.put(id, opt);
|
||||
if (asRoot) {
|
||||
rootIds.add(id);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.AudioBundle;
|
||||
import de.blight.common.model.AudioBundleIO;
|
||||
import de.blight.common.model.TextBundle;
|
||||
import de.blight.common.model.TextBundleIO;
|
||||
import de.blight.common.model.TextKeyStore;
|
||||
@@ -13,7 +15,9 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -22,12 +26,19 @@ public class LocalizationEditorView extends BorderPane {
|
||||
|
||||
private final Path locDir;
|
||||
|
||||
private final ComboBox<String> langCombo = new ComboBox<>();
|
||||
// Sprache
|
||||
private final ComboBox<String> langCombo = new ComboBox<>();
|
||||
|
||||
// Text-Tab
|
||||
private final ObservableList<String[]> tableData = FXCollections.observableArrayList();
|
||||
private final TableView<String[]> table = new TableView<>(tableData);
|
||||
|
||||
private TextBundle currentBundle = null;
|
||||
|
||||
// Audio-Tab
|
||||
private final ObservableList<String[]> audioData = FXCollections.observableArrayList();
|
||||
private final TableView<String[]> audioTable = new TableView<>(audioData);
|
||||
private AudioBundle currentAudioBundle = null;
|
||||
|
||||
public LocalizationEditorView(Path locDir) {
|
||||
this.locDir = locDir;
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
@@ -44,53 +55,64 @@ public class LocalizationEditorView extends BorderPane {
|
||||
Button addLangBtn = new Button("+ Sprache");
|
||||
addLangBtn.setOnAction(e -> addLanguage());
|
||||
|
||||
Button delLangBtn = new Button("− Sprache");
|
||||
Button delLangBtn = new Button("- Sprache");
|
||||
delLangBtn.setOnAction(e -> deleteLanguage());
|
||||
|
||||
Button addKeyBtn = new Button("+ Schlüssel");
|
||||
addKeyBtn.setOnAction(e -> addKey());
|
||||
|
||||
Button delKeyBtn = new Button("− Entfernen");
|
||||
delKeyBtn.setOnAction(e -> deleteKey());
|
||||
|
||||
Button sortBtn = new Button("A→Z");
|
||||
sortBtn.setOnAction(e -> tableData.sort(Comparator.comparing(r -> r[0])));
|
||||
|
||||
Button saveBtn = new Button("Speichern");
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrent());
|
||||
|
||||
HBox toolbar = new HBox(8, langLbl, langCombo, addLangBtn, delLangBtn,
|
||||
new Separator(javafx.geometry.Orientation.VERTICAL),
|
||||
addKeyBtn, delKeyBtn, sortBtn,
|
||||
new Separator(javafx.geometry.Orientation.VERTICAL),
|
||||
saveBtn);
|
||||
toolbar.setPadding(new Insets(8, 12, 8, 12));
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
toolbar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;");
|
||||
|
||||
Tab textTab = new Tab("Text", buildTextTabContent());
|
||||
Tab audioTab = new Tab("Audio", buildAudioTabContent());
|
||||
textTab.setClosable(false);
|
||||
audioTab.setClosable(false);
|
||||
|
||||
TabPane tabs = new TabPane(textTab, audioTab);
|
||||
tabs.setStyle("-fx-background-color: #1e1e2e;");
|
||||
|
||||
setTop(toolbar);
|
||||
setCenter(tabs);
|
||||
}
|
||||
|
||||
// Text-Tab
|
||||
|
||||
private VBox buildTextTabContent() {
|
||||
Button addKeyBtn = new Button("+ Schluessel");
|
||||
addKeyBtn.setOnAction(e -> addKey());
|
||||
|
||||
Button delKeyBtn = new Button("- Entfernen");
|
||||
delKeyBtn.setOnAction(e -> deleteKey());
|
||||
|
||||
Button sortBtn = new Button("A->Z");
|
||||
sortBtn.setOnAction(e -> tableData.sort(Comparator.comparing(r -> r[0])));
|
||||
|
||||
HBox subBar = new HBox(6, addKeyBtn, delKeyBtn, sortBtn);
|
||||
subBar.setPadding(new Insets(6, 12, 6, 12));
|
||||
subBar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;");
|
||||
|
||||
table.setEditable(true);
|
||||
table.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
@SuppressWarnings("deprecation")
|
||||
var policy = TableView.CONSTRAINED_RESIZE_POLICY;
|
||||
table.setColumnResizePolicy(policy);
|
||||
|
||||
TableColumn<String[], String> keyCol = new TableColumn<>("Schlüssel");
|
||||
TableColumn<String[], String> keyCol = new TableColumn<>("Schluessel");
|
||||
keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0]));
|
||||
keyCol.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
keyCol.setOnEditCommit(e -> {
|
||||
e.getRowValue()[0] = e.getNewValue().trim();
|
||||
table.refresh();
|
||||
});
|
||||
keyCol.setOnEditCommit(e -> { e.getRowValue()[0] = e.getNewValue().trim(); table.refresh(); });
|
||||
keyCol.setPrefWidth(280);
|
||||
|
||||
TableColumn<String[], String> valCol = new TableColumn<>("Text");
|
||||
valCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1]));
|
||||
valCol.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
valCol.setOnEditCommit(e -> {
|
||||
e.getRowValue()[1] = e.getNewValue();
|
||||
table.refresh();
|
||||
});
|
||||
valCol.setOnEditCommit(e -> { e.getRowValue()[1] = e.getNewValue(); table.refresh(); });
|
||||
|
||||
table.getColumns().addAll(List.of(keyCol, valCol));
|
||||
VBox.setVgrow(table, Priority.ALWAYS);
|
||||
@@ -98,14 +120,87 @@ public class LocalizationEditorView extends BorderPane {
|
||||
Label hint = new Label("Doppelklick zum Bearbeiten eines Eintrags.");
|
||||
hint.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-padding: 2 12 4 12;");
|
||||
|
||||
VBox center = new VBox(table, hint);
|
||||
VBox box = new VBox(subBar, table, hint);
|
||||
VBox.setVgrow(table, Priority.ALWAYS);
|
||||
|
||||
setTop(toolbar);
|
||||
setCenter(center);
|
||||
return box;
|
||||
}
|
||||
|
||||
// ── Sprach-Verwaltung ─────────────────────────────────────────────────────
|
||||
// Audio-Tab
|
||||
|
||||
private VBox buildAudioTabContent() {
|
||||
Button addKeyBtn = new Button("+ Schluessel");
|
||||
addKeyBtn.setOnAction(e -> addAudioKey());
|
||||
|
||||
Button delKeyBtn = new Button("- Entfernen");
|
||||
delKeyBtn.setOnAction(e -> deleteAudioKey());
|
||||
|
||||
Button sortBtn = new Button("A->Z");
|
||||
sortBtn.setOnAction(e -> audioData.sort(Comparator.comparing(r -> r[0])));
|
||||
|
||||
HBox subBar = new HBox(6, addKeyBtn, delKeyBtn, sortBtn);
|
||||
subBar.setPadding(new Insets(6, 12, 6, 12));
|
||||
subBar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;");
|
||||
|
||||
audioTable.setEditable(true);
|
||||
audioTable.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
@SuppressWarnings("deprecation")
|
||||
var policy2 = TableView.CONSTRAINED_RESIZE_POLICY;
|
||||
audioTable.setColumnResizePolicy(policy2);
|
||||
|
||||
TableColumn<String[], String> keyCol = new TableColumn<>("Schluessel");
|
||||
keyCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[0]));
|
||||
keyCol.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
keyCol.setOnEditCommit(e -> { e.getRowValue()[0] = e.getNewValue().trim(); audioTable.refresh(); });
|
||||
keyCol.setPrefWidth(220);
|
||||
|
||||
TableColumn<String[], String> pathCol = new TableColumn<>("Datei");
|
||||
pathCol.setCellValueFactory(cd -> new SimpleStringProperty(cd.getValue()[1]));
|
||||
pathCol.setCellFactory(TextFieldTableCell.forTableColumn());
|
||||
pathCol.setOnEditCommit(e -> { e.getRowValue()[1] = e.getNewValue(); audioTable.refresh(); });
|
||||
|
||||
TableColumn<String[], Void> browseCol = new TableColumn<>("");
|
||||
browseCol.setPrefWidth(36);
|
||||
browseCol.setMinWidth(36);
|
||||
browseCol.setMaxWidth(36);
|
||||
browseCol.setCellFactory(tc -> new TableCell<>() {
|
||||
private final Button btn = new Button("...");
|
||||
{
|
||||
btn.setStyle("-fx-padding: 2 4 2 4;");
|
||||
btn.setOnAction(ev -> {
|
||||
int idx = getIndex();
|
||||
if (idx < 0 || idx >= audioData.size()) return;
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Audio-Datei waehlen");
|
||||
fc.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("Audio", "*.ogg", "*.wav", "*.mp3"),
|
||||
new FileChooser.ExtensionFilter("Alle Dateien", "*.*")
|
||||
);
|
||||
File file = fc.showOpenDialog(getScene().getWindow());
|
||||
if (file != null) {
|
||||
audioData.get(idx)[1] = file.getAbsolutePath();
|
||||
audioTable.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
@Override
|
||||
protected void updateItem(Void v, boolean empty) {
|
||||
super.updateItem(v, empty);
|
||||
setGraphic(empty ? null : btn);
|
||||
}
|
||||
});
|
||||
|
||||
audioTable.getColumns().addAll(List.of(keyCol, pathCol, browseCol));
|
||||
VBox.setVgrow(audioTable, Priority.ALWAYS);
|
||||
|
||||
Label hint = new Label("Schluessel muss dem AudioReference-Key im Dialog entsprechen.");
|
||||
hint.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-padding: 2 12 4 12;");
|
||||
|
||||
VBox box = new VBox(subBar, audioTable, hint);
|
||||
VBox.setVgrow(audioTable, Priority.ALWAYS);
|
||||
return box;
|
||||
}
|
||||
|
||||
// Sprach-Verwaltung
|
||||
|
||||
private void refreshLangList() {
|
||||
String selected = langCombo.getValue();
|
||||
@@ -116,13 +211,12 @@ public class LocalizationEditorView extends BorderPane {
|
||||
langCombo.setValue(langCombo.getItems().get(0));
|
||||
} else {
|
||||
tableData.clear();
|
||||
audioData.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadLanguage(String lang) {
|
||||
if (lang == null) {
|
||||
return;
|
||||
}
|
||||
if (lang == null) return;
|
||||
try {
|
||||
currentBundle = TextBundleIO.load(locDir.resolve("messages_" + lang + ".properties"));
|
||||
} catch (IOException e) {
|
||||
@@ -132,17 +226,19 @@ public class LocalizationEditorView extends BorderPane {
|
||||
currentBundle.getEntries().forEach((k, v) -> tableData.add(new String[]{k, v}));
|
||||
TextRegistry.clear();
|
||||
TextRegistry.registerAll(currentBundle.getEntries());
|
||||
|
||||
currentAudioBundle = AudioBundleIO.loadOrEmpty(lang, locDir);
|
||||
audioData.clear();
|
||||
currentAudioBundle.getEntries().forEach((k, v) -> audioData.add(new String[]{k, v}));
|
||||
}
|
||||
|
||||
private void addLanguage() {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.setTitle("Sprache hinzufügen");
|
||||
dlg.setTitle("Sprache hinzufuegen");
|
||||
dlg.setHeaderText("Sprach-Code (z. B. de, en, fr):");
|
||||
dlg.showAndWait().ifPresent(lang -> {
|
||||
lang = lang.trim().toLowerCase();
|
||||
if (lang.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (lang.isBlank()) return;
|
||||
TextBundle bundle = new TextBundle(lang);
|
||||
try {
|
||||
TextBundleIO.save(bundle, locDir);
|
||||
@@ -156,17 +252,18 @@ public class LocalizationEditorView extends BorderPane {
|
||||
|
||||
private void deleteLanguage() {
|
||||
String lang = langCombo.getValue();
|
||||
if (lang == null) {
|
||||
return;
|
||||
}
|
||||
if (lang == null) return;
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
"Sprache '" + lang + "' wirklich löschen?", ButtonType.YES, ButtonType.NO);
|
||||
"Sprache '" + lang + "' wirklich loeschen?", ButtonType.YES, ButtonType.NO);
|
||||
confirm.showAndWait().ifPresent(bt -> {
|
||||
if (bt == ButtonType.YES) {
|
||||
try {
|
||||
TextBundleIO.delete(lang, locDir);
|
||||
AudioBundleIO.delete(lang, locDir);
|
||||
currentBundle = null;
|
||||
currentAudioBundle = null;
|
||||
tableData.clear();
|
||||
audioData.clear();
|
||||
refreshLangList();
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
@@ -175,30 +272,25 @@ public class LocalizationEditorView extends BorderPane {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Eintrags-Verwaltung ───────────────────────────────────────────────────
|
||||
// Text-Eintrags-Verwaltung
|
||||
|
||||
private void addKey() {
|
||||
if (currentBundle == null) {
|
||||
return;
|
||||
}
|
||||
if (currentBundle == null) return;
|
||||
Set<String> existing = currentBundle.getEntries().keySet();
|
||||
List<String> available = new ArrayList<>(TextKeyStore.getKeys());
|
||||
available.removeAll(existing);
|
||||
|
||||
if (available.isEmpty()) {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.setTitle("Schlüssel hinzufügen");
|
||||
dlg.setHeaderText("Alle bekannten Schlüssel bereits vorhanden.\nNeuen Schlüssel eingeben:");
|
||||
dlg.showAndWait()
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isBlank())
|
||||
.ifPresent(this::insertKey);
|
||||
dlg.setTitle("Schluessel hinzufuegen");
|
||||
dlg.setHeaderText("Alle bekannten Schluessel bereits vorhanden.\nNeuen Schluessel eingeben:");
|
||||
dlg.showAndWait().map(String::trim).filter(s -> !s.isBlank()).ifPresent(this::insertKey);
|
||||
return;
|
||||
}
|
||||
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Schlüssel wählen");
|
||||
dlg.setHeaderText("Noch nicht übersetzte Schlüssel:");
|
||||
dlg.setTitle("Schluessel waehlen");
|
||||
dlg.setHeaderText("Noch nicht uebersetzte Schluessel:");
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
ObservableList<String> items = FXCollections.observableArrayList(available);
|
||||
@@ -208,7 +300,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
listView.getSelectionModel().selectFirst();
|
||||
|
||||
TextField filterField = new TextField();
|
||||
filterField.setPromptText("Filtern…");
|
||||
filterField.setPromptText("Filtern...");
|
||||
filterField.textProperty().addListener((obs, o, n) ->
|
||||
filtered.setPredicate(s -> n.isBlank() || s.contains(n)));
|
||||
|
||||
@@ -216,9 +308,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
dlg.setResultConverter(bt ->
|
||||
bt == ButtonType.OK ? listView.getSelectionModel().getSelectedItem() : null);
|
||||
|
||||
dlg.showAndWait()
|
||||
.filter(k -> k != null && !k.isBlank())
|
||||
.ifPresent(this::insertKey);
|
||||
dlg.showAndWait().filter(k -> k != null && !k.isBlank()).ifPresent(this::insertKey);
|
||||
}
|
||||
|
||||
private void insertKey(String key) {
|
||||
@@ -229,27 +319,54 @@ public class LocalizationEditorView extends BorderPane {
|
||||
|
||||
private void deleteKey() {
|
||||
String[] sel = table.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) {
|
||||
tableData.remove(sel);
|
||||
}
|
||||
if (sel != null) tableData.remove(sel);
|
||||
}
|
||||
|
||||
// Audio-Eintrags-Verwaltung
|
||||
|
||||
private void addAudioKey() {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.setTitle("Audio-Schluessel hinzufuegen");
|
||||
dlg.setHeaderText("Schluessel eingeben (muss dem AudioReference-Key im Dialog entsprechen):");
|
||||
dlg.showAndWait().map(String::trim).filter(s -> !s.isBlank()).ifPresent(key -> {
|
||||
audioData.add(new String[]{key, ""});
|
||||
audioTable.scrollTo(audioData.size() - 1);
|
||||
audioTable.getSelectionModel().select(audioData.size() - 1);
|
||||
});
|
||||
}
|
||||
|
||||
private void deleteAudioKey() {
|
||||
String[] sel = audioTable.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) audioData.remove(sel);
|
||||
}
|
||||
|
||||
// Speichern
|
||||
|
||||
private void saveCurrent() {
|
||||
if (currentBundle == null) {
|
||||
new Alert(Alert.AlertType.WARNING, "Keine Sprache ausgewählt.", ButtonType.OK).showAndWait();
|
||||
new Alert(Alert.AlertType.WARNING, "Keine Sprache ausgewaehlt.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
Map<String, String> entries = new LinkedHashMap<>();
|
||||
Map<String, String> textEntries = new LinkedHashMap<>();
|
||||
for (String[] row : tableData) {
|
||||
if (!row[0].isBlank()) {
|
||||
entries.put(row[0].trim(), row[1]);
|
||||
}
|
||||
if (!row[0].isBlank()) textEntries.put(row[0].trim(), row[1]);
|
||||
}
|
||||
currentBundle.setEntries(entries);
|
||||
currentBundle.setEntries(textEntries);
|
||||
|
||||
Map<String, String> audioEntries = new LinkedHashMap<>();
|
||||
for (String[] row : audioData) {
|
||||
if (!row[0].isBlank()) audioEntries.put(row[0].trim(), row[1]);
|
||||
}
|
||||
if (currentAudioBundle == null) {
|
||||
currentAudioBundle = new AudioBundle(currentBundle.getLanguage());
|
||||
}
|
||||
currentAudioBundle.setEntries(audioEntries);
|
||||
|
||||
try {
|
||||
TextBundleIO.save(currentBundle, locDir);
|
||||
AudioBundleIO.save(currentAudioBundle, locDir);
|
||||
TextRegistry.clear();
|
||||
TextRegistry.registerAll(entries);
|
||||
TextRegistry.registerAll(textEntries);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.MonologueIO;
|
||||
import de.blight.common.model.*;
|
||||
import de.blight.common.model.quests.Quest;
|
||||
import de.blight.common.model.quests.QuestIO;
|
||||
import de.blight.editor.ProjectRoot;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Modality;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Editor für Monologe des Hauptcharakters (Selbstgespräche). */
|
||||
public class MonologueEditorView extends SplitPane {
|
||||
|
||||
private final ObservableList<Monologue> monologues = FXCollections.observableArrayList();
|
||||
private final ListView<Monologue> listView = new ListView<>(monologues);
|
||||
|
||||
// Formular-Felder
|
||||
private TextField idField;
|
||||
private Spinner<Integer> chapterSpinner;
|
||||
private CheckBox playOnceCheck;
|
||||
private ListView<String> stepsView;
|
||||
private TextField recvQuestField;
|
||||
private TextField fulfillsQuestField;
|
||||
private ListView<String> abortsQuestsView;
|
||||
|
||||
private Monologue current = null;
|
||||
private boolean loading = false;
|
||||
|
||||
public MonologueEditorView() {
|
||||
setDividerPositions(0.25);
|
||||
getItems().addAll(buildList(), buildForm());
|
||||
loadAll();
|
||||
}
|
||||
|
||||
// ── Liste ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildList() {
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Monologue m, boolean empty) {
|
||||
super.updateItem(m, empty);
|
||||
setText(empty || m == null ? null : (m.getId().isBlank() ? "(kein Name)" : m.getId()));
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty().addListener((obs, o, n) -> {
|
||||
if (!loading) loadMonologue(n);
|
||||
});
|
||||
|
||||
Button addBtn = new Button("+ Neu");
|
||||
addBtn.setOnAction(e -> createMonologue());
|
||||
|
||||
Button delBtn = new Button("- Löschen");
|
||||
delBtn.setOnAction(e -> deleteMonologue());
|
||||
|
||||
Button saveBtn = new Button("Speichern");
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveAll());
|
||||
|
||||
HBox buttons = new HBox(6, addBtn, delBtn, saveBtn);
|
||||
buttons.setPadding(new Insets(6));
|
||||
|
||||
VBox box = new VBox(buttons, listView);
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
box.setStyle("-fx-background-color: #1a1a2a;");
|
||||
box.setMinWidth(160);
|
||||
return box;
|
||||
}
|
||||
|
||||
// ── Formular ──────────────────────────────────────────────────────────────
|
||||
|
||||
private ScrollPane buildForm() {
|
||||
idField = new TextField();
|
||||
idField.setPromptText("monologue.my_intro");
|
||||
idField.textProperty().addListener((obs, o, n) -> {
|
||||
if (current != null && !loading) {
|
||||
current.setId(n.trim());
|
||||
listView.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
chapterSpinner = new Spinner<>(0, 99, 0);
|
||||
chapterSpinner.setEditable(true);
|
||||
chapterSpinner.setPrefWidth(80);
|
||||
|
||||
playOnceCheck = new CheckBox("Nur einmal abspielen");
|
||||
playOnceCheck.setSelected(true);
|
||||
|
||||
// Schritte
|
||||
stepsView = new ListView<>();
|
||||
stepsView.setPrefHeight(120);
|
||||
stepsView.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
||||
|
||||
Button addStepBtn = new Button("+ Schritt");
|
||||
addStepBtn.setOnAction(e -> {
|
||||
if (current == null) return;
|
||||
String base = "monologue." + (current.getId().isBlank() ? "id" : current.getId());
|
||||
stepsView.getItems().add(base + ".text_" + stepsView.getItems().size());
|
||||
});
|
||||
Button delStepBtn = new Button("- Entfernen");
|
||||
delStepBtn.setOnAction(e -> {
|
||||
String sel = stepsView.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) stepsView.getItems().remove(sel);
|
||||
});
|
||||
|
||||
stepsView.setEditable(true);
|
||||
stepsView.setCellFactory(javafx.scene.control.cell.TextFieldListCell.forListView());
|
||||
|
||||
HBox stepBtns = new HBox(6, addStepBtn, delStepBtn);
|
||||
|
||||
// Quest-Folgen
|
||||
recvQuestField = questField("Quest erhalten (ID)");
|
||||
fulfillsQuestField = questField("Quest erfüllen (ID)");
|
||||
|
||||
abortsQuestsView = new ListView<>();
|
||||
abortsQuestsView.setPrefHeight(80);
|
||||
abortsQuestsView.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
||||
Button addAbortBtn = new Button("+ Quest abbrechen");
|
||||
addAbortBtn.setOnAction(e -> {
|
||||
String id = showQuestPicker();
|
||||
if (id != null && !abortsQuestsView.getItems().contains(id)) abortsQuestsView.getItems().add(id);
|
||||
});
|
||||
Button delAbortBtn = new Button("- Entfernen");
|
||||
delAbortBtn.setOnAction(e -> {
|
||||
String sel = abortsQuestsView.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) abortsQuestsView.getItems().remove(sel);
|
||||
});
|
||||
HBox abortBtns = new HBox(6, addAbortBtn, delAbortBtn);
|
||||
|
||||
Label stepsLbl = sectionLabel("Schritte (Textschlüssel)");
|
||||
Label questLbl = sectionLabel("Quest-Folgen");
|
||||
|
||||
VBox form = new VBox(10);
|
||||
form.setPadding(new Insets(12));
|
||||
form.setStyle("-fx-background-color: #1e1e2e;");
|
||||
form.getChildren().addAll(
|
||||
row("Monolog-ID:", idField),
|
||||
row("Kapitel (mind.):", chapterSpinner),
|
||||
playOnceCheck,
|
||||
new Separator(),
|
||||
stepsLbl, stepsView, stepBtns,
|
||||
new Separator(),
|
||||
questLbl,
|
||||
row("Erhält Quest:", recvQuestField),
|
||||
row("Erfüllt Quest:", fulfillsQuestField),
|
||||
new Label("Bricht Quests ab:"),
|
||||
abortsQuestsView, abortBtns
|
||||
);
|
||||
|
||||
ScrollPane scroll = new ScrollPane(form);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
|
||||
return scroll;
|
||||
}
|
||||
|
||||
// ── Daten laden/speichern ─────────────────────────────────────────────────
|
||||
|
||||
private void loadAll() {
|
||||
loading = true;
|
||||
monologues.setAll(MonologueIO.load());
|
||||
loading = false;
|
||||
if (!monologues.isEmpty()) {
|
||||
listView.getSelectionModel().select(0);
|
||||
loadMonologue(monologues.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadMonologue(Monologue m) {
|
||||
current = m;
|
||||
loading = true;
|
||||
try {
|
||||
if (m == null) {
|
||||
clearForm();
|
||||
return;
|
||||
}
|
||||
idField.setText(m.getId());
|
||||
chapterSpinner.getValueFactory().setValue(m.getRequiresChapter());
|
||||
playOnceCheck.setSelected(m.isPlayOnce());
|
||||
|
||||
stepsView.getItems().clear();
|
||||
for (DialogStep step : m.getHeroSteps()) {
|
||||
String key = step.getText() != null ? step.getText().id() : "";
|
||||
stepsView.getItems().add(key);
|
||||
}
|
||||
|
||||
recvQuestField.setText(m.getRecievesQuest() != null ? m.getRecievesQuest().getQuestId() : "");
|
||||
fulfillsQuestField.setText(m.getFulfillsQuest() != null ? m.getFulfillsQuest().getQuestId() : "");
|
||||
abortsQuestsView.getItems().clear();
|
||||
if (m.getAbortsQuests() != null) {
|
||||
m.getAbortsQuests().stream()
|
||||
.map(QuestRef::getQuestId)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.forEach(abortsQuestsView.getItems()::add);
|
||||
}
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyFormToMonologue() {
|
||||
if (current == null || loading) return;
|
||||
current.setId(idField.getText().trim());
|
||||
current.setRequiresChapter(chapterSpinner.getValue());
|
||||
current.setPlayOnce(playOnceCheck.isSelected());
|
||||
|
||||
List<DialogStep> steps = new ArrayList<>();
|
||||
for (String key : stepsView.getItems()) {
|
||||
if (!key.isBlank()) {
|
||||
steps.add(new DialogStep(new TextReference(key), new AudioReference(key)));
|
||||
}
|
||||
}
|
||||
current.setHeroSteps(steps);
|
||||
|
||||
current.setRecievesQuest(questRefFromText(recvQuestField.getText()));
|
||||
current.setFulfillsQuest(questRefFromText(fulfillsQuestField.getText()));
|
||||
List<QuestRef> aborts = new ArrayList<>();
|
||||
for (String id : abortsQuestsView.getItems()) {
|
||||
QuestRef q = new QuestRef();
|
||||
q.setQuestId(id);
|
||||
aborts.add(q);
|
||||
}
|
||||
current.setAbortsQuests(aborts);
|
||||
}
|
||||
|
||||
private void saveAll() {
|
||||
applyFormToMonologue();
|
||||
try {
|
||||
MonologueIO.save(new ArrayList<>(monologues));
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
private void createMonologue() {
|
||||
applyFormToMonologue();
|
||||
Monologue m = new Monologue();
|
||||
m.setId("monologue.new_" + monologues.size());
|
||||
monologues.add(m);
|
||||
listView.getSelectionModel().select(m);
|
||||
loadMonologue(m);
|
||||
}
|
||||
|
||||
private void deleteMonologue() {
|
||||
Monologue sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
"Monolog '" + sel.getId() + "' wirklich löschen?", ButtonType.YES, ButtonType.NO);
|
||||
confirm.showAndWait().ifPresent(bt -> {
|
||||
if (bt == ButtonType.YES) {
|
||||
monologues.remove(sel);
|
||||
current = null;
|
||||
clearForm();
|
||||
if (!monologues.isEmpty()) listView.getSelectionModel().select(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
chapterSpinner.getValueFactory().setValue(0);
|
||||
playOnceCheck.setSelected(true);
|
||||
stepsView.getItems().clear();
|
||||
recvQuestField.clear();
|
||||
fulfillsQuestField.clear();
|
||||
abortsQuestsView.getItems().clear();
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private TextField questField(String prompt) {
|
||||
TextField tf = new TextField();
|
||||
tf.setPromptText(prompt);
|
||||
tf.setEditable(false);
|
||||
tf.setCursor(Cursor.HAND);
|
||||
tf.setOnMouseClicked(e -> {
|
||||
String id = showQuestPicker();
|
||||
if (id != null) tf.setText(id);
|
||||
});
|
||||
return tf;
|
||||
}
|
||||
|
||||
private String showQuestPicker() {
|
||||
Path questDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("quests");
|
||||
List<Quest> quests = QuestIO.loadAll(questDir);
|
||||
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Quest auswählen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
|
||||
ListView<Quest> chooser = new ListView<>();
|
||||
chooser.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Quest q, boolean empty) {
|
||||
super.updateItem(q, empty);
|
||||
if (empty || q == null) { setText(null); return; }
|
||||
String id = q.getQuestId() != null ? q.getQuestId() : "—";
|
||||
String name = q.getText() != null ? q.getText().id() : "";
|
||||
setText(name.isBlank() ? id : id + " — " + name);
|
||||
setStyle("-fx-text-fill: #cccccc;");
|
||||
}
|
||||
});
|
||||
chooser.getItems().setAll(quests);
|
||||
chooser.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
chooser.setPrefSize(380, 280);
|
||||
|
||||
dlg.getDialogPane().setContent(quests.isEmpty()
|
||||
? new Label("Keine Quests gefunden.") : chooser);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
dlg.getDialogPane().setStyle("-fx-background-color: #252535;");
|
||||
|
||||
Button okBtn = (Button) dlg.getDialogPane().lookupButton(ButtonType.OK);
|
||||
okBtn.setDisable(true);
|
||||
chooser.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, o, n) -> okBtn.setDisable(n == null));
|
||||
chooser.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() == 2 && !chooser.getSelectionModel().isEmpty()) okBtn.fire();
|
||||
});
|
||||
dlg.setResultConverter(bt -> {
|
||||
if (bt != ButtonType.OK) return null;
|
||||
Quest sel = chooser.getSelectionModel().getSelectedItem();
|
||||
return sel != null ? sel.getQuestId() : null;
|
||||
});
|
||||
return dlg.showAndWait().orElse(null);
|
||||
}
|
||||
|
||||
private static QuestRef questRefFromText(String text) {
|
||||
if (text == null || text.isBlank()) return null;
|
||||
QuestRef q = new QuestRef();
|
||||
q.setQuestId(text.trim());
|
||||
return q;
|
||||
}
|
||||
|
||||
private static Label sectionLabel(String text) {
|
||||
Label l = new Label(text);
|
||||
l.setStyle("-fx-font-weight: bold; -fx-text-fill: #aaa; -fx-font-size: 11;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, javafx.scene.Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(130);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.MonologueIO;
|
||||
import de.blight.common.model.Monologue;
|
||||
import de.blight.common.model.QuestRef;
|
||||
import de.blight.common.model.Status;
|
||||
import de.blight.common.model.trigger.*;
|
||||
@@ -25,10 +27,11 @@ import java.util.UUID;
|
||||
*/
|
||||
public class TriggerDialog extends Dialog<Trigger> {
|
||||
|
||||
private static final String TYPE_QUEST = "Quest starten";
|
||||
private static final String TYPE_NPC = "NPC-Status ändern";
|
||||
private static final String TYPE_FRACTION = "Fraktions-Status ändern";
|
||||
private static final String TYPE_ROUTINE = "Routine ändern";
|
||||
private static final String TYPE_QUEST = "Quest starten";
|
||||
private static final String TYPE_NPC = "NPC-Status ändern";
|
||||
private static final String TYPE_FRACTION = "Fraktions-Status ändern";
|
||||
private static final String TYPE_ROUTINE = "Routine ändern";
|
||||
private static final String TYPE_MONOLOGUE = "Monolog starten";
|
||||
|
||||
// Gemeinsam
|
||||
private final ComboBox<String> typeCombo = new ComboBox<>();
|
||||
@@ -50,6 +53,9 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
private TextField routineNpcIdField;
|
||||
private TextField routineNameField;
|
||||
|
||||
// Monolog
|
||||
private ComboBox<String> monologueIdCombo;
|
||||
|
||||
/** Öffnet den Dialog für einen neuen Trigger. */
|
||||
public TriggerDialog() {
|
||||
this(null);
|
||||
@@ -61,7 +67,7 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
setResizable(true);
|
||||
|
||||
typeCombo.getItems().addAll(TYPE_QUEST, TYPE_NPC, TYPE_FRACTION, TYPE_ROUTINE);
|
||||
typeCombo.getItems().addAll(TYPE_QUEST, TYPE_NPC, TYPE_FRACTION, TYPE_ROUTINE, TYPE_MONOLOGUE);
|
||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
typeCombo.setOnAction(e -> rebuildDynamic(typeCombo.getValue()));
|
||||
|
||||
@@ -102,10 +108,11 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
dynamicArea.getChildren().clear();
|
||||
if (type == null) return;
|
||||
switch (type) {
|
||||
case TYPE_QUEST -> buildQuestFields();
|
||||
case TYPE_NPC -> buildNpcFields();
|
||||
case TYPE_FRACTION -> buildFractionFields();
|
||||
case TYPE_ROUTINE -> buildRoutineFields();
|
||||
case TYPE_QUEST -> buildQuestFields();
|
||||
case TYPE_NPC -> buildNpcFields();
|
||||
case TYPE_FRACTION -> buildFractionFields();
|
||||
case TYPE_ROUTINE -> buildRoutineFields();
|
||||
case TYPE_MONOLOGUE -> buildMonologueFields();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +154,23 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
);
|
||||
}
|
||||
|
||||
private void buildMonologueFields() {
|
||||
monologueIdCombo = new ComboBox<>();
|
||||
monologueIdCombo.setEditable(true);
|
||||
monologueIdCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
monologueIdCombo.setPromptText("Monolog-ID eingeben oder wählen...");
|
||||
try {
|
||||
de.blight.common.MonologueIO.load().stream()
|
||||
.map(Monologue::getId)
|
||||
.filter(id -> !id.isBlank())
|
||||
.forEach(monologueIdCombo.getItems()::add);
|
||||
} catch (Exception ignored) {}
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("Monolog starten"),
|
||||
row("Monolog-ID:", monologueIdCombo)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Trigger bauen ─────────────────────────────────────────────────────────
|
||||
|
||||
private Trigger buildTrigger() {
|
||||
@@ -183,6 +207,14 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
if (routineNameField != null) r.setRoutineName(routineNameField.getText().trim());
|
||||
yield r;
|
||||
}
|
||||
case TYPE_MONOLOGUE -> {
|
||||
MonologueTrigger mo = new MonologueTrigger();
|
||||
if (monologueIdCombo != null) {
|
||||
String id = monologueIdCombo.getValue();
|
||||
if (id != null) mo.setMonologueId(id.trim());
|
||||
}
|
||||
yield mo;
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
if (t != null) t.setRequiresChapter(chapterSpinner.getValue());
|
||||
@@ -214,6 +246,10 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
routineNpcIdField.setText(r.getNpcId());
|
||||
if (routineNameField != null && r.getRoutineName() != null)
|
||||
routineNameField.setText(r.getRoutineName());
|
||||
} else if (t instanceof MonologueTrigger mo) {
|
||||
typeCombo.setValue(TYPE_MONOLOGUE);
|
||||
if (monologueIdCombo != null && mo.getMonologueId() != null)
|
||||
monologueIdCombo.setValue(mo.getMonologueId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.trigger.*;
|
||||
import de.blight.common.MonologueRegistry;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -108,6 +109,8 @@ public class TriggerListEditor extends VBox {
|
||||
if (t instanceof ChangeRoutineTrigger r)
|
||||
return "Routine ändern: " + nullSafe(r.getNpcId())
|
||||
+ " -> \"" + nullSafe(r.getRoutineName()) + "\"" + chapter;
|
||||
if (t instanceof MonologueTrigger mo)
|
||||
return "Monolog: " + nullSafe(mo.getMonologueId()) + chapter;
|
||||
return t.getClass().getSimpleName() + chapter;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user