Weitere Anpassungen bezüglich der Voxel und des Zoning Systems
This commit is contained in:
@@ -6,7 +6,7 @@ plugins {
|
||||
|
||||
javafx {
|
||||
version = '26'
|
||||
modules = ['javafx.controls', 'javafx.swing']
|
||||
modules = ['javafx.controls', 'javafx.swing', 'javafx.media']
|
||||
}
|
||||
|
||||
application {
|
||||
|
||||
@@ -180,6 +180,7 @@ public class AudioPreviewPopup {
|
||||
private void buildStage() {
|
||||
stage = new Stage(StageStyle.UTILITY);
|
||||
stage.setTitle("Audio-Vorschau");
|
||||
stage.initOwner(de.blight.editor.ui.Dialogs.primaryWindow());
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setResizable(false);
|
||||
stage.setOnCloseRequest(e -> stopClip());
|
||||
|
||||
@@ -240,6 +240,10 @@ public class EditorApp extends Application {
|
||||
// Location-Zonen-Werkzeug-Zustand
|
||||
private VBox locationZoneDynamicContent;
|
||||
|
||||
// Zonen-Werkzeug-Zustand (vereint Sound, Area, Location)
|
||||
private VBox zonenDynamicContent;
|
||||
private Runnable zonenKindStyleUpdater;
|
||||
|
||||
// Spiel-Starten-Werkzeug-Zustand
|
||||
private TextField spawnXField;
|
||||
private TextField spawnZField;
|
||||
@@ -324,9 +328,7 @@ public class EditorApp extends Application {
|
||||
private ToggleButton emitterBtn;
|
||||
private ToggleButton waterBtn;
|
||||
private ToggleButton riverBtn;
|
||||
private ToggleButton soundAreaBtn;
|
||||
private ToggleButton areaBtn;
|
||||
private ToggleButton locationZoneBtn;
|
||||
private ToggleButton zonenBtn;
|
||||
private ToggleButton playToolBtn;
|
||||
private ToggleButton voxelBtn;
|
||||
private ToggleButton voxelCliffBtn;
|
||||
@@ -350,6 +352,8 @@ public class EditorApp extends Application {
|
||||
private double editPressX, editPressY;
|
||||
private int editPressAction;
|
||||
private javafx.animation.Timeline editTimer;
|
||||
private final javafx.animation.PauseTransition worldAutoSave =
|
||||
new javafx.animation.PauseTransition(javafx.util.Duration.seconds(8));
|
||||
|
||||
// Asset-Tree-Items (müssen beim Refresh-Signal erreichbar sein)
|
||||
private TreeItem<String> assetTreeRoot;
|
||||
@@ -672,7 +676,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.areaOverlapRejected) {
|
||||
input.areaOverlapRejected = false;
|
||||
setStatus("Bereich abgelehnt: überschneidet einen bestehenden Bereich.");
|
||||
setStatus("Area abgelehnt: überschneidet eine bestehende Area.");
|
||||
}
|
||||
|
||||
if (input.locationZoneSelectionChanged) {
|
||||
@@ -680,6 +684,12 @@ public class EditorApp extends Application {
|
||||
updateLocationZonePanel(input.selectedLocationZoneInfo);
|
||||
}
|
||||
|
||||
if (input.zonenSelectionChanged) {
|
||||
input.zonenSelectionChanged = false;
|
||||
updateZonenPanel(input.zonenSelectedKind);
|
||||
if (zonenKindStyleUpdater != null) zonenKindStyleUpdater.run();
|
||||
}
|
||||
|
||||
if (input.cliffZoneSelectionChanged) {
|
||||
input.cliffZoneSelectionChanged = false;
|
||||
updateVoxelCliffPanel(input.selectedCliffZoneIdx >= 0);
|
||||
@@ -852,16 +862,12 @@ public class EditorApp extends Application {
|
||||
statusPoller.setCycleCount(javafx.animation.Timeline.INDEFINITE);
|
||||
statusPoller.play();
|
||||
|
||||
javafx.animation.Timeline autoSave = new javafx.animation.Timeline(
|
||||
new javafx.animation.KeyFrame(javafx.util.Duration.seconds(60), ev -> {
|
||||
if (!input.saveRequested) {
|
||||
input.saveRequested = true;
|
||||
setStatus("Auto-Speicherung…");
|
||||
}
|
||||
})
|
||||
);
|
||||
autoSave.setCycleCount(javafx.animation.Timeline.INDEFINITE);
|
||||
autoSave.play();
|
||||
worldAutoSave.setOnFinished(ev -> {
|
||||
if (!input.saveRequested) {
|
||||
input.saveRequested = true;
|
||||
setStatus("Auto-Speicherung…");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Modus-Wechsel ────────────────────────────────────────────────────────
|
||||
@@ -1345,6 +1351,7 @@ public class EditorApp extends Application {
|
||||
MenuItem ctEditItem = new MenuItem("Crafting-Table-Manager");
|
||||
MenuItem fractionEditItem = new MenuItem("Fraktionen-Manager");
|
||||
MenuItem locationEditItem = new MenuItem("Locations-Manager");
|
||||
MenuItem areaEditItem = new MenuItem("Areas-Manager");
|
||||
MenuItem localizationEditItem = new MenuItem("Lokalisierungs-Editor");
|
||||
vegetationsItem.setOnAction(e -> switchToVegetationGenerator());
|
||||
ezTreeItem.setOnAction(e -> switchToEzTree());
|
||||
@@ -1359,11 +1366,12 @@ public class EditorApp extends Application {
|
||||
ctEditItem.setOnAction(e -> switchToCraftingTableEditor());
|
||||
fractionEditItem.setOnAction(e -> switchToFractionEditor());
|
||||
locationEditItem.setOnAction(e -> switchToLocationEditor());
|
||||
areaEditItem.setOnAction(e -> switchToAreaEditor());
|
||||
localizationEditItem.setOnAction(e -> switchToLocalizationEditor());
|
||||
toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem,
|
||||
animPrevItem, objEditorItem, worldEditItem, charEditItem,
|
||||
questEditItem, itemEditItem, recipeEditItem, ctEditItem, fractionEditItem,
|
||||
locationEditItem, localizationEditItem);
|
||||
locationEditItem, areaEditItem, localizationEditItem);
|
||||
|
||||
Menu viewMenu = new Menu("Ansicht");
|
||||
MenuItem resetCam = new MenuItem("Kamera zurücksetzen");
|
||||
@@ -1425,9 +1433,7 @@ public class EditorApp extends Application {
|
||||
emitterBtn = new ToggleButton("🔥 Emitter");
|
||||
waterBtn = new ToggleButton("💧 Wasser");
|
||||
riverBtn = new ToggleButton("↯ Wasserfall");
|
||||
soundAreaBtn = new ToggleButton("🔊 Sound");
|
||||
areaBtn = new ToggleButton("🗺 Bereiche");
|
||||
locationZoneBtn = new ToggleButton("📍 Locations");
|
||||
zonenBtn = new ToggleButton("🗺 Zonen");
|
||||
playToolBtn = new ToggleButton("🎮 Spielen");
|
||||
voxelBtn = new ToggleButton("⬡ Voxel");
|
||||
voxelCliffBtn = new ToggleButton("⛰ Klippe");
|
||||
@@ -1442,9 +1448,7 @@ public class EditorApp extends Application {
|
||||
emitterBtn.setStyle("-fx-font-weight:bold;");
|
||||
waterBtn.setStyle("-fx-font-weight:bold;");
|
||||
riverBtn.setStyle("-fx-font-weight:bold;");
|
||||
soundAreaBtn.setStyle("-fx-font-weight:bold;");
|
||||
areaBtn.setStyle("-fx-font-weight:bold;");
|
||||
locationZoneBtn.setStyle("-fx-font-weight:bold;");
|
||||
zonenBtn.setStyle("-fx-font-weight:bold;");
|
||||
playToolBtn.setStyle("-fx-font-weight:bold;");
|
||||
voxelBtn.setStyle("-fx-font-weight:bold;");
|
||||
voxelCliffBtn.setStyle("-fx-font-weight:bold;");
|
||||
@@ -1461,9 +1465,7 @@ public class EditorApp extends Application {
|
||||
emitterBtn.setToggleGroup(layerGroup);
|
||||
waterBtn.setToggleGroup(layerGroup);
|
||||
riverBtn.setToggleGroup(layerGroup);
|
||||
soundAreaBtn.setToggleGroup(layerGroup);
|
||||
areaBtn.setToggleGroup(layerGroup);
|
||||
locationZoneBtn.setToggleGroup(layerGroup);
|
||||
zonenBtn.setToggleGroup(layerGroup);
|
||||
playToolBtn.setToggleGroup(layerGroup);
|
||||
voxelBtn.setToggleGroup(layerGroup);
|
||||
voxelCliffBtn.setToggleGroup(layerGroup);
|
||||
@@ -1523,17 +1525,9 @@ public class EditorApp extends Application {
|
||||
input.activeLayer = SharedInput.LAYER_WATERFALL;
|
||||
root.setRight(buildWaterfallPanel());
|
||||
});
|
||||
soundAreaBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_SOUND_AREAS;
|
||||
root.setRight(buildSoundAreaPanel());
|
||||
});
|
||||
areaBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_AREAS;
|
||||
root.setRight(buildAreaPanel());
|
||||
});
|
||||
locationZoneBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_LOCATION_ZONES;
|
||||
root.setRight(buildLocationZonePanel());
|
||||
zonenBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_ZONEN;
|
||||
root.setRight(buildZonenPanel());
|
||||
});
|
||||
playToolBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_PLAY_TOOL;
|
||||
@@ -1574,7 +1568,7 @@ public class EditorApp extends Application {
|
||||
new Separator(Orientation.VERTICAL), emitterBtn,
|
||||
new Separator(Orientation.VERTICAL), waterBtn,
|
||||
new Separator(Orientation.VERTICAL), riverBtn,
|
||||
new Separator(Orientation.VERTICAL), soundAreaBtn, areaBtn, locationZoneBtn,
|
||||
new Separator(Orientation.VERTICAL), zonenBtn,
|
||||
new Separator(Orientation.VERTICAL), playToolBtn,
|
||||
new Separator(Orientation.VERTICAL), voxelBtn, voxelCliffBtn,
|
||||
new Separator(Orientation.VERTICAL), stoneBtn,
|
||||
@@ -4643,9 +4637,9 @@ public class EditorApp extends Application {
|
||||
case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; root.setRight(buildLightPanel()); }
|
||||
case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; root.setRight(buildEmitterPanel()); }
|
||||
case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; root.setRight(buildWaterPanel()); }
|
||||
case "soundarea" -> { input.activeLayer = SharedInput.LAYER_SOUND_AREAS; root.setRight(buildSoundAreaPanel()); }
|
||||
case "area" -> { input.activeLayer = SharedInput.LAYER_AREAS; root.setRight(buildAreaPanel()); }
|
||||
case "locationzone" -> { input.activeLayer = SharedInput.LAYER_LOCATION_ZONES; root.setRight(buildLocationZonePanel()); }
|
||||
case "soundarea" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "sound"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); }
|
||||
case "area" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "area"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); }
|
||||
case "locationzone" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "location"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); }
|
||||
case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; root.setRight(buildWaterfallPanel()); }
|
||||
}
|
||||
},
|
||||
@@ -4707,6 +4701,7 @@ public class EditorApp extends Application {
|
||||
input.reloadPlacedOther = true;
|
||||
}
|
||||
}
|
||||
scheduleAutoSave();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7857,19 +7852,30 @@ public class EditorApp extends Application {
|
||||
} else if (bothDown) {
|
||||
stopEditTimer();
|
||||
} else if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) {
|
||||
boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|
||||
|| input.activeLayer == SharedInput.LAYER_ZONEN
|
||||
|| input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES;
|
||||
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
|
||||
// Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam)
|
||||
input.playToolClickQueue.offer(
|
||||
new SharedInput.PlayToolClick((float) e.getX(), (float) e.getY()));
|
||||
} else if (singleClickLayer) {
|
||||
submitEdit(e.getX(), e.getY(), +1);
|
||||
} else {
|
||||
editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
|
||||
submitEdit(editPressX, editPressY, editPressAction);
|
||||
startEditTimer();
|
||||
}
|
||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||
boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_ZONEN
|
||||
|| input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES;
|
||||
editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1;
|
||||
submitEdit(editPressX, editPressY, editPressAction);
|
||||
startEditTimer();
|
||||
if (!singleClickLayer) startEditTimer();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7969,6 +7975,8 @@ public class EditorApp extends Application {
|
||||
if (editTimer != null) { editTimer.stop(); editTimer = null; }
|
||||
}
|
||||
|
||||
private void scheduleAutoSave() { worldAutoSave.playFromStart(); }
|
||||
|
||||
private void submitEdit(double x, double y, int action) {
|
||||
switch (input.activeLayer) {
|
||||
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
|
||||
@@ -7992,6 +8000,11 @@ 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_ZONEN -> {
|
||||
input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0));
|
||||
input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0));
|
||||
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 -> {
|
||||
@@ -8008,6 +8021,7 @@ public class EditorApp extends Application {
|
||||
new SharedInput.ModelInteractableClick((float) x, (float) y));
|
||||
}
|
||||
}
|
||||
scheduleAutoSave();
|
||||
}
|
||||
|
||||
// ── Statusleiste ─────────────────────────────────────────────────────────
|
||||
@@ -8328,6 +8342,7 @@ public class EditorApp extends Application {
|
||||
if (pressed && (input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_AREAS
|
||||
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|
||||
|| input.activeLayer == SharedInput.LAYER_ZONEN
|
||||
|| input.activeLayer == SharedInput.LAYER_WATER
|
||||
|| input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF))
|
||||
input.cancelZoneDrawing = true;
|
||||
@@ -8337,12 +8352,17 @@ public class EditorApp extends Application {
|
||||
if (input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)
|
||||
input.deleteSelectedRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS)
|
||||
input.deleteSoundAreaRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_AREAS)
|
||||
input.deleteAreaRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES)
|
||||
input.deleteLocationZoneRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
|
||||
input.deleteSoundAreaRequested = true; scheduleAutoSave();
|
||||
} else if (input.activeLayer == SharedInput.LAYER_AREAS) {
|
||||
input.deleteAreaRequested = true; scheduleAutoSave();
|
||||
} else if (input.activeLayer == SharedInput.LAYER_LOCATION_ZONES) {
|
||||
input.deleteLocationZoneRequested = true; scheduleAutoSave();
|
||||
} else if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
if ("sound".equals(input.zonenSelectedKind)) { input.deleteSoundAreaRequested = true; scheduleAutoSave(); }
|
||||
else if ("area".equals(input.zonenSelectedKind)) { input.deleteAreaRequested = true; scheduleAutoSave(); }
|
||||
else if ("location".equals(input.zonenSelectedKind)) { input.deleteLocationZoneRequested = true; scheduleAutoSave(); }
|
||||
}
|
||||
else if (input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF)
|
||||
input.deleteVoxelCliffZoneRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_WATER)
|
||||
@@ -8509,6 +8529,115 @@ public class EditorApp extends Application {
|
||||
if (voxelCliffGenerateBtn != null) voxelCliffGenerateBtn.setDisable(false);
|
||||
}
|
||||
|
||||
// ── Zonen-Panel (vereint Sound, Area, Location) ───────────────────────────
|
||||
|
||||
private VBox buildZonenPanel() {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
|
||||
final String STYLE_AREA_ON = "-fx-background-color: #e05050; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
|
||||
final String STYLE_AREA_OFF = "-fx-background-color: #b03030; -fx-text-fill: #ddd; -fx-font-weight: bold;";
|
||||
final String STYLE_SOUND_ON = "-fx-background-color: #e07818; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
|
||||
final String STYLE_SOUND_OFF = "-fx-background-color: #a05010; -fx-text-fill: #ddd; -fx-font-weight: bold;";
|
||||
final String STYLE_LOC_ON = "-fx-background-color: #c8a800; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
|
||||
final String STYLE_LOC_OFF = "-fx-background-color: #8a7200; -fx-text-fill: #ddd; -fx-font-weight: bold;";
|
||||
|
||||
Button areaKindBtn = new Button("+ Neue Area zeichnen");
|
||||
Button soundKindBtn = new Button("+ Neuen Sound zeichnen");
|
||||
Button locationKindBtn = new Button("+ Neue Location zeichnen");
|
||||
areaKindBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
soundKindBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
locationKindBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
Runnable updateKindStyles = () -> {
|
||||
String k = input.zonenNewKind; // null = kein Zeichenmodus aktiv
|
||||
areaKindBtn.setStyle("area".equals(k) ? STYLE_AREA_ON : STYLE_AREA_OFF);
|
||||
soundKindBtn.setStyle("sound".equals(k) ? STYLE_SOUND_ON : STYLE_SOUND_OFF);
|
||||
locationKindBtn.setStyle("location".equals(k) ? STYLE_LOC_ON : STYLE_LOC_OFF);
|
||||
};
|
||||
updateKindStyles.run();
|
||||
zonenKindStyleUpdater = updateKindStyles;
|
||||
|
||||
areaKindBtn.setOnAction(e -> {
|
||||
input.zonenNewKind = "area";
|
||||
updateKindStyles.run();
|
||||
});
|
||||
soundKindBtn.setOnAction(e -> {
|
||||
input.zonenNewKind = "sound";
|
||||
updateKindStyles.run();
|
||||
});
|
||||
locationKindBtn.setOnAction(e -> {
|
||||
input.zonenNewKind = "location";
|
||||
updateKindStyles.run();
|
||||
});
|
||||
|
||||
areaKindBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||
"Area: Musik-Zone mit Tag-, Nacht- und Kampf-Track.\nAreas dürfen sich nicht überschneiden."));
|
||||
soundKindBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||
"Sound: Ambient-Soundbereich mit wählbarer Audio-Datei,\nLautstärke und Loop-Crossfade."));
|
||||
locationKindBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||
"Location: Gameplay-Trigger-Zone, z.B. zum Starten\nvon Quests oder Dialogen beim Betreten."));
|
||||
|
||||
inner.getChildren().addAll(
|
||||
sectionTitle("Neue Zone"),
|
||||
areaKindBtn, soundKindBtn, locationKindBtn,
|
||||
new Separator(),
|
||||
styledHint("L-Klick → Polygon-Punkte setzen"),
|
||||
styledHint("R-Klick (beim Zeichnen) → letzten Punkt rückgängig"),
|
||||
styledHint("R-Klick (sonst) → Auswahl aufheben"),
|
||||
styledHint("ESC → Zeichnen abbrechen"),
|
||||
styledHint("Entf → Gewählte Zone löschen"),
|
||||
new Separator(),
|
||||
sectionTitle("Gewählte Zone"),
|
||||
new Separator());
|
||||
|
||||
zonenDynamicContent = new VBox(6);
|
||||
Label noSel = new Label("Keine Zone ausgewählt");
|
||||
noSel.setStyle("-fx-text-fill: #888;");
|
||||
zonenDynamicContent.getChildren().add(noSel);
|
||||
inner.getChildren().add(zonenDynamicContent);
|
||||
|
||||
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 updateZonenPanel(String kind) {
|
||||
if (zonenDynamicContent == null) return;
|
||||
zonenDynamicContent.getChildren().clear();
|
||||
switch (kind != null ? kind : "") {
|
||||
case "area" -> {
|
||||
VBox saved = areaDynamicContent;
|
||||
areaDynamicContent = zonenDynamicContent;
|
||||
updateAreaPanel(input.selectedAreaInfo);
|
||||
areaDynamicContent = saved;
|
||||
}
|
||||
case "sound" -> {
|
||||
VBox saved = soundAreaDynamicContent;
|
||||
soundAreaDynamicContent = zonenDynamicContent;
|
||||
updateSoundAreaPanel(input.selectedSoundAreaInfo);
|
||||
soundAreaDynamicContent = saved;
|
||||
}
|
||||
case "location" -> {
|
||||
VBox saved = locationZoneDynamicContent;
|
||||
locationZoneDynamicContent = zonenDynamicContent;
|
||||
updateLocationZonePanel(input.selectedLocationZoneInfo);
|
||||
locationZoneDynamicContent = saved;
|
||||
}
|
||||
default -> {
|
||||
Label noSel = new Label("Keine Zone ausgewählt");
|
||||
noSel.setStyle("-fx-text-fill: #888;");
|
||||
zonenDynamicContent.getChildren().add(noSel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private VBox buildSoundAreaPanel() {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
@@ -8575,27 +8704,17 @@ public class EditorApp extends Application {
|
||||
soundLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
|
||||
soundLabel.setWrapText(true);
|
||||
|
||||
Button browseBtn = new Button("📂 Datei wählen…");
|
||||
Button browseBtn = new Button("🔊 Sound wählen…");
|
||||
browseBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
browseBtn.setOnAction(e -> {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Sound-Datei wählen");
|
||||
fc.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg", "*.wav", "*.mp3"));
|
||||
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
if (java.nio.file.Files.isDirectory(assetRoot))
|
||||
fc.setInitialDirectory(assetRoot.toFile());
|
||||
File chosen = fc.showOpenDialog(primaryStage);
|
||||
if (chosen != null) {
|
||||
try {
|
||||
String rel = ensureOgg(chosen, assetRoot);
|
||||
soundPath[0] = rel;
|
||||
soundLabel.setText("Sound: " + rel);
|
||||
sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
|
||||
} catch (Exception ex) {
|
||||
setStatus("Fehler bei OGG-Konvertierung: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
new de.blight.editor.ui.SoundChooser(assetRoot, de.blight.editor.ui.SoundChooser.Mode.ALL)
|
||||
.showAndWait()
|
||||
.ifPresent(rel -> {
|
||||
soundPath[0] = rel;
|
||||
soundLabel.setText("Sound: " + rel);
|
||||
sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
|
||||
});
|
||||
});
|
||||
|
||||
Label volLbl = new Label("Lautstärke");
|
||||
@@ -8639,6 +8758,7 @@ public class EditorApp extends Application {
|
||||
// pendingSoundArea != null and idx matches to update only the props.
|
||||
input.pendingSoundArea.set(new de.blight.common.PlacedSoundArea(
|
||||
new float[0], new float[0], soundPath, volume, crossfade));
|
||||
scheduleAutoSave();
|
||||
}
|
||||
|
||||
// ── Musik-Bereich-Panel ───────────────────────────────────────────────────
|
||||
@@ -8647,18 +8767,18 @@ public class EditorApp extends Application {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
inner.getChildren().addAll(
|
||||
sectionTitle("Bereiche"),
|
||||
sectionTitle("Areas"),
|
||||
styledHint("L-Klick → Polygon-Punkte setzen"),
|
||||
styledHint("R-Klick → Polygon schließen / Auswahl aufheben"),
|
||||
styledHint("ESC → Zeichnen abbrechen"),
|
||||
styledHint("Entf → Bereich löschen"),
|
||||
styledHint("Bereiche dürfen sich nicht überschneiden"),
|
||||
styledHint("Entf → Area löschen"),
|
||||
styledHint("Areas dürfen sich nicht überschneiden"),
|
||||
new Separator(),
|
||||
sectionTitle("Gewählter Bereich"),
|
||||
sectionTitle("Gewählte Area"),
|
||||
new Separator());
|
||||
|
||||
areaDynamicContent = new VBox(6);
|
||||
Label noSel = new Label("Kein Bereich ausgewählt");
|
||||
Label noSel = new Label("Keine Area ausgewählt");
|
||||
noSel.setStyle("-fx-text-fill: #888;");
|
||||
areaDynamicContent.getChildren().add(noSel);
|
||||
inner.getChildren().add(areaDynamicContent);
|
||||
@@ -8679,71 +8799,80 @@ public class EditorApp extends Application {
|
||||
areaDynamicContent.getChildren().clear();
|
||||
|
||||
if (info == null) {
|
||||
Label noSel = new Label("Kein Bereich ausgewählt");
|
||||
Label noSel = new Label("Keine Area ausgewählt");
|
||||
noSel.setStyle("-fx-text-fill: #888;");
|
||||
areaDynamicContent.getChildren().add(noSel);
|
||||
return;
|
||||
}
|
||||
// Format: "idx|nameId|dayTrack|nightTrack|combatTrack"
|
||||
String[] p = info.split("\\|", -1);
|
||||
if (p.length < 5) return;
|
||||
// Format: "idx|areaId|triggersJson"
|
||||
String[] p = info.split("\\|", 3);
|
||||
if (p.length < 1) return;
|
||||
try {
|
||||
int idx = Integer.parseInt(p[0]);
|
||||
final String[] nameRef = {p[1]};
|
||||
final String[] tracks = {p[2], p[3], p[4]};
|
||||
String[] trackLabels = {"☀ Tag-Track", "🌙 Nacht-Track", "⚔ Kampf-Track"};
|
||||
String currentAreaId = p.length > 1 ? p[1] : "";
|
||||
java.util.List<de.blight.common.model.trigger.Trigger> existingTriggers =
|
||||
p.length > 2
|
||||
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
|
||||
: new java.util.ArrayList<>();
|
||||
|
||||
Runnable publish = () ->
|
||||
input.pendingArea.set(new de.blight.common.PlacedArea(
|
||||
new float[0], new float[0], nameRef[0], tracks[0], tracks[1], tracks[2]));
|
||||
// Verfügbare Area-Definitionen laden
|
||||
java.util.List<String> areaIds = new java.util.ArrayList<>();
|
||||
areaIds.add("");
|
||||
try {
|
||||
de.blight.common.AreaDefinitionIO.load().stream()
|
||||
.map(de.blight.common.AreaDefinition::id)
|
||||
.filter(s -> !s.isBlank())
|
||||
.forEach(areaIds::add);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// Name (TextReference-ID)
|
||||
Label nameLbl = new Label("Name (TextReference):");
|
||||
nameLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||
TextField nameTf = new TextField(nameRef[0]);
|
||||
nameTf.setPromptText("z.B. area.village");
|
||||
nameTf.textProperty().addListener((obs, o, n) -> { nameRef[0] = n; publish.run(); });
|
||||
areaDynamicContent.getChildren().addAll(nameLbl, nameTf, new Separator());
|
||||
Label areaLbl = new Label("Verknüpfte Area:");
|
||||
areaLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||
|
||||
for (int ti = 0; ti < 3; ti++) {
|
||||
final int tIdx = ti;
|
||||
Label lbl = new Label(trackLabels[ti] + ": "
|
||||
+ (tracks[ti].isEmpty() ? "(keine)" : tracks[ti]));
|
||||
lbl.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
|
||||
lbl.setWrapText(true);
|
||||
Label nameKeyLbl = new Label(currentAreaId.isBlank() ? "" : currentAreaId + ".name");
|
||||
nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;");
|
||||
|
||||
Button btn = new Button("📂 Wählen…");
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
btn.setOnAction(e -> {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle(trackLabels[tIdx] + " wählen");
|
||||
fc.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg", "*.wav", "*.mp3"));
|
||||
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
if (java.nio.file.Files.isDirectory(assetRoot))
|
||||
fc.setInitialDirectory(assetRoot.toFile());
|
||||
File chosen = fc.showOpenDialog(primaryStage);
|
||||
if (chosen != null) {
|
||||
try {
|
||||
Path assetRootPath = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
tracks[tIdx] = ensureOgg(chosen, assetRootPath);
|
||||
lbl.setText(trackLabels[tIdx] + ": " + tracks[tIdx]);
|
||||
publish.run();
|
||||
} catch (Exception ex) {
|
||||
setStatus("Fehler bei OGG-Konvertierung: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
// editorHolder muss vor areaCombo deklariert sein (Lambda-Forward-Ref)
|
||||
de.blight.editor.ui.TriggerListEditor[] editorHolder = {null};
|
||||
|
||||
areaDynamicContent.getChildren().addAll(lbl, btn);
|
||||
if (ti < 2) areaDynamicContent.getChildren().add(new Separator());
|
||||
}
|
||||
ComboBox<String> areaCombo = new ComboBox<>();
|
||||
areaCombo.getItems().setAll(areaIds);
|
||||
areaCombo.setValue(areaIds.contains(currentAreaId) ? currentAreaId : "");
|
||||
areaCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
areaCombo.setPromptText("Area wählen…");
|
||||
areaCombo.valueProperty().addListener((obs, o, n) -> {
|
||||
String aid = n != null ? n : "";
|
||||
nameKeyLbl.setText(aid.isBlank() ? "" : aid + ".name");
|
||||
java.util.List<de.blight.common.model.trigger.Trigger> curTriggers =
|
||||
editorHolder[0] != null
|
||||
? new java.util.ArrayList<>(editorHolder[0].getTriggers())
|
||||
: existingTriggers;
|
||||
input.pendingArea.set(new de.blight.common.PlacedArea(
|
||||
new float[0], new float[0], aid, curTriggers));
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
Label triggerLbl = new Label("Trigger:");
|
||||
triggerLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||
|
||||
editorHolder[0] = new de.blight.editor.ui.TriggerListEditor(existingTriggers, () -> {
|
||||
String aid = areaCombo.getValue() != null ? areaCombo.getValue() : currentAreaId;
|
||||
input.pendingArea.set(new de.blight.common.PlacedArea(
|
||||
new float[0], new float[0], aid,
|
||||
new java.util.ArrayList<>(editorHolder[0].getTriggers())));
|
||||
scheduleAutoSave();
|
||||
});
|
||||
|
||||
Button delBtn = new Button("🗑 Löschen");
|
||||
delBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
delBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||
delBtn.setOnAction(e -> input.deleteAreaRequested = true);
|
||||
areaDynamicContent.getChildren().addAll(new Separator(), delBtn);
|
||||
delBtn.setOnAction(e -> { input.deleteAreaRequested = true; scheduleAutoSave(); });
|
||||
|
||||
areaDynamicContent.getChildren().addAll(
|
||||
areaLbl, areaCombo, nameKeyLbl,
|
||||
new Separator(),
|
||||
triggerLbl, editorHolder[0],
|
||||
new Separator(),
|
||||
delBtn);
|
||||
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
@@ -8795,14 +8924,11 @@ public class EditorApp extends Application {
|
||||
try {
|
||||
int idx = Integer.parseInt(p[0]);
|
||||
String nameId = p.length > 1 ? p[1] : "";
|
||||
java.util.List<de.blight.common.model.trigger.Trigger> initTriggers =
|
||||
java.util.List<de.blight.common.model.trigger.Trigger> existingTriggers =
|
||||
p.length > 2
|
||||
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
|
||||
: new java.util.ArrayList<>();
|
||||
|
||||
// Holder damit triggerEditor und nameTf sich gegenseitig referenzieren können
|
||||
final de.blight.editor.ui.TriggerListEditor[] teHolder = {null};
|
||||
|
||||
// Verfügbare Locations aus LocationIO laden
|
||||
java.util.List<String> locationIds = new java.util.ArrayList<>();
|
||||
locationIds.add("");
|
||||
@@ -8815,44 +8941,30 @@ public class EditorApp extends Application {
|
||||
|
||||
Label nameLbl = new Label("Verknüpfte Location:");
|
||||
nameLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||
|
||||
Label nameKeyLbl = new Label(nameId.isBlank() ? "" : nameId + ".name");
|
||||
nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;");
|
||||
|
||||
ComboBox<String> locationCombo = new ComboBox<>();
|
||||
locationCombo.getItems().setAll(locationIds);
|
||||
locationCombo.setValue(locationIds.contains(nameId) ? nameId : "");
|
||||
locationCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
locationCombo.setPromptText("Location wählen…");
|
||||
// Fallback: Freitext falls Location nicht in Liste
|
||||
TextField nameTf = new TextField(nameId);
|
||||
nameTf.setPromptText("oder manuell: location.village");
|
||||
locationCombo.valueProperty().addListener((obs, o, n) -> {
|
||||
if (n != null && !n.isBlank()) nameTf.setText(n);
|
||||
String lid = n != null ? n : "";
|
||||
nameKeyLbl.setText(lid.isBlank() ? "" : lid + ".name");
|
||||
input.pendingLocationZone.set(new de.blight.common.PlacedLocationZone(
|
||||
new float[0], new float[0],
|
||||
n != null && !n.isBlank() ? n : nameTf.getText(),
|
||||
teHolder[0] != null ? teHolder[0].getTriggers() : initTriggers));
|
||||
new float[0], new float[0], lid, existingTriggers));
|
||||
scheduleAutoSave();
|
||||
});
|
||||
nameTf.textProperty().addListener((obs, o, n) ->
|
||||
input.pendingLocationZone.set(new de.blight.common.PlacedLocationZone(
|
||||
new float[0], new float[0], n,
|
||||
teHolder[0] != null ? teHolder[0].getTriggers() : initTriggers)));
|
||||
|
||||
Label triggerLbl = new Label("Trigger:");
|
||||
triggerLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
|
||||
de.blight.editor.ui.TriggerListEditor triggerEditor =
|
||||
new de.blight.editor.ui.TriggerListEditor(initTriggers, () ->
|
||||
input.pendingLocationZone.set(new de.blight.common.PlacedLocationZone(
|
||||
new float[0], new float[0], nameTf.getText(),
|
||||
teHolder[0] != null ? teHolder[0].getTriggers() : initTriggers)));
|
||||
teHolder[0] = triggerEditor;
|
||||
|
||||
Button delBtn = new Button("🗑 Löschen");
|
||||
delBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
delBtn.setStyle("-fx-text-fill: #c0392b;");
|
||||
delBtn.setOnAction(e -> input.deleteLocationZoneRequested = true);
|
||||
delBtn.setOnAction(e -> { input.deleteLocationZoneRequested = true; scheduleAutoSave(); });
|
||||
|
||||
locationZoneDynamicContent.getChildren().addAll(
|
||||
nameLbl, locationCombo, nameTf,
|
||||
new Separator(),
|
||||
triggerLbl, triggerEditor,
|
||||
nameLbl, locationCombo, nameKeyLbl,
|
||||
new Separator(),
|
||||
delBtn);
|
||||
|
||||
@@ -11239,6 +11351,20 @@ public class EditorApp extends Application {
|
||||
root.setRight(null);
|
||||
}
|
||||
|
||||
private void switchToAreaEditor() {
|
||||
onF5 = null;
|
||||
currentTool = "areaEditor";
|
||||
ToolBar tb = new ToolBar();
|
||||
Button backBtn = new Button("← Welteneditor");
|
||||
backBtn.setOnAction(e -> switchToWorldEditor());
|
||||
Label label = new Label("Areas-Manager");
|
||||
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label);
|
||||
topBar.getChildren().set(1, tb);
|
||||
root.setCenter(new de.blight.editor.ui.AreaEditorView());
|
||||
root.setRight(null);
|
||||
}
|
||||
|
||||
private void switchToLocalizationEditor() {
|
||||
onF5 = null;
|
||||
currentTool = "localizationEditor";
|
||||
|
||||
@@ -356,9 +356,11 @@ public class SharedInput {
|
||||
public volatile float pendingGotoPitch = Float.NaN;
|
||||
|
||||
// ── Reload-Signale nach Löschung aus MapObjectsView ──────────────────────
|
||||
public volatile boolean reloadPlacedModels = false;
|
||||
public volatile boolean reloadPlacedItems = false;
|
||||
public volatile boolean reloadPlacedOther = false; // Lichter, Emitter, Wasser, Bereiche, Zonen
|
||||
public volatile boolean reloadPlacedModels = false;
|
||||
public volatile boolean reloadPlacedItems = false;
|
||||
public volatile boolean reloadPlacedOther = false; // Lichter, Emitter, Wasser, Bereiche, Zonen
|
||||
/** Nur Lichter + Emitter neu laden (kein Zonen-Reload), gesetzt von SceneObjectState. */
|
||||
public volatile boolean reloadLightsEmitters = false;
|
||||
/** Yaw in Grad: 0° = Süden (−Z), 90° = Westen (−X), ±180° = Norden (+Z). */
|
||||
public volatile float camYaw = 0f;
|
||||
/** Pitch in Grad: positiv = Blick nach oben, negativ = nach unten. */
|
||||
@@ -546,6 +548,18 @@ public class SharedInput {
|
||||
/** JavaFX → JME: Laufendes Polygon-Zeichnen abbrechen (ESC). */
|
||||
public volatile boolean cancelZoneDrawing = false;
|
||||
|
||||
// ── Zonen-Werkzeug (vereint Sound, Area, Location) ────────────────────────
|
||||
/** activeLayer==32 → alle Zonen-Typen gleichzeitig sichtbar und editierbar */
|
||||
public static final int LAYER_ZONEN = 32;
|
||||
|
||||
/** JavaFX → JME: welcher Zonen-Typ neu gezeichnet wird ("area"|"sound"|"location"). */
|
||||
public volatile String zonenNewKind = "area";
|
||||
|
||||
/** JME → JavaFX: Typ der aktuell selektierten Zone ("area"|"sound"|"location"|null). */
|
||||
public volatile String zonenSelectedKind = null;
|
||||
/** JME → JavaFX: Selektion hat sich geändert → Zonen-Panel aktualisieren. */
|
||||
public volatile boolean zonenSelectionChanged = false;
|
||||
|
||||
// ── Spiel-Starten-Werkzeug ────────────────────────────────────────────────
|
||||
/** Klick/Drag-Ereignisse im Viewport für das Play-Tool. */
|
||||
public record PlayToolClick(float screenX, float screenY) {}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.PlacedArea;
|
||||
import de.blight.common.model.trigger.TriggerIO;
|
||||
import de.blight.editor.SharedInput;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
@@ -23,10 +24,10 @@ public class AreaState extends BaseAppState {
|
||||
|
||||
private static final float LINE_OFFSET_Y = 0.35f;
|
||||
|
||||
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.5f, 0.3f, 1f, 1f);
|
||||
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 0.9f, 0.2f, 1f);
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.8f, 0.5f, 1f, 1f);
|
||||
private static final ColorRGBA COLOR_OVERLAP = new ColorRGBA(1f, 0.2f, 0.2f, 1f);
|
||||
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.90f, 0.15f, 0.15f, 1f); // Rot
|
||||
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1.00f, 0.55f, 0.55f, 1f); // Hell-Rot
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(1.00f, 0.40f, 0.40f, 1f); // Mittel-Rot
|
||||
private static final ColorRGBA COLOR_OVERLAP = new ColorRGBA(1.00f, 0.85f, 0.00f, 1f); // Gelb (Fehler)
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
@@ -79,7 +80,7 @@ public class AreaState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (input.activeLayer != SharedInput.LAYER_AREAS) {
|
||||
if (input.activeLayer != SharedInput.LAYER_AREAS && input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||
if (placing) cancelPoly();
|
||||
return;
|
||||
}
|
||||
@@ -94,9 +95,9 @@ public class AreaState extends BaseAppState {
|
||||
applyProperty(selectedIdx, pending);
|
||||
}
|
||||
|
||||
if (input.cancelZoneDrawing) {
|
||||
if (input.cancelZoneDrawing && placing) {
|
||||
input.cancelZoneDrawing = false;
|
||||
if (placing) cancelPoly();
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
if (input.deleteAreaRequested) {
|
||||
@@ -116,16 +117,13 @@ public class AreaState extends BaseAppState {
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
if (click.rightButton()) {
|
||||
if (placing) closePoly();
|
||||
if (placing) undoLastPoint();
|
||||
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();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
float hitX = pt.x, hitZ = pt.z;
|
||||
|
||||
if (placing) {
|
||||
@@ -133,10 +131,11 @@ public class AreaState extends BaseAppState {
|
||||
hitX = snapped[0];
|
||||
hitZ = snapped[1];
|
||||
|
||||
// auto-close only when snapped exactly to own first vertex
|
||||
if (currX.size() >= 3) {
|
||||
float dx = hitX - currX.get(0);
|
||||
float dz = hitZ - currZ.get(0);
|
||||
if (dx * dx + dz * dz < SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST * 0.25f) {
|
||||
if (dx * dx + dz * dz < 0.01f) {
|
||||
closePoly();
|
||||
return;
|
||||
}
|
||||
@@ -146,19 +145,41 @@ public class AreaState extends BaseAppState {
|
||||
currZ.add(hitZ);
|
||||
updateInProgressGeo();
|
||||
} else {
|
||||
// don't select while another state is drawing
|
||||
SoundAreaState sas0 = getStateManager().getState(SoundAreaState.class);
|
||||
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
|
||||
if ((sas0 != null && sas0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
|
||||
|
||||
// pass 1: clearly inside
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedArea a = areas.get(i);
|
||||
if (SoundAreaState.pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
|
||||
selectArea(i);
|
||||
return;
|
||||
selectArea(i); return;
|
||||
}
|
||||
}
|
||||
// pass 2: within 50 cm of any edge
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedArea a = areas.get(i);
|
||||
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
|
||||
selectArea(i); return;
|
||||
}
|
||||
}
|
||||
// cross-type: if another zone type is here, don't start drawing
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||
if (sas != null && sas.findZoneAt(hitX, hitZ) >= 0) return;
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null && lzs.findZoneAt(hitX, hitZ) >= 0) return;
|
||||
}
|
||||
deselect();
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"area".equals(input.zonenNewKind)) return;
|
||||
placing = true;
|
||||
currX.clear();
|
||||
currZ.clear();
|
||||
currX.add(hitX);
|
||||
currZ.add(hitZ);
|
||||
// snap first point to nearby existing vertices
|
||||
float[] startSnap = snapVertex(hitX, hitZ);
|
||||
currX.add(startSnap[0]);
|
||||
currZ.add(startSnap[1]);
|
||||
updateInProgressGeo();
|
||||
}
|
||||
}
|
||||
@@ -195,9 +216,10 @@ public class AreaState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
PlacedArea area = new PlacedArea(xs, zs, "", "", "", "");
|
||||
PlacedArea area = new PlacedArea(xs, zs, "");
|
||||
addArea(area);
|
||||
selectArea(areas.size() - 1);
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) input.zonenNewKind = null;
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
@@ -209,6 +231,14 @@ public class AreaState extends BaseAppState {
|
||||
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||
}
|
||||
|
||||
private void undoLastPoint() {
|
||||
if (currX.isEmpty()) { cancelPoly(); return; }
|
||||
currX.remove(currX.size() - 1);
|
||||
currZ.remove(currZ.size() - 1);
|
||||
if (currX.isEmpty()) cancelPoly();
|
||||
else updateInProgressGeo();
|
||||
}
|
||||
|
||||
private void updateInProgressGeo() {
|
||||
if (inProgGeo != null) rootNode.detachChild(inProgGeo);
|
||||
int n = currX.size();
|
||||
@@ -224,7 +254,7 @@ public class AreaState extends BaseAppState {
|
||||
|
||||
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 y = getHeightAt(x, z) + LINE_OFFSET_Y + 0.05f;
|
||||
float s = 1.5f;
|
||||
|
||||
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||
@@ -251,25 +281,42 @@ public class AreaState extends BaseAppState {
|
||||
private void selectArea(int idx) {
|
||||
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
|
||||
}
|
||||
// cross-state deselect
|
||||
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||
if (sas != null) sas.deselectSilent();
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null) lzs.deselectSilent();
|
||||
selectedIdx = idx;
|
||||
areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
|
||||
areaGeos.get(idx).getMaterial().getAdditionalRenderState().setLineWidth(8f);
|
||||
publishSelection(idx);
|
||||
}
|
||||
|
||||
private void deselect() {
|
||||
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
|
||||
}
|
||||
selectedIdx = -1;
|
||||
input.selectedAreaInfo = null;
|
||||
input.areaSelectionChanged = true;
|
||||
if ("area".equals(input.zonenSelectedKind)) {
|
||||
input.zonenSelectedKind = null;
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void publishSelection(int idx) {
|
||||
PlacedArea a = areas.get(idx);
|
||||
input.selectedAreaInfo = idx + "|" + a.nameId() + "|" + a.dayTrack() + "|" + a.nightTrack() + "|" + a.combatTrack();
|
||||
String triggersJson = TriggerIO.serializeList(a.triggers());
|
||||
input.selectedAreaInfo = idx + "|" + a.areaId() + "|" + triggersJson;
|
||||
input.areaSelectionChanged = true;
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
input.zonenSelectedKind = "area";
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add / Remove / Apply ──────────────────────────────────────────────────
|
||||
@@ -303,9 +350,9 @@ public class AreaState extends BaseAppState {
|
||||
private void applyProperty(int idx, PlacedArea updated) {
|
||||
if (updated.pointsX().length == 0) {
|
||||
PlacedArea existing = areas.get(idx);
|
||||
areas.set(idx, new PlacedArea(
|
||||
existing.pointsX(), existing.pointsZ(),
|
||||
updated.nameId(), updated.dayTrack(), updated.nightTrack(), updated.combatTrack()));
|
||||
areas.set(idx, new PlacedArea(existing.pointsX(), existing.pointsZ(),
|
||||
updated.areaId(),
|
||||
updated.triggers() != null ? updated.triggers() : existing.triggers()));
|
||||
} else {
|
||||
areas.set(idx, updated);
|
||||
}
|
||||
@@ -317,7 +364,7 @@ public class AreaState extends BaseAppState {
|
||||
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;
|
||||
float hy = getHeightAt(xs.get(i), zs.get(i));
|
||||
posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i));
|
||||
}
|
||||
posBuffer.flip();
|
||||
@@ -329,13 +376,29 @@ public class AreaState extends BaseAppState {
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setLineWidth(2f);
|
||||
mat.getAdditionalRenderState().setLineWidth(4f);
|
||||
|
||||
Geometry geo = new Geometry(name, mesh);
|
||||
geo.setMaterial(mat);
|
||||
return geo;
|
||||
}
|
||||
|
||||
public boolean isPlacing() { return placing; }
|
||||
|
||||
void deselectSilent() {
|
||||
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
|
||||
}
|
||||
selectedIdx = -1;
|
||||
input.selectedAreaInfo = null;
|
||||
input.areaSelectionChanged = true;
|
||||
if ("area".equals(input.zonenSelectedKind)) {
|
||||
input.zonenSelectedKind = null;
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Overlap detection ─────────────────────────────────────────────────────
|
||||
|
||||
private boolean overlapsExistingAreas(float[] xs, float[] zs) {
|
||||
@@ -383,6 +446,15 @@ public class AreaState extends BaseAppState {
|
||||
return new ArrayList<>(areas);
|
||||
}
|
||||
|
||||
public int findZoneAt(float wx, float wz) {
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedArea a = areas.get(i);
|
||||
if (SoundAreaState.pointInPolygon(wx, wz, a.pointsX(), a.pointsZ())
|
||||
|| SoundAreaState.pointNearPolygonEdge(wx, wz, a.pointsX(), a.pointsZ(), 0.5f)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void loadAreas(List<PlacedArea> loaded) {
|
||||
if (rootNode == null) {
|
||||
pendingAreas = new ArrayList<>(loaded);
|
||||
@@ -405,4 +477,66 @@ public class AreaState extends BaseAppState {
|
||||
for (float f : arr) l.add(f);
|
||||
return l;
|
||||
}
|
||||
|
||||
private Vector3f raycastAll(Ray ray) {
|
||||
Vector3f best = null;
|
||||
float bestDistSq = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() > 0) {
|
||||
best = hits.getClosestCollision().getContactPoint();
|
||||
bestDistSq = ray.getOrigin().distanceSquared(best);
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(vp);
|
||||
if (d < bestDistSq) { best = vp; bestDistSq = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(sp);
|
||||
if (d < bestDistSq) { best = sp; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private float getHeightAt(float wx, float wz) {
|
||||
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
|
||||
float best = 0f;
|
||||
float bestDistFromTop = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
terrain.collideWith(ray, res);
|
||||
if (res.size() > 0) {
|
||||
float y = res.getClosestCollision().getContactPoint().y;
|
||||
float d = 9999f - y;
|
||||
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = 9999f - vp.y;
|
||||
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = 9999f - sp.y;
|
||||
if (d < bestDistFromTop) { best = sp.y; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,11 +119,8 @@ public class EmitterState extends BaseAppState {
|
||||
|
||||
if (click.rightButton()) { deselect(); return; }
|
||||
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
|
||||
PlacedEmitter pe = createPreset(input.emitterPreset, pt.x, pt.y, pt.z);
|
||||
addEmitter(pe);
|
||||
@@ -327,4 +324,34 @@ public class EmitterState extends BaseAppState {
|
||||
clearAll();
|
||||
for (PlacedEmitter pe : loaded) addEmitter(pe);
|
||||
}
|
||||
|
||||
private Vector3f raycastAll(Ray ray) {
|
||||
Vector3f best = null;
|
||||
float bestDistSq = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() > 0) {
|
||||
best = hits.getClosestCollision().getContactPoint();
|
||||
bestDistSq = ray.getOrigin().distanceSquared(best);
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(vp);
|
||||
if (d < bestDistSq) { best = vp; bestDistSq = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(sp);
|
||||
if (d < bestDistSq) { best = sp; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,12 +129,8 @@ public class LightState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
// Neues Licht auf dem Terrain platzieren
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
|
||||
PlacedLight pl = new PlacedLight(pt.x, pt.y + 1f, pt.z, 1f, 1f, 1f, 1f, 20f);
|
||||
addLight(pl);
|
||||
@@ -317,4 +313,34 @@ public class LightState extends BaseAppState {
|
||||
addLight(pl);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3f raycastAll(Ray ray) {
|
||||
Vector3f best = null;
|
||||
float bestDistSq = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() > 0) {
|
||||
best = hits.getClosestCollision().getContactPoint();
|
||||
bestDistSq = ray.getOrigin().distanceSquared(best);
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(vp);
|
||||
if (d < bestDistSq) { best = vp; bestDistSq = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(sp);
|
||||
if (d < bestDistSq) { best = sp; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ public class LocationZoneState extends BaseAppState {
|
||||
|
||||
private static final float LINE_OFFSET_Y = 0.40f;
|
||||
|
||||
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.2f, 0.8f, 0.4f, 1f);
|
||||
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 0.9f, 0.2f, 1f);
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.5f, 1f, 0.6f, 1f);
|
||||
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(1.00f, 0.85f, 0.00f, 1f); // Gelb
|
||||
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1.00f, 1.00f, 0.55f, 1f); // Hell-Gelb
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(1.00f, 0.95f, 0.30f, 1f); // Mittel-Gelb
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
@@ -78,7 +78,7 @@ public class LocationZoneState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (input.activeLayer != SharedInput.LAYER_LOCATION_ZONES) {
|
||||
if (input.activeLayer != SharedInput.LAYER_LOCATION_ZONES && input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||
if (placing) cancelPoly();
|
||||
return;
|
||||
}
|
||||
@@ -93,9 +93,9 @@ public class LocationZoneState extends BaseAppState {
|
||||
applyProperty(selectedIdx, pending);
|
||||
}
|
||||
|
||||
if (input.cancelZoneDrawing) {
|
||||
if (input.cancelZoneDrawing && placing) {
|
||||
input.cancelZoneDrawing = false;
|
||||
if (placing) cancelPoly();
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
if (input.deleteLocationZoneRequested) {
|
||||
@@ -115,16 +115,13 @@ public class LocationZoneState extends BaseAppState {
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
if (click.rightButton()) {
|
||||
if (placing) closePoly();
|
||||
if (placing) undoLastPoint();
|
||||
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();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
float hitX = pt.x, hitZ = pt.z;
|
||||
|
||||
if (placing) {
|
||||
@@ -132,10 +129,11 @@ public class LocationZoneState extends BaseAppState {
|
||||
hitX = snapped[0];
|
||||
hitZ = snapped[1];
|
||||
|
||||
// auto-close only when snapped exactly to own first vertex
|
||||
if (currX.size() >= 3) {
|
||||
float dx = hitX - currX.get(0);
|
||||
float dz = hitZ - currZ.get(0);
|
||||
if (dx * dx + dz * dz < SoundAreaState.SNAP_DIST * SoundAreaState.SNAP_DIST * 0.25f) {
|
||||
if (dx * dx + dz * dz < 0.01f) {
|
||||
closePoly();
|
||||
return;
|
||||
}
|
||||
@@ -145,18 +143,40 @@ public class LocationZoneState extends BaseAppState {
|
||||
currZ.add(hitZ);
|
||||
updateInProgressGeo();
|
||||
} else {
|
||||
// don't select while another state is drawing
|
||||
AreaState as0 = getStateManager().getState(AreaState.class);
|
||||
SoundAreaState sas0 = getStateManager().getState(SoundAreaState.class);
|
||||
if ((as0 != null && as0.isPlacing()) || (sas0 != null && sas0.isPlacing())) return;
|
||||
|
||||
// pass 1: clearly inside
|
||||
for (int i = 0; i < zones.size(); i++) {
|
||||
PlacedLocationZone z = zones.get(i);
|
||||
if (SoundAreaState.pointInPolygon(hitX, hitZ, z.pointsX(), z.pointsZ())) {
|
||||
selectZone(i);
|
||||
return;
|
||||
selectZone(i); return;
|
||||
}
|
||||
}
|
||||
// pass 2: within 50 cm of any edge
|
||||
for (int i = 0; i < zones.size(); i++) {
|
||||
PlacedLocationZone z = zones.get(i);
|
||||
if (SoundAreaState.pointNearPolygonEdge(hitX, hitZ, z.pointsX(), z.pointsZ(), 0.5f)) {
|
||||
selectZone(i); return;
|
||||
}
|
||||
}
|
||||
// cross-type: if another zone type is here, don't start drawing
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
AreaState as = getStateManager().getState(AreaState.class);
|
||||
if (as != null && as.findZoneAt(hitX, hitZ) >= 0) return;
|
||||
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||
if (sas != null && sas.findZoneAt(hitX, hitZ) >= 0) return;
|
||||
}
|
||||
deselect();
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"location".equals(input.zonenNewKind)) return;
|
||||
placing = true;
|
||||
currX.clear();
|
||||
currZ.clear();
|
||||
currX.add(hitX);
|
||||
// snap first point to nearby existing vertices
|
||||
float[] startSnap = snapVertex(hitX, hitZ);
|
||||
currX.add(startSnap[0]);
|
||||
currZ.add(hitZ);
|
||||
updateInProgressGeo();
|
||||
}
|
||||
@@ -189,6 +209,7 @@ public class LocationZoneState extends BaseAppState {
|
||||
PlacedLocationZone zone = new PlacedLocationZone(xs, zs, "");
|
||||
addZone(zone);
|
||||
selectZone(zones.size() - 1);
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) input.zonenNewKind = null;
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
@@ -200,6 +221,14 @@ public class LocationZoneState extends BaseAppState {
|
||||
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||
}
|
||||
|
||||
private void undoLastPoint() {
|
||||
if (currX.isEmpty()) { cancelPoly(); return; }
|
||||
currX.remove(currX.size() - 1);
|
||||
currZ.remove(currZ.size() - 1);
|
||||
if (currX.isEmpty()) cancelPoly();
|
||||
else updateInProgressGeo();
|
||||
}
|
||||
|
||||
private void updateInProgressGeo() {
|
||||
if (inProgGeo != null) rootNode.detachChild(inProgGeo);
|
||||
int n = currX.size();
|
||||
@@ -215,7 +244,7 @@ public class LocationZoneState extends BaseAppState {
|
||||
|
||||
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 y = getHeightAt(x, z) + LINE_OFFSET_Y + 0.05f;
|
||||
float s = 1.5f;
|
||||
|
||||
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||
@@ -242,19 +271,31 @@ public class LocationZoneState extends BaseAppState {
|
||||
private void selectZone(int idx) {
|
||||
if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) {
|
||||
zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
zoneGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(6f);
|
||||
}
|
||||
// cross-state deselect
|
||||
AreaState as = getStateManager().getState(AreaState.class);
|
||||
if (as != null) as.deselectSilent();
|
||||
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
|
||||
if (sas != null) sas.deselectSilent();
|
||||
selectedIdx = idx;
|
||||
zoneGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
|
||||
zoneGeos.get(idx).getMaterial().getAdditionalRenderState().setLineWidth(10f);
|
||||
publishSelection(idx);
|
||||
}
|
||||
|
||||
private void deselect() {
|
||||
if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) {
|
||||
zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
zoneGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(6f);
|
||||
}
|
||||
selectedIdx = -1;
|
||||
input.selectedLocationZoneInfo = null;
|
||||
input.locationZoneSelectionChanged = true;
|
||||
if ("location".equals(input.zonenSelectedKind)) {
|
||||
input.zonenSelectedKind = null;
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void publishSelection(int idx) {
|
||||
@@ -262,6 +303,10 @@ public class LocationZoneState extends BaseAppState {
|
||||
String triggersJson = de.blight.common.model.trigger.TriggerIO.serializeList(z.triggers());
|
||||
input.selectedLocationZoneInfo = idx + "|" + z.nameId() + "|" + triggersJson;
|
||||
input.locationZoneSelectionChanged = true;
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
input.zonenSelectedKind = "location";
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add / Remove / Apply ──────────────────────────────────────────────────
|
||||
@@ -310,7 +355,7 @@ public class LocationZoneState extends BaseAppState {
|
||||
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;
|
||||
float hy = getHeightAt(xs.get(i), zs.get(i));
|
||||
posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i));
|
||||
}
|
||||
posBuffer.flip();
|
||||
@@ -322,19 +367,44 @@ public class LocationZoneState extends BaseAppState {
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setLineWidth(2f);
|
||||
mat.getAdditionalRenderState().setLineWidth(6f);
|
||||
|
||||
Geometry geo = new Geometry(name, mesh);
|
||||
geo.setMaterial(mat);
|
||||
return geo;
|
||||
}
|
||||
|
||||
public boolean isPlacing() { return placing; }
|
||||
|
||||
void deselectSilent() {
|
||||
if (selectedIdx >= 0 && selectedIdx < zoneGeos.size()) {
|
||||
zoneGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
zoneGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(6f);
|
||||
}
|
||||
selectedIdx = -1;
|
||||
input.selectedLocationZoneInfo = null;
|
||||
input.locationZoneSelectionChanged = true;
|
||||
if ("location".equals(input.zonenSelectedKind)) {
|
||||
input.zonenSelectedKind = null;
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save / Load ───────────────────────────────────────────────────────────
|
||||
|
||||
public List<PlacedLocationZone> getPlacedZones() {
|
||||
return new ArrayList<>(zones);
|
||||
}
|
||||
|
||||
public int findZoneAt(float wx, float wz) {
|
||||
for (int i = 0; i < zones.size(); i++) {
|
||||
PlacedLocationZone z = zones.get(i);
|
||||
if (SoundAreaState.pointInPolygon(wx, wz, z.pointsX(), z.pointsZ())
|
||||
|| SoundAreaState.pointNearPolygonEdge(wx, wz, z.pointsX(), z.pointsZ(), 0.5f)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void loadZones(List<PlacedLocationZone> loaded) {
|
||||
if (rootNode == null) {
|
||||
pendingZones = new ArrayList<>(loaded);
|
||||
@@ -357,4 +427,66 @@ public class LocationZoneState extends BaseAppState {
|
||||
for (float f : arr) l.add(f);
|
||||
return l;
|
||||
}
|
||||
|
||||
private Vector3f raycastAll(Ray ray) {
|
||||
Vector3f best = null;
|
||||
float bestDistSq = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() > 0) {
|
||||
best = hits.getClosestCollision().getContactPoint();
|
||||
bestDistSq = ray.getOrigin().distanceSquared(best);
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(vp);
|
||||
if (d < bestDistSq) { best = vp; bestDistSq = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(sp);
|
||||
if (d < bestDistSq) { best = sp; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private float getHeightAt(float wx, float wz) {
|
||||
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
|
||||
float best = 0f;
|
||||
float bestDistFromTop = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
terrain.collideWith(ray, res);
|
||||
if (res.size() > 0) {
|
||||
float y = res.getClosestCollision().getContactPoint().y;
|
||||
float d = 9999f - y;
|
||||
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = 9999f - vp.y;
|
||||
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = 9999f - sp.y;
|
||||
if (d < bestDistFromTop) { best = sp.y; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,7 +859,7 @@ public class SceneObjectState extends BaseAppState {
|
||||
al.r(), al.g(), al.b(), al.intensity(), al.radius()));
|
||||
}
|
||||
de.blight.common.LightIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
input.reloadLightsEmitters = true;
|
||||
} catch (java.io.IOException e) {
|
||||
log.error("[SceneObject] Anhang-Licht speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
@@ -883,7 +883,7 @@ public class SceneObjectState extends BaseAppState {
|
||||
list.add(pe);
|
||||
}
|
||||
de.blight.common.EmitterIO.save(list);
|
||||
input.reloadPlacedOther = true;
|
||||
input.reloadLightsEmitters = true;
|
||||
} catch (java.io.IOException e) {
|
||||
log.error("[SceneObject] Anhang-Emitter speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
|
||||
@@ -850,6 +850,7 @@ public class SculptedMeshEditorState extends BaseAppState {
|
||||
|
||||
/** Raycast gegen alle gebackenen Voxel-Meshes; gibt den nächsten Treffpunkt oder null zurück. */
|
||||
public com.jme3.math.Vector3f raycastGeometry(com.jme3.math.Ray ray) {
|
||||
if (sculptRoot == null) return null;
|
||||
CollisionResults cr = new CollisionResults();
|
||||
sculptRoot.collideWith(ray, cr);
|
||||
return cr.size() > 0 ? cr.getClosestCollision().getContactPoint() : null;
|
||||
|
||||
@@ -26,9 +26,9 @@ public class SoundAreaState extends BaseAppState {
|
||||
static final float SNAP_DIST = 8f;
|
||||
private static final float LINE_OFFSET_Y = 0.3f;
|
||||
|
||||
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(0.1f, 0.85f, 0.4f, 1f);
|
||||
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1f, 1f, 0f, 1f);
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(0.3f, 0.9f, 1f, 1f);
|
||||
private static final ColorRGBA COLOR_NORMAL = new ColorRGBA(1.00f, 0.50f, 0.05f, 1f); // Orange
|
||||
private static final ColorRGBA COLOR_SELECTED = new ColorRGBA(1.00f, 0.75f, 0.45f, 1f); // Hell-Orange
|
||||
private static final ColorRGBA COLOR_INPROG = new ColorRGBA(1.00f, 0.65f, 0.20f, 1f); // Mittel-Orange
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
@@ -82,7 +82,7 @@ public class SoundAreaState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (input.activeLayer != SharedInput.LAYER_SOUND_AREAS) {
|
||||
if (input.activeLayer != SharedInput.LAYER_SOUND_AREAS && input.activeLayer != SharedInput.LAYER_ZONEN) {
|
||||
if (placing) cancelPoly();
|
||||
return;
|
||||
}
|
||||
@@ -97,9 +97,9 @@ public class SoundAreaState extends BaseAppState {
|
||||
applyProperty(selectedIdx, pending);
|
||||
}
|
||||
|
||||
if (input.cancelZoneDrawing) {
|
||||
if (input.cancelZoneDrawing && placing) {
|
||||
input.cancelZoneDrawing = false;
|
||||
if (placing) cancelPoly();
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
if (input.deleteSoundAreaRequested) {
|
||||
@@ -119,30 +119,25 @@ public class SoundAreaState extends BaseAppState {
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
if (click.rightButton()) {
|
||||
if (placing) closePoly();
|
||||
if (placing) undoLastPoint();
|
||||
else deselect();
|
||||
return;
|
||||
}
|
||||
|
||||
// get terrain hit
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
float hitX = pt.x, hitZ = pt.z;
|
||||
|
||||
if (placing) {
|
||||
// snap to existing vertex?
|
||||
float[] snapped = snapVertex(hitX, hitZ);
|
||||
hitX = snapped[0];
|
||||
hitZ = snapped[1];
|
||||
|
||||
// auto-close: if close to first vertex and ≥3 points
|
||||
// auto-close only when snapped exactly to own first vertex
|
||||
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 * 0.25f) {
|
||||
if (dx * dx + dz * dz < 0.01f) {
|
||||
closePoly();
|
||||
return;
|
||||
}
|
||||
@@ -152,21 +147,41 @@ public class SoundAreaState extends BaseAppState {
|
||||
currZ.add(hitZ);
|
||||
updateInProgressGeo();
|
||||
} else {
|
||||
// try to select existing area
|
||||
// don't select while another state is drawing
|
||||
AreaState as0 = getStateManager().getState(AreaState.class);
|
||||
LocationZoneState lzs0 = getStateManager().getState(LocationZoneState.class);
|
||||
if ((as0 != null && as0.isPlacing()) || (lzs0 != null && lzs0.isPlacing())) return;
|
||||
|
||||
// pass 1: clearly inside
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedSoundArea a = areas.get(i);
|
||||
if (pointInPolygon(hitX, hitZ, a.pointsX(), a.pointsZ())) {
|
||||
selectArea(i);
|
||||
return;
|
||||
selectArea(i); return;
|
||||
}
|
||||
}
|
||||
// no area hit – start new polygon
|
||||
// pass 2: within 50 cm of any edge
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedSoundArea a = areas.get(i);
|
||||
if (pointNearPolygonEdge(hitX, hitZ, a.pointsX(), a.pointsZ(), 0.5f)) {
|
||||
selectArea(i); return;
|
||||
}
|
||||
}
|
||||
// cross-type: if another zone type is here, don't start drawing
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
AreaState as = getStateManager().getState(AreaState.class);
|
||||
if (as != null && as.findZoneAt(hitX, hitZ) >= 0) return;
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null && lzs.findZoneAt(hitX, hitZ) >= 0) return;
|
||||
}
|
||||
deselect();
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"sound".equals(input.zonenNewKind)) return;
|
||||
placing = true;
|
||||
currX.clear();
|
||||
currZ.clear();
|
||||
currX.add(hitX);
|
||||
currZ.add(hitZ);
|
||||
// snap first point to nearby existing vertices
|
||||
float[] startSnap = snapVertex(hitX, hitZ);
|
||||
currX.add(startSnap[0]);
|
||||
currZ.add(startSnap[1]);
|
||||
updateInProgressGeo();
|
||||
}
|
||||
}
|
||||
@@ -201,6 +216,7 @@ public class SoundAreaState extends BaseAppState {
|
||||
PlacedSoundArea area = new PlacedSoundArea(xs, zs, "", 1f, false);
|
||||
addArea(area);
|
||||
selectArea(areas.size() - 1);
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) input.zonenNewKind = null;
|
||||
cancelPoly();
|
||||
}
|
||||
|
||||
@@ -212,6 +228,14 @@ public class SoundAreaState extends BaseAppState {
|
||||
if (lastPointMarker != null) { rootNode.detachChild(lastPointMarker); lastPointMarker = null; }
|
||||
}
|
||||
|
||||
private void undoLastPoint() {
|
||||
if (currX.isEmpty()) { cancelPoly(); return; }
|
||||
currX.remove(currX.size() - 1);
|
||||
currZ.remove(currZ.size() - 1);
|
||||
if (currX.isEmpty()) cancelPoly();
|
||||
else updateInProgressGeo();
|
||||
}
|
||||
|
||||
// ── In-progress visual ────────────────────────────────────────────────────
|
||||
|
||||
private void updateInProgressGeo() {
|
||||
@@ -229,7 +253,7 @@ public class SoundAreaState extends BaseAppState {
|
||||
|
||||
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 y = getHeightAt(x, z) + LINE_OFFSET_Y + 0.05f;
|
||||
float s = 1.5f;
|
||||
|
||||
FloatBuffer buf = BufferUtils.createFloatBuffer(4 * 3);
|
||||
@@ -256,25 +280,41 @@ public class SoundAreaState extends BaseAppState {
|
||||
private void selectArea(int idx) {
|
||||
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
|
||||
}
|
||||
// cross-state deselect
|
||||
AreaState as = getStateManager().getState(AreaState.class);
|
||||
if (as != null) as.deselectSilent();
|
||||
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
|
||||
if (lzs != null) lzs.deselectSilent();
|
||||
selectedIdx = idx;
|
||||
areaGeos.get(idx).getMaterial().setColor("Color", COLOR_SELECTED);
|
||||
areaGeos.get(idx).getMaterial().getAdditionalRenderState().setLineWidth(8f);
|
||||
publishSelection(idx);
|
||||
}
|
||||
|
||||
private void deselect() {
|
||||
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
|
||||
}
|
||||
selectedIdx = -1;
|
||||
input.selectedSoundAreaInfo = null;
|
||||
input.soundAreaSelectionChanged = true;
|
||||
if ("sound".equals(input.zonenSelectedKind)) {
|
||||
input.zonenSelectedKind = null;
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void publishSelection(int idx) {
|
||||
PlacedSoundArea a = areas.get(idx);
|
||||
input.selectedSoundAreaInfo = idx + "|" + a.soundPath() + "|" + a.volume() + "|" + a.crossfade();
|
||||
input.soundAreaSelectionChanged = true;
|
||||
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
|
||||
input.zonenSelectedKind = "sound";
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add / Remove / Apply ──────────────────────────────────────────────────
|
||||
@@ -325,7 +365,7 @@ public class SoundAreaState extends BaseAppState {
|
||||
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;
|
||||
float hy = getHeightAt(xs.get(i), zs.get(i));
|
||||
posBuffer.put(xs.get(i)).put(hy + LINE_OFFSET_Y).put(zs.get(i));
|
||||
}
|
||||
posBuffer.flip();
|
||||
@@ -337,19 +377,44 @@ public class SoundAreaState extends BaseAppState {
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setLineWidth(2f);
|
||||
mat.getAdditionalRenderState().setLineWidth(4f);
|
||||
|
||||
Geometry geo = new Geometry(name, mesh);
|
||||
geo.setMaterial(mat);
|
||||
return geo;
|
||||
}
|
||||
|
||||
public boolean isPlacing() { return placing; }
|
||||
|
||||
void deselectSilent() {
|
||||
if (selectedIdx >= 0 && selectedIdx < areaGeos.size()) {
|
||||
areaGeos.get(selectedIdx).getMaterial().setColor("Color", COLOR_NORMAL);
|
||||
areaGeos.get(selectedIdx).getMaterial().getAdditionalRenderState().setLineWidth(4f);
|
||||
}
|
||||
selectedIdx = -1;
|
||||
input.selectedSoundAreaInfo = null;
|
||||
input.soundAreaSelectionChanged = true;
|
||||
if ("sound".equals(input.zonenSelectedKind)) {
|
||||
input.zonenSelectedKind = null;
|
||||
input.zonenSelectionChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Save / Load ───────────────────────────────────────────────────────────
|
||||
|
||||
public List<PlacedSoundArea> getPlacedAreas() {
|
||||
return new ArrayList<>(areas);
|
||||
}
|
||||
|
||||
public int findZoneAt(float wx, float wz) {
|
||||
for (int i = 0; i < areas.size(); i++) {
|
||||
PlacedSoundArea a = areas.get(i);
|
||||
if (pointInPolygon(wx, wz, a.pointsX(), a.pointsZ())
|
||||
|| pointNearPolygonEdge(wx, wz, a.pointsX(), a.pointsZ(), 0.5f)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void loadAreas(List<PlacedSoundArea> loaded) {
|
||||
if (rootNode == null) {
|
||||
pendingAreas = new ArrayList<>(loaded);
|
||||
@@ -359,7 +424,28 @@ public class SoundAreaState extends BaseAppState {
|
||||
for (PlacedSoundArea a : loaded) addArea(a);
|
||||
}
|
||||
|
||||
// ── Point-in-polygon (ray casting) ────────────────────────────────────────
|
||||
// ── Point-in-polygon + edge proximity ────────────────────────────────────
|
||||
|
||||
static boolean pointNearPolygonEdge(float px, float pz, float[] xs, float[] zs, float threshold) {
|
||||
int n = xs.length;
|
||||
float t2 = threshold * threshold;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int j = (i + 1) % n;
|
||||
float ax = xs[i], az = zs[i], bx = xs[j], bz = zs[j];
|
||||
float dx = bx - ax, dz = bz - az;
|
||||
float lenSq = dx * dx + dz * dz;
|
||||
float t;
|
||||
if (lenSq < 0.0001f) {
|
||||
t = 0f;
|
||||
} else {
|
||||
t = ((px - ax) * dx + (pz - az) * dz) / lenSq;
|
||||
t = Math.max(0f, Math.min(1f, t));
|
||||
}
|
||||
float nx = ax + t * dx - px, nz = az + t * dz - pz;
|
||||
if (nx * nx + nz * nz < t2) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) {
|
||||
int n = xs.length;
|
||||
@@ -387,4 +473,66 @@ public class SoundAreaState extends BaseAppState {
|
||||
for (float f : arr) l.add(f);
|
||||
return l;
|
||||
}
|
||||
|
||||
private Vector3f raycastAll(Ray ray) {
|
||||
Vector3f best = null;
|
||||
float bestDistSq = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() > 0) {
|
||||
best = hits.getClosestCollision().getContactPoint();
|
||||
bestDistSq = ray.getOrigin().distanceSquared(best);
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(vp);
|
||||
if (d < bestDistSq) { best = vp; bestDistSq = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(sp);
|
||||
if (d < bestDistSq) { best = sp; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private float getHeightAt(float wx, float wz) {
|
||||
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
|
||||
float best = 0f;
|
||||
float bestDistFromTop = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
terrain.collideWith(ray, res);
|
||||
if (res.size() > 0) {
|
||||
float y = res.getClosestCollision().getContactPoint().y;
|
||||
float d = 9999f - y;
|
||||
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = 9999f - vp.y;
|
||||
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = 9999f - sp.y;
|
||||
if (d < bestDistFromTop) { best = sp.y; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,9 +195,8 @@ public class StoneEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void placePendingStoneAt(float wx, float wz) {
|
||||
if (terrain == null || pendingStone == null) { return; }
|
||||
float th = terrain.getHeight(new Vector2f(wx, wz));
|
||||
if (!Float.isFinite(th)) { return; }
|
||||
if (pendingStone == null) { return; }
|
||||
if (!Float.isFinite(getHeightAt(wx, wz))) { return; }
|
||||
PlacedStone stone = new PlacedStone(wx, wz, pendingStone.radius(), pendingStone.rotY(),
|
||||
pendingStone.textureSlot(), pendingStone.sinkFraction(), pendingStone.noiseSeed());
|
||||
int ci = chunkIndex(wx, wz);
|
||||
@@ -247,8 +246,7 @@ public class StoneEditorState extends BaseAppState {
|
||||
float sx = wx + (float)(Math.cos(angle) * dist);
|
||||
float sz = wz + (float)(Math.sin(angle) * dist);
|
||||
|
||||
float th = terrain.getHeight(new Vector2f(sx, sz));
|
||||
if (!Float.isFinite(th)) continue;
|
||||
if (!Float.isFinite(getHeightAt(sx, sz))) continue;
|
||||
|
||||
float radius = (float)(minR + random.nextDouble() * (maxR - minR));
|
||||
float rotY = random.nextFloat() * 360f;
|
||||
@@ -605,12 +603,71 @@ public class StoneEditorState extends BaseAppState {
|
||||
|
||||
private Vector3f raycastTerrain(float sx, float sy) {
|
||||
if (terrain == null) return null;
|
||||
Ray ray = new Ray(cam.getWorldCoordinates(new Vector2f(sx, sy), 0f),
|
||||
cam.getWorldCoordinates(new Vector2f(sx, sy), 1f));
|
||||
ray.getDirection().subtractLocal(ray.getOrigin()).normalizeLocal();
|
||||
Vector3f origin = cam.getWorldCoordinates(new Vector2f(sx, sy), 0f);
|
||||
Vector3f target = cam.getWorldCoordinates(new Vector2f(sx, sy), 1f);
|
||||
Ray ray = new Ray(origin, target.subtractLocal(origin).normalizeLocal());
|
||||
|
||||
CollisionResults res = new CollisionResults();
|
||||
terrain.collideWith(ray, res);
|
||||
return res.size() > 0 ? res.getClosestCollision().getContactPoint() : null;
|
||||
Vector3f best = res.size() > 0 ? res.getClosestCollision().getContactPoint() : null;
|
||||
float bestDistSq = best != null ? origin.distanceSquared(best) : Float.MAX_VALUE;
|
||||
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = origin.distanceSquared(vp);
|
||||
if (d < bestDistSq) { best = vp; bestDistSq = d; }
|
||||
}
|
||||
}
|
||||
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = origin.distanceSquared(sp);
|
||||
if (d < bestDistSq) { best = sp; }
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
/** Höhe an (wx, wz): nimmt das höchste Ergebnis aus Terrain, Voxeln und Sculpted Mesh. */
|
||||
private float getHeightAt(float wx, float wz) {
|
||||
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
|
||||
float best = Float.NaN;
|
||||
float bestDistFromTop = Float.MAX_VALUE;
|
||||
|
||||
if (terrain != null) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
terrain.collideWith(ray, res);
|
||||
if (res.size() > 0) {
|
||||
float y = res.getClosestCollision().getContactPoint().y;
|
||||
float d = 9999f - y;
|
||||
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = 9999f - vp.y;
|
||||
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = 9999f - sp.y;
|
||||
if (d < bestDistFromTop) { best = sp.y; }
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
@@ -631,10 +688,8 @@ public class StoneEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private float stoneWorldY(PlacedStone s) {
|
||||
if (terrain == null) return 0f;
|
||||
float h = terrain.getHeight(new Vector2f(s.x(), s.z()));
|
||||
if (!Float.isFinite(h)) return 0f;
|
||||
return h < -1e10f ? 0f : h;
|
||||
float h = getHeightAt(s.x(), s.z());
|
||||
return Float.isFinite(h) ? h : 0f;
|
||||
}
|
||||
|
||||
// ── Persistenz ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1062,6 +1062,12 @@ public class TerrainEditorState extends BaseAppState {
|
||||
terrain.setMaterial(terrainMat = buildTerrainMaterial());
|
||||
}
|
||||
|
||||
if (input.reloadLightsEmitters) {
|
||||
input.reloadLightsEmitters = false;
|
||||
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
|
||||
try { if (emitterState != null) emitterState.loadPlacedEmitters(de.blight.common.EmitterIO.load()); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (input.reloadPlacedOther) {
|
||||
input.reloadPlacedOther = false;
|
||||
try { if (lightState != null) lightState.loadPlacedLights(de.blight.common.LightIO.load()); } catch (Exception ignored) {}
|
||||
|
||||
@@ -1333,6 +1333,7 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
/** Raycast gegen reine Voxel-Geometrie; gibt nächsten Treffpunkt oder null zurück. */
|
||||
public Vector3f raycastVoxelGeometry(com.jme3.math.Ray ray) {
|
||||
if (voxelRoot == null) return null;
|
||||
CollisionResults results = new CollisionResults();
|
||||
voxelRoot.collideWith(ray, results);
|
||||
if (results.size() == 0) return null;
|
||||
|
||||
@@ -165,11 +165,8 @@ public class WaterBodyState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
if (terrain == null) return;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) return;
|
||||
float hitX = pt.x, hitZ = pt.z;
|
||||
|
||||
if (placing) {
|
||||
@@ -273,15 +270,14 @@ public class WaterBodyState extends BaseAppState {
|
||||
private void updatePillarGeo() {
|
||||
if (pillarGeo != null) { rootNode.detachChild(pillarGeo); pillarGeo = null; }
|
||||
int n = currX.size();
|
||||
if (n == 0 || terrain == null) return;
|
||||
if (n == 0) return;
|
||||
|
||||
FloatBuffer pos = BufferUtils.createFloatBuffer(n * 2 * 3);
|
||||
FloatBuffer col = BufferUtils.createFloatBuffer(n * 2 * 4);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
float x = currX.get(i), z = currZ.get(i);
|
||||
Float th = terrain.getHeight(new com.jme3.math.Vector2f(x, z));
|
||||
float ty = th != null ? th : currentWaterHeight;
|
||||
float ty = getHeightAt(x, z);
|
||||
float wy = currentWaterHeight;
|
||||
ColorRGBA c = (ty < wy) ? PILLAR_SUB : PILLAR_DRY;
|
||||
|
||||
@@ -307,18 +303,15 @@ public class WaterBodyState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void updateCursorPillar() {
|
||||
if (terrain == null || input.mouseScreenX < 0) { removeCursorPillar(); return; }
|
||||
if (input.mouseScreenX < 0) { removeCursorPillar(); return; }
|
||||
|
||||
float jmeX = input.mouseScreenX * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - input.mouseScreenY * (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());
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) { removeCursorPillar(); return; }
|
||||
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = raycastAll(ray);
|
||||
if (pt == null) { removeCursorPillar(); return; }
|
||||
float ty = pt.y, wy = currentWaterHeight;
|
||||
float delta = wy - ty;
|
||||
ColorRGBA c = (delta > 0) ? PILLAR_SUB : PILLAR_DRY;
|
||||
@@ -432,16 +425,13 @@ public class WaterBodyState extends BaseAppState {
|
||||
// ── Height sampling ───────────────────────────────────────────────────────
|
||||
|
||||
private float sampleTerrainAtCursor() {
|
||||
if (terrain == null) return Float.NaN;
|
||||
float jmeX = input.mouseScreenX * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - input.mouseScreenY * (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());
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return Float.NaN;
|
||||
return hits.getClosestCollision().getContactPoint().y;
|
||||
Vector3f pt = raycastAll(ray);
|
||||
return pt != null ? pt.y : Float.NaN;
|
||||
}
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────────
|
||||
@@ -639,4 +629,66 @@ public class WaterBodyState extends BaseAppState {
|
||||
for (float f : arr) l.add(f);
|
||||
return l;
|
||||
}
|
||||
|
||||
private Vector3f raycastAll(Ray ray) {
|
||||
Vector3f best = null;
|
||||
float bestDistSq = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() > 0) {
|
||||
best = hits.getClosestCollision().getContactPoint();
|
||||
bestDistSq = ray.getOrigin().distanceSquared(best);
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(vp);
|
||||
if (d < bestDistSq) { best = vp; bestDistSq = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = ray.getOrigin().distanceSquared(sp);
|
||||
if (d < bestDistSq) { best = sp; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private float getHeightAt(float wx, float wz) {
|
||||
Ray ray = new Ray(new Vector3f(wx, 9999f, wz), new Vector3f(0f, -1f, 0f));
|
||||
float best = 0f;
|
||||
float bestDistFromTop = Float.MAX_VALUE;
|
||||
if (terrain != null) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
terrain.collideWith(ray, res);
|
||||
if (res.size() > 0) {
|
||||
float y = res.getClosestCollision().getContactPoint().y;
|
||||
float d = 9999f - y;
|
||||
if (d < bestDistFromTop) { best = y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||
if (vp != null) {
|
||||
float d = 9999f - vp.y;
|
||||
if (d < bestDistFromTop) { best = vp.y; bestDistFromTop = d; }
|
||||
}
|
||||
}
|
||||
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||
if (smes != null) {
|
||||
Vector3f sp = smes.raycastGeometry(ray);
|
||||
if (sp != null) {
|
||||
float d = 9999f - sp.y;
|
||||
if (d < bestDistFromTop) { best = sp.y; }
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.AreaDefinition;
|
||||
import de.blight.common.AreaDefinitionIO;
|
||||
import de.blight.editor.ProjectRoot;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class AreaEditorView extends BorderPane {
|
||||
|
||||
private static final String PREFIX = "area.";
|
||||
private static final String[] TRACK_NAMES = {"☀ Tag-Track", "🌙 Nacht-Track", "⚔ Kampf-Track"};
|
||||
|
||||
private final ObservableList<AreaDefinition> areas = FXCollections.observableArrayList();
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
private final String[] tracks = {"", "", ""};
|
||||
|
||||
private ListView<AreaDefinition> listView;
|
||||
private Button deleteBtn;
|
||||
private AreaDefinition current = null;
|
||||
private boolean reloading = false;
|
||||
|
||||
private TextField idField;
|
||||
private Label nameKeyLabel;
|
||||
private Label[] trackLabels = new Label[3];
|
||||
private VBox formContainer;
|
||||
|
||||
{ savePause.setOnFinished(e -> persistCurrent()); }
|
||||
|
||||
public AreaEditorView() {
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
reload();
|
||||
|
||||
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||
split.setDividerPositions(0.28);
|
||||
setCenter(split);
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() { savePause.playFromStart(); }
|
||||
|
||||
private void persistCurrent() {
|
||||
if (current == null || reloading) return;
|
||||
AreaDefinition saved = formToArea();
|
||||
if (saved.id().isBlank()) return;
|
||||
int idx = areas.indexOf(current);
|
||||
if (idx < 0) return;
|
||||
reloading = true;
|
||||
areas.set(idx, saved);
|
||||
current = saved;
|
||||
reloading = false;
|
||||
persist();
|
||||
}
|
||||
|
||||
// ── List panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildListPanel() {
|
||||
listView = new ListView<>(areas);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(AreaDefinition d, boolean empty) {
|
||||
super.updateItem(d, empty);
|
||||
if (empty || d == null) { setText(null); setStyle(""); return; }
|
||||
String shortId = d.id().startsWith(PREFIX) ? d.id().substring(PREFIX.length()) : d.id();
|
||||
setText(shortId.isBlank() ? "—" : shortId);
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent #cc6644;"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onSelected(old, nw));
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
|
||||
Button newBtn = new Button("Neue Area");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createArea());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
Button refreshBtn = new Button("↺ Neu laden");
|
||||
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
refreshBtn.setOnAction(e -> reload());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
|
||||
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||
return panel;
|
||||
}
|
||||
|
||||
// ── Form panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private ScrollPane buildFormPanel() {
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
ScrollPane scroll = new ScrollPane(formContainer);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(12));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
idField = field("z. B. wald");
|
||||
nameKeyLabel = new Label("Name-Key: —");
|
||||
nameKeyLabel.setStyle("-fx-text-fill: #aaaaaa; -fx-font-style: italic; -fx-font-size: 11;");
|
||||
idField.textProperty().addListener((obs, o, n) -> {
|
||||
String key = n == null || n.isBlank() ? "" : PREFIX + n.trim() + ".name";
|
||||
nameKeyLabel.setText("Name-Key: " + (key.isBlank() ? "—" : key));
|
||||
scheduleSave();
|
||||
});
|
||||
idField.focusedProperty().addListener((obs, was, is) -> {
|
||||
if (!is) { savePause.stop(); persistCurrent(); }
|
||||
});
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
trackLabels[i] = new Label("(keine)");
|
||||
trackLabels[i].setStyle("-fx-text-fill: #aaaaaa; -fx-font-size: 11;");
|
||||
trackLabels[i].setWrapText(true);
|
||||
}
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Kennung"),
|
||||
new Separator(),
|
||||
row("ID:", idField),
|
||||
nameKeyLabel,
|
||||
sectionTitle("Musik-Tracks"),
|
||||
new Separator(),
|
||||
trackRow(0),
|
||||
trackRow(1),
|
||||
trackRow(2)
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
private VBox trackRow(int idx) {
|
||||
Button btn = new Button("🎵 Wählen…");
|
||||
btn.setStyle("-fx-background-color: #3a3a5a; -fx-text-fill: #ddd; -fx-font-size: 11;");
|
||||
btn.setOnAction(e -> {
|
||||
Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
new SoundChooser(assetRoot, SoundChooser.Mode.MUSIC)
|
||||
.showAndWait()
|
||||
.ifPresent(rel -> {
|
||||
tracks[idx] = rel;
|
||||
trackLabels[idx].setText(rel.isEmpty() ? "(keine)" : rel);
|
||||
scheduleSave();
|
||||
});
|
||||
});
|
||||
|
||||
Button clearBtn = new Button("✕");
|
||||
clearBtn.setStyle("-fx-background-color: #5a2a2a; -fx-text-fill: #ddd; -fx-font-size: 10;");
|
||||
clearBtn.setOnAction(e -> {
|
||||
tracks[idx] = "";
|
||||
trackLabels[idx].setText("(keine)");
|
||||
scheduleSave();
|
||||
});
|
||||
|
||||
Label header = new Label(TRACK_NAMES[idx] + ":");
|
||||
header.setStyle("-fx-text-fill: #88aacc; -fx-font-size: 11;");
|
||||
|
||||
HBox btns = new HBox(4, btn, clearBtn);
|
||||
return new VBox(2, header, trackLabels[idx], btns);
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onSelected(AreaDefinition old, AreaDefinition nw) {
|
||||
if (reloading) return;
|
||||
if (old != null) { saveFormToArea(old); persist(); }
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) { formContainer.setDisable(false); loadForm(nw); }
|
||||
else { formContainer.setDisable(true); clearForm(); }
|
||||
}
|
||||
|
||||
private void loadForm(AreaDefinition d) {
|
||||
String shortId = d.id().startsWith(PREFIX) ? d.id().substring(PREFIX.length()) : d.id();
|
||||
idField.setText(shortId);
|
||||
nameKeyLabel.setText("Name-Key: " + (shortId.isBlank() ? "—" : PREFIX + shortId + ".name"));
|
||||
tracks[0] = d.dayTrack();
|
||||
tracks[1] = d.nightTrack();
|
||||
tracks[2] = d.combatTrack();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
trackLabels[i].setText(tracks[i].isEmpty() ? "(keine)" : tracks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private AreaDefinition formToArea() {
|
||||
String shortId = idField.getText().trim();
|
||||
String fullId = shortId.isBlank() ? "" : PREFIX + shortId;
|
||||
return new AreaDefinition(fullId, tracks[0], tracks[1], tracks[2]);
|
||||
}
|
||||
|
||||
private void saveFormToArea(AreaDefinition old) {
|
||||
int idx = areas.indexOf(old);
|
||||
if (idx < 0) return;
|
||||
areas.set(idx, formToArea());
|
||||
current = areas.get(idx);
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
if (nameKeyLabel != null) nameKeyLabel.setText("Name-Key: —");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
tracks[i] = "";
|
||||
if (trackLabels[i] != null) trackLabels[i].setText("(keine)");
|
||||
}
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
|
||||
private void createArea() {
|
||||
AreaDefinition d = new AreaDefinition(PREFIX + "neu_" + System.currentTimeMillis(), "", "", "");
|
||||
areas.add(d);
|
||||
listView.getSelectionModel().select(d);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
if (current == null) return;
|
||||
areas.remove(current);
|
||||
current = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
persist();
|
||||
reload();
|
||||
}
|
||||
|
||||
private void persist() {
|
||||
try { AreaDefinitionIO.save(areas); }
|
||||
catch (IOException e) {
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
try { areas.setAll(AreaDefinitionIO.load()); }
|
||||
catch (IOException e) { areas.clear(); }
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static Label sectionTitle(String text) {
|
||||
Label l = new Label(text);
|
||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #cc8866;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(50);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static TextField field(String prompt) {
|
||||
TextField tf = new TextField();
|
||||
tf.setPromptText(prompt);
|
||||
return tf;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.trigger.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Modality;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class ConditionDialog extends Dialog<Condition> {
|
||||
|
||||
private static final String TYPE_CHAPTER = "Kapitel erreicht";
|
||||
private static final String TYPE_FIRST_TIME = "Erstes Mal";
|
||||
private static final String TYPE_QUEST_COMPLETED = "Quest abgeschlossen";
|
||||
private static final String TYPE_QUEST_ACCEPTED = "Quest angenommen";
|
||||
private static final String TYPE_QUEST_REJECTED = "Quest abgelehnt";
|
||||
private static final String TYPE_GAME_VARIABLE = "Spielvariable gesetzt";
|
||||
private static final String TYPE_FACTION_MEMBER = "Fraktionsmitglied";
|
||||
|
||||
private final ComboBox<String> typeCombo = new ComboBox<>();
|
||||
private final VBox dynamicArea = new VBox(6);
|
||||
|
||||
private Spinner<Integer> chapterSpinner;
|
||||
private TextField zoneIdField;
|
||||
private TextField questIdField;
|
||||
private TextField varKeyField, varValueField;
|
||||
private TextField fractionIdField;
|
||||
|
||||
public ConditionDialog() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public ConditionDialog(Condition existing) {
|
||||
setTitle(existing == null ? "Bedingung hinzufügen" : "Bedingung bearbeiten");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
typeCombo.getItems().addAll(TYPE_CHAPTER, TYPE_FIRST_TIME,
|
||||
TYPE_QUEST_COMPLETED, TYPE_QUEST_ACCEPTED, TYPE_QUEST_REJECTED,
|
||||
TYPE_GAME_VARIABLE, TYPE_FACTION_MEMBER);
|
||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
typeCombo.setOnAction(e -> rebuildDynamic(typeCombo.getValue()));
|
||||
|
||||
VBox content = new VBox(10);
|
||||
content.setPadding(new Insets(16));
|
||||
content.setPrefWidth(380);
|
||||
content.getChildren().addAll(row("Bedingungstyp:", typeCombo), new Separator(), dynamicArea);
|
||||
|
||||
getDialogPane().setContent(content);
|
||||
getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||
okBtn.setDisable(true);
|
||||
typeCombo.valueProperty().addListener((obs, o, n) -> okBtn.setDisable(n == null));
|
||||
|
||||
setResultConverter(bt -> bt == ButtonType.OK ? buildCondition() : null);
|
||||
|
||||
if (existing != null) {
|
||||
preload(existing);
|
||||
} else {
|
||||
typeCombo.setValue(TYPE_CHAPTER);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Felder aufbauen ───────────────────────────────────────────────────────
|
||||
|
||||
private void rebuildDynamic(String type) {
|
||||
dynamicArea.getChildren().clear();
|
||||
if (type == null) return;
|
||||
switch (type) {
|
||||
case TYPE_CHAPTER -> buildChapterFields();
|
||||
case TYPE_FIRST_TIME -> buildFirstTimeFields();
|
||||
case TYPE_QUEST_COMPLETED,
|
||||
TYPE_QUEST_ACCEPTED,
|
||||
TYPE_QUEST_REJECTED -> buildQuestFields();
|
||||
case TYPE_GAME_VARIABLE -> buildVarFields();
|
||||
case TYPE_FACTION_MEMBER -> buildFractionFields();
|
||||
}
|
||||
}
|
||||
|
||||
private void buildChapterFields() {
|
||||
chapterSpinner = new Spinner<>(0, 99, 0);
|
||||
chapterSpinner.setEditable(true);
|
||||
chapterSpinner.setPrefWidth(80);
|
||||
dynamicArea.getChildren().add(row("Mindest-Kapitel:", chapterSpinner));
|
||||
}
|
||||
|
||||
private void buildFirstTimeFields() {
|
||||
zoneIdField = field("Zone-ID (nameId der Zone)");
|
||||
dynamicArea.getChildren().add(row("Zone-ID:", zoneIdField));
|
||||
}
|
||||
|
||||
private void buildQuestFields() {
|
||||
questIdField = field("Quest-ID (z. B. q_main_001)");
|
||||
dynamicArea.getChildren().add(row("Quest-ID:", questIdField));
|
||||
}
|
||||
|
||||
private void buildVarFields() {
|
||||
varKeyField = field("Variablenname");
|
||||
varValueField = field("Erwarteter Wert (leer = nur Existenz prüfen)");
|
||||
dynamicArea.getChildren().addAll(
|
||||
row("Schlüssel:", varKeyField),
|
||||
row("Wert:", varValueField));
|
||||
}
|
||||
|
||||
private void buildFractionFields() {
|
||||
fractionIdField = field("UUID der Fraktion");
|
||||
dynamicArea.getChildren().add(row("Fraktions-UUID:", fractionIdField));
|
||||
}
|
||||
|
||||
// ── Bedingung bauen ───────────────────────────────────────────────────────
|
||||
|
||||
private Condition buildCondition() {
|
||||
String type = typeCombo.getValue();
|
||||
if (type == null) return null;
|
||||
return switch (type) {
|
||||
case TYPE_CHAPTER -> {
|
||||
ChapterCondition c = new ChapterCondition();
|
||||
if (chapterSpinner != null) c.setMinChapter(chapterSpinner.getValue());
|
||||
yield c;
|
||||
}
|
||||
case TYPE_FIRST_TIME -> {
|
||||
FirstTimeCondition c = new FirstTimeCondition();
|
||||
if (zoneIdField != null) c.setZoneId(zoneIdField.getText().trim());
|
||||
yield c;
|
||||
}
|
||||
case TYPE_QUEST_COMPLETED -> {
|
||||
QuestCompletedCondition c = new QuestCompletedCondition();
|
||||
if (questIdField != null) c.setQuestId(questIdField.getText().trim());
|
||||
yield c;
|
||||
}
|
||||
case TYPE_QUEST_ACCEPTED -> {
|
||||
QuestAcceptedCondition c = new QuestAcceptedCondition();
|
||||
if (questIdField != null) c.setQuestId(questIdField.getText().trim());
|
||||
yield c;
|
||||
}
|
||||
case TYPE_QUEST_REJECTED -> {
|
||||
QuestRejectedCondition c = new QuestRejectedCondition();
|
||||
if (questIdField != null) c.setQuestId(questIdField.getText().trim());
|
||||
yield c;
|
||||
}
|
||||
case TYPE_GAME_VARIABLE -> {
|
||||
GameVariableCondition c = new GameVariableCondition();
|
||||
if (varKeyField != null) c.setKey(varKeyField.getText().trim());
|
||||
if (varValueField != null) c.setValue(varValueField.getText().trim());
|
||||
yield c;
|
||||
}
|
||||
case TYPE_FACTION_MEMBER -> {
|
||||
FactionMemberCondition c = new FactionMemberCondition();
|
||||
if (fractionIdField != null && !fractionIdField.getText().isBlank()) {
|
||||
try { c.setFractionId(UUID.fromString(fractionIdField.getText().trim())); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
yield c;
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
// ── Vorladen ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void preload(Condition c) {
|
||||
if (c instanceof ChapterCondition cc) {
|
||||
typeCombo.setValue(TYPE_CHAPTER);
|
||||
if (chapterSpinner != null) chapterSpinner.getValueFactory().setValue(cc.getMinChapter());
|
||||
} else if (c instanceof FirstTimeCondition ft) {
|
||||
typeCombo.setValue(TYPE_FIRST_TIME);
|
||||
if (zoneIdField != null && ft.getZoneId() != null) zoneIdField.setText(ft.getZoneId());
|
||||
} else if (c instanceof QuestCompletedCondition qc) {
|
||||
typeCombo.setValue(TYPE_QUEST_COMPLETED);
|
||||
if (questIdField != null && qc.getQuestId() != null) questIdField.setText(qc.getQuestId());
|
||||
} else if (c instanceof QuestAcceptedCondition qa) {
|
||||
typeCombo.setValue(TYPE_QUEST_ACCEPTED);
|
||||
if (questIdField != null && qa.getQuestId() != null) questIdField.setText(qa.getQuestId());
|
||||
} else if (c instanceof QuestRejectedCondition qr) {
|
||||
typeCombo.setValue(TYPE_QUEST_REJECTED);
|
||||
if (questIdField != null && qr.getQuestId() != null) questIdField.setText(qr.getQuestId());
|
||||
} else if (c instanceof GameVariableCondition gv) {
|
||||
typeCombo.setValue(TYPE_GAME_VARIABLE);
|
||||
if (varKeyField != null && gv.getKey() != null) varKeyField.setText(gv.getKey());
|
||||
if (varValueField != null && gv.getValue() != null) varValueField.setText(gv.getValue());
|
||||
} else if (c instanceof FactionMemberCondition fm) {
|
||||
typeCombo.setValue(TYPE_FACTION_MEMBER);
|
||||
if (fractionIdField != null && fm.getFractionId() != null)
|
||||
fractionIdField.setText(fm.getFractionId().toString());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
static String describe(Condition c) {
|
||||
if (c instanceof ChapterCondition cc) return "Kapitel ≥ " + cc.getMinChapter();
|
||||
if (c instanceof FirstTimeCondition ft) return "Erstes Mal: " + nullSafe(ft.getZoneId());
|
||||
if (c instanceof QuestCompletedCondition q) return "Quest abgeschlossen: " + nullSafe(q.getQuestId());
|
||||
if (c instanceof QuestAcceptedCondition q) return "Quest angenommen: " + nullSafe(q.getQuestId());
|
||||
if (c instanceof QuestRejectedCondition q) return "Quest abgelehnt: " + nullSafe(q.getQuestId());
|
||||
if (c instanceof GameVariableCondition gv) return "Var " + nullSafe(gv.getKey()) + " = " + nullSafe(gv.getValue());
|
||||
if (c instanceof FactionMemberCondition fm) return "Fraktion: " + (fm.getFractionId() != null ? fm.getFractionId().toString().substring(0, 8) + "…" : "?");
|
||||
return c.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
private static String nullSafe(String s) { return s != null && !s.isBlank() ? s : "?"; }
|
||||
|
||||
private static TextField field(String prompt) {
|
||||
TextField tf = new TextField();
|
||||
tf.setPromptText(prompt);
|
||||
return tf;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(130);
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -35,6 +37,12 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
private TextField objectPathField;
|
||||
private VBox formContainer;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> persistCurrent());
|
||||
}
|
||||
|
||||
public CraftingTableEditorView(Path tableDir) {
|
||||
this.tableDir = tableDir;
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
@@ -108,14 +116,11 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
|
||||
nameIdField = new TextField();
|
||||
nameIdField.setPromptText("Text-Referenz ID (z. B. ui.crafting.alchemy_table)");
|
||||
nameIdField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
objectPathField = new TextField();
|
||||
objectPathField.setPromptText("Asset-Pfad zum 3D-Objekt (z. B. Models/crafting/alchemy_table.j3o)");
|
||||
|
||||
Button saveBtn = new Button("Crafting Table speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentTable());
|
||||
objectPathField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
form.getChildren().addAll(
|
||||
formTypeLabel,
|
||||
@@ -124,13 +129,22 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
row("Name-ID:", nameIdField),
|
||||
new Separator(),
|
||||
sectionTitle("3D-Objekt"),
|
||||
row("Pfad:", objectPathField),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
row("Pfad:", objectPathField)
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() {
|
||||
savePause.playFromStart();
|
||||
}
|
||||
|
||||
private void persistCurrent() {
|
||||
if (currentType == null) { return; }
|
||||
saveCurrentTable();
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onTypeSelected(CraftingTable.CraftingTableType old, CraftingTable.CraftingTableType nw) {
|
||||
@@ -174,7 +188,7 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
try {
|
||||
CraftingTableIO.save(t, tableDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
reloadMap();
|
||||
@@ -187,7 +201,7 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
try {
|
||||
CraftingTableIO.delete(currentType, tableDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
reloadMap();
|
||||
|
||||
@@ -696,11 +696,11 @@ public class DialogEditorView extends BorderPane {
|
||||
}
|
||||
String id = result.get().trim();
|
||||
if (id.isBlank()) {
|
||||
new Alert(Alert.AlertType.WARNING, "ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.WARNING, "ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
if (allOptions.containsKey(id)) {
|
||||
new Alert(Alert.AlertType.WARNING, "ID '" + id + "' bereits vorhanden.", ButtonType.OK).showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.WARNING, "ID '" + id + "' bereits vorhanden.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
DialogOption opt = new DialogOption();
|
||||
@@ -749,6 +749,7 @@ public class DialogEditorView extends BorderPane {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Option auswählen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
|
||||
ListView<String> chooser = new ListView<>();
|
||||
chooser.setCellFactory(lv -> new ListCell<>() {
|
||||
@@ -794,6 +795,7 @@ public class DialogEditorView extends BorderPane {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Quest auswählen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
|
||||
ListView<Quest> chooser = new ListView<>();
|
||||
chooser.setCellFactory(lv -> new ListCell<>() {
|
||||
@@ -1119,6 +1121,7 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
private static Trigger buildTriggerDialog(String type) {
|
||||
Dialog<Trigger> dlg = new Dialog<>();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setTitle("Trigger konfigurieren: " + type);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
|
||||
21
blight-editor/src/main/java/de/blight/editor/ui/Dialogs.java
Normal file
21
blight-editor/src/main/java/de/blight/editor/ui/Dialogs.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.stage.Window;
|
||||
|
||||
public final class Dialogs {
|
||||
private Dialogs() {}
|
||||
|
||||
public static Window primaryWindow() {
|
||||
return Window.getWindows().stream()
|
||||
.filter(Window::isShowing)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public static Alert alert(Alert.AlertType type, String msg, ButtonType... btns) {
|
||||
Alert a = new Alert(type, msg, btns);
|
||||
a.initOwner(primaryWindow());
|
||||
return a;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
@@ -9,6 +10,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -41,6 +43,12 @@ public class FractionEditorView extends BorderPane {
|
||||
private TextField rank3Field;
|
||||
private VBox formContainer;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> persistCurrent());
|
||||
}
|
||||
|
||||
public FractionEditorView(Path fractionDir) {
|
||||
this.fractionDir = fractionDir;
|
||||
this.sortedFractions = new SortedList<>(fractions, FractionIO.SORT_ORDER);
|
||||
@@ -120,16 +128,17 @@ public class FractionEditorView extends BorderPane {
|
||||
idLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-font-family: monospace;");
|
||||
|
||||
nameField = field("z. B. faction.guards");
|
||||
nameField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
maleMemberField = field("z. B. faction.guards.member.male");
|
||||
maleMemberField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
femaleMemberField = field("z. B. faction.guards.member.female");
|
||||
femaleMemberField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
rank1Field = field("z. B. faction.guards.rank1");
|
||||
rank1Field.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
rank2Field = field("z. B. faction.guards.rank2");
|
||||
rank2Field.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
rank3Field = field("z. B. faction.guards.rank3");
|
||||
|
||||
Button saveBtn = new Button("Fraktion speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentFraction());
|
||||
rank3Field.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Kennung"),
|
||||
@@ -144,17 +153,37 @@ public class FractionEditorView extends BorderPane {
|
||||
new Separator(),
|
||||
row("Rang 1:", rank1Field),
|
||||
row("Rang 2:", rank2Field),
|
||||
row("Rang 3:", rank3Field),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
row("Rang 3:", rank3Field)
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() {
|
||||
savePause.playFromStart();
|
||||
}
|
||||
|
||||
private void persistCurrent() {
|
||||
if (current == null) { return; }
|
||||
saveFormToFraction(current);
|
||||
if (current.getFractionId() == null) { return; }
|
||||
try {
|
||||
FractionIO.save(current, fractionDir);
|
||||
} catch (IOException e) {
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onFractionSelected(Fraction old, Fraction nw) {
|
||||
if (old != null) saveFormToFraction(old);
|
||||
if (old != null) {
|
||||
saveFormToFraction(old);
|
||||
if (old.getFractionId() != null) {
|
||||
try { FractionIO.save(old, fractionDir); } catch (IOException e) { /* ignore on navigation */ }
|
||||
}
|
||||
}
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
@@ -217,28 +246,6 @@ public class FractionEditorView extends BorderPane {
|
||||
reload();
|
||||
}
|
||||
|
||||
private void saveCurrentFraction() {
|
||||
if (current == null) return;
|
||||
saveFormToFraction(current);
|
||||
if (current.getFractionId() == null) {
|
||||
new Alert(Alert.AlertType.ERROR,
|
||||
"Fraktion hat keine UUID – bitte neu erstellen.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
FractionIO.save(current, fractionDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
final UUID fid = current.getFractionId();
|
||||
fractions.stream()
|
||||
.filter(f -> fid.equals(f.getFractionId()))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
fractions.setAll(FractionIO.loadAll(fractionDir));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import de.blight.common.model.ItemIO;
|
||||
import de.blight.common.model.ItemSubCategory;
|
||||
import de.blight.common.model.ObjectReference;
|
||||
import de.blight.common.model.TextReference;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
@@ -18,6 +19,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -51,6 +53,12 @@ public class ItemEditorView extends BorderPane {
|
||||
private VBox consumablesSection;
|
||||
private VBox effectsRows;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> persistCurrent());
|
||||
}
|
||||
|
||||
public ItemEditorView(Path itemDir) {
|
||||
this.itemDir = itemDir;
|
||||
this.assetRoot = itemDir.getParent(); // items/ ist direkt unter assetRoot
|
||||
@@ -188,6 +196,7 @@ public class ItemEditorView extends BorderPane {
|
||||
if (wasFocused && !isFocused) onIdCommitted();
|
||||
});
|
||||
idField.setOnAction(e -> onIdCommitted());
|
||||
idField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
catCombo = new ComboBox<>();
|
||||
catCombo.getItems().addAll(ItemCategory.values());
|
||||
@@ -237,15 +246,20 @@ public class ItemEditorView extends BorderPane {
|
||||
} else {
|
||||
subCatCombo.setValue(null);
|
||||
}
|
||||
scheduleSave();
|
||||
});
|
||||
subCatCombo.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
nameField = new TextField();
|
||||
nameField.setPromptText("TextReference-Schlüssel");
|
||||
nameField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
descField = new TextField();
|
||||
descField.setPromptText("TextReference-Schlüssel");
|
||||
descField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
goldSpinner = new Spinner<>(0, 999999, 0);
|
||||
goldSpinner.setEditable(true);
|
||||
goldSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
goldSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
modelRefField = new TextField();
|
||||
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
|
||||
@@ -287,13 +301,9 @@ public class ItemEditorView extends BorderPane {
|
||||
consumableCheck.selectedProperty().addListener((obs, wasSelected, isSelected) -> {
|
||||
consumablesSection.setVisible(isSelected);
|
||||
consumablesSection.setManaged(isSelected);
|
||||
scheduleSave();
|
||||
});
|
||||
|
||||
Button saveBtn = new Button("Item speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentItem());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Item"),
|
||||
new Separator(),
|
||||
@@ -310,9 +320,7 @@ public class ItemEditorView extends BorderPane {
|
||||
row("Modell:", modelFullRow),
|
||||
new Separator(),
|
||||
consumableCheck,
|
||||
consumablesSection,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
consumablesSection
|
||||
);
|
||||
return form;
|
||||
}
|
||||
@@ -331,10 +339,32 @@ public class ItemEditorView extends BorderPane {
|
||||
if (descField.getText().isBlank()) descField.setText(id + ".description");
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() {
|
||||
savePause.playFromStart();
|
||||
}
|
||||
|
||||
private void persistCurrent() {
|
||||
if (current == null) { return; }
|
||||
saveFormToItem(current);
|
||||
if (current.getItemId() == null || current.getItemId().isBlank()) { return; }
|
||||
try {
|
||||
ItemIO.save(current, itemDir);
|
||||
} catch (IOException e) {
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onItemSelected(Item old, Item nw) {
|
||||
if (old != null) saveFormToItem(old);
|
||||
if (old != null) {
|
||||
saveFormToItem(old);
|
||||
if (old.getItemId() != null && !old.getItemId().isBlank()) {
|
||||
try { ItemIO.save(old, itemDir); } catch (IOException e) { /* ignore on navigation */ }
|
||||
}
|
||||
}
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
@@ -424,24 +454,6 @@ public class ItemEditorView extends BorderPane {
|
||||
reload();
|
||||
}
|
||||
|
||||
private void saveCurrentItem() {
|
||||
if (current == null) return;
|
||||
saveFormToItem(current);
|
||||
if (current.getItemId() == null || current.getItemId().isBlank()) {
|
||||
new Alert(Alert.AlertType.ERROR, "Item-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ItemIO.save(current, itemDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
String savedId = current.getItemId();
|
||||
reload();
|
||||
selectItem(savedId);
|
||||
}
|
||||
|
||||
// ── Effect rows ───────────────────────────────────────────────────────────
|
||||
|
||||
private void addEffectRow(CharacterStat stat, int value) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import de.blight.common.model.TextBundle;
|
||||
import de.blight.common.model.TextBundleIO;
|
||||
import de.blight.common.model.TextKeyStore;
|
||||
import de.blight.common.model.TextRegistry;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -16,6 +17,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -39,6 +41,12 @@ public class LocalizationEditorView extends BorderPane {
|
||||
private final TableView<String[]> audioTable = new TableView<>(audioData);
|
||||
private AudioBundle currentAudioBundle = null;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> saveCurrent());
|
||||
}
|
||||
|
||||
public LocalizationEditorView(Path locDir) {
|
||||
this.locDir = locDir;
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
@@ -58,13 +66,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
Button delLangBtn = new Button("- Sprache");
|
||||
delLangBtn.setOnAction(e -> deleteLanguage());
|
||||
|
||||
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),
|
||||
saveBtn);
|
||||
HBox toolbar = new HBox(8, langLbl, langCombo, addLangBtn, delLangBtn);
|
||||
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;");
|
||||
@@ -106,13 +108,13 @@ public class LocalizationEditorView extends BorderPane {
|
||||
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(); scheduleSave(); });
|
||||
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(); scheduleSave(); });
|
||||
|
||||
table.getColumns().addAll(List.of(keyCol, valCol));
|
||||
VBox.setVgrow(table, Priority.ALWAYS);
|
||||
@@ -150,13 +152,13 @@ public class LocalizationEditorView extends BorderPane {
|
||||
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.setOnEditCommit(e -> { e.getRowValue()[0] = e.getNewValue().trim(); audioTable.refresh(); scheduleSave(); });
|
||||
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(); });
|
||||
pathCol.setOnEditCommit(e -> { e.getRowValue()[1] = e.getNewValue(); audioTable.refresh(); scheduleSave(); });
|
||||
|
||||
TableColumn<String[], Void> browseCol = new TableColumn<>("");
|
||||
browseCol.setPrefWidth(36);
|
||||
@@ -179,6 +181,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
if (file != null) {
|
||||
audioData.get(idx)[1] = file.getAbsolutePath();
|
||||
audioTable.refresh();
|
||||
scheduleSave();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -234,6 +237,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
|
||||
private void addLanguage() {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setTitle("Sprache hinzufuegen");
|
||||
dlg.setHeaderText("Sprach-Code (z. B. de, en, fr):");
|
||||
dlg.showAndWait().ifPresent(lang -> {
|
||||
@@ -245,7 +249,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
refreshLangList();
|
||||
langCombo.setValue(lang);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -253,7 +257,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
private void deleteLanguage() {
|
||||
String lang = langCombo.getValue();
|
||||
if (lang == null) return;
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
Alert confirm = Dialogs.alert(Alert.AlertType.CONFIRMATION,
|
||||
"Sprache '" + lang + "' wirklich loeschen?", ButtonType.YES, ButtonType.NO);
|
||||
confirm.showAndWait().ifPresent(bt -> {
|
||||
if (bt == ButtonType.YES) {
|
||||
@@ -266,7 +270,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
audioData.clear();
|
||||
refreshLangList();
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -282,6 +286,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
|
||||
if (available.isEmpty()) {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
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);
|
||||
@@ -289,6 +294,7 @@ public class LocalizationEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setTitle("Schluessel waehlen");
|
||||
dlg.setHeaderText("Noch nicht uebersetzte Schluessel:");
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
@@ -315,36 +321,45 @@ public class LocalizationEditorView extends BorderPane {
|
||||
tableData.add(new String[]{key, ""});
|
||||
table.scrollTo(tableData.size() - 1);
|
||||
table.getSelectionModel().select(tableData.size() - 1);
|
||||
scheduleSave();
|
||||
}
|
||||
|
||||
private void deleteKey() {
|
||||
String[] sel = table.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) tableData.remove(sel);
|
||||
if (sel != null) { tableData.remove(sel); scheduleSave(); }
|
||||
}
|
||||
|
||||
// Audio-Eintrags-Verwaltung
|
||||
|
||||
private void addAudioKey() {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
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);
|
||||
scheduleSave();
|
||||
});
|
||||
}
|
||||
|
||||
private void deleteAudioKey() {
|
||||
String[] sel = audioTable.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) audioData.remove(sel);
|
||||
if (sel != null) { audioData.remove(sel); scheduleSave(); }
|
||||
}
|
||||
|
||||
// Auto-save
|
||||
|
||||
private void scheduleSave() {
|
||||
savePause.playFromStart();
|
||||
}
|
||||
|
||||
// Speichern
|
||||
|
||||
private void saveCurrent() {
|
||||
if (currentBundle == null) {
|
||||
new Alert(Alert.AlertType.WARNING, "Keine Sprache ausgewaehlt.", ButtonType.OK).showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.WARNING, "Keine Sprache ausgewaehlt.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
Map<String, String> textEntries = new LinkedHashMap<>();
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.blight.editor.ui;
|
||||
import de.blight.common.LocationIO;
|
||||
import de.blight.common.model.Location;
|
||||
import de.blight.common.model.TextReference;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -10,32 +11,29 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Locations-Verwaltung: Liste links, Formular rechts.
|
||||
* Alle Locations werden gemeinsam in einer Datei gespeichert (LocationIO).
|
||||
*/
|
||||
public class LocationEditorView extends BorderPane {
|
||||
|
||||
private final ObservableList<Location> locations = FXCollections.observableArrayList();
|
||||
private static final String PREFIX = "location.";
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────────────────
|
||||
private final ObservableList<Location> locations = FXCollections.observableArrayList();
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
private ListView<Location> listView;
|
||||
private Button deleteBtn;
|
||||
private Location current = null;
|
||||
private Location current = null;
|
||||
private boolean reloading = false;
|
||||
|
||||
// ── Form fields ───────────────────────────────────────────────────────────
|
||||
|
||||
private TextField nameIdField;
|
||||
private TextField centerXField;
|
||||
private TextField centerZField;
|
||||
private TextField radiusField;
|
||||
private TextField idField;
|
||||
private Label nameKeyLabel;
|
||||
private TriggerListEditor triggerEditor;
|
||||
private VBox formContainer;
|
||||
private VBox formContainer;
|
||||
|
||||
{ savePause.setOnFinished(e -> persistCurrent()); }
|
||||
|
||||
public LocationEditorView() {
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
@@ -46,6 +44,18 @@ public class LocationEditorView extends BorderPane {
|
||||
setCenter(split);
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() { savePause.playFromStart(); }
|
||||
|
||||
private void persistCurrent() {
|
||||
if (current == null || reloading) return;
|
||||
saveFormToLocation(current);
|
||||
if (current.getId() == null || current.getId().isBlank()) return;
|
||||
persist();
|
||||
listView.refresh();
|
||||
}
|
||||
|
||||
// ── List panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildListPanel() {
|
||||
@@ -55,7 +65,9 @@ public class LocationEditorView extends BorderPane {
|
||||
@Override protected void updateItem(Location loc, boolean empty) {
|
||||
super.updateItem(loc, empty);
|
||||
if (empty || loc == null) { setText(null); setStyle(""); return; }
|
||||
setText(loc.getId().isBlank() ? "—" : loc.getId());
|
||||
String shortId = loc.getId().startsWith(PREFIX)
|
||||
? loc.getId().substring(PREFIX.length()) : loc.getId();
|
||||
setText(shortId.isBlank() ? "—" : shortId);
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent #66aacc;"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
@@ -108,30 +120,28 @@ public class LocationEditorView extends BorderPane {
|
||||
form.setPadding(new Insets(12));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
nameIdField = field("z. B. location.village");
|
||||
centerXField = field("X-Koordinate");
|
||||
centerZField = field("Z-Koordinate");
|
||||
radiusField = field("Radius in Meter");
|
||||
idField = field("z. B. village");
|
||||
nameKeyLabel = new Label("Name-Key: —");
|
||||
nameKeyLabel.setStyle("-fx-text-fill: #aaaaaa; -fx-font-style: italic; -fx-font-size: 11;");
|
||||
idField.textProperty().addListener((obs, o, n) -> {
|
||||
String key = n == null || n.isBlank() ? "" : PREFIX + n.trim() + ".name";
|
||||
nameKeyLabel.setText("Name-Key: " + (key.isBlank() ? "—" : key));
|
||||
scheduleSave();
|
||||
});
|
||||
idField.focusedProperty().addListener((obs, was, is) -> {
|
||||
if (!is) { savePause.stop(); persistCurrent(); }
|
||||
});
|
||||
|
||||
triggerEditor = new TriggerListEditor(List.of(), () -> {});
|
||||
|
||||
Button saveBtn = new Button("Location speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrent());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Kennung & Position"),
|
||||
sectionTitle("Kennung"),
|
||||
new Separator(),
|
||||
row("Name-ID:", nameIdField),
|
||||
row("Mitte X:", centerXField),
|
||||
row("Mitte Z:", centerZField),
|
||||
row("Radius:", radiusField),
|
||||
row("ID:", idField),
|
||||
nameKeyLabel,
|
||||
sectionTitle("Trigger"),
|
||||
new Separator(),
|
||||
triggerEditor,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
triggerEditor
|
||||
);
|
||||
return form;
|
||||
}
|
||||
@@ -139,7 +149,8 @@ public class LocationEditorView extends BorderPane {
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onSelected(Location old, Location nw) {
|
||||
if (old != null) saveFormToLocation(old);
|
||||
if (reloading) return;
|
||||
if (old != null) { saveFormToLocation(old); persist(); }
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) { formContainer.setDisable(false); loadForm(nw); }
|
||||
@@ -147,38 +158,34 @@ public class LocationEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
private void loadForm(Location loc) {
|
||||
nameIdField.setText(loc.getId());
|
||||
centerXField.setText(String.valueOf(loc.getCenterX()));
|
||||
centerZField.setText(String.valueOf(loc.getCenterZ()));
|
||||
radiusField.setText(String.valueOf(loc.getRadius()));
|
||||
String fullId = loc.getId();
|
||||
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
|
||||
idField.setText(shortId);
|
||||
nameKeyLabel.setText("Name-Key: " + (shortId.isBlank() ? "—" : PREFIX + shortId + ".name"));
|
||||
|
||||
int idx = formContainer.getChildren().indexOf(triggerEditor);
|
||||
triggerEditor = new TriggerListEditor(
|
||||
loc.getTriggers() != null ? loc.getTriggers() : List.of(), () -> {});
|
||||
loc.getTriggers() != null ? loc.getTriggers() : List.of(), () -> scheduleSave());
|
||||
if (idx >= 0) formContainer.getChildren().set(idx, triggerEditor);
|
||||
}
|
||||
|
||||
private void saveFormToLocation(Location loc) {
|
||||
String nameId = nameIdField.getText().trim();
|
||||
loc.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
||||
loc.setCenterX(parseFloat(centerXField.getText()));
|
||||
loc.setCenterZ(parseFloat(centerZField.getText()));
|
||||
loc.setRadius(parseFloat(radiusField.getText()));
|
||||
String shortId = idField.getText().trim();
|
||||
String fullId = shortId.isBlank() ? "" : PREFIX + shortId;
|
||||
loc.setName(fullId.isBlank() ? null : new TextReference(fullId));
|
||||
loc.setTriggers(triggerEditor.getTriggers());
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
nameIdField.clear();
|
||||
centerXField.clear();
|
||||
centerZField.clear();
|
||||
radiusField.clear();
|
||||
idField.clear();
|
||||
nameKeyLabel.setText("Name-Key: —");
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
|
||||
private void createLocation() {
|
||||
Location loc = new Location();
|
||||
loc.setName(new TextReference("location.neu_" + System.currentTimeMillis()));
|
||||
loc.setName(new TextReference(PREFIX + "neu_" + System.currentTimeMillis()));
|
||||
locations.add(loc);
|
||||
listView.getSelectionModel().select(loc);
|
||||
}
|
||||
@@ -194,26 +201,10 @@ public class LocationEditorView extends BorderPane {
|
||||
reload();
|
||||
}
|
||||
|
||||
private void saveCurrent() {
|
||||
if (current == null) return;
|
||||
saveFormToLocation(current);
|
||||
if (current.getId().isBlank()) {
|
||||
new Alert(Alert.AlertType.ERROR, "Name-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
String savedId = current.getId();
|
||||
persist();
|
||||
reload();
|
||||
locations.stream()
|
||||
.filter(l -> savedId.equals(l.getId()))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
private void persist() {
|
||||
try { LocationIO.save(locations); }
|
||||
catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,11 +215,6 @@ public class LocationEditorView extends BorderPane {
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static float parseFloat(String s) {
|
||||
try { return Float.parseFloat(s.trim().replace(',', '.')); }
|
||||
catch (NumberFormatException ignored) { return 0f; }
|
||||
}
|
||||
|
||||
private static Label sectionTitle(String text) {
|
||||
Label l = new Label(text);
|
||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
||||
@@ -237,7 +223,7 @@ public class LocationEditorView extends BorderPane {
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(80);
|
||||
lbl.setMinWidth(50);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
|
||||
@@ -115,11 +115,9 @@ public class MapObjectsView extends VBox {
|
||||
// ── Löschen ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void confirmAndDelete(String label, Entry entry) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
Alert alert = Dialogs.alert(Alert.AlertType.CONFIRMATION, label, ButtonType.YES, ButtonType.NO);
|
||||
alert.setTitle("Objekt löschen");
|
||||
alert.setHeaderText("Objekt wirklich löschen?");
|
||||
alert.setContentText(label);
|
||||
alert.getButtonTypes().setAll(ButtonType.YES, ButtonType.NO);
|
||||
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
if (result.isEmpty() || result.get() != ButtonType.YES) return;
|
||||
@@ -128,8 +126,7 @@ public class MapObjectsView extends VBox {
|
||||
onDelete.execute(entry.placedObj(), entry.index(), entry.toolHint());
|
||||
refresh();
|
||||
} catch (Exception ex) {
|
||||
Alert err = new Alert(Alert.AlertType.ERROR, "Fehler: " + ex.getMessage(), ButtonType.OK);
|
||||
err.showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + ex.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,12 +240,12 @@ public class MapObjectsView extends VBox {
|
||||
private void loadAreas() {
|
||||
try {
|
||||
List<PlacedArea> list = AreaIO.load();
|
||||
TreeItem<String> group = group("Bereiche", list.size());
|
||||
TreeItem<String> group = group("Areas", list.size());
|
||||
for (int idx = 0; idx < list.size(); idx++) {
|
||||
PlacedArea a = list.get(idx);
|
||||
float cx = centroid(a.pointsX()), cz = centroid(a.pointsZ());
|
||||
String label = a.nameId() != null && !a.nameId().isBlank()
|
||||
? a.nameId() : "Bereich #" + (idx + 1);
|
||||
String label = a.areaId() != null && !a.areaId().isBlank()
|
||||
? a.areaId() : "Area #" + (idx + 1);
|
||||
TreeItem<String> item = leaf(label + " " + posXZ(cx, cz));
|
||||
entryMap.put(item, new Entry(cx, Float.NaN, cz, "area", a, idx));
|
||||
group.getChildren().add(item);
|
||||
|
||||
@@ -34,6 +34,7 @@ public class MaterialChooser extends Dialog<String> {
|
||||
public MaterialChooser(Path matDefsRoot) {
|
||||
setTitle("Material auswählen");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
VBox contentBox = new VBox(12);
|
||||
|
||||
@@ -39,6 +39,7 @@ public class ModelChooser extends Dialog<String> {
|
||||
this.assetRoot = assetRoot;
|
||||
setTitle("Modell auswählen");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
contentBox.setPadding(new Insets(4));
|
||||
|
||||
@@ -237,7 +237,7 @@ public class MonologueEditorView extends SplitPane {
|
||||
try {
|
||||
MonologueIO.save(new ArrayList<>(monologues));
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ public class MonologueEditorView extends SplitPane {
|
||||
private void deleteMonologue() {
|
||||
Monologue sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
Alert confirm = Dialogs.alert(Alert.AlertType.CONFIRMATION,
|
||||
"Monolog '" + sel.getId() + "' wirklich löschen?", ButtonType.YES, ButtonType.NO);
|
||||
confirm.showAndWait().ifPresent(bt -> {
|
||||
if (bt == ButtonType.YES) {
|
||||
@@ -296,6 +296,7 @@ public class MonologueEditorView extends SplitPane {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Quest auswählen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
|
||||
ListView<Quest> chooser = new ListView<>();
|
||||
chooser.setCellFactory(lv -> new ListCell<>() {
|
||||
|
||||
@@ -7,6 +7,7 @@ import de.blight.common.model.NPC;
|
||||
import de.blight.common.model.Location;
|
||||
import de.blight.common.model.TextReference;
|
||||
import de.blight.common.model.quests.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -14,6 +15,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -43,13 +45,19 @@ public class QuestEditorView extends BorderPane {
|
||||
private ComboBox<String> typeCombo;
|
||||
private VBox dynamicArea;
|
||||
private VBox formContainer;
|
||||
private Button saveBtn;
|
||||
|
||||
// ── Type-specific fields ──────────────────────────────────────────────────
|
||||
|
||||
private TextField f1, f2, f3;
|
||||
private Spinner<Integer> countSpinner;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
private boolean reloading = false;
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> persistCurrent());
|
||||
}
|
||||
|
||||
public QuestEditorView(Path questDir) {
|
||||
this.questDir = questDir;
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
@@ -134,17 +142,22 @@ public class QuestEditorView extends BorderPane {
|
||||
idField.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
|
||||
if (!isFocused) autoFillTextRefs();
|
||||
});
|
||||
idField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
xpSpinner = new Spinner<>(0, 99999, 0);
|
||||
xpSpinner.setEditable(true);
|
||||
xpSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
xpSpinner.valueProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
textField = new TextField();
|
||||
textField.setPromptText("TextReference-Schlüssel");
|
||||
textField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
descField = new TextField();
|
||||
descField.setPromptText("TextReference-Schlüssel");
|
||||
descField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
successField = new TextField();
|
||||
successField.setPromptText("TextReference-Schlüssel");
|
||||
successField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Quest"),
|
||||
@@ -163,7 +176,7 @@ public class QuestEditorView extends BorderPane {
|
||||
typeCombo.getItems().addAll("BringQuest", "FollowQuest", "InteractQuest", "ItemQuest", "TalkQuest");
|
||||
typeCombo.setPromptText("Typ auswählen…");
|
||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
typeCombo.setOnAction(e -> rebuildDynamicArea(typeCombo.getValue()));
|
||||
typeCombo.setOnAction(e -> { rebuildDynamicArea(typeCombo.getValue()); scheduleSave(); });
|
||||
|
||||
dynamicArea = new VBox(6);
|
||||
|
||||
@@ -174,12 +187,6 @@ public class QuestEditorView extends BorderPane {
|
||||
new Separator()
|
||||
);
|
||||
|
||||
saveBtn = new Button("Quest speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentQuest());
|
||||
form.getChildren().add(saveBtn);
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
@@ -243,10 +250,40 @@ public class QuestEditorView extends BorderPane {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() {
|
||||
savePause.playFromStart();
|
||||
}
|
||||
|
||||
private void persistCurrent() {
|
||||
if (current == null || reloading) { return; }
|
||||
Quest built = buildQuestFromForm();
|
||||
if (built == null || built.getQuestId() == null || built.getQuestId().isBlank()) { return; }
|
||||
int idx = quests.indexOf(current);
|
||||
if (idx >= 0) { quests.set(idx, built); }
|
||||
else { quests.add(built); }
|
||||
current = built;
|
||||
try {
|
||||
QuestIO.save(built, questDir);
|
||||
} catch (IOException e) {
|
||||
showError("Fehler beim Speichern: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onQuestSelected(Quest old, Quest nw) {
|
||||
if (old != null) saveFormToQuest(old);
|
||||
if (reloading) { return; }
|
||||
if (old != null) {
|
||||
saveFormToQuest(old);
|
||||
Quest built = buildQuestFromForm();
|
||||
if (built != null && built.getQuestId() != null && !built.getQuestId().isBlank()) {
|
||||
int idx = quests.indexOf(old);
|
||||
if (idx >= 0) { quests.set(idx, built); }
|
||||
try { QuestIO.save(built, questDir); } catch (IOException e) { /* ignore on navigation */ }
|
||||
}
|
||||
}
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
@@ -406,30 +443,6 @@ public class QuestEditorView extends BorderPane {
|
||||
reload();
|
||||
}
|
||||
|
||||
private void saveCurrentQuest() {
|
||||
Quest built = buildQuestFromForm();
|
||||
if (built == null) {
|
||||
showError("Bitte einen Typ wählen.");
|
||||
return;
|
||||
}
|
||||
if (built.getQuestId() == null || built.getQuestId().isBlank()) {
|
||||
showError("Quest-ID darf nicht leer sein.");
|
||||
return;
|
||||
}
|
||||
int idx = quests.indexOf(current);
|
||||
if (idx >= 0) quests.set(idx, built);
|
||||
else quests.add(built);
|
||||
current = built;
|
||||
listView.getSelectionModel().select(built);
|
||||
try {
|
||||
QuestIO.save(built, questDir);
|
||||
} catch (IOException e) {
|
||||
showError("Fehler beim Speichern: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
List<Quest> loaded = QuestIO.loadAll(questDir);
|
||||
quests.setAll(loaded);
|
||||
@@ -467,7 +480,6 @@ public class QuestEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
private void showError(String msg) {
|
||||
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
|
||||
a.showAndWait();
|
||||
Dialogs.alert(Alert.AlertType.ERROR, msg, ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.model.*;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
@@ -10,6 +11,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -49,6 +51,12 @@ public class RecipeEditorView extends BorderPane {
|
||||
private Spinner<Integer> engineeringSpinner;
|
||||
private VBox formContainer;
|
||||
|
||||
private final PauseTransition savePause = new PauseTransition(Duration.millis(600));
|
||||
|
||||
{
|
||||
savePause.setOnFinished(e -> persistCurrent());
|
||||
}
|
||||
|
||||
public RecipeEditorView(Path recipeDir) {
|
||||
this.recipeDir = recipeDir;
|
||||
this.sortedRecipes = new SortedList<>(recipes, RecipeIO.SORT_ORDER);
|
||||
@@ -129,6 +137,7 @@ public class RecipeEditorView extends BorderPane {
|
||||
|
||||
createsField = new TextField();
|
||||
createsField.setPromptText("Item-ID des erstellten Items");
|
||||
createsField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
componentsList = new ListView<>();
|
||||
componentsList.setPrefHeight(110);
|
||||
@@ -156,7 +165,7 @@ public class RecipeEditorView extends BorderPane {
|
||||
}
|
||||
tableCombo.setValue(NO_TABLE);
|
||||
tableCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
tableCombo.setOnAction(e -> updateRequirementRows(tableCombo.getValue()));
|
||||
tableCombo.setOnAction(e -> { updateRequirementRows(tableCombo.getValue()); scheduleSave(); });
|
||||
|
||||
alchemySpinner = lvlSpinner();
|
||||
enchantingSpinner = lvlSpinner();
|
||||
@@ -168,11 +177,6 @@ public class RecipeEditorView extends BorderPane {
|
||||
smitheryRow = requirementRow("Lvl Schmieden:", smitherySpinner);
|
||||
engineeringRow = requirementRow("Lvl Engineering:", engineeringSpinner);
|
||||
|
||||
Button saveBtn = new Button("Rezept speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentRecipe());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Ergebnis"),
|
||||
new Separator(),
|
||||
@@ -186,9 +190,7 @@ public class RecipeEditorView extends BorderPane {
|
||||
alchemyRow,
|
||||
enchantingRow,
|
||||
smitheryRow,
|
||||
engineeringRow,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
engineeringRow
|
||||
);
|
||||
|
||||
updateRequirementRows(NO_TABLE);
|
||||
@@ -216,10 +218,38 @@ public class RecipeEditorView extends BorderPane {
|
||||
row.setManaged(visible);
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() {
|
||||
savePause.playFromStart();
|
||||
}
|
||||
|
||||
private void persistCurrent() {
|
||||
if (current == null) { return; }
|
||||
saveFormToRecipe(current);
|
||||
String newFileId = RecipeIO.fileId(current);
|
||||
if (newFileId.startsWith("unbenanntes")) { return; }
|
||||
try {
|
||||
if (oldFileId != null && !oldFileId.equals(newFileId)) {
|
||||
RecipeIO.delete(oldFileId, recipeDir);
|
||||
}
|
||||
RecipeIO.save(current, recipeDir);
|
||||
oldFileId = newFileId;
|
||||
} catch (IOException e) {
|
||||
Dialogs.alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onRecipeSelected(Recipe old, Recipe nw) {
|
||||
if (old != null) saveFormToRecipe(old);
|
||||
if (old != null) {
|
||||
saveFormToRecipe(old);
|
||||
String fid = RecipeIO.fileId(old);
|
||||
if (!fid.startsWith("unbenanntes")) {
|
||||
try { RecipeIO.save(old, recipeDir); } catch (IOException e) { /* ignore on navigation */ }
|
||||
}
|
||||
}
|
||||
current = nw;
|
||||
oldFileId = nw != null ? RecipeIO.fileId(nw) : null;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
@@ -342,38 +372,11 @@ public class RecipeEditorView extends BorderPane {
|
||||
reload();
|
||||
}
|
||||
|
||||
private void saveCurrentRecipe() {
|
||||
if (current == null) return;
|
||||
saveFormToRecipe(current);
|
||||
|
||||
String newFileId = RecipeIO.fileId(current);
|
||||
if (newFileId.startsWith("unbenanntes")) {
|
||||
new Alert(Alert.AlertType.ERROR,
|
||||
"Item-ID des erstellten Items darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (oldFileId != null && !oldFileId.equals(newFileId)) {
|
||||
RecipeIO.delete(oldFileId, recipeDir);
|
||||
}
|
||||
RecipeIO.save(current, recipeDir);
|
||||
oldFileId = newFileId;
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
final String fid = newFileId;
|
||||
recipes.stream()
|
||||
.filter(r -> fid.equals(RecipeIO.fileId(r)))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
private void addComponent() {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Zutat hinzufügen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
|
||||
TextField itemIdField = new TextField();
|
||||
itemIdField.setPromptText("Item-ID");
|
||||
|
||||
@@ -462,6 +462,7 @@ public class RoutineEditorView extends VBox {
|
||||
|
||||
private void addRoutine() {
|
||||
TextInputDialog dlg = new TextInputDialog("Routine " + (routines.size() + 1));
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setHeaderText("Name des Tagesablaufs:");
|
||||
dlg.showAndWait().ifPresent(name -> {
|
||||
if (name.isBlank()) return;
|
||||
@@ -475,6 +476,7 @@ public class RoutineEditorView extends VBox {
|
||||
private void renameRoutine() {
|
||||
if (activeRoutine == null) return;
|
||||
TextInputDialog dlg = new TextInputDialog(activeRoutine.getName());
|
||||
dlg.initOwner(Dialogs.primaryWindow());
|
||||
dlg.setHeaderText("Neuer Name:");
|
||||
dlg.showAndWait().ifPresent(name -> {
|
||||
if (!name.isBlank()) {
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.stage.Modality;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* List-based sound/music picker dialog.
|
||||
*
|
||||
* Two modes:
|
||||
* MUSIC — scans only {@code audio/music/} inside the asset root.
|
||||
* ALL — scans the entire {@code audio/} tree.
|
||||
*
|
||||
* Returns the selected JME asset path (relative to asset root) or {@code null} on cancel.
|
||||
* Double-click or OK confirms the selection; a small play button previews the file.
|
||||
*/
|
||||
public class SoundChooser extends Dialog<String> {
|
||||
|
||||
public enum Mode { MUSIC, ALL }
|
||||
|
||||
private static final Set<String> AUDIO_EXTS = Set.of(".ogg", ".wav", ".mp3");
|
||||
|
||||
private final List<String> allPaths = new ArrayList<>();
|
||||
private final ListView<String> listView = new ListView<>();
|
||||
private final TextField filterField = new TextField();
|
||||
private MediaPlayer preview = null;
|
||||
private String selected = null;
|
||||
|
||||
public SoundChooser(Path assetRoot, Mode mode) {
|
||||
setTitle(mode == Mode.MUSIC ? "Musik auswählen" : "Sound auswählen");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
// ── scan files ────────────────────────────────────────────────────────
|
||||
if (assetRoot != null) {
|
||||
Path scanRoot = mode == Mode.MUSIC
|
||||
? assetRoot.resolve("audio/music")
|
||||
: assetRoot.resolve("audio");
|
||||
if (Files.isDirectory(scanRoot)) {
|
||||
try (Stream<Path> walk = Files.walk(scanRoot)) {
|
||||
walk.filter(Files::isRegularFile)
|
||||
.filter(p -> isAudioFile(p.getFileName().toString()))
|
||||
.sorted(Comparator.comparing(p -> assetRoot.relativize(p).toString().toLowerCase()))
|
||||
.forEach(p -> allPaths.add(assetRoot.relativize(p).toString().replace('\\', '/')));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
listView.getItems().setAll(allPaths);
|
||||
listView.setPrefHeight(420);
|
||||
|
||||
// ── filter ────────────────────────────────────────────────────────────
|
||||
filterField.setPromptText("Filtern…");
|
||||
filterField.textProperty().addListener((obs, o, n) -> applyFilter(n == null ? "" : n));
|
||||
|
||||
// ── preview bar ───────────────────────────────────────────────────────
|
||||
Button playBtn = new Button("▶");
|
||||
Button stopBtn = new Button("■");
|
||||
stopBtn.setDisable(true);
|
||||
Label previewLbl = new Label("(nichts gewählt)");
|
||||
previewLbl.setStyle("-fx-text-fill: #666; -fx-font-size: 11;");
|
||||
|
||||
playBtn.setOnAction(e -> {
|
||||
stopPreview();
|
||||
String sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null || assetRoot == null) return;
|
||||
Path file = assetRoot.resolve(sel.replace('/', java.io.File.separatorChar));
|
||||
try {
|
||||
Media media = new Media(file.toUri().toString());
|
||||
preview = new MediaPlayer(media);
|
||||
preview.setOnEndOfMedia(() -> {
|
||||
stopBtn.setDisable(true);
|
||||
playBtn.setDisable(false);
|
||||
});
|
||||
preview.play();
|
||||
stopBtn.setDisable(false);
|
||||
playBtn.setDisable(true);
|
||||
} catch (Exception ex) {
|
||||
previewLbl.setText("Fehler: " + ex.getMessage());
|
||||
}
|
||||
});
|
||||
stopBtn.setOnAction(e -> {
|
||||
stopPreview();
|
||||
stopBtn.setDisable(true);
|
||||
playBtn.setDisable(false);
|
||||
});
|
||||
|
||||
HBox previewBar = new HBox(6, playBtn, stopBtn, previewLbl);
|
||||
previewBar.setAlignment(Pos.CENTER_LEFT);
|
||||
previewBar.setPadding(new Insets(4, 0, 0, 0));
|
||||
|
||||
// ── empty hint ────────────────────────────────────────────────────────
|
||||
if (allPaths.isEmpty()) {
|
||||
String hint = mode == Mode.MUSIC
|
||||
? "Keine Musik gefunden.\nDateien nach audio/music/ importieren."
|
||||
: "Keine Audio-Dateien gefunden.";
|
||||
listView.setPlaceholder(new Label(hint));
|
||||
}
|
||||
|
||||
// ── layout ────────────────────────────────────────────────────────────
|
||||
VBox root = new VBox(8, filterField, listView, previewBar);
|
||||
root.setPadding(new Insets(10));
|
||||
|
||||
getDialogPane().setContent(root);
|
||||
getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
getDialogPane().setPrefSize(480, 540);
|
||||
|
||||
// ── selection tracking (buttons must exist before lookup) ─────────────
|
||||
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||
if (okBtn != null) okBtn.setDisable(true);
|
||||
|
||||
listView.getSelectionModel().selectedItemProperty().addListener((obs, o, n) -> {
|
||||
selected = n;
|
||||
previewLbl.setText(n != null ? lastName(n) : "(nichts gewählt)");
|
||||
stopPreview(); stopBtn.setDisable(true); playBtn.setDisable(n == null);
|
||||
if (okBtn != null) okBtn.setDisable(n == null);
|
||||
});
|
||||
|
||||
listView.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() == 2 && selected != null) {
|
||||
if (okBtn != null) okBtn.fire();
|
||||
}
|
||||
});
|
||||
|
||||
setOnHidden(e -> stopPreview());
|
||||
setResultConverter(btn -> btn == ButtonType.OK ? selected : null);
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void applyFilter(String raw) {
|
||||
String lo = raw.toLowerCase();
|
||||
if (lo.isBlank()) {
|
||||
listView.getItems().setAll(allPaths);
|
||||
} else {
|
||||
listView.getItems().setAll(
|
||||
allPaths.stream().filter(p -> p.toLowerCase().contains(lo)).toList());
|
||||
}
|
||||
}
|
||||
|
||||
private void stopPreview() {
|
||||
if (preview != null) {
|
||||
preview.stop();
|
||||
preview.dispose();
|
||||
preview = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAudioFile(String name) {
|
||||
String lo = name.toLowerCase();
|
||||
return AUDIO_EXTS.stream().anyMatch(lo::endsWith);
|
||||
}
|
||||
|
||||
private static String lastName(String path) {
|
||||
int i = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||
return i >= 0 ? path.substring(i + 1) : path;
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ public class TextureChooser extends Dialog<String> {
|
||||
public TextureChooser(Path assetRoot, boolean includeJmeBuiltin) {
|
||||
setTitle("Textur auswählen");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
contentBox.setPadding(new Insets(4));
|
||||
|
||||
@@ -5,6 +5,8 @@ import de.blight.common.model.Monologue;
|
||||
import de.blight.common.model.QuestRef;
|
||||
import de.blight.common.model.Status;
|
||||
import de.blight.common.model.trigger.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -12,19 +14,9 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Modality;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Modal-Dialog zum Anlegen oder Bearbeiten eines {@link Trigger}.
|
||||
*
|
||||
* <p>Ablauf:
|
||||
* <ol>
|
||||
* <li>Typ-ComboBox wählen</li>
|
||||
* <li>Typ-spezifische Felder füllen</li>
|
||||
* <li>Kapitel-Anforderung (optional)</li>
|
||||
* </ol>
|
||||
* Rückgabewert: der fertig gebaute {@link Trigger} oder {@code null} bei Abbruch.
|
||||
*/
|
||||
public class TriggerDialog extends Dialog<Trigger> {
|
||||
|
||||
private static final String TYPE_QUEST = "Quest starten";
|
||||
@@ -32,6 +24,7 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
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";
|
||||
private static final String TYPE_NPC_DIALOG = "NPC-Dialog starten";
|
||||
|
||||
// Gemeinsam
|
||||
private final ComboBox<String> typeCombo = new ComboBox<>();
|
||||
@@ -56,33 +49,89 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
// Monolog
|
||||
private ComboBox<String> monologueIdCombo;
|
||||
|
||||
/** Öffnet den Dialog für einen neuen Trigger. */
|
||||
// NPC-Dialog
|
||||
private TextField npcDialogIdField;
|
||||
|
||||
// Bedingungen
|
||||
private final ComboBox<String> conditionModeCombo = new ComboBox<>();
|
||||
private final ObservableList<Condition> conditions = FXCollections.observableArrayList();
|
||||
private final ListView<Condition> conditionList = new ListView<>(conditions);
|
||||
|
||||
public TriggerDialog() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
/** Öffnet den Dialog im Bearbeitungsmodus mit einem vorhandenen Trigger. */
|
||||
public TriggerDialog(Trigger existing) {
|
||||
setTitle(existing == null ? "Trigger hinzufügen" : "Trigger bearbeiten");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
typeCombo.getItems().addAll(TYPE_QUEST, TYPE_NPC, TYPE_FRACTION, TYPE_ROUTINE, TYPE_MONOLOGUE);
|
||||
typeCombo.getItems().addAll(TYPE_QUEST, TYPE_NPC, TYPE_FRACTION, TYPE_ROUTINE,
|
||||
TYPE_MONOLOGUE, TYPE_NPC_DIALOG);
|
||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
typeCombo.setOnAction(e -> rebuildDynamic(typeCombo.getValue()));
|
||||
|
||||
chapterSpinner.setEditable(true);
|
||||
chapterSpinner.setPrefWidth(80);
|
||||
|
||||
conditionModeCombo.getItems().addAll("ALL – alle müssen gelten", "ANY – mindestens eine");
|
||||
conditionModeCombo.setValue("ALL – alle müssen gelten");
|
||||
conditionModeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
conditionList.setPrefHeight(90);
|
||||
conditionList.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Condition c, boolean empty) {
|
||||
super.updateItem(c, empty);
|
||||
setText(empty || c == null ? null : ConditionDialog.describe(c));
|
||||
setStyle(empty ? "" : "-fx-font-size: 11;");
|
||||
}
|
||||
});
|
||||
|
||||
Button addCondBtn = new Button("+");
|
||||
Button editCondBtn = new Button("✎");
|
||||
Button delCondBtn = new Button("-");
|
||||
editCondBtn.setDisable(true);
|
||||
delCondBtn.setDisable(true);
|
||||
conditionList.getSelectionModel().selectedItemProperty().addListener((obs, o, n) -> {
|
||||
boolean sel = n != null;
|
||||
editCondBtn.setDisable(!sel);
|
||||
delCondBtn.setDisable(!sel);
|
||||
});
|
||||
addCondBtn.setOnAction(e ->
|
||||
new ConditionDialog().showAndWait().ifPresent(conditions::add));
|
||||
editCondBtn.setOnAction(e -> {
|
||||
Condition sel = conditionList.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
new ConditionDialog(sel).showAndWait().ifPresent(updated -> {
|
||||
int idx = conditions.indexOf(sel);
|
||||
if (idx >= 0) conditions.set(idx, updated);
|
||||
});
|
||||
});
|
||||
delCondBtn.setOnAction(e -> {
|
||||
Condition sel = conditionList.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) conditions.remove(sel);
|
||||
});
|
||||
|
||||
HBox condButtons = new HBox(4, addCondBtn, editCondBtn, delCondBtn);
|
||||
condButtons.setPadding(new Insets(2, 0, 0, 0));
|
||||
|
||||
Label condSectionLbl = new Label("Bedingungen");
|
||||
condSectionLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 12;");
|
||||
|
||||
VBox content = new VBox(10);
|
||||
content.setPadding(new Insets(16));
|
||||
content.setPrefWidth(400);
|
||||
|
||||
content.setPrefWidth(420);
|
||||
content.getChildren().addAll(
|
||||
row("Trigger-Typ:", typeCombo),
|
||||
row("Trigger-Typ:", typeCombo),
|
||||
row("Kapitel (mind.):", chapterSpinner),
|
||||
new Separator(),
|
||||
dynamicArea
|
||||
dynamicArea,
|
||||
new Separator(),
|
||||
condSectionLbl,
|
||||
row("Verknüpfung:", conditionModeCombo),
|
||||
conditionList,
|
||||
condButtons
|
||||
);
|
||||
|
||||
getDialogPane().setContent(content);
|
||||
@@ -90,14 +139,10 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
|
||||
Button okBtn = (Button) getDialogPane().lookupButton(ButtonType.OK);
|
||||
okBtn.setDisable(true);
|
||||
|
||||
// Validierung: OK nur wenn Typ gewählt und Pflichtfelder gefüllt
|
||||
typeCombo.valueProperty().addListener((obs, o, n) -> okBtn.setDisable(n == null));
|
||||
|
||||
// Ergebnis-Konverter
|
||||
setResultConverter(bt -> bt == ButtonType.OK ? buildTrigger() : null);
|
||||
|
||||
// Vorhandenen Trigger laden
|
||||
if (existing != null) preload(existing);
|
||||
else typeCombo.setValue(TYPE_QUEST);
|
||||
}
|
||||
@@ -108,20 +153,18 @@ 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_MONOLOGUE -> buildMonologueFields();
|
||||
case TYPE_QUEST -> buildQuestFields();
|
||||
case TYPE_NPC -> buildNpcFields();
|
||||
case TYPE_FRACTION -> buildFractionFields();
|
||||
case TYPE_ROUTINE -> buildRoutineFields();
|
||||
case TYPE_MONOLOGUE -> buildMonologueFields();
|
||||
case TYPE_NPC_DIALOG -> buildNpcDialogFields();
|
||||
}
|
||||
}
|
||||
|
||||
private void buildQuestFields() {
|
||||
questIdField = field("Quest-ID (z. B. q_main_001)");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("Quest"),
|
||||
row("Quest-ID:", questIdField)
|
||||
);
|
||||
dynamicArea.getChildren().addAll(sectionTitle("Quest"), row("Quest-ID:", questIdField));
|
||||
}
|
||||
|
||||
private void buildNpcFields() {
|
||||
@@ -129,7 +172,7 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
npcStatusCombo = statusCombo();
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("NPC-Status"),
|
||||
row("NPC-ID:", npcIdField),
|
||||
row("NPC-ID:", npcIdField),
|
||||
row("Neuer Status:", npcStatusCombo)
|
||||
);
|
||||
}
|
||||
@@ -139,8 +182,8 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
fractionStatusCombo = statusCombo();
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("Fraktions-Status"),
|
||||
row("Fraktions-UUID:", fractionIdField),
|
||||
row("Neuer Status:", fractionStatusCombo)
|
||||
row("Fraktions-UUID:", fractionIdField),
|
||||
row("Neuer Status:", fractionStatusCombo)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,8 +192,8 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
routineNameField = field("Name der Routine");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("Routine ändern"),
|
||||
row("NPC-ID:", routineNpcIdField),
|
||||
row("Routine-Name:", routineNameField)
|
||||
row("NPC-ID:", routineNpcIdField),
|
||||
row("Routine-Name:", routineNameField)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,14 +203,19 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
monologueIdCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
monologueIdCombo.setPromptText("Monolog-ID eingeben oder wählen...");
|
||||
try {
|
||||
de.blight.common.MonologueIO.load().stream()
|
||||
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));
|
||||
}
|
||||
|
||||
private void buildNpcDialogFields() {
|
||||
npcDialogIdField = field("Character-ID des NPCs");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("Monolog starten"),
|
||||
row("Monolog-ID:", monologueIdCombo)
|
||||
sectionTitle("NPC-Dialog starten"),
|
||||
row("NPC-ID:", npcDialogIdField)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,7 +236,7 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
}
|
||||
case TYPE_NPC -> {
|
||||
NpcStatusTrigger n = new NpcStatusTrigger();
|
||||
if (npcIdField != null) n.setNpcId(npcIdField.getText().trim());
|
||||
if (npcIdField != null) n.setNpcId(npcIdField.getText().trim());
|
||||
if (npcStatusCombo != null) n.setTargetStatus(npcStatusCombo.getValue());
|
||||
yield n;
|
||||
}
|
||||
@@ -215,41 +263,56 @@ public class TriggerDialog extends Dialog<Trigger> {
|
||||
}
|
||||
yield mo;
|
||||
}
|
||||
case TYPE_NPC_DIALOG -> {
|
||||
NpcDialogTrigger nd = new NpcDialogTrigger();
|
||||
if (npcDialogIdField != null) nd.setNpcId(npcDialogIdField.getText().trim());
|
||||
yield nd;
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
if (t != null) t.setRequiresChapter(chapterSpinner.getValue());
|
||||
if (t == null) return null;
|
||||
t.setRequiresChapter(chapterSpinner.getValue());
|
||||
String modeStr = conditionModeCombo.getValue();
|
||||
t.setConditionMode(modeStr != null && modeStr.startsWith("ANY") ? ConditionMode.ANY : ConditionMode.ALL);
|
||||
t.setConditions(new ArrayList<>(conditions));
|
||||
return t;
|
||||
}
|
||||
|
||||
// ── Vorhandenen Trigger vorfüllen ─────────────────────────────────────────
|
||||
// ── Vorladen ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void preload(Trigger t) {
|
||||
chapterSpinner.getValueFactory().setValue(t.getRequiresChapter());
|
||||
conditionModeCombo.setValue(t.getConditionMode() == ConditionMode.ANY
|
||||
? "ANY – mindestens eine" : "ALL – alle müssen gelten");
|
||||
if (t.getConditions() != null) conditions.setAll(t.getConditions());
|
||||
|
||||
if (t instanceof QuestStartTrigger q) {
|
||||
typeCombo.setValue(TYPE_QUEST);
|
||||
if (questIdField != null && q.getQuest() != null)
|
||||
questIdField.setText(nullSafe(q.getQuest().getQuestId()));
|
||||
} else if (t instanceof NpcStatusTrigger n) {
|
||||
typeCombo.setValue(TYPE_NPC);
|
||||
if (npcIdField != null) npcIdField.setText(nullSafe(n.getNpcId()));
|
||||
if (npcIdField != null) npcIdField.setText(nullSafe(n.getNpcId()));
|
||||
if (npcStatusCombo != null && n.getTargetStatus() != null)
|
||||
npcStatusCombo.setValue(n.getTargetStatus());
|
||||
} else if (t instanceof FractionStatusTrigger f) {
|
||||
typeCombo.setValue(TYPE_FRACTION);
|
||||
if (fractionIdField != null && f.getFractionId() != null)
|
||||
if (fractionIdField != null && f.getFractionId() != null)
|
||||
fractionIdField.setText(f.getFractionId().toString());
|
||||
if (fractionStatusCombo != null && f.getTargetStatus() != null)
|
||||
fractionStatusCombo.setValue(f.getTargetStatus());
|
||||
} else if (t instanceof ChangeRoutineTrigger r) {
|
||||
typeCombo.setValue(TYPE_ROUTINE);
|
||||
if (routineNpcIdField != null && r.getNpcId() != null)
|
||||
routineNpcIdField.setText(r.getNpcId());
|
||||
if (routineNameField != null && r.getRoutineName() != null)
|
||||
routineNameField.setText(r.getRoutineName());
|
||||
if (routineNpcIdField != null && r.getNpcId() != null) 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());
|
||||
} else if (t instanceof NpcDialogTrigger nd) {
|
||||
typeCombo.setValue(TYPE_NPC_DIALOG);
|
||||
if (npcDialogIdField != null && nd.getNpcId() != null)
|
||||
npcDialogIdField.setText(nd.getNpcId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ public class TriggerListEditor extends VBox {
|
||||
+ " -> \"" + nullSafe(r.getRoutineName()) + "\"" + chapter;
|
||||
if (t instanceof MonologueTrigger mo)
|
||||
return "Monolog: " + nullSafe(mo.getMonologueId()) + chapter;
|
||||
if (t instanceof NpcDialogTrigger nd)
|
||||
return "NPC-Dialog: " + nullSafe(nd.getNpcId()) + chapter;
|
||||
return t.getClass().getSimpleName() + chapter;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user