Einige Bug Fixes
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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("&", "&").replace("<", "<").replace(">", ">");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user