Weitere Anpassungen bezüglich der Voxel und des Zoning Systems

This commit is contained in:
2026-08-12 23:01:09 +02:00
parent ae3caacebf
commit 400a736804
88 changed files with 2826 additions and 686 deletions

View File

@@ -6,7 +6,7 @@ plugins {
javafx {
version = '26'
modules = ['javafx.controls', 'javafx.swing']
modules = ['javafx.controls', 'javafx.swing', 'javafx.media']
}
application {

View File

@@ -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());

View File

@@ -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";

View File

@@ -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) {}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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 ───────────────────────────────────────────────────────────

View File

@@ -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) {}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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);

View 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;
}
}

View File

@@ -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));
}

View File

@@ -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) {

View File

@@ -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<>();

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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));

View File

@@ -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<>() {

View File

@@ -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();
}
}

View File

@@ -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");

View File

@@ -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()) {

View File

@@ -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;
}
}

View File

@@ -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));

View File

@@ -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());
}
}

View File

@@ -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;
}