Einige Bug Fixes

This commit is contained in:
2026-08-16 09:28:55 +02:00
parent 47b2d43c50
commit e79f5b4755
29 changed files with 1030 additions and 348 deletions

View File

@@ -282,6 +282,9 @@ public class EditorApp extends Application {
private VBox zonenDynamicContent;
private Runnable zonenKindStyleUpdater;
// Auswahl-Werkzeug dynamischer Bereich für selektierte Items
private VBox auswahlDynamicContent;
// Spiel-Starten-Werkzeug-Zustand
private TextField spawnXField;
private TextField spawnZField;
@@ -461,7 +464,7 @@ public class EditorApp extends Application {
centerStack = new StackPane(worldViewport);
setupAssetOverlay();
root.setCenter(centerStack);
root.setRight(toolPanel);
root.setRight(buildAuswahlPanel());
worldBottomBar = buildBottomBox();
root.setBottom(worldBottomBar);
@@ -752,6 +755,18 @@ public class EditorApp extends Application {
updateLocationZonePanel(input.selectedLocationZoneInfo);
}
if (input.auswahlZoneSelected) {
input.auswahlZoneSelected = false;
input.activeLayer = SharedInput.LAYER_ZONEN;
if (zonenBtn != null) zonenBtn.setSelected(true);
root.setRight(buildZonenPanel());
}
if (input.itemSelectionChanged) {
input.itemSelectionChanged = false;
updateAuswahlItemPanel(input.selectedItemInfo);
}
if (input.zonenSelectionChanged) {
input.zonenSelectionChanged = false;
updateZonenPanel(input.zonenSelectedKind);
@@ -969,7 +984,11 @@ public class EditorApp extends Application {
} else {
setCenterView(worldViewport);
}
root.setRight(toolPanel);
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) {
root.setRight(buildAuswahlPanel());
} else {
root.setRight(toolPanel);
}
root.setBottom(worldBottomBar);
if (wasObjEditor && (input.activeLayer == SharedInput.LAYER_OBJECTS
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)) {
@@ -1590,9 +1609,11 @@ public class EditorApp extends Application {
zonenBtn.setToggleGroup(layerGroup);
playToolBtn.setToggleGroup(layerGroup);
auswahlBtn.setSelected(true);
input.activeLayer = SharedInput.LAYER_AUSWAHL;
auswahlBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_AUSWAHL;
input.activeLayer = SharedInput.LAYER_AUSWAHL;
input.deselectAllOnAuswahl = true;
root.setRight(buildAuswahlPanel());
});
baseBtn.setOnAction(e -> {
@@ -1871,7 +1892,8 @@ public class EditorApp extends Application {
private boolean isObjectMode() {
return input.activeLayer == SharedInput.LAYER_OBJECTS
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT;
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT
|| input.activeLayer == SharedInput.LAYER_AUSWAHL;
}
private static Label styledHint(String text) {
@@ -3584,7 +3606,7 @@ public class EditorApp extends Application {
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;");
panel.getChildren().addAll(title, new Separator());
panel.getChildren().addAll(
styledHint("L-Klick: Objekt auswählen"),
styledHint("L-Klick: Element auswählen"),
styledHint(" → wechselt automatisch zum"),
styledHint(" passenden Werkzeug"),
new Separator(),
@@ -3593,10 +3615,37 @@ public class EditorApp extends Application {
styledHint(" * Lichter / Emitter"),
styledHint(" ~ Wasser / Wasserfall"),
styledHint(" -- Wege / Knoten"),
styledHint(" [ ] Zonen"));
styledHint(" [ ] Zonen"),
styledHint(" o Spawnpunkte (Items)"),
new Separator());
auswahlDynamicContent = new VBox(6);
Label noSel = new Label("Kein Element ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
auswahlDynamicContent.getChildren().add(noSel);
panel.getChildren().add(auswahlDynamicContent);
return panel;
}
private void updateAuswahlItemPanel(String info) {
if (auswahlDynamicContent == null) return;
auswahlDynamicContent.getChildren().clear();
if (info == null) {
Label noSel = new Label("Kein Element ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
auswahlDynamicContent.getChildren().add(noSel);
return;
}
String[] parts = info.split("\\|");
String itemId = parts.length > 1 ? parts[1] : "?";
Label idLabel = new Label("Item: " + itemId);
idLabel.setStyle("-fx-font-weight: bold;");
Button deleteBtn = new Button("Löschen");
deleteBtn.setMaxWidth(Double.MAX_VALUE);
deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;");
deleteBtn.setOnAction(e -> input.deleteSelectedItemRequested = true);
auswahlDynamicContent.getChildren().addAll(idLabel, deleteBtn);
}
private VBox buildAtmospherePanel() {
VBox panel = new VBox(10);
panel.setPadding(new Insets(10));
@@ -12022,6 +12071,7 @@ public class EditorApp extends Application {
private void switchToContentEditor() {
currentTool = "content";
if (jmeApp != null) jmeApp.setRenderThrottled(true);
root.setTop(topBar);
topBar.getChildren().set(1, buildSingleWindowContentToolBar());
root.setLeft(buildContentTreePanel());
root.setBottom(null);

View File

@@ -1114,6 +1114,18 @@ public class SharedInput {
/** activeLayer==33 → Auswahl-Modus: kein Terrain-Editing, Klick selektiert Objekte */
public static final int LAYER_AUSWAHL = 33;
/** JavaFX → JME3: Alle Selektionen aufheben wenn in Auswahl-Modus gewechselt wird. */
public volatile boolean deselectAllOnAuswahl = false;
/** JME3 → JavaFX: Zone wurde im Auswahl-Modus selektiert → Zonen-Werkzeug aktivieren. */
public volatile boolean auswahlZoneSelected = false;
/** JME3 → JavaFX: Item-Spawn im Auswahl-Modus selektiert. Format "idx|itemId|x|y|z" oder null. */
public volatile String selectedItemInfo = null;
public volatile boolean itemSelectionChanged = false;
/** JavaFX → JME3: Selektierten Item-Spawn löschen. */
public volatile boolean deleteSelectedItemRequested = false;
/** JME3-intern: Auswahl-Klick für ItemPlacementState (kein Objekt/keine Zone getroffen). */
public final ConcurrentLinkedQueue<ObjectClick> auswahlClickQueue = new ConcurrentLinkedQueue<>();
// ── Voxel-Textur-Malen ────────────────────────────────────────────────────
/** Parallel-Queue zu textureEditQueue wird von submitEdit(layer=4) mitbefüllt,
* damit VoxelEditorState unabhängig vom Terrain seine Splatmap beschreiben kann. */

View File

@@ -80,7 +80,8 @@ public class AreaState extends BaseAppState {
@Override
public void update(float tpf) {
if (input.activeLayer != SharedInput.LAYER_AREAS && input.activeLayer != SharedInput.LAYER_ZONEN) {
if (input.activeLayer != SharedInput.LAYER_AREAS && input.activeLayer != SharedInput.LAYER_ZONEN
&& input.activeLayer != SharedInput.LAYER_AUSWAHL) {
if (placing) cancelPoly();
return;
}
@@ -172,6 +173,7 @@ public class AreaState extends BaseAppState {
if (lzs != null && lzs.findZoneAt(hitX, hitZ) >= 0) return;
}
deselect();
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) return;
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"area".equals(input.zonenNewKind)) return;
placing = true;
currX.clear();
@@ -313,10 +315,13 @@ public class AreaState extends BaseAppState {
String triggersJson = TriggerIO.serializeList(a.triggers());
input.selectedAreaInfo = idx + "|" + a.areaId() + "|" + triggersJson;
input.areaSelectionChanged = true;
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
if (input.activeLayer == SharedInput.LAYER_ZONEN || input.activeLayer == SharedInput.LAYER_AUSWAHL) {
input.zonenSelectedKind = "area";
input.zonenSelectionChanged = true;
}
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) {
input.auswahlZoneSelected = true;
}
}
// ── Add / Remove / Apply ──────────────────────────────────────────────────

View File

@@ -46,6 +46,8 @@ public class ItemPlacementState extends BaseAppState {
private final Map<String, Item> itemDefs = new HashMap<>();
private Node itemRoot;
private Node previewNode;
private int selectedItemIdx = -1;
private Node selectedItemNode = null;
public ItemPlacementState(SharedInput input) {
this.input = input;
@@ -119,8 +121,15 @@ public class ItemPlacementState extends BaseAppState {
public void update(float tpf) {
if (input.reloadPlacedItems) {
input.reloadPlacedItems = false;
deselectItem();
reloadFromDisk();
}
// Highlight entfernen sobald wir nicht mehr im Auswahl-Modus sind
if (selectedItemIdx >= 0 && input.activeLayer != SharedInput.LAYER_AUSWAHL) {
deselectItem();
}
updatePreview();
SharedInput.ObjectClick click;
@@ -132,6 +141,19 @@ public class ItemPlacementState extends BaseAppState {
handlePlace(click);
}
}
SharedInput.ObjectClick auswahlClick;
while ((auswahlClick = input.auswahlClickQueue.poll()) != null) {
handleAuswahlClick(auswahlClick);
}
if (input.deleteSelectedItemRequested) {
input.deleteSelectedItemRequested = false;
if (selectedItemIdx >= 0) {
removeItem(selectedItemIdx);
deselectItem();
}
}
}
private void updatePreview() {
@@ -183,6 +205,68 @@ public class ItemPlacementState extends BaseAppState {
}
}
private void handleAuswahlClick(SharedInput.ObjectClick click) {
float jmeX = click.screenX() * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
Ray ray = screenToRay(jmeX, jmeY);
CollisionResults hits = new CollisionResults();
itemRoot.collideWith(ray, hits);
if (hits.size() > 0) {
Spatial geo = hits.getClosestCollision().getGeometry();
Node parent = geo.getParent();
while (parent != null && !nodes.contains(parent)) {
parent = parent.getParent();
}
if (parent != null) {
selectItem(nodes.indexOf(parent));
return;
}
}
deselectItem();
}
private void selectItem(int idx) {
deselectItem();
selectedItemIdx = idx;
selectedItemNode = nodes.get(idx);
Geometry highlight = new Geometry("item_sel",
new com.jme3.scene.shape.Box(0.28f, 0.28f, 0.28f));
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new com.jme3.math.ColorRGBA(1f, 0.6f, 0.1f, 1f));
mat.getAdditionalRenderState().setWireframe(true);
mat.getAdditionalRenderState().setDepthTest(false);
highlight.setMaterial(mat);
highlight.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
selectedItemNode.attachChild(highlight);
PlacedItem pi = items.get(idx);
input.selectedItemInfo = idx + "|" + pi.itemId() + "|" + pi.x() + "|" + pi.y() + "|" + pi.z();
input.itemSelectionChanged = true;
}
private void deselectItem() {
if (selectedItemNode != null) {
Spatial h = selectedItemNode.getChild("item_sel");
if (h != null) h.removeFromParent();
selectedItemNode = null;
}
if (selectedItemIdx >= 0) {
selectedItemIdx = -1;
input.selectedItemInfo = null;
input.itemSelectionChanged = true;
}
}
private void removeItem(int idx) {
itemRoot.detachChild(nodes.get(idx));
items.remove(idx);
nodes.remove(idx);
try {
PlacedItemIO.save(items);
} catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
}
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private Node buildItemNode(String itemId) {

View File

@@ -78,7 +78,8 @@ public class LocationZoneState extends BaseAppState {
@Override
public void update(float tpf) {
if (input.activeLayer != SharedInput.LAYER_LOCATION_ZONES && input.activeLayer != SharedInput.LAYER_ZONEN) {
if (input.activeLayer != SharedInput.LAYER_LOCATION_ZONES && input.activeLayer != SharedInput.LAYER_ZONEN
&& input.activeLayer != SharedInput.LAYER_AUSWAHL) {
if (placing) cancelPoly();
return;
}
@@ -170,6 +171,7 @@ public class LocationZoneState extends BaseAppState {
if (sas != null && sas.findZoneAt(hitX, hitZ) >= 0) return;
}
deselect();
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) return;
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"location".equals(input.zonenNewKind)) return;
placing = true;
currX.clear();
@@ -303,10 +305,13 @@ 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) {
if (input.activeLayer == SharedInput.LAYER_ZONEN || input.activeLayer == SharedInput.LAYER_AUSWAHL) {
input.zonenSelectedKind = "location";
input.zonenSelectionChanged = true;
}
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) {
input.auswahlZoneSelected = true;
}
}
// ── Add / Remove / Apply ──────────────────────────────────────────────────

View File

@@ -399,6 +399,17 @@ public class SceneObjectState extends BaseAppState {
|| input.activeLayer == SharedInput.LAYER_AUSWAHL;
if (!isObjectLayer) return;
if (input.deselectAllOnAuswahl) {
input.deselectAllOnAuswahl = false;
deselectAll();
AreaState as = getStateManager().getState(AreaState.class);
if (as != null) as.deselectSilent();
SoundAreaState sas = getStateManager().getState(SoundAreaState.class);
if (sas != null) sas.deselectSilent();
LocationZoneState lzs = getStateManager().getState(LocationZoneState.class);
if (lzs != null) lzs.deselectSilent();
}
// Animation-Clip-Zuweisung von JavaFX
String animClipPending = input.pendingAnimClip;
if (animClipPending != null) {
@@ -696,8 +707,16 @@ public class SceneObjectState extends BaseAppState {
deselectAll();
return;
}
// Auswahl-Modus: kein Objekt angeklickt → nur deselektieren, Layer bleibt
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) { deselectAll(); return; }
// Auswahl-Modus: kein Objekt angeklickt → deselektieren + Zonen/Items prüfen lassen
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) {
deselectAll();
float sx = click.screenX(), sy = click.screenY();
input.areaClickQueue.offer(new SharedInput.AreaClick(sx, sy, false));
input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick(sx, sy, false));
input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick(sx, sy, false));
input.auswahlClickQueue.offer(click);
return;
}
if (input.activeLayer != SharedInput.LAYER_OBJECTS) { deselectAll(); return; }
String modelPath = input.pendingModelPath;

View File

@@ -82,7 +82,8 @@ public class SoundAreaState extends BaseAppState {
@Override
public void update(float tpf) {
if (input.activeLayer != SharedInput.LAYER_SOUND_AREAS && input.activeLayer != SharedInput.LAYER_ZONEN) {
if (input.activeLayer != SharedInput.LAYER_SOUND_AREAS && input.activeLayer != SharedInput.LAYER_ZONEN
&& input.activeLayer != SharedInput.LAYER_AUSWAHL) {
if (placing) cancelPoly();
return;
}
@@ -174,6 +175,7 @@ public class SoundAreaState extends BaseAppState {
if (lzs != null && lzs.findZoneAt(hitX, hitZ) >= 0) return;
}
deselect();
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) return;
if (input.activeLayer == SharedInput.LAYER_ZONEN && !"sound".equals(input.zonenNewKind)) return;
placing = true;
currX.clear();
@@ -311,10 +313,13 @@ public class SoundAreaState extends BaseAppState {
PlacedSoundArea a = areas.get(idx);
input.selectedSoundAreaInfo = idx + "|" + a.soundPath() + "|" + a.volume() + "|" + a.crossfade();
input.soundAreaSelectionChanged = true;
if (input.activeLayer == SharedInput.LAYER_ZONEN) {
if (input.activeLayer == SharedInput.LAYER_ZONEN || input.activeLayer == SharedInput.LAYER_AUSWAHL) {
input.zonenSelectedKind = "sound";
input.zonenSelectionChanged = true;
}
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) {
input.auswahlZoneSelected = true;
}
}
// ── Add / Remove / Apply ──────────────────────────────────────────────────

View File

@@ -179,7 +179,7 @@ public class DialogEditorView extends BorderPane {
skriptBtn = new Button("Skript");
skriptBtn.setStyle("-fx-background-color: #3a4a6a; -fx-text-fill: white;");
skriptBtn.setDisable(true);
skriptBtn.setOnAction(e -> { exportNpcScript(); exportHeroScript(); });
skriptBtn.setOnAction(e -> { exportDialogOdt(); exportHeroOdt(); });
ttsBtn = new Button("TTS");
ttsBtn.setStyle("-fx-background-color: #5a3a2a; -fx-text-fill: white;");
@@ -1278,7 +1278,7 @@ public class DialogEditorView extends BorderPane {
// ── Skript-Export ──────────────────────────────────────────────────────────
private void exportNpcScript() {
private void exportDialogOdt() {
saveCurrentForm();
if (currentNpcId.isBlank() || allOptions.isEmpty()) {
Dialogs.alert(Alert.AlertType.WARNING,
@@ -1286,39 +1286,91 @@ public class DialogEditorView extends BorderPane {
return;
}
Map<String, String> texts = loadTexts();
StringBuilder sb = new StringBuilder();
sb.append("=== NPC-Skript: ").append(currentNpcId).append(" ===\n");
sb.append("Exportiert: ").append(LocalDate.now()).append("\n\n");
OdtScriptExporter odt = new OdtScriptExporter();
odt.h1("Dialog-Skript: " + currentNpcId);
odt.normal("Exportiert: " + LocalDate.now());
odt.empty();
for (Map.Entry<String, DialogOption> entry : allOptions.entrySet()) {
DialogOption opt = entry.getValue();
String optId = entry.getKey();
String base = "dialog." + currentNpcId + "." + optId;
List<String> npcKeys = resolveStepKeys(opt.getNpcSteps(), opt.getTextNpc(), base, ".textnpc");
if (npcKeys.isEmpty()) continue;
String npcLabel = currentNpcId.replace('_', ' ').toUpperCase();
sb.append("--- ").append(optId);
if (rootIds.contains(optId)) sb.append(" [ROOT]");
sb.append(" ---\n");
appendStepLines(sb, npcKeys, "NPC", texts);
sb.append("\n");
List<String> ordered = new ArrayList<>(rootIds);
for (String id : allOptions.keySet()) {
if (!rootIds.contains(id)) ordered.add(id);
}
writeScript(ProjectRoot.resolve("dialog_script_npc_" + currentNpcId + ".txt"), sb.toString());
for (String optId : ordered) {
DialogOption opt = allOptions.get(optId);
if (opt == null) continue;
String base = "dialog." + currentNpcId + "." + optId;
List<String> heroKeys = resolveStepKeys(opt.getHeroSteps(), opt.getTextHero(), base, ".textmainchar");
List<String> npcKeys = resolveStepKeys(opt.getNpcSteps(), opt.getTextNpc(), base, ".textnpc");
if (heroKeys.isEmpty() && npcKeys.isEmpty()) continue;
odt.h2(optId + (rootIds.contains(optId) ? " [ROOT]" : ""));
List<String> conds = conditions(opt);
if (!conds.isEmpty()) odt.meta("Bedingungen: " + String.join(" / ", conds));
if (!heroKeys.isEmpty()) {
odt.speakerHeld(heroKeys.size() > 1 ? "HELD (" + heroKeys.size() + " Varianten):" : "HELD:");
for (int i = 0; i < heroKeys.size(); i++) {
String key = heroKeys.get(i);
String textVal = texts.getOrDefault(key, "(nicht übersetzt)");
if (heroKeys.size() > 1) odt.meta("Variante " + (i + 1) + ":");
odt.speechHeld("" + textVal + "");
odt.meta("Key: " + key);
odt.meta("Audio: " + audioPathForKey(key));
}
}
if (!npcKeys.isEmpty()) {
odt.speakerNpc(npcLabel + ":");
for (int i = 0; i < npcKeys.size(); i++) {
String key = npcKeys.get(i);
String textVal = texts.getOrDefault(key, "(nicht übersetzt)");
if (npcKeys.size() > 1) odt.meta("Schritt " + (i + 1) + ":");
odt.speechNpc("" + textVal + "");
odt.meta("Key: " + key);
odt.meta("Audio: " + audioPathForKey(key));
}
}
if (opt.getNextOptions() != null && !opt.getNextOptions().isEmpty()) {
List<String> ids = opt.getNextOptions().stream()
.filter(o -> o != null && o.getId() != null && !o.getId().isBlank())
.map(DialogOption::getId).toList();
if (!ids.isEmpty()) odt.meta("→ Danach: " + String.join(", ", ids));
}
if (opt.getDisablesOptions() != null && !opt.getDisablesOptions().isEmpty()) {
List<String> ids = opt.getDisablesOptions().stream()
.filter(o -> o != null && o.getId() != null && !o.getId().isBlank())
.map(DialogOption::getId).toList();
if (!ids.isEmpty()) odt.meta("✗ Deaktiviert: " + String.join(", ", ids));
}
odt.empty();
}
writeOdt(ProjectRoot.resolve("dialog_script_" + currentNpcId + ".odt"), odt);
}
private void exportHeroScript() {
private void exportHeroOdt() {
Map<String, String> texts = loadTexts();
StringBuilder sb = new StringBuilder();
sb.append("=== Held-Skript ===\n");
sb.append("Exportiert: ").append(LocalDate.now()).append("\n\n");
OdtScriptExporter odt = new OdtScriptExporter();
odt.h1("Held-Skript");
odt.normal("Exportiert: " + LocalDate.now());
odt.empty();
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character");
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
if (!(gc instanceof NPC npc)) continue;
if (npc.getDialogOptions() == null || npc.getDialogOptions().isEmpty()) continue;
String npcLabel = (npc.getCharacterId() != null ? npc.getCharacterId() : "NPC")
.replace('_', ' ').toUpperCase();
boolean npcHeaderWritten = false;
for (Map.Entry<String, DialogOption> entry : npc.getDialogOptions().entrySet()) {
String optId = entry.getKey();
DialogOption opt = entry.getValue();
@@ -1327,31 +1379,73 @@ public class DialogEditorView extends BorderPane {
if (heroKeys.isEmpty()) continue;
if (!npcHeaderWritten) {
sb.append("=== NPC: ").append(npc.getCharacterId()).append(" ===\n\n");
odt.h1("NPC: " + npc.getCharacterId());
odt.empty();
npcHeaderWritten = true;
}
sb.append("--- ").append(optId).append(" ---\n");
appendStepLines(sb, heroKeys, "HELD", texts);
sb.append("\n");
odt.h2(optId);
List<String> npcKeys = resolveStepKeys(opt.getNpcSteps(), opt.getTextNpc(), base, ".textnpc");
if (!npcKeys.isEmpty()) {
odt.speakerNpc(npcLabel + " (Kontext):");
for (String key : npcKeys) {
odt.contextNpc("" + texts.getOrDefault(key, "(nicht übersetzt)") + "");
}
}
odt.speakerHeld("HELD:");
for (int i = 0; i < heroKeys.size(); i++) {
String key = heroKeys.get(i);
if (heroKeys.size() > 1) odt.meta("Variante " + (i + 1) + ":");
odt.speechHeld("" + texts.getOrDefault(key, "(nicht übersetzt)") + "");
odt.meta("Key: " + key);
odt.meta("Audio: " + audioPathForKey(key));
}
odt.empty();
}
}
List<Monologue> monologues = MonologueIO.load();
if (!monologues.isEmpty()) {
sb.append("=== Monologe ===\n\n");
odt.h1("Monologe");
odt.empty();
for (Monologue m : monologues) {
if (m.getHeroSteps() == null || m.getHeroSteps().isEmpty()) continue;
sb.append("--- ").append(m.getId()).append(" ---\n");
List<String> keys = m.getHeroSteps().stream()
.filter(s -> s.getText() != null && !s.getText().id().isBlank())
.map(s -> s.getText().id())
.toList();
appendStepLines(sb, keys, "HELD", texts);
sb.append("\n");
.map(s -> s.getText().id()).toList();
if (keys.isEmpty()) continue;
odt.h2(m.getId());
odt.speakerHeld("HELD:");
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
if (keys.size() > 1) odt.meta("Schritt " + (i + 1) + ":");
odt.speechHeld("" + texts.getOrDefault(key, "(nicht übersetzt)") + "");
odt.meta("Key: " + key);
odt.meta("Audio: " + audioPathForKey(key));
}
odt.empty();
}
}
writeScript(ProjectRoot.resolve("dialog_script_held.txt"), sb.toString());
writeOdt(ProjectRoot.resolve("dialog_script_held.odt"), odt);
}
private List<String> conditions(DialogOption opt) {
List<String> conds = new ArrayList<>();
if (opt.getRequiresChapter() > 0)
conds.add("Kapitel ≥ " + opt.getRequiresChapter());
if (opt.getRequiresStatus() != null)
conds.add("Status: " + opt.getRequiresStatus());
if (opt.getRequiresQuestOpen() != null
&& opt.getRequiresQuestOpen().getQuestId() != null
&& !opt.getRequiresQuestOpen().getQuestId().isBlank())
conds.add("Quest offen: " + opt.getRequiresQuestOpen().getQuestId());
if (opt.getRequiresQuestComplete() != null
&& opt.getRequiresQuestComplete().getQuestId() != null
&& !opt.getRequiresQuestComplete().getQuestId().isBlank())
conds.add("Quest erfüllt: " + opt.getRequiresQuestComplete().getQuestId());
return conds;
}
private Map<String, String> loadTexts() {
@@ -1363,20 +1457,9 @@ public class DialogEditorView extends BorderPane {
return new LinkedHashMap<>();
}
private static void appendStepLines(StringBuilder sb, List<String> keys,
String label, Map<String, String> texts) {
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
sb.append(keys.size() > 1 ? " " + label + " " + (i + 1) + ":\n" : " " + label + ":\n");
sb.append(" Key: ").append(key).append("\n");
sb.append(" Text: \"").append(texts.getOrDefault(key, "(nicht übersetzt)")).append("\"\n");
sb.append(" Audio: ").append(audioPathForKey(key)).append("\n");
}
}
private void writeScript(Path out, String content) {
private void writeOdt(Path out, OdtScriptExporter odt) {
try {
Files.writeString(out, content);
odt.write(out);
Dialogs.alert(Alert.AlertType.INFORMATION,
"Skript exportiert nach:\n" + out, ButtonType.OK).showAndWait();
} catch (IOException ex) {
@@ -1399,7 +1482,7 @@ public class DialogEditorView extends BorderPane {
return List.of(base + suffix);
}
private static String audioPathForKey(String key) {
static String audioPathForKey(String key) {
if (key == null) return "";
if (key.startsWith("dialog.")) {
String without = key.substring("dialog.".length());

View File

@@ -46,20 +46,30 @@ public class MonologueEditorView extends SplitPane {
// ── Liste ─────────────────────────────────────────────────────────────────
private VBox buildList() {
listView.setStyle("-fx-control-inner-background: #2a2a3a; -fx-text-fill: #ddd;"
+ " -fx-selection-bar: #3a5a8a; -fx-selection-bar-text: white;");
listView.setCellFactory(lv -> new ListCell<>() {
{ selectedProperty().addListener((obs, old, sel) -> applyStyle()); }
@Override protected void updateItem(Monologue m, boolean empty) {
super.updateItem(m, empty);
setText(empty || m == null ? null : (m.getId().isBlank() ? "(kein Name)" : m.getId()));
if (empty || m == null) { setText(null); setStyle(""); return; }
setText(m.getId().isBlank() ? "(kein Name)" : m.getId());
applyStyle();
}
private void applyStyle() {
if (getItem() == null || isEmpty()) { setStyle(""); return; }
String bg = isSelected() ? "#3a5a8a" : "#2a2a3a";
setStyle("-fx-background-color: " + bg + "; -fx-text-fill: #cccccc;");
}
});
listView.getSelectionModel().selectedItemProperty().addListener((obs, o, n) -> {
if (!loading) loadMonologue(n);
});
Button addBtn = new Button("+ Neu");
Button addBtn = new Button("+ Neu");
addBtn.setOnAction(e -> createMonologue());
Button delBtn = new Button("- Löschen");
Button delBtn = new Button("- Löschen");
delBtn.setOnAction(e -> deleteMonologue());
Button saveBtn = new Button("Speichern");
@@ -94,11 +104,14 @@ public class MonologueEditorView extends SplitPane {
playOnceCheck = new CheckBox("Nur einmal abspielen");
playOnceCheck.setSelected(true);
playOnceCheck.setStyle("-fx-text-fill: #ccc;");
// Schritte
stepsView = new ListView<>();
stepsView.setPrefHeight(120);
stepsView.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
stepsView.setCellFactory(javafx.scene.control.cell.TextFieldListCell.forListView());
stepsView.setEditable(true);
Button addStepBtn = new Button("+ Schritt");
addStepBtn.setOnAction(e -> {
@@ -111,22 +124,18 @@ public class MonologueEditorView extends SplitPane {
String sel = stepsView.getSelectionModel().getSelectedItem();
if (sel != null) stepsView.getItems().remove(sel);
});
stepsView.setEditable(true);
stepsView.setCellFactory(javafx.scene.control.cell.TextFieldListCell.forListView());
HBox stepBtns = new HBox(6, addStepBtn, delStepBtn);
// Quest-Folgen
recvQuestField = questField("Quest erhalten (ID)");
fulfillsQuestField = questField("Quest erfüllen (ID)");
recvQuestField = questField("Quest wählen…");
fulfillsQuestField = questField("Quest wählen…");
abortsQuestsView = new ListView<>();
abortsQuestsView.setPrefHeight(80);
abortsQuestsView.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
Button addAbortBtn = new Button("+ Quest abbrechen");
addAbortBtn.setOnAction(e -> {
String id = showQuestPicker();
String id = showQuestPickerDialog();
if (id != null && !abortsQuestsView.getItems().contains(id)) abortsQuestsView.getItems().add(id);
});
Button delAbortBtn = new Button("- Entfernen");
@@ -136,9 +145,6 @@ public class MonologueEditorView extends SplitPane {
});
HBox abortBtns = new HBox(6, addAbortBtn, delAbortBtn);
Label stepsLbl = sectionLabel("Schritte (Textschlüssel)");
Label questLbl = sectionLabel("Quest-Folgen");
VBox form = new VBox(10);
form.setPadding(new Insets(12));
form.setStyle("-fx-background-color: #1e1e2e;");
@@ -147,12 +153,13 @@ public class MonologueEditorView extends SplitPane {
row("Kapitel (mind.):", chapterSpinner),
playOnceCheck,
new Separator(),
stepsLbl, stepsView, stepBtns,
sectionLabel("Schritte (Textschlüssel)"),
stepsView, stepBtns,
new Separator(),
questLbl,
sectionLabel("Quest-Folgen"),
row("Erhält Quest:", recvQuestField),
row("Erfüllt Quest:", fulfillsQuestField),
new Label("Bricht Quests ab:"),
sectionLabel("Bricht Quests ab:"),
abortsQuestsView, abortBtns
);
@@ -179,10 +186,7 @@ public class MonologueEditorView extends SplitPane {
current = m;
loading = true;
try {
if (m == null) {
clearForm();
return;
}
if (m == null) { clearForm(); return; }
idField.setText(m.getId());
chapterSpinner.getValueFactory().setValue(m.getRequiresChapter());
playOnceCheck.setSelected(m.isPlayOnce());
@@ -215,9 +219,8 @@ public class MonologueEditorView extends SplitPane {
List<DialogStep> steps = new ArrayList<>();
for (String key : stepsView.getItems()) {
if (!key.isBlank()) {
if (!key.isBlank())
steps.add(new DialogStep(new TextReference(key), new AudioReference(key)));
}
}
current.setHeroSteps(steps);
@@ -283,13 +286,13 @@ public class MonologueEditorView extends SplitPane {
tf.setEditable(false);
tf.setCursor(Cursor.HAND);
tf.setOnMouseClicked(e -> {
String id = showQuestPicker();
String id = showQuestPickerDialog();
if (id != null) tf.setText(id);
});
return tf;
}
private String showQuestPicker() {
private String showQuestPickerDialog() {
Path questDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("quests");
List<Quest> quests = QuestIO.loadAll(questDir);
@@ -310,11 +313,17 @@ public class MonologueEditorView extends SplitPane {
}
});
chooser.getItems().setAll(quests);
chooser.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
chooser.setPrefSize(380, 280);
chooser.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;"
+ " -fx-selection-bar: #3a5a8a;");
chooser.setPrefSize(380, 300);
dlg.getDialogPane().setContent(quests.isEmpty()
? new Label("Keine Quests gefunden.") : chooser);
if (quests.isEmpty()) {
Label hint = new Label("Keine Quests gefunden.\nQuests im Quest-Editor anlegen.");
hint.setStyle("-fx-text-fill: #888; -fx-font-style: italic;");
dlg.getDialogPane().setContent(hint);
} else {
dlg.getDialogPane().setContent(chooser);
}
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
dlg.getDialogPane().setStyle("-fx-background-color: #252535;");
@@ -342,13 +351,13 @@ public class MonologueEditorView extends SplitPane {
private static Label sectionLabel(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-text-fill: #aaa; -fx-font-size: 11;");
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
return l;
}
private static HBox row(String labelText, javafx.scene.Node control) {
Label lbl = new Label(labelText);
lbl.setMinWidth(130);
lbl.setMinWidth(150);
lbl.setStyle("-fx-text-fill: #aaa;");
HBox.setHgrow(control, Priority.ALWAYS);
HBox box = new HBox(8, lbl, control);

View File

@@ -0,0 +1,134 @@
package de.blight.editor.ui;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.*;
/** Builds an .odt (Open Document Text) ZIP archive for dialog script exports. */
public class OdtScriptExporter {
private final StringBuilder body = new StringBuilder();
public void h1(String text) { para("P_H1", text); }
public void h2(String text) { para("P_H2", text); }
public void normal(String text) { para("P_normal", text); }
public void meta(String text) { para("P_meta", text); }
public void speakerHeld(String text) { para("P_speaker_held",text); }
public void speakerNpc(String text) { para("P_speaker_npc", text); }
public void contextNpc(String text) { para("P_context_npc", text); }
/** Hero speech text — italic + highlighted (yellow background). */
public void speechHeld(String text) {
body.append("<text:p text:style-name='P_speech_held'>")
.append("<text:span text:style-name='C_hi'>").append(esc(text)).append("</text:span>")
.append("</text:p>\n");
}
/** NPC speech text — bold + highlighted (yellow background). */
public void speechNpc(String text) {
body.append("<text:p text:style-name='P_speech_npc'>")
.append("<text:span text:style-name='C_hi'>").append(esc(text)).append("</text:span>")
.append("</text:p>\n");
}
public void empty() {
body.append("<text:p text:style-name='P_normal'/>\n");
}
private void para(String style, String text) {
body.append("<text:p text:style-name='").append(style).append("'>")
.append(esc(text)).append("</text:p>\n");
}
public void write(Path out) throws IOException {
byte[] content = buildContentXml().getBytes(StandardCharsets.UTF_8);
byte[] manifest = MANIFEST.getBytes(StandardCharsets.UTF_8);
byte[] mime = "application/vnd.oasis.opendocument.text".getBytes(StandardCharsets.US_ASCII);
try (ZipOutputStream zip = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(out)))) {
// mimetype MUST be first entry and MUST use STORED (uncompressed)
var crc = new CRC32();
crc.update(mime);
ZipEntry mimeEntry = new ZipEntry("mimetype");
mimeEntry.setMethod(ZipEntry.STORED);
mimeEntry.setSize(mime.length);
mimeEntry.setCompressedSize(mime.length);
mimeEntry.setCrc(crc.getValue());
zip.putNextEntry(mimeEntry);
zip.write(mime);
zip.closeEntry();
zip.putNextEntry(new ZipEntry("META-INF/manifest.xml"));
zip.write(manifest);
zip.closeEntry();
zip.putNextEntry(new ZipEntry("content.xml"));
zip.write(content);
zip.closeEntry();
}
}
private String buildContentXml() {
return """
<?xml version='1.0' encoding='UTF-8'?>
<office:document-content
xmlns:office='urn:oasis:names:tc:opendocument:xmlns:office:1.0'
xmlns:text='urn:oasis:names:tc:opendocument:xmlns:text:1.0'
xmlns:style='urn:oasis:names:tc:opendocument:xmlns:style:1.0'
xmlns:fo='urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'
office:version='1.3'>
<office:automatic-styles>
""" + STYLES + """
</office:automatic-styles>
<office:body><office:text>
""" + body + """
</office:text></office:body>
</office:document-content>
""";
}
private static final String STYLES =
ps("P_H1", "fo:margin-top='0.5cm' fo:margin-bottom='0.2cm'",
"fo:font-size='16pt' fo:font-weight='bold' fo:color='#003366'")
+ ps("P_H2", "fo:margin-top='0.6cm' fo:margin-bottom='0.1cm'"
+ " fo:border-top='0.5pt solid #aaaaaa' fo:padding-top='0.15cm'",
"fo:font-size='12pt' fo:font-weight='bold' fo:color='#224466'")
+ ps("P_normal", "", "fo:font-size='10pt'")
+ ps("P_meta", "fo:margin-left='1.2cm'",
"fo:font-size='9pt' fo:color='#888888'")
+ ps("P_speaker_held","fo:margin-top='0.3cm' fo:margin-left='0.3cm'",
"fo:font-size='10pt' fo:font-weight='bold' fo:font-style='italic' fo:color='#445577'")
+ ps("P_speaker_npc", "fo:margin-top='0.3cm' fo:margin-left='0.3cm'",
"fo:font-size='10pt' fo:font-weight='bold' fo:color='#222222'")
+ ps("P_speech_held", "fo:margin-left='1.2cm' fo:margin-top='0.05cm' fo:margin-bottom='0.05cm'",
"fo:font-size='11pt' fo:font-style='italic' fo:color='#334466'")
+ ps("P_speech_npc", "fo:margin-left='1.2cm' fo:margin-top='0.05cm' fo:margin-bottom='0.05cm'",
"fo:font-size='11pt' fo:color='#111111'")
+ ps("P_context_npc", "fo:margin-left='1.2cm' fo:margin-top='0.02cm' fo:margin-bottom='0.02cm'",
"fo:font-size='10pt' fo:color='#999999'")
+ " <style:style style:name='C_hi' style:family='text'>\n"
+ " <style:text-properties fo:font-weight='bold' fo:background-color='#ffffcc'/>\n"
+ " </style:style>\n";
private static String ps(String name, String pProp, String tProp) {
String s = " <style:style style:name='" + name + "' style:family='paragraph'>\n";
if (!pProp.isBlank()) s += " <style:paragraph-properties " + pProp + "/>\n";
if (!tProp.isBlank()) s += " <style:text-properties " + tProp + "/>\n";
return s + " </style:style>\n";
}
private static final String MANIFEST = """
<?xml version='1.0' encoding='UTF-8'?>
<manifest:manifest xmlns:manifest='urn:oasis:names:tc:opendocument:xmlns:manifest:1.0' manifest:version='1.3'>
<manifest:file-entry manifest:full-path='/' manifest:media-type='application/vnd.oasis.opendocument.text'/>
<manifest:file-entry manifest:full-path='content.xml' manifest:media-type='text/xml'/>
</manifest:manifest>
""";
private static String esc(String s) {
if (s == null) return "";
return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
}