diff --git a/blight-assets/build.gradle b/blight-assets/build.gradle index a4b67eb..eaf90a0 100644 --- a/blight-assets/build.gradle +++ b/blight-assets/build.gradle @@ -12,3 +12,9 @@ sourceSets { resources { srcDirs = ['src/main/resources'] } } } + +// Binärdateien (OGG, PNG, J3O, …) sind bereits komprimiert — STORED spart +// CPU-Zeit beim Einpacken, ohne den Classpath-Zugriff zu beeinflussen. +jar { + entryCompression = ZipEntryCompression.STORED +} diff --git a/blight-assets/src/main/resources/Models/Chars/silas.j3o b/blight-assets/src/main/resources/Models/Chars/silas.j3o index 652e4be..1f05a18 100644 Binary files a/blight-assets/src/main/resources/Models/Chars/silas.j3o and b/blight-assets/src/main/resources/Models/Chars/silas.j3o differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/abschluss_letzte_worte_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/abschluss_letzte_worte_textmainchar.ogg new file mode 100644 index 0000000..46c495d Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/abschluss_letzte_worte_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/abschluss_letzte_worte_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/abschluss_letzte_worte_textnpc.ogg new file mode 100644 index 0000000..979f4be Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/abschluss_letzte_worte_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/abschluss_phase1_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/abschluss_phase1_textmainchar.ogg new file mode 100644 index 0000000..b3536cf Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/abschluss_phase1_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/abschluss_phase1_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/abschluss_phase1_textnpc.ogg new file mode 100644 index 0000000..6ac92ff Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/abschluss_phase1_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/antwort_gedaechtnislos_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/antwort_gedaechtnislos_textmainchar.ogg new file mode 100644 index 0000000..04ec777 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/antwort_gedaechtnislos_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/antwort_gedaechtnislos_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/antwort_gedaechtnislos_textnpc.ogg new file mode 100644 index 0000000..e6751c5 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/antwort_gedaechtnislos_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/antwort_gesandter_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/antwort_gesandter_textmainchar.ogg new file mode 100644 index 0000000..ae4f026 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/antwort_gesandter_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/antwort_gesandter_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/antwort_gesandter_textnpc.ogg new file mode 100644 index 0000000..0be0bc4 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/antwort_gesandter_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/antwort_sklave_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/antwort_sklave_textmainchar.ogg new file mode 100644 index 0000000..2a000a9 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/antwort_sklave_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/antwort_sklave_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/antwort_sklave_textnpc.ogg new file mode 100644 index 0000000..df8732b Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/antwort_sklave_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/ausruestung_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/ausruestung_textmainchar.ogg new file mode 100644 index 0000000..c447d9b Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/ausruestung_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/ausruestung_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/ausruestung_textnpc.ogg new file mode 100644 index 0000000..70926ae Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/ausruestung_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/begruessung_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/begruessung_textmainchar.ogg new file mode 100644 index 0000000..7c2a875 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/begruessung_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/begruessung_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/begruessung_textnpc.ogg new file mode 100644 index 0000000..cf2d2bd Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/begruessung_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/dankeschoen_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/dankeschoen_textmainchar.ogg new file mode 100644 index 0000000..7299ae1 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/dankeschoen_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/dankeschoen_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/dankeschoen_textnpc.ogg new file mode 100644 index 0000000..b910571 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/dankeschoen_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/erstaunt_ruhe_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/erstaunt_ruhe_textmainchar.ogg new file mode 100644 index 0000000..2789635 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/erstaunt_ruhe_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/erstaunt_ruhe_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/erstaunt_ruhe_textnpc.ogg new file mode 100644 index 0000000..43255cf Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/erstaunt_ruhe_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/gibts_was_rum_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/gibts_was_rum_textmainchar.ogg new file mode 100644 index 0000000..a680bd3 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/gibts_was_rum_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/gibts_was_rum_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/gibts_was_rum_textnpc.ogg new file mode 100644 index 0000000..0f2b218 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/gibts_was_rum_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/ich_bin_zaeh_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/ich_bin_zaeh_textmainchar.ogg new file mode 100644 index 0000000..975c787 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/ich_bin_zaeh_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/ich_bin_zaeh_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/ich_bin_zaeh_textnpc.ogg new file mode 100644 index 0000000..3a38636 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/ich_bin_zaeh_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/ich_weiss_genug_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/ich_weiss_genug_textmainchar.ogg new file mode 100644 index 0000000..b33ca07 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/ich_weiss_genug_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/ich_weiss_genug_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/ich_weiss_genug_textnpc.ogg new file mode 100644 index 0000000..9ed6c02 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/ich_weiss_genug_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/info_dorf_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/info_dorf_textmainchar.ogg new file mode 100644 index 0000000..6763e88 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/info_dorf_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/info_dorf_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/info_dorf_textnpc.ogg new file mode 100644 index 0000000..5cf9646 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/info_dorf_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/info_osten_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/info_osten_textmainchar.ogg new file mode 100644 index 0000000..3c4a054 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/info_osten_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/info_osten_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/info_osten_textnpc.ogg new file mode 100644 index 0000000..1474189 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/info_osten_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/info_pass_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/info_pass_textmainchar.ogg new file mode 100644 index 0000000..dcc1847 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/info_pass_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/info_pass_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/info_pass_textnpc.ogg new file mode 100644 index 0000000..25aeeb5 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/info_pass_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/info_westen_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/info_westen_textmainchar.ogg new file mode 100644 index 0000000..ccd399f Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/info_westen_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/info_westen_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/info_westen_textnpc.ogg new file mode 100644 index 0000000..fdb6b4f Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/info_westen_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/karte_zeichnen_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/karte_zeichnen_textmainchar.ogg new file mode 100644 index 0000000..6842c5e Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/karte_zeichnen_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/karte_zeichnen_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/karte_zeichnen_textnpc.ogg new file mode 100644 index 0000000..235cebd Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/karte_zeichnen_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/rueckkehr_beobachtet_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/rueckkehr_beobachtet_textmainchar.ogg new file mode 100644 index 0000000..4f64dde Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/rueckkehr_beobachtet_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/rueckkehr_beobachtet_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/rueckkehr_beobachtet_textnpc.ogg new file mode 100644 index 0000000..9a2351d Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/rueckkehr_beobachtet_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/tipp_rum_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/tipp_rum_textmainchar.ogg new file mode 100644 index 0000000..ece9eb8 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/tipp_rum_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/tipp_rum_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/tipp_rum_textnpc.ogg new file mode 100644 index 0000000..b7658f0 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/tipp_rum_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/was_leuchten_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/was_leuchten_textmainchar.ogg new file mode 100644 index 0000000..ad53f3f Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/was_leuchten_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/was_leuchten_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/was_leuchten_textnpc.ogg new file mode 100644 index 0000000..b2b280c Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/was_leuchten_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/was_ueber_insel_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/was_ueber_insel_textmainchar.ogg new file mode 100644 index 0000000..03ca99f Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/was_ueber_insel_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/was_ueber_insel_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/was_ueber_insel_textnpc.ogg new file mode 100644 index 0000000..ae11049 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/was_ueber_insel_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/weg_ins_innere_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/weg_ins_innere_textmainchar.ogg new file mode 100644 index 0000000..5b2c8fa Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/weg_ins_innere_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/weg_ins_innere_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/weg_ins_innere_textnpc.ogg new file mode 100644 index 0000000..a31444c Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/weg_ins_innere_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/welchen_weg_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/welchen_weg_textmainchar.ogg new file mode 100644 index 0000000..720c6c0 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/welchen_weg_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/welchen_weg_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/welchen_weg_textnpc.ogg new file mode 100644 index 0000000..ca469f5 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/welchen_weg_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/wer_bist_du_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/wer_bist_du_textmainchar.ogg new file mode 100644 index 0000000..9601cf1 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/wer_bist_du_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/wer_bist_du_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/wer_bist_du_textnpc.ogg new file mode 100644 index 0000000..e392090 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/wer_bist_du_textnpc.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/wer_seid_ihr_textmainchar.ogg b/blight-assets/src/main/resources/audio/dialog/silas/wer_seid_ihr_textmainchar.ogg new file mode 100644 index 0000000..4abb6c0 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/wer_seid_ihr_textmainchar.ogg differ diff --git a/blight-assets/src/main/resources/audio/dialog/silas/wer_seid_ihr_textnpc.ogg b/blight-assets/src/main/resources/audio/dialog/silas/wer_seid_ihr_textnpc.ogg new file mode 100644 index 0000000..41ef695 Binary files /dev/null and b/blight-assets/src/main/resources/audio/dialog/silas/wer_seid_ihr_textnpc.ogg differ diff --git a/blight-common/src/main/java/de/blight/common/VoxelChunk.java b/blight-common/src/main/java/de/blight/common/VoxelChunk.java index ee48988..38ceebf 100644 --- a/blight-common/src/main/java/de/blight/common/VoxelChunk.java +++ b/blight-common/src/main/java/de/blight/common/VoxelChunk.java @@ -29,6 +29,8 @@ public final class VoxelChunk { private byte[] density; /** Material-IDs, lazy: null = alles 0. */ private byte[] material; + /** Anzahl solider Voxel (density > 0). Wird bei jeder Änderung aktualisiert. */ + private int solidCount = 0; public volatile boolean dirty = false; @@ -49,7 +51,11 @@ public final class VoxelChunk { public void setDensity(int x, int y, int z, byte d) { if (density == null) { density = new byte[SIZE*SIZE*SIZE]; Arrays.fill(density, Byte.MIN_VALUE); } - density[idx(x, y, z)] = d; + int i = idx(x, y, z); + byte old = density[i]; + density[i] = d; + if (old <= 0 && d > 0) solidCount++; + else if (old > 0 && d <= 0) solidCount--; dirty = true; } @@ -65,9 +71,9 @@ public final class VoxelChunk { public boolean isSolid(int x, int y, int z) { return getDensity(x, y, z) > 0; } - public boolean isEmpty() { return density == null; } + public boolean isEmpty() { return solidCount == 0; } - public void clear() { density = null; material = null; dirty = true; } + public void clear() { density = null; material = null; solidCount = 0; dirty = true; } /** * Gibt die Y-Ausdehnung (in Voxel) der soliden Voxel zurück. @@ -143,7 +149,9 @@ public final class VoxelChunk { int i = idx(x, y, z); int d = density[i]; if (d <= 0) continue; - density[i] = (byte) Math.max(Byte.MIN_VALUE, d - step); + byte nd = (byte) Math.max(Byte.MIN_VALUE, d - step); + density[i] = nd; + if (nd <= 0) solidCount--; changed = true; } } @@ -173,6 +181,7 @@ public final class VoxelChunk { density[idx(x,y+1,z)] <= 0 && density[idx(x,y-1,z)] <= 0 && density[idx(x,y,z+1)] <= 0 && density[idx(x,y,z-1)] <= 0) { density[idx(x, y, z)] = Byte.MIN_VALUE; + solidCount--; changed = true; } } @@ -189,6 +198,7 @@ public final class VoxelChunk { /** Setzt das Dichte-Array direkt (für Undo/Redo). */ public void setDensityArray(byte[] d) { this.density = d; + recomputeSolidCount(); dirty = true; } @@ -203,6 +213,7 @@ public final class VoxelChunk { for (int z = 0; z < SIZE; z++) for (int x = 0; x < SIZE; x++) density[idx(x, y, z)] = (byte) 127; + recomputeSolidCount(); } // ── Serialisierung ──────────────────────────────────────────────────────── @@ -242,10 +253,17 @@ public final class VoxelChunk { in.readFully(c.material); } } + c.recomputeSolidCount(); c.dirty = false; return c; } + private void recomputeSolidCount() { + solidCount = 0; + if (density == null) return; + for (byte d : density) { if (d > 0) solidCount++; } + } + // ── Koordinaten-Hilfsmethoden ───────────────────────────────────────────── /** Welt-Y des Voxels mit localY, ausgehend von diesem cy-Layer. */ diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 014bd29..f49a96f 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -86,6 +86,23 @@ public class EditorApp extends Application { private javafx.scene.canvas.Canvas compassCanvas; private VBox topBar; // MenuBar + aktuelle Toolbar private ToolBar worldToolBar; // Welt-Editor-Toolbar (Layer-Buttons) + private Label worldHintLabel; // Kamera-Info rechts neben Spielen + + // Content Editor (Dual-Window-Modus) + private Stage contentEditorStage; + private BorderPane contentEditorRoot; + private boolean dualWindowMode = false; + private String contentLastSection = "character"; + private ToggleButton contentCharBtn, contentQuestBtn, contentFraktionBtn, + contentItemBtn, contentRezeptBtn, contentCraftingBtn, + contentAreaBtn, contentLocationBtn; + private CheckMenuItem dualWindowMenuItem; + private Button contentSwitchBtn; // "> Content" in Welt-Toolbar (nur 1-Fenster) + + // Content-Baum (Instanzfeld, damit wir Knoten gezielt expandieren/befüllen können) + private javafx.scene.control.TreeView contentTreeView; + private javafx.scene.control.TreeItem contentCharNode; + private ImageView treePreviewView; // aktualisiert wenn JME3 Bild neu erstellt private StackPane plantPreviewPanel; // einmaliges Vorschau-Panel für alle Generatoren private Stage primaryStage; @@ -222,6 +239,7 @@ public class EditorApp extends Application { // Wasser-Werkzeug-Zustand private VBox waterDynamicContent; private Label waterCurrentHeightLabel; + private VBox wasserContentBox; // contentBox im Wasser-Merged-Panel für Live-Updates // Ansichts-Umschalter private boolean wireframeActive = false; @@ -318,20 +336,26 @@ public class EditorApp extends Application { // Toolbar-Buttons (müssen vom Status-Poller erreichbar sein) private ToggleButton baseBtn; - private ToggleButton grassBtn; - private ToggleButton grassVertexBtn; + private ToggleButton grassBtn; // einziger Toolbar-Button für beide Gras-Tools + private ToggleButton grassSubTexBtn; // Sub-Toggle im Panel: Textur-Gras + private ToggleButton grassSubVertBtn; // Sub-Toggle im Panel: Vertex-Gras private ToggleButton textureBtn; private ToggleButton stoneBtn; - private ToggleButton objPlaceBtn; - private ToggleButton objEditBtn; - private ToggleButton lightBtn; - private ToggleButton emitterBtn; - private ToggleButton waterBtn; - private ToggleButton riverBtn; + private ToggleButton objPlaceBtn; // Unified Objekte-Button (Place + Edit) + private ToggleButton objEditBtn; // nur im Object-Editor-Toolbar (nicht Welt-Toolbar) + private ToggleButton lightBtn; // Sub-Toggle in Atmosphäre-Panel + private ToggleButton emitterBtn; // Sub-Toggle in Atmosphäre-Panel + private ToggleButton waterBtn; // Sub-Toggle in Wasser-Panel + private ToggleButton riverBtn; // Sub-Toggle in Wasser-Panel + private ToggleButton wasserMergedBtn; // Haupt-Toolbar: Wasser + Wasserfall + private ToggleButton wegeBtn; // Haupt-Toolbar: Wege / Pfadnetz private ToggleButton zonenBtn; private ToggleButton playToolBtn; - private ToggleButton voxelBtn; - private ToggleButton voxelCliffBtn; + private ToggleButton voxelBtn; // Sub-Toggle in Voxel-Panel + private ToggleButton voxelCliffBtn; // Sub-Toggle in Voxel-Panel + private ToggleButton auswahlBtn; // Universelles Selektionswerkzeug (F1) + private ToggleButton atmosphaereBtn; // Merged: Licht + Emitter + private ToggleButton voxelMergedBtn; // Merged: Voxel Sculpting + Klippe private RadioMenuItem camOrbitItem; private RadioMenuItem camFreeItem; @@ -433,8 +457,34 @@ public class EditorApp extends Application { input.camMode = SharedInput.CAM_ORBIT; if (camOrbitItem != null) { camOrbitItem.setSelected(true); } } + if (worldHintLabel != null) worldHintLabel.setText(camHintText()); return; } + if (e.getCode() == KeyCode.R && e.isAltDown()) { + e.consume(); + input.resetCameraRequested.set(true); + return; + } + if (e.isControlDown()) { + int idx = switch (e.getCode()) { + case F1 -> 0; case F2 -> 1; case F3 -> 2; case F4 -> 3; + case F5 -> 4; case F6 -> 5; case F7 -> 6; case F8 -> 7; + default -> -1; + }; + if (idx >= 0) { e.consume(); activateContentSection(idx); return; } + } + if (!e.isControlDown() && !e.isAltDown() && "content".equals(currentTool)) { + boolean isFKey = switch (e.getCode()) { + case F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12 -> true; + default -> false; + }; + if (isFKey) { + e.consume(); + switchToWorldEditor(); + handleKeyPress(e.getCode(), true); + return; + } + } handleKeyPress(e.getCode(), true); }); scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_RELEASED, e -> handleKeyPress(e.getCode(), false)); @@ -492,6 +542,7 @@ public class EditorApp extends Application { if (input.jmeReady) { pollerRef[0].stop(); stage.show(); + restoreEditorPrefs(); javafx.animation.PauseTransition closeDelay = new javafx.animation.PauseTransition(javafx.util.Duration.millis(400)); closeDelay.setOnFinished(e -> splash.close()); @@ -610,11 +661,17 @@ public class EditorApp extends Application { input.objectJustPlaced = false; if (multiPlaceCB == null || !multiPlaceCB.isSelected()) { input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; - if (objEditBtn != null) objEditBtn.setSelected(true); + if (objPlaceBtn != null) objPlaceBtn.setSelected(true); root.setRight(buildObjectEditPanel()); } } + if (input.objectClickedWhileInPlaceMode) { + input.objectClickedWhileInPlaceMode = false; + if (objPlaceBtn != null) objPlaceBtn.setSelected(true); + root.setRight(buildObjectEditPanel()); + } + if (input.objectSelectionChanged) { input.objectSelectionChanged = false; updateObjectPanel(input.selectedObjectInfo); @@ -704,7 +761,7 @@ public class EditorApp extends Application { if (input.cliffGenComplete) { input.cliffGenComplete = false; // Zu Voxel-Tool wechseln, damit der User direkt backen kann - Platform.runLater(() -> { if (voxelBtn != null) voxelBtn.fire(); }); + Platform.runLater(() -> { if (voxelMergedBtn != null) voxelMergedBtn.fire(); }); } if (input.spawnPickChanged) { @@ -883,12 +940,19 @@ public class EditorApp extends Application { boolean wasObjEditor = "objecteditor".equals(currentTool); boolean wasModelEditor = "modeleditor".equals(currentTool); currentTool = "world"; + root.setLeft(null); // ggf. gesetzten Content-Tree entfernen topBar.getChildren().set(1, worldToolBar); if (routineJmeSlot != null) { routineJmeSlot.getChildren().remove(worldViewport); routineJmeSlot = null; + // centerStack-Kinder neu aufbauen: worldViewport könnte verschoben worden sein, + // und der Charakter-Editor-Platzhalter muss weg + centerStack.getChildren().setAll(worldViewport, assetPanel, assetTabBtn); + assetTabBtn.setTranslateX(assetPanel.getTranslateX() + 420); + if (root.getCenter() != centerStack) root.setCenter(centerStack); + } else { + setCenterView(worldViewport); } - setCenterView(worldViewport); root.setRight(toolPanel); if (wasObjEditor && (input.activeLayer == SharedInput.LAYER_OBJECTS || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)) { @@ -919,8 +983,8 @@ public class EditorApp extends Application { Button backBtn = new Button("← Welteneditor"); backBtn.setOnAction(e -> switchToWorldEditor()); - objPlaceBtn = new ToggleButton("📦 Platzieren"); - objEditBtn = new ToggleButton("🔧 Bearbeiten"); + objPlaceBtn = new ToggleButton("+ Platzieren"); + objEditBtn = new ToggleButton("# Bearbeiten"); objPlaceBtn.setStyle("-fx-font-weight: bold;"); objEditBtn.setStyle("-fx-font-weight: bold;"); @@ -1318,24 +1382,31 @@ public class EditorApp extends Application { MenuItem saveItem = new MenuItem("Speichern"); saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S")); saveItem.setOnAction(e -> { input.saveRequested = true; setStatus("Speichern…"); }); - MenuItem importModelLodItem = new MenuItem("Modell mit LOD-Editor…"); - MenuItem importTexItem = new MenuItem("Texturen…"); - MenuItem importTexZipItem = new MenuItem("Texturen-ZIP…"); - MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…"); - MenuItem importAudioItem = new MenuItem("Audio…"); - MenuItem importAnimItem = new MenuItem("Animationen…"); + MenuItem screenshotItem = new MenuItem("Screenshot"); + screenshotItem.setOnAction(e -> takeEditorScreenshot()); + MenuItem settingsItem = new MenuItem("Einstellungen…"); + settingsItem.setOnAction(e -> openSettings()); + MenuItem quitItem = new MenuItem("Beenden"); + quitItem.setOnAction(e -> javafx.application.Platform.exit()); + fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(), + screenshotItem, new SeparatorMenuItem(), settingsItem, + new SeparatorMenuItem(), quitItem); + + Menu importMenu = new Menu("Import"); + MenuItem importModelLodItem = new MenuItem("Modell mit LOD-Editor…"); + MenuItem importTexItem = new MenuItem("Texturen…"); + MenuItem importTexZipItem = new MenuItem("Texturen-ZIP…"); + MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…"); + MenuItem importAudioItem = new MenuItem("Audio…"); + MenuItem importAnimItem = new MenuItem("Animationen…"); importModelLodItem.setOnAction(e -> openModelImport(primaryStage)); importTexItem.setOnAction(e -> handleTextureImport(primaryStage)); importTexZipItem.setOnAction(e -> handleZipTextureImport(primaryStage)); importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage)); importAudioItem.setOnAction(e -> handleAudioImport(primaryStage)); importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage)); - MenuItem screenshotItem = new MenuItem("Screenshot"); - screenshotItem.setOnAction(e -> takeEditorScreenshot()); - fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(), - importModelLodItem, importTexItem, importTexZipItem, importTexSetItem, - importAudioItem, importAnimItem, - new SeparatorMenuItem(), screenshotItem); + importMenu.getItems().addAll(importModelLodItem, importTexItem, importTexZipItem, + importTexSetItem, importAudioItem, importAnimItem); Menu toolsMenu = new Menu("Werkzeuge"); MenuItem vegetationsItem = new MenuItem("Vegetations Generator"); @@ -1344,38 +1415,18 @@ public class EditorApp extends Application { MenuItem animPrevItem = new MenuItem("Animationseditor"); MenuItem objEditorItem = new MenuItem("Model Editor"); MenuItem worldEditItem = new MenuItem("Welteneditor"); - MenuItem charEditItem = new MenuItem("Character Editor"); - MenuItem questEditItem = new MenuItem("Quest-Manager"); - MenuItem itemEditItem = new MenuItem("Item-Manager"); - MenuItem recipeEditItem = new MenuItem("Rezept-Manager"); - 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()); tripoItem.setOnAction(e -> switchToTripo()); animPrevItem.setOnAction(e -> switchToAnimPreview()); objEditorItem.setOnAction(e -> openModelEditor(null, null)); worldEditItem.setOnAction(e -> switchToWorldEditor()); - charEditItem.setOnAction(e -> switchToCharacterEditor()); - questEditItem.setOnAction(e -> switchToQuestEditor()); - itemEditItem.setOnAction(e -> switchToItemEditor()); - recipeEditItem.setOnAction(e -> switchToRecipeEditor()); - 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, areaEditItem, localizationEditItem); + animPrevItem, objEditorItem, worldEditItem); Menu viewMenu = new Menu("Ansicht"); - MenuItem resetCam = new MenuItem("Kamera zurücksetzen"); - resetCam.setOnAction(e -> input.addMouseDelta(0, 0)); + MenuItem resetCam = new MenuItem("Kamera zurücksetzen (Alt+R)"); + resetCam.setOnAction(e -> input.resetCameraRequested.set(true)); MenuItem viewTexture = new MenuItem("Textur (Alt+G)"); MenuItem viewWireframe = new MenuItem("Drahtgitter (Alt+G)"); viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Alt+T)"); @@ -1396,14 +1447,22 @@ public class EditorApp extends Application { camOrbitItem.setToggleGroup(camMenuGroup); camFreeItem.setToggleGroup(camMenuGroup); camOrbitItem.setSelected(true); - camOrbitItem.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT); - camFreeItem.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY); + camOrbitItem.setOnAction(e -> { input.camMode = SharedInput.CAM_ORBIT; if (worldHintLabel != null) worldHintLabel.setText(camHintText()); }); + camFreeItem.setOnAction(e -> { input.camMode = SharedInput.CAM_FREEFLY; if (worldHintLabel != null) worldHintLabel.setText(camHintText()); }); CheckMenuItem debugHeightItem = new CheckMenuItem("Höhen-Overlay (Alt+H)"); debugHeightItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+H")); debugHeightItem.setOnAction(e -> input.debugHeightOverlayEnabled = debugHeightItem.isSelected()); + dualWindowMenuItem = new CheckMenuItem("2-Fenster-Modus (Alt+W)"); + dualWindowMenuItem.setAccelerator( + javafx.scene.input.KeyCombination.keyCombination("Alt+W")); + dualWindowMenuItem.setOnAction(e -> { + if (dualWindowMenuItem.isSelected()) enableDualWindowMode(); + else disableDualWindowMode(); + }); + viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe, new SeparatorMenuItem(), viewTopologyItem, new SeparatorMenuItem(), debugHeightItem, @@ -1420,58 +1479,62 @@ public class EditorApp extends Application { item.setOnAction(e -> input.pendingTimeOfDay = t); zeitMenu.getItems().add(item); } - menuBar.getMenus().addAll(fileMenu, toolsMenu, viewMenu, zeitMenu); + menuBar.getMenus().addAll(fileMenu, toolsMenu, importMenu, viewMenu, zeitMenu); ToolBar toolBar = new ToolBar(); - baseBtn = new ToggleButton("▲▼ Basis-Terrain"); - grassBtn = new ToggleButton("🌿 Gras (Textur)"); - grassVertexBtn = new ToggleButton("🌱 Gras (Vertices)"); - textureBtn = new ToggleButton("🎨 Textur"); - objPlaceBtn = new ToggleButton("📦 Platzieren"); - objEditBtn = new ToggleButton("🔧 Bearbeiten"); - lightBtn = new ToggleButton("💡 Licht"); - emitterBtn = new ToggleButton("🔥 Emitter"); - waterBtn = new ToggleButton("💧 Wasser"); - riverBtn = new ToggleButton("↯ Wasserfall"); - zonenBtn = new ToggleButton("🗺 Zonen"); - playToolBtn = new ToggleButton("🎮 Spielen"); - voxelBtn = new ToggleButton("⬡ Voxel"); - voxelCliffBtn = new ToggleButton("⛰ Klippe"); - stoneBtn = new ToggleButton("🪨 Steine"); - baseBtn.setStyle("-fx-font-weight:bold;"); - grassBtn.setStyle("-fx-font-weight:bold;"); - grassVertexBtn.setStyle("-fx-font-weight:bold;"); - textureBtn.setStyle("-fx-font-weight:bold;"); - objPlaceBtn.setStyle("-fx-font-weight:bold;"); - objEditBtn.setStyle("-fx-font-weight:bold;"); - lightBtn.setStyle("-fx-font-weight:bold;"); - emitterBtn.setStyle("-fx-font-weight:bold;"); - waterBtn.setStyle("-fx-font-weight:bold;"); - riverBtn.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;"); - stoneBtn.setStyle("-fx-font-weight:bold;"); + auswahlBtn = new ToggleButton("[k] Auswahl"); + baseBtn = new ToggleButton("▲▼ Terrain"); + voxelMergedBtn = new ToggleButton("◈ Voxel"); + textureBtn = new ToggleButton(": : Textur"); + grassBtn = new ToggleButton("// Gras"); + stoneBtn = new ToggleButton("o Steine"); + objPlaceBtn = new ToggleButton("+ Objekte"); + atmosphaereBtn = new ToggleButton("* Atmosphare"); + wasserMergedBtn = new ToggleButton("~ Wasser"); + wegeBtn = new ToggleButton("-- Wege"); + zonenBtn = new ToggleButton("[ ] Zonen"); + playToolBtn = new ToggleButton("▶ Spielen"); + // Interne Sub-Toggle-Buttons (nicht in Toolbar, werden in Panel-Buildern erzeugt) + lightBtn = null; + emitterBtn = null; + voxelBtn = null; + voxelCliffBtn = null; + waterBtn = null; + riverBtn = null; + + final String BOLD = "-fx-font-weight:bold;"; + auswahlBtn.setStyle(BOLD); + baseBtn.setStyle(BOLD); + voxelMergedBtn.setStyle(BOLD); + textureBtn.setStyle(BOLD); + grassBtn.setStyle(BOLD); + stoneBtn.setStyle(BOLD); + objPlaceBtn.setStyle(BOLD); + atmosphaereBtn.setStyle(BOLD); + wasserMergedBtn.setStyle(BOLD); + wegeBtn.setStyle(BOLD); + zonenBtn.setStyle(BOLD); + playToolBtn.setStyle(BOLD); ToggleGroup layerGroup = new ToggleGroup(); + auswahlBtn.setToggleGroup(layerGroup); baseBtn.setToggleGroup(layerGroup); - grassBtn.setToggleGroup(layerGroup); - grassVertexBtn.setToggleGroup(layerGroup); + voxelMergedBtn.setToggleGroup(layerGroup); textureBtn.setToggleGroup(layerGroup); + grassBtn.setToggleGroup(layerGroup); + stoneBtn.setToggleGroup(layerGroup); objPlaceBtn.setToggleGroup(layerGroup); - objEditBtn.setToggleGroup(layerGroup); - lightBtn.setToggleGroup(layerGroup); - emitterBtn.setToggleGroup(layerGroup); - waterBtn.setToggleGroup(layerGroup); - riverBtn.setToggleGroup(layerGroup); + atmosphaereBtn.setToggleGroup(layerGroup); + wasserMergedBtn.setToggleGroup(layerGroup); + wegeBtn.setToggleGroup(layerGroup); zonenBtn.setToggleGroup(layerGroup); playToolBtn.setToggleGroup(layerGroup); - voxelBtn.setToggleGroup(layerGroup); - voxelCliffBtn.setToggleGroup(layerGroup); - stoneBtn.setToggleGroup(layerGroup); - baseBtn.setSelected(true); + auswahlBtn.setSelected(true); + auswahlBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_AUSWAHL; + root.setRight(buildAuswahlPanel()); + }); baseBtn.setOnAction(e -> { input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue()); input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue()); @@ -1480,50 +1543,31 @@ public class EditorApp extends Application { root.setRight(toolPanel); showToolParameters(toolPanel, input.activeTool); }); - grassBtn.setOnAction(e -> { - input.activeLayer = 3; input.activeTool = input.grassTool; - root.setRight(toolPanel); - showToolParameters(toolPanel, input.activeTool); - }); - grassVertexBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_GRASS_VERTEX; - input.activeTool = input.grassVertexTool; - root.setRight(toolPanel); - showToolParameters(toolPanel, input.activeTool); - }); + voxelMergedBtn.setOnAction(e -> root.setRight(buildVoxelMergedPanel())); textureBtn.setOnAction(e -> { input.activeLayer = 4; input.activeTool = input.textureTool; root.setRight(toolPanel); showToolParameters(toolPanel, input.activeTool); }); + grassBtn.setOnAction(e -> root.setRight(buildGrassPanel())); + stoneBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_STONE; + input.activeTool = input.stoneTool; + root.setRight(toolPanel); + showToolParameters(toolPanel, input.activeTool); + }); objPlaceBtn.setOnAction(e -> { input.activeLayer = SharedInput.LAYER_OBJECTS; + input.pendingModelPath = null; if (modelsNode != null) modelsNode.setExpanded(true); openAssetOverlay(); root.setRight(buildObjectPlacePanel()); }); - objEditBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; - input.pendingModelPath = null; - if (modelsNode != null) modelsNode.setExpanded(true); - openAssetOverlay(); - root.setRight(buildObjectEditPanel()); - }); - lightBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_LIGHTS; - root.setRight(buildLightPanel()); - }); - emitterBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_EMITTERS; - root.setRight(buildEmitterPanel()); - }); - waterBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_WATER; - root.setRight(buildWaterPanel()); - }); - riverBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_WATERFALL; - root.setRight(buildWaterfallPanel()); + atmosphaereBtn.setOnAction(e -> root.setRight(buildAtmospherePanel())); + wasserMergedBtn.setOnAction(e -> root.setRight(buildWasserPanel())); + wegeBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_PATH_NETWORK; + root.setRight(buildWegePanel()); }); zonenBtn.setOnAction(e -> { input.activeLayer = SharedInput.LAYER_ZONEN; @@ -1534,46 +1578,22 @@ public class EditorApp extends Application { input.playToolMode = SharedInput.PlayToolMode.EDIT; root.setRight(buildPlayToolPanel()); }); - voxelBtn.setOnAction(e -> { - input.voxelTool.brushRadius.setValue(input.heightTool.brushRadius.getValue()); - input.voxelTool.brushStrength.setValue(input.heightTool.brushStrength.getValue()); - input.voxelTool.plateauTarget.setValue(input.heightTool.plateauHeight.getValue()); - input.activeLayer = SharedInput.LAYER_VOXEL; - input.activeTool = input.voxelTool; - root.setRight(toolPanel); - showToolParameters(toolPanel, input.activeTool); - }); - voxelCliffBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_VOXEL_CLIFF; - root.setRight(buildVoxelCliffPanel()); - }); - stoneBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_STONE; - input.activeTool = input.stoneTool; - root.setRight(toolPanel); - showToolParameters(toolPanel, input.activeTool); - }); - Button camResetBtn = new Button("⌂ Reset"); - camResetBtn.setStyle("-fx-font-weight:bold;"); - camResetBtn.setTooltip(new javafx.scene.control.Tooltip("Kamera auf x=0, z=0, y=Terrain+10m zurücksetzen")); - camResetBtn.setOnAction(e -> input.resetCameraRequested.set(true)); + worldHintLabel = new Label(camHintText()); + worldHintLabel.setStyle("-fx-text-fill: #555;"); - Label hint = new Label("WASD/QE: Kamera | Mitte-Drag / L+R-Drag: Drehen | L-Klick: hoch | R-Klick: tief"); - hint.setStyle("-fx-text-fill: #555;"); - - toolBar.getItems().addAll(baseBtn, grassBtn, grassVertexBtn, textureBtn, - new Separator(Orientation.VERTICAL), objPlaceBtn, objEditBtn, - new Separator(Orientation.VERTICAL), lightBtn, - new Separator(Orientation.VERTICAL), emitterBtn, - new Separator(Orientation.VERTICAL), waterBtn, - new Separator(Orientation.VERTICAL), riverBtn, + toolBar.getItems().addAll( + auswahlBtn, + new Separator(Orientation.VERTICAL), baseBtn, voxelMergedBtn, + new Separator(Orientation.VERTICAL), textureBtn, grassBtn, stoneBtn, + new Separator(Orientation.VERTICAL), objPlaceBtn, + new Separator(Orientation.VERTICAL), atmosphaereBtn, + new Separator(Orientation.VERTICAL), wasserMergedBtn, wegeBtn, new Separator(Orientation.VERTICAL), zonenBtn, new Separator(Orientation.VERTICAL), playToolBtn, - new Separator(Orientation.VERTICAL), voxelBtn, voxelCliffBtn, - new Separator(Orientation.VERTICAL), stoneBtn, - new Separator(Orientation.VERTICAL), camResetBtn, - new Separator(Orientation.VERTICAL), hint); + new Separator(Orientation.VERTICAL), worldHintLabel, + new Separator(Orientation.VERTICAL), createToolBarSpacer(), + buildContentSwitchBtn()); worldToolBar = toolBar; return new VBox(menuBar, toolBar); @@ -2777,7 +2797,12 @@ public class EditorApp extends Application { private void updateWaterfallPanel(String info) { if (input.activeLayer != SharedInput.LAYER_WATERFALL) return; - root.setRight(info != null ? buildWaterfallSelectionPanel(info) : buildWaterfallPanel()); + VBox panel = info != null ? buildWaterfallSelectionPanel(info) : buildWaterfallPanel(); + if (wasserContentBox != null) { + wasserContentBox.getChildren().setAll(panel); + } else { + root.setRight(buildWasserPanel()); + } } private VBox buildWaterfallSelectionPanel(String info) { @@ -2893,9 +2918,9 @@ public class EditorApp extends Application { new Separator()); // Preset-Buttons (setzen emitterPreset; wirken sofort auf selektierten Emitter) - Button fireBtn = new Button("🔥 Feuer"); - Button smokeBtn = new Button("💨 Rauch"); - Button sparksBtn = new Button("✨ Funken"); + Button fireBtn = new Button("* Feuer"); + Button smokeBtn = new Button("~ Rauch"); + Button sparksBtn = new Button("* Funken"); fireBtn.setMaxWidth(Double.MAX_VALUE); smokeBtn.setMaxWidth(Double.MAX_VALUE); sparksBtn.setMaxWidth(Double.MAX_VALUE); @@ -3395,6 +3420,303 @@ public class EditorApp extends Application { return panel; } + // ── Kombiniertes Gras-Panel ─────────────────────────────────────────────── + + private VBox buildGrassPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + + Label title = new Label("Gras"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); + panel.getChildren().addAll(title, new Separator()); + + // Sub-Tool-Auswahl + grassSubTexBtn = new ToggleButton(":: Gras (Textur)"); + grassSubVertBtn = new ToggleButton("|| Gras (Vertices)"); + grassSubTexBtn.setMaxWidth(Double.MAX_VALUE); + grassSubVertBtn.setMaxWidth(Double.MAX_VALUE); + ToggleGroup subGroup = new ToggleGroup(); + grassSubTexBtn.setToggleGroup(subGroup); + grassSubVertBtn.setToggleGroup(subGroup); + subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); + HBox subRow = new HBox(4, grassSubTexBtn, grassSubVertBtn); + HBox.setHgrow(grassSubTexBtn, Priority.ALWAYS); + HBox.setHgrow(grassSubVertBtn, Priority.ALWAYS); + panel.getChildren().add(subRow); + + // Parameter-Bereich (wird beim Sub-Tool-Wechsel ersetzt) + VBox paramsBox = new VBox(10); + panel.getChildren().add(paramsBox); + + // Globale Farben + panel.getChildren().add(new Separator()); + Label colorTitle = new Label("Globale Farben"); + colorTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #333;"); + panel.getChildren().add(colorTitle); + + javafx.scene.control.ColorPicker freshPicker = new javafx.scene.control.ColorPicker( + javafx.scene.paint.Color.color(input.grassFreshR, input.grassFreshG, input.grassFreshB)); + freshPicker.setMaxWidth(Double.MAX_VALUE); + freshPicker.setOnAction(e -> { + javafx.scene.paint.Color c = freshPicker.getValue(); + input.grassFreshR = (float) c.getRed(); + input.grassFreshG = (float) c.getGreen(); + input.grassFreshB = (float) c.getBlue(); + input.grassColorsChanged = true; + }); + + javafx.scene.control.ColorPicker dryPicker = new javafx.scene.control.ColorPicker( + javafx.scene.paint.Color.color(input.grassDryR, input.grassDryG, input.grassDryB)); + dryPicker.setMaxWidth(Double.MAX_VALUE); + dryPicker.setOnAction(e -> { + javafx.scene.paint.Color c = dryPicker.getValue(); + input.grassDryR = (float) c.getRed(); + input.grassDryG = (float) c.getGreen(); + input.grassDryB = (float) c.getBlue(); + input.grassColorsChanged = true; + }); + + panel.getChildren().addAll( + new Label("Unvertrocknet:"), freshPicker, + new Label("Vertrocknet:"), dryPicker); + + // Sub-Tool-Handler + Runnable activateTex = () -> { + input.activeLayer = 3; + input.activeTool = input.grassTool; + paramsBox.getChildren().clear(); + VBox inner = new VBox(10); + showToolParameters(inner, input.activeTool); + paramsBox.getChildren().add(inner); + }; + Runnable activateVert = () -> { + input.activeLayer = SharedInput.LAYER_GRASS_VERTEX; + input.activeTool = input.grassVertexTool; + paramsBox.getChildren().clear(); + VBox inner = new VBox(10); + showToolParameters(inner, input.activeTool); + paramsBox.getChildren().add(inner); + }; + + grassSubTexBtn.setOnAction(e -> activateTex.run()); + grassSubVertBtn.setOnAction(e -> activateVert.run()); + + // Standard: zuletzt aktives Sub-Tool + if (input.activeLayer == SharedInput.LAYER_GRASS_VERTEX) { + grassSubVertBtn.setSelected(true); + activateVert.run(); + } else { + grassSubTexBtn.setSelected(true); + activateTex.run(); + } + + return panel; + } + + private VBox buildAuswahlPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + Label title = new Label("Auswahl"); + 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(" → wechselt automatisch zum"), + styledHint(" passenden Werkzeug"), + new Separator(), + styledHint("Selektierbar:"), + styledHint(" + Platzierte Objekte"), + styledHint(" * Lichter / Emitter"), + styledHint(" ~ Wasser / Wasserfall"), + styledHint(" -- Wege / Knoten"), + styledHint(" [ ] Zonen")); + return panel; + } + + private VBox buildAtmospherePanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + Label title = new Label("Atmosphare"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); + panel.getChildren().addAll(title, new Separator()); + + ToggleButton subLichtBtn = new ToggleButton("* Licht"); + ToggleButton subEmitterBtn = new ToggleButton("~ Emitter"); + subLichtBtn.setMaxWidth(Double.MAX_VALUE); + subEmitterBtn.setMaxWidth(Double.MAX_VALUE); + ToggleGroup subGroup = new ToggleGroup(); + subLichtBtn.setToggleGroup(subGroup); + subEmitterBtn.setToggleGroup(subGroup); + subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); + HBox subRow = new HBox(4, subLichtBtn, subEmitterBtn); + HBox.setHgrow(subLichtBtn, Priority.ALWAYS); + HBox.setHgrow(subEmitterBtn, Priority.ALWAYS); + panel.getChildren().add(subRow); + + VBox contentBox = new VBox(0); + panel.getChildren().add(contentBox); + + Runnable activateLicht = () -> { + input.activeLayer = SharedInput.LAYER_LIGHTS; + lightBtn = subLichtBtn; + contentBox.getChildren().setAll(buildLightPanel()); + }; + Runnable activateEmitter = () -> { + input.activeLayer = SharedInput.LAYER_EMITTERS; + emitterBtn = subEmitterBtn; + contentBox.getChildren().setAll(buildEmitterPanel()); + }; + + subLichtBtn.setOnAction(e -> activateLicht.run()); + subEmitterBtn.setOnAction(e -> activateEmitter.run()); + + if (input.activeLayer == SharedInput.LAYER_EMITTERS) { + subEmitterBtn.setSelected(true); + activateEmitter.run(); + } else { + subLichtBtn.setSelected(true); + activateLicht.run(); + } + + return panel; + } + + private VBox buildWasserPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + Label title = new Label("Wasser"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); + panel.getChildren().addAll(title, new Separator()); + + ToggleButton subWasserBtn = new ToggleButton("~ Wasser"); + ToggleButton subWasserfallBtn = new ToggleButton("↯ Wasserfall"); + subWasserBtn.setMaxWidth(Double.MAX_VALUE); + subWasserfallBtn.setMaxWidth(Double.MAX_VALUE); + ToggleGroup subGroup = new ToggleGroup(); + subWasserBtn.setToggleGroup(subGroup); + subWasserfallBtn.setToggleGroup(subGroup); + subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); + HBox subRow = new HBox(4, subWasserBtn, subWasserfallBtn); + HBox.setHgrow(subWasserBtn, Priority.ALWAYS); + HBox.setHgrow(subWasserfallBtn, Priority.ALWAYS); + panel.getChildren().add(subRow); + + VBox contentBox = new VBox(0); + wasserContentBox = contentBox; + panel.getChildren().add(contentBox); + + Runnable activateWasser = () -> { + input.activeLayer = SharedInput.LAYER_WATER; + waterBtn = subWasserBtn; + contentBox.getChildren().setAll(buildWaterPanel()); + }; + Runnable activateWasserfall = () -> { + input.activeLayer = SharedInput.LAYER_WATERFALL; + riverBtn = subWasserfallBtn; + contentBox.getChildren().setAll(buildWaterfallPanel()); + }; + + subWasserBtn.setOnAction(e -> activateWasser.run()); + subWasserfallBtn.setOnAction(e -> activateWasserfall.run()); + + if (input.activeLayer == SharedInput.LAYER_WATERFALL) { + subWasserfallBtn.setSelected(true); + activateWasserfall.run(); + } else { + subWasserBtn.setSelected(true); + activateWasser.run(); + } + + return panel; + } + + private VBox buildWegePanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + inner.getChildren().addAll( + sectionTitle("Wegnetz"), + new Separator(), + styledHint("L-Klick: neuen Knoten setzen"), + styledHint("L-Klick auf Knoten: auswählen"), + styledHint(" (2 gewählte → Kante erzeugen)"), + styledHint("L-Klick + gewählter Knoten:"), + styledHint(" neuen Knoten + Kante"), + styledHint("R-Klick auf Knoten: löschen"), + styledHint("R-Klick auf Terrain: abwählen")); + + 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(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + private VBox buildVoxelMergedPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + + ToggleButton subSculptBtn = new ToggleButton("◈ Sculpting"); + ToggleButton subCliffBtn = new ToggleButton("/\\ Klippe"); + subSculptBtn.setMaxWidth(Double.MAX_VALUE); + subCliffBtn.setMaxWidth(Double.MAX_VALUE); + ToggleGroup subGroup = new ToggleGroup(); + subSculptBtn.setToggleGroup(subGroup); + subCliffBtn.setToggleGroup(subGroup); + subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); + HBox subRow = new HBox(4, subSculptBtn, subCliffBtn); + HBox.setHgrow(subSculptBtn, Priority.ALWAYS); + HBox.setHgrow(subCliffBtn, Priority.ALWAYS); + panel.getChildren().add(subRow); + + VBox contentBox = new VBox(0); + panel.getChildren().add(contentBox); + + Runnable activateSculpt = () -> { + input.voxelTool.brushRadius.setValue(input.heightTool.brushRadius.getValue()); + input.voxelTool.brushStrength.setValue(input.heightTool.brushStrength.getValue()); + input.voxelTool.plateauTarget.setValue(input.heightTool.plateauHeight.getValue()); + input.activeLayer = SharedInput.LAYER_VOXEL; + input.activeTool = input.voxelTool; + voxelBtn = subSculptBtn; + showToolParameters(toolPanel, input.activeTool); + contentBox.getChildren().setAll(toolPanel); + }; + Runnable activateCliff = () -> { + input.activeLayer = SharedInput.LAYER_VOXEL_CLIFF; + voxelCliffBtn = subCliffBtn; + contentBox.getChildren().setAll(buildVoxelCliffPanel()); + }; + + subSculptBtn.setOnAction(e -> activateSculpt.run()); + subCliffBtn.setOnAction(e -> activateCliff.run()); + + if (input.activeLayer == SharedInput.LAYER_VOXEL_CLIFF) { + subCliffBtn.setSelected(true); + activateCliff.run(); + } else { + subSculptBtn.setSelected(true); + activateSculpt.run(); + } + + return panel; + } + private void showToolParameters(VBox panel, EditorTool tool) { panel.getChildren().clear(); @@ -4587,12 +4909,8 @@ public class EditorApp extends Application { openAnimSetEditor(relPath, p); } else if (relPath.endsWith(".character")) { String charName = p.getFileName().toString().replace(".character", ""); - Path charDir = ASSET_ROOT.resolve("character"); switchToCharacterEditor(); - if (charListView != null) { - charListView.getSelectionModel().select(charName); - loadSelectedCharacter(charDir); - } + loadCharacterById(charName); } }); @@ -4636,11 +4954,11 @@ public class EditorApp extends Application { case "item" -> { input.activeLayer = SharedInput.LAYER_ITEMS; root.setRight(buildItemPlacePanel(null)); } 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 "water" -> { input.activeLayer = SharedInput.LAYER_WATER; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); root.setRight(buildWasserPanel()); } 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()); } + case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); root.setRight(buildWasserPanel()); } } }, (obj, index, toolHint) -> { @@ -8273,6 +8591,132 @@ public class EditorApp extends Application { gc.fillOval(cx - 2.5, cy - 2.5, 5, 5); } + private void openSettings() { + Path prefsPath = BlightHome.resolve("config", "editor.prefs"); + java.util.Properties p = new java.util.Properties(); + if (java.nio.file.Files.exists(prefsPath)) { + try (java.io.Reader r = Files.newBufferedReader(prefsPath)) { p.load(r); } + catch (Exception ignored) {} + } + // einmalige Migration aus der alten tts_prefs.properties + if (!p.containsKey("piper.path")) { + Path oldPrefs = de.blight.editor.ProjectRoot.resolve("tts_prefs.properties"); + if (java.nio.file.Files.exists(oldPrefs)) { + try (java.io.Reader r = Files.newBufferedReader(oldPrefs)) { + java.util.Properties old = new java.util.Properties(); + old.load(r); + old.forEach((k, v) -> p.putIfAbsent(k, v)); + } catch (Exception ignored) {} + } + } + + // ── TTS ────────────────────────────────────────────────────────────── + javafx.scene.control.TextField piperField = + new javafx.scene.control.TextField(p.getProperty("piper.path", "")); + piperField.setPromptText("/pfad/zur/piper-binary oder piper"); + + javafx.scene.control.TextField modelsDirField = + new javafx.scene.control.TextField(p.getProperty("piper.models.dir", "")); + modelsDirField.setPromptText("/pfad/zu/models/"); + Button browseDirBtn = new Button("…"); + browseDirBtn.setOnAction(ev -> { + javafx.stage.DirectoryChooser dc = new javafx.stage.DirectoryChooser(); + dc.setTitle("Piper-Modell-Verzeichnis"); + String cur = modelsDirField.getText().trim(); + if (!cur.isBlank()) { + java.io.File f = new java.io.File(cur); + if (f.isDirectory()) dc.setInitialDirectory(f); + } + java.io.File chosen = dc.showDialog(primaryStage); + if (chosen != null) modelsDirField.setText(chosen.getAbsolutePath()); + }); + HBox dirRow = new HBox(4, modelsDirField, browseDirBtn); + HBox.setHgrow(modelsDirField, Priority.ALWAYS); + + javafx.scene.control.TextField modelFileField = + new javafx.scene.control.TextField(p.getProperty("piper.model", "")); + modelFileField.setPromptText("/pfad/zum/modell.onnx"); + Button browseModelBtn = new Button("…"); + browseModelBtn.setOnAction(ev -> { + javafx.stage.FileChooser fc = new javafx.stage.FileChooser(); + fc.setTitle("Piper-Stimme wählen"); + fc.getExtensionFilters().add( + new javafx.stage.FileChooser.ExtensionFilter("ONNX-Modell", "*.onnx")); + String curDir = modelsDirField.getText().trim(); + if (!curDir.isBlank()) { + java.io.File d = new java.io.File(curDir); + if (d.isDirectory()) fc.setInitialDirectory(d); + } + java.io.File chosen = fc.showOpenDialog(primaryStage); + if (chosen != null) modelFileField.setText(chosen.getAbsolutePath()); + }); + HBox modelRow = new HBox(4, modelFileField, browseModelBtn); + HBox.setHgrow(modelFileField, Priority.ALWAYS); + + // ── Fenstermodus ────────────────────────────────────────────────────── + javafx.scene.control.RadioButton oneWinBtn = + new javafx.scene.control.RadioButton("1-Fenster-Modus"); + javafx.scene.control.RadioButton twoWinBtn = + new javafx.scene.control.RadioButton("2-Fenster-Modus (Alt+W)"); + javafx.scene.control.ToggleGroup wg = new javafx.scene.control.ToggleGroup(); + oneWinBtn.setToggleGroup(wg); + twoWinBtn.setToggleGroup(wg); + (dualWindowMode ? twoWinBtn : oneWinBtn).setSelected(true); + + // ── Layout ──────────────────────────────────────────────────────────── + GridPane grid = new GridPane(); + grid.setHgap(12); + grid.setVgap(8); + grid.setPadding(new Insets(16)); + GridPane.setHgrow(piperField, javafx.scene.layout.Priority.ALWAYS); + GridPane.setHgrow(dirRow, javafx.scene.layout.Priority.ALWAYS); + GridPane.setHgrow(modelRow, javafx.scene.layout.Priority.ALWAYS); + + int row = 0; + Label ttsTtl = new Label("TTS (Piper)"); + ttsTtl.setStyle("-fx-font-weight:bold;"); + grid.add(ttsTtl, 0, row++, 2, 1); + grid.add(new Label("Piper-Binary:"), 0, row); grid.add(piperField, 1, row++); + grid.add(new Label("Modell-Verzeichnis:"), 0, row); grid.add(dirRow, 1, row++); + grid.add(new Label("Stimme (.onnx):"), 0, row); grid.add(modelRow, 1, row++); + + grid.add(new Separator(), 0, row++, 2, 1); + + Label winTtl = new Label("Fenster"); + winTtl.setStyle("-fx-font-weight:bold;"); + grid.add(winTtl, 0, row++, 2, 1); + grid.add(oneWinBtn, 0, row++, 2, 1); + grid.add(twoWinBtn, 0, row++, 2, 1); + + javafx.scene.control.Dialog dlg = + new javafx.scene.control.Dialog<>(); + dlg.initOwner(primaryStage); + dlg.setTitle("Einstellungen"); + dlg.getDialogPane().setContent(grid); + dlg.getDialogPane().getButtonTypes().addAll( + javafx.scene.control.ButtonType.OK, + javafx.scene.control.ButtonType.CANCEL); + + dlg.showAndWait().ifPresent(bt -> { + if (bt != javafx.scene.control.ButtonType.OK) return; + // TTS-Werte speichern + p.setProperty("piper.path", piperField.getText().trim()); + p.setProperty("piper.models.dir", modelsDirField.getText().trim()); + p.setProperty("piper.model", modelFileField.getText().trim()); + try { + Files.createDirectories(BlightHome.resolve("config")); + try (java.io.Writer w = Files.newBufferedWriter(prefsPath)) { + p.store(w, "Blight Editor Settings"); + } + } catch (Exception ignored) {} + // Fenstermodus anwenden + boolean wantDual = twoWinBtn.isSelected(); + if (wantDual && !dualWindowMode) enableDualWindowMode(); + else if (!wantDual && dualWindowMode) disableDualWindowMode(); + if (dualWindowMenuItem != null) dualWindowMenuItem.setSelected(dualWindowMode); + }); + } + private void saveCameraPrefs() { if (!Float.isFinite(input.camX) || !Float.isFinite(input.camY) || !Float.isFinite(input.camZ)) return; java.util.Properties p = new java.util.Properties(); @@ -8286,6 +8730,14 @@ public class EditorApp extends Application { p.setProperty("tempSpawn.z", String.valueOf(input.tempSpawnZ)); p.setProperty("tempSpawn.yaw", String.valueOf(input.tempSpawnYaw)); } + p.setProperty("dual.window", String.valueOf(dualWindowMode)); + p.setProperty("content.last.section", contentLastSection); + if (contentEditorStage != null && contentEditorStage.isShowing()) { + p.setProperty("content.x", String.valueOf(contentEditorStage.getX())); + p.setProperty("content.y", String.valueOf(contentEditorStage.getY())); + p.setProperty("content.w", String.valueOf(contentEditorStage.getWidth())); + p.setProperty("content.h", String.valueOf(contentEditorStage.getHeight())); + } try { Files.createDirectories(BlightHome.resolve("config")); try (java.io.Writer w = Files.newBufferedWriter(BlightHome.resolve("config", "editor.prefs"))) { @@ -8300,6 +8752,11 @@ public class EditorApp extends Application { Platform.runLater(() -> statusLabel.setText(msg)); } + private String camHintText() { + String mode = (input.camMode == SharedInput.CAM_FREEFLY) ? "FreeFly" : "Orbit"; + return "WASD/QE: Kamera | Mitte-Drag: Drehen | Alt+C: Kamera wechseln | Kamera: " + mode; + } + // ── Tastatur-Handling ──────────────────────────────────────────────────── private void handleKeyPress(KeyCode code, boolean pressed) { @@ -8390,29 +8847,36 @@ public class EditorApp extends Application { input.topologyRequest = sel ? 1 : 2; } } - case F1 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> baseBtn.fire()); } - case F2 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> grassBtn.fire()); } - case F5 -> { + case F1 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (auswahlBtn != null) auswahlBtn.fire(); }); } + case F2 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (baseBtn != null) baseBtn.fire(); }); } + case F3 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (voxelMergedBtn != null) voxelMergedBtn.fire(); }); } + case F4 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (textureBtn != null) textureBtn.fire(); }); } + case F5 -> { if (pressed) { - if ("world".equals(currentTool)) Platform.runLater(() -> textureBtn.fire()); + if ("world".equals(currentTool)) Platform.runLater(() -> { if (grassBtn != null) grassBtn.fire(); }); else onF5.run(); } } - case F6 -> { + case F6 -> { if (pressed) { - if ("world".equals(currentTool) || "objecteditor".equals(currentTool)) { - if (objPlaceBtn != null) Platform.runLater(() -> objPlaceBtn.fire()); - } else onF6.run(); + if ("world".equals(currentTool)) Platform.runLater(() -> { if (stoneBtn != null) stoneBtn.fire(); }); + else onF6.run(); } } - case F7 -> { - if (pressed && ("world".equals(currentTool) || "objecteditor".equals(currentTool))) { - if (objEditBtn != null) Platform.runLater(() -> objEditBtn.fire()); - } + case F7 -> { + if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (objPlaceBtn != null) objPlaceBtn.fire(); }); + else if (pressed && "objecteditor".equals(currentTool)) Platform.runLater(() -> { if (objPlaceBtn != null) objPlaceBtn.fire(); }); } - case F8 -> { - if (pressed && input.ctrlHeld) input.debugNoLightToggle = true; + case F8 -> { + if (pressed) { + if ("world".equals(currentTool)) Platform.runLater(() -> { if (atmosphaereBtn != null) atmosphaereBtn.fire(); }); + else if (input.ctrlHeld) input.debugNoLightToggle = true; + } else if (input.ctrlHeld) input.debugNoLightToggle = true; } + case F9 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wasserMergedBtn != null) wasserMergedBtn.fire(); }); } + case F10 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wegeBtn != null) wegeBtn.fire(); }); } + case F11 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (zonenBtn != null) zonenBtn.fire(); }); } + case F12 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (playToolBtn != null) playToolBtn.fire(); }); } } } @@ -8502,7 +8966,7 @@ public class EditorApp extends Application { return; } - voxelCliffGenerateBtn = new Button("⛰ Voxel generieren"); + voxelCliffGenerateBtn = new Button("/\\ Voxel generieren"); voxelCliffGenerateBtn.setMaxWidth(Double.MAX_VALUE); voxelCliffGenerateBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;"); voxelCliffGenerateBtn.setOnAction(e -> { @@ -10612,6 +11076,13 @@ public class EditorApp extends Application { // ── Character-Editor ────────────────────────────────────────────────────── private void switchToCharacterEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "character"; + if (contentCharBtn != null) contentCharBtn.setSelected(true); + buildCharacterViewInContent(); + return; + } onF5 = null; currentTool = "charEditor"; topBar.getChildren().set(1, buildCharacterEditorToolBar()); @@ -10642,10 +11113,17 @@ public class EditorApp extends Application { new javafx.scene.control.Tab("Monologe", monologueEditorView) ); root.setCenter(centerTabs); - root.setRight(buildCharacterEditorPanel()); + root.setRight(null); } private void switchToRecipeEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "recipe"; + if (contentRezeptBtn != null) contentRezeptBtn.setSelected(true); + buildRecipeViewInContent(); + return; + } onF5 = null; currentTool = "recipeEditor"; ToolBar tb = new ToolBar(); @@ -10660,6 +11138,13 @@ public class EditorApp extends Application { } private void switchToCraftingTableEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "crafting"; + if (contentCraftingBtn != null) contentCraftingBtn.setSelected(true); + buildCraftingViewInContent(); + return; + } onF5 = null; currentTool = "craftingTableEditor"; ToolBar tb = new ToolBar(); @@ -10674,6 +11159,13 @@ public class EditorApp extends Application { } private void switchToFractionEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "fraction"; + if (contentFraktionBtn != null) contentFraktionBtn.setSelected(true); + buildFractionViewInContent(); + return; + } onF5 = null; currentTool = "fractionEditor"; ToolBar tb = new ToolBar(); @@ -10715,6 +11207,17 @@ public class EditorApp extends Application { } private void switchToItemEditor(String selectItemId) { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "item"; + if (contentItemBtn != null) contentItemBtn.setSelected(true); + de.blight.editor.ui.ItemEditorView view = + new de.blight.editor.ui.ItemEditorView(ASSET_ROOT.resolve("items")); + contentTarget().setCenter(view); + contentTarget().setRight(null); + if (selectItemId != null) view.selectItem(selectItemId); + return; + } onF5 = null; currentTool = "itemEditor"; ToolBar tb = new ToolBar(); @@ -10732,6 +11235,13 @@ public class EditorApp extends Application { } private void switchToQuestEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "quest"; + if (contentQuestBtn != null) contentQuestBtn.setSelected(true); + buildQuestViewInContent(); + return; + } onF5 = null; currentTool = "questEditor"; ToolBar tb = new ToolBar(); @@ -10756,41 +11266,42 @@ public class EditorApp extends Application { return tb; } - private javafx.scene.layout.VBox buildCharacterEditorPanel() { + private ScrollPane buildCharacterSettingsPane() { Path charDir = ASSET_ROOT.resolve("character"); - Path setDir = ASSET_ROOT.resolve("animations").resolve("sets"); VBox inner = new VBox(8); inner.setPadding(new Insets(10)); - // ── Vorhandene Charaktere ───────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Vorhandene Charaktere"), new Separator()); - charListView = new javafx.scene.control.ListView<>(); - charListView.setPrefHeight(120); - refreshCharacterList(); - Button loadCharBtn = new Button("Laden"); - loadCharBtn.setMaxWidth(Double.MAX_VALUE); - loadCharBtn.setOnAction(e -> loadSelectedCharacter(charDir)); - Button newCharBtn = new Button("Neuer Charakter"); + Button newCharBtn = new Button("Neuer Charakter"); newCharBtn.setMaxWidth(Double.MAX_VALUE); newCharBtn.setOnAction(e -> clearCharacterForm()); - inner.getChildren().addAll(charListView, loadCharBtn, newCharBtn); + inner.getChildren().add(newCharBtn); - charEditorStatusLabel = new Label("Kein Charakter geladen – bitte laden oder neu erstellen"); + charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen"); charEditorStatusLabel.setWrapText(true); - charEditorStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #888;"); + charEditorStatusLabel.setStyle("-fx-font-size:11;-fx-text-fill:#777;"); inner.getChildren().add(charEditorStatusLabel); - // ── Editierbarer Bereich (deaktiviert bis Character geladen) ────────── - charEditContainer = new VBox(8); - charEditContainer.setDisable(true); + // Inline-Helfer für dunkles Theme + final String LBL = "-fx-text-fill:#ccc;"; + final String SEC = "-fx-font-weight:bold;-fx-font-size:11;-fx-text-fill:#8ab4d4;"; + java.util.function.Function dl = t -> { Label l = new Label(t); l.setStyle(LBL); return l; }; + java.util.function.Function dsl = t -> { Label l = new Label(t); l.setStyle(SEC); VBox.setMargin(l, new Insets(8,0,2,0)); return l; }; - charEditContainer.getChildren().addAll(new Separator(), sectionTitle("Character-Daten"), new Separator()); + inner.setStyle("-fx-background-color:#1a1a2a;"); + + // ── Editierbarer Bereich (deaktiviert bis Character geladen) ────────── + charEditContainer = new VBox(6); + charEditContainer.setDisable(true); + charEditContainer.setStyle("-fx-background-color:#1a1a2a;"); + + charEditContainer.getChildren().addAll(new Separator(), dsl.apply("Character-Daten"), new Separator()); charIdField = new javafx.scene.control.TextField(); charIdField.setPromptText("eindeutige ID (Dateiname)"); charNameField = new javafx.scene.control.TextField(); charNameField.setPromptText("Anzeigename"); + charNameField.setEditable(false); charTypeCombo = new ComboBox<>(); charTypeCombo.getItems().addAll("MainCharacter", "NPC"); @@ -10829,9 +11340,9 @@ public class EditorApp extends Application { }); charEditContainer.getChildren().addAll( - new Label("ID:"), charIdField, - new Label("Name:"), charNameField, - new Label("Typ:"), charTypeCombo + dl.apply("ID:"), charIdField, + dl.apply("Name:"), charNameField, + dl.apply("Typ:"), charTypeCombo ); // ── Fraktion (NPC-only) ─────────────────────────────────────────────── @@ -10851,8 +11362,7 @@ public class EditorApp extends Application { } }); refreshFractionCombo(); - Label fractionLbl = new Label("Fraktion:"); - charFractionSection = new VBox(2, fractionLbl, charFractionCombo); + charFractionSection = new VBox(2, dl.apply("Fraktion:"), charFractionCombo); boolean initiallyNpc = "NPC".equals(charTypeCombo.getValue()); charFractionSection.setVisible(initiallyNpc); charFractionSection.setManaged(initiallyNpc); @@ -10870,13 +11380,16 @@ public class EditorApp extends Application { charStatusCombo.setMaxWidth(Double.MAX_VALUE); charTraderCheck = new CheckBox("Ist Händler"); + charTraderCheck.setStyle(LBL); charTraderItemsView = new javafx.scene.control.ListView<>(); charTraderItemsView.setPrefHeight(80); + charTraderItemsView.setStyle("-fx-background-color:#2a2a3a;-fx-control-inner-background:#2a2a3a;-fx-text-fill:#ddd;"); Button addTraderItemBtn = new Button("+"); Button delTraderItemBtn = new Button("−"); addTraderItemBtn.setOnAction(e -> { javafx.scene.control.TextInputDialog d = new javafx.scene.control.TextInputDialog(); + d.initOwner(primaryStage); d.setTitle("Item-ID"); d.setHeaderText("Item-ID der Handelsware:"); d.showAndWait().ifPresent(id -> { if (!id.isBlank()) charTraderItemsView.getItems().add(id.trim()); }); @@ -10887,10 +11400,6 @@ public class EditorApp extends Application { }); HBox traderBtns = new HBox(4, addTraderItemBtn, delTraderItemBtn); - Label npcStatusLbl = new Label("Status:"); - Label npcTraderLbl = new Label("Handel:"); - Label npcItemsLbl = new Label("Waren:"); - charMsgFriendly = new javafx.scene.control.TextField(); charMsgFriendly.setPromptText("TextReference-Schlüssel (z.B. silas.msg.friendly)"); charMsgNeutral = new javafx.scene.control.TextField(); @@ -10901,65 +11410,36 @@ public class EditorApp extends Application { charMsgEnemy.setPromptText("TextReference-Schlüssel"); charNpcSection = new VBox(4, - npcStatusLbl, charStatusCombo, - npcTraderLbl, charTraderCheck, - npcItemsLbl, charTraderItemsView, traderBtns, - sectionTitle("Standard-Nachrichten je Status"), - new Label("Friendly:"), charMsgFriendly, - new Label("Neutral:"), charMsgNeutral, - new Label("Enraged:"), charMsgEnraged, - new Label("Enemy:"), charMsgEnemy); + dl.apply("Status:"), charStatusCombo, + dl.apply("Handel:"), charTraderCheck, + dl.apply("Waren:"), charTraderItemsView, traderBtns, + dsl.apply("Standard-Nachrichten je Status"), + dl.apply("Friendly:"), charMsgFriendly, + dl.apply("Neutral:"), charMsgNeutral, + dl.apply("Enraged:"), charMsgEnraged, + dl.apply("Enemy:"), charMsgEnemy); boolean npcInitial = "NPC".equals(charTypeCombo.getValue()); charNpcSection.setVisible(npcInitial); charNpcSection.setManaged(npcInitial); - // ── MainCharacter-spezifische Felder (Abilities) ───────────────────── - abMagicSpin = abSpinner(); abStaffSpin = abSpinner(); - abSwordSpin = abSpinner(); abArcherySpin = abSpinner(); - abHeavySpin = abSpinner(); abCrossbowSpin = abSpinner(); - abThieverySpin = ab3Spinner(); abAlchemySpin = ab3Spinner(); - abEngineeringSpin = ab3Spinner(); abSmitherySpin = ab3Spinner(); - abEnchantingSpin = ab3Spinner(); - - charMainCharSection = new VBox(4, - sectionTitle("Fähigkeiten"), - abRow("Magie (1-10):", abMagicSpin), - abRow("Stab-Kampf (1-10):", abStaffSpin), - abRow("Schwertkampf (1-10):",abSwordSpin), - abRow("Bogenschießen:", abArcherySpin), - abRow("Schwere Waffen:", abHeavySpin), - abRow("Armbrust:", abCrossbowSpin), - abRow("Diebstahl (1-3):", abThieverySpin), - abRow("Alchemie (1-3):", abAlchemySpin), - abRow("Engineering (1-3):", abEngineeringSpin), - abRow("Schmieden (1-3):", abSmitherySpin), - abRow("Verzauberung (1-3):", abEnchantingSpin) - ); - boolean mainInitial = "MainCharacter".equals(charTypeCombo.getValue()); - charMainCharSection.setVisible(mainInitial); - charMainCharSection.setManaged(mainInitial); - - // Typ-Listener für NPC/MainChar-Sichtbarkeit (ergänzen was schon für Fraktion vorhanden) charTypeCombo.valueProperty().addListener((obs, ov, nv) -> { - boolean isNpc = "NPC".equals(nv); - boolean isMain = "MainCharacter".equals(nv); - charNpcSection.setVisible(isNpc); charNpcSection.setManaged(isNpc); - charMainCharSection.setVisible(isMain); charMainCharSection.setManaged(isMain); + boolean isNpc = "NPC".equals(nv); + charNpcSection.setVisible(isNpc); + charNpcSection.setManaged(isNpc); }); - charEditContainer.getChildren().addAll(charNpcSection, charMainCharSection); - + charEditContainer.getChildren().add(charNpcSection); charEditContainer.getChildren().addAll( - new Label("Modell:"), charModelCombo, - new Label("Anim-Set:"), charAnimSetCombo, + dl.apply("Modell:"), charModelCombo, + dl.apply("Anim-Set:"), charAnimSetCombo, stripClipsBtn ); - // ── Aktions-Zuweisung ────────────────────────────────────────────────── - charEditContainer.getChildren().addAll(new Separator(), sectionTitle("Aktions-Zuweisung"), new Separator()); + // ── Aktions-Zuweisung ───────────────────────────────────────────────── + charEditContainer.getChildren().addAll(new Separator(), dsl.apply("Aktions-Zuweisung"), new Separator()); charActionLabelsBox = new VBox(4); Label actionHint = new Label("(wird aus Animations-Set geladen)"); - actionHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); + actionHint.setStyle("-fx-font-size:10;-fx-text-fill:#666;"); charActionLabelsBox.getChildren().add(actionHint); charEditContainer.getChildren().add(charActionLabelsBox); @@ -10967,7 +11447,7 @@ public class EditorApp extends Application { charEditContainer.getChildren().add(new Separator()); Button saveCharBtn = new Button("Charakter speichern"); saveCharBtn.setMaxWidth(Double.MAX_VALUE); - saveCharBtn.setStyle("-fx-font-weight: bold;"); + saveCharBtn.setStyle("-fx-font-weight:bold;-fx-background-color:#3a7a3a;-fx-text-fill:white;"); saveCharBtn.setOnAction(e -> saveCharacter(charDir)); charEditContainer.getChildren().add(saveCharBtn); @@ -10976,24 +11456,12 @@ public class EditorApp extends Application { ScrollPane scroll = new ScrollPane(inner); scroll.setFitToWidth(true); scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(300); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; + scroll.setStyle("-fx-background-color:#1a1a2a;-fx-background:#1a1a2a;"); + return scroll; } private void refreshCharacterList() { - if (charListView == null) return; - charListView.getItems().clear(); - Path charDir = ASSET_ROOT.resolve("character"); - if (!java.nio.file.Files.isDirectory(charDir)) return; - try (var walk = java.nio.file.Files.list(charDir)) { - walk.filter(p -> p.toString().endsWith(".character")) - .map(p -> p.getFileName().toString().replace(".character", "")) - .sorted() - .forEach(charListView.getItems()::add); - } catch (IOException ignored) {} + refreshContentCharacterTree(); } private void refreshCharAnimSetCombo() { @@ -11017,7 +11485,7 @@ public class EditorApp extends Application { String setName = charAnimSetCombo != null ? charAnimSetCombo.getValue() : null; if (setName == null || setName.isBlank()) { Label hint = new Label("(kein Animations-Set gewählt)"); - hint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); + hint.setStyle("-fx-font-size:10;-fx-text-fill:#555;"); charActionLabelsBox.getChildren().add(hint); return; } @@ -11031,7 +11499,7 @@ public class EditorApp extends Application { java.util.Map actionMap = animSet.getActionMap(); if (actionMap == null || actionMap.isEmpty()) { Label hint = new Label("(keine Aktions-Zuweisung im Set)"); - hint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); + hint.setStyle("-fx-font-size:10;-fx-text-fill:#555;"); charActionLabelsBox.getChildren().add(hint); return; } @@ -11042,9 +11510,9 @@ public class EditorApp extends Application { row.setAlignment(Pos.CENTER_LEFT); Label lbl = new Label(action.displayName() + ":"); lbl.setMinWidth(50); - lbl.setStyle("-fx-font-weight: bold;"); + lbl.setStyle("-fx-font-weight:bold;-fx-text-fill:#ccc;"); Label val = new Label(clip != null && !clip.isBlank() ? clip : "—"); - val.setStyle("-fx-text-fill: " + (clip != null && !clip.isBlank() ? "#333" : "#aaa") + ";"); + val.setStyle("-fx-text-fill:" + (clip != null && !clip.isBlank() ? "#aad4aa" : "#555") + ";"); row.getChildren().addAll(lbl, val); charActionLabelsBox.getChildren().add(row); } @@ -11261,6 +11729,7 @@ public class EditorApp extends Application { private static javafx.scene.layout.HBox abRow(String label, javafx.scene.control.Spinner s) { javafx.scene.control.Label l = new javafx.scene.control.Label(label); l.setMinWidth(160); + l.setStyle("-fx-text-fill:#ccc;"); javafx.scene.layout.HBox.setHgrow(s, javafx.scene.layout.Priority.ALWAYS); javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(6, l, s); row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); @@ -11338,6 +11807,13 @@ public class EditorApp extends Application { } private void switchToLocationEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "location"; + if (contentLocationBtn != null) contentLocationBtn.setSelected(true); + buildLocationViewInContent(); + return; + } onF5 = null; currentTool = "locationEditor"; ToolBar tb = new ToolBar(); @@ -11352,6 +11828,13 @@ public class EditorApp extends Application { } private void switchToAreaEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "area"; + if (contentAreaBtn != null) contentAreaBtn.setSelected(true); + buildAreaViewInContent(); + return; + } onF5 = null; currentTool = "areaEditor"; ToolBar tb = new ToolBar(); @@ -11380,4 +11863,442 @@ public class EditorApp extends Application { root.setRight(null); } + // ── Content Editor ──────────────────────────────────────────────────────── + + /** Gibt den Ziel-BorderPane für Content-Editor-Operationen zurück. */ + private BorderPane contentTarget() { + return (dualWindowMode && contentEditorRoot != null) ? contentEditorRoot : root; + } + + private javafx.scene.layout.Region createToolBarSpacer() { + javafx.scene.layout.Region spacer = new javafx.scene.layout.Region(); + javafx.scene.layout.HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS); + return spacer; + } + + /** "> Content"-Button für den rechten Rand der Welt-Toolbar (nur 1-Fenster-Modus). */ + private Button buildContentSwitchBtn() { + contentSwitchBtn = new Button("> Content"); + contentSwitchBtn.setStyle("-fx-font-weight:bold;"); + contentSwitchBtn.setOnAction(e -> switchToContentEditor()); + return contentSwitchBtn; + } + + /** 2-Fenster-Modus aktivieren: Titelleisten-Buttons ausblenden, Content-Stage öffnen. */ + private void enableDualWindowMode() { + dualWindowMode = true; + if (contentSwitchBtn != null) { + contentSwitchBtn.setVisible(false); + contentSwitchBtn.setManaged(false); + } + showContentEditor(); + restoreContentSection(contentLastSection); + } + + /** Content-Editor-Stage erstellen und anzeigen. + * Schließen per X oder Taskbar ist geblockt; Min/Max bleiben erhalten. + * initOwner gruppiert beide Fenster zu einer Taskbar-Instanz. */ + private void showContentEditor() { + if (contentEditorStage == null) { + contentEditorStage = new Stage(); + contentEditorStage.setTitle("Blight Content Editor"); + contentEditorStage.setOnCloseRequest(e -> e.consume()); + try (var is = getClass().getResourceAsStream("/icon_editor.png")) { + if (is != null) contentEditorStage.getIcons().add(new Image(is)); + } catch (Exception ignored) {} + + contentEditorRoot = new BorderPane(); + contentEditorRoot.setTop(buildContentTopBar()); + contentEditorRoot.setLeft(buildContentTree()); + contentEditorRoot.setCenter(new Label("Wähle einen Bereich in der Toolbar.")); + + Scene scene = new Scene(contentEditorRoot, 1200, 750); + java.net.URL cssUrl = getClass().getResource("/editor.css"); + if (cssUrl != null) scene.getStylesheets().add(cssUrl.toExternalForm()); + scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> { + if (!e.isControlDown()) return; + int idx = switch (e.getCode()) { + case F1 -> 0; case F2 -> 1; case F3 -> 2; case F4 -> 3; + case F5 -> 4; case F6 -> 5; case F7 -> 6; case F8 -> 7; + default -> -1; + }; + if (idx >= 0) { e.consume(); fireContentBtn(idx); } + }); + + contentEditorStage.setScene(scene); + contentEditorStage.setMinWidth(800); + contentEditorStage.setMinHeight(500); + } + contentEditorStage.show(); + contentEditorStage.toFront(); + } + + /** 2-Fenster-Modus deaktivieren: Stage verstecken, Wechsel-Button wieder zeigen. */ + private void disableDualWindowMode() { + dualWindowMode = false; + if (dualWindowMenuItem != null) dualWindowMenuItem.setSelected(false); + if (contentEditorStage != null) contentEditorStage.hide(); + if (contentSwitchBtn != null) { + contentSwitchBtn.setVisible(true); + contentSwitchBtn.setManaged(true); + } + } + + /** Wechselt im 1-Fenster-Modus zur Content-Editor-Ansicht in der Welt-Stage. */ + private void switchToContentEditor() { + currentTool = "content"; + topBar.getChildren().set(1, buildSingleWindowContentToolBar()); + root.setLeft(buildContentTree()); + restoreContentSection(contentLastSection); + } + + /** Content-Toolbar für 1-Fenster-Modus: mit "< Welt"-Button. */ + private ToolBar buildSingleWindowContentToolBar() { + return buildContentEditorToolBar(true); + } + + /** Obere Leiste des Content-Editors im 2-Fenster-Modus: nur Toolbar (kein "< Welt"). */ + private VBox buildContentTopBar() { + ToolBar tb = buildContentEditorToolBar(false); + return new VBox(tb); + } + + private ToolBar buildContentEditorToolBar(boolean includeWeltBtn) { + contentCharBtn = new ToggleButton("@ Character"); + contentQuestBtn = new ToggleButton("! Quest"); + contentFraktionBtn = new ToggleButton("* Fraktion"); + contentItemBtn = new ToggleButton("+ Item"); + contentRezeptBtn = new ToggleButton("= Rezept"); + contentCraftingBtn = new ToggleButton("# Crafting"); + contentAreaBtn = new ToggleButton("□ Area"); + contentLocationBtn = new ToggleButton("^ Location"); + + String bold = "-fx-font-weight:bold;"; + for (ToggleButton b : new ToggleButton[]{contentCharBtn, contentQuestBtn, contentFraktionBtn, + contentItemBtn, contentRezeptBtn, contentCraftingBtn, contentAreaBtn, contentLocationBtn}) { + b.setStyle(bold); + } + + ToggleGroup cg = new ToggleGroup(); + contentCharBtn.setToggleGroup(cg); + contentQuestBtn.setToggleGroup(cg); + contentFraktionBtn.setToggleGroup(cg); + contentItemBtn.setToggleGroup(cg); + contentRezeptBtn.setToggleGroup(cg); + contentCraftingBtn.setToggleGroup(cg); + contentAreaBtn.setToggleGroup(cg); + contentLocationBtn.setToggleGroup(cg); + + contentCharBtn.setOnAction(e -> { + contentLastSection = "character"; buildCharacterViewInContent(); + }); + contentQuestBtn.setOnAction(e -> { + contentLastSection = "quest"; buildQuestViewInContent(); + }); + contentFraktionBtn.setOnAction(e -> { + contentLastSection = "fraction"; buildFractionViewInContent(); + }); + contentItemBtn.setOnAction(e -> { + contentLastSection = "item"; buildItemViewInContent(); + }); + contentRezeptBtn.setOnAction(e -> { + contentLastSection = "recipe"; buildRecipeViewInContent(); + }); + contentCraftingBtn.setOnAction(e -> { + contentLastSection = "crafting"; buildCraftingViewInContent(); + }); + contentAreaBtn.setOnAction(e -> { + contentLastSection = "area"; buildAreaViewInContent(); + }); + contentLocationBtn.setOnAction(e -> { + contentLastSection = "location"; buildLocationViewInContent(); + }); + + ToolBar tb = new ToolBar(); + tb.getItems().addAll( + contentCharBtn, + new Separator(Orientation.VERTICAL), contentQuestBtn, + new Separator(Orientation.VERTICAL), contentFraktionBtn, + new Separator(Orientation.VERTICAL), contentItemBtn, + new Separator(Orientation.VERTICAL), contentRezeptBtn, contentCraftingBtn, + new Separator(Orientation.VERTICAL), contentAreaBtn, contentLocationBtn); + if (includeWeltBtn) { + Button weltBtn = new Button("< Welt"); + weltBtn.setStyle("-fx-font-weight:bold;"); + weltBtn.setOnAction(e -> switchToWorldEditor()); + tb.getItems().addAll(createToolBarSpacer(), weltBtn); + } + return tb; + } + + private javafx.scene.control.TreeView buildContentTree() { + javafx.scene.control.TreeItem treeRoot = + new javafx.scene.control.TreeItem<>("Content"); + treeRoot.setExpanded(true); + + contentCharNode = new javafx.scene.control.TreeItem<>("Characters"); + contentCharNode.setExpanded(false); + refreshContentCharacterTree(); + + String[] otherSections = { + "Quests", "Fraktionen", "Items", "Rezepte", "Crafting Tables", "Areas", "Locations" + }; + treeRoot.getChildren().add(contentCharNode); + for (String s : otherSections) { + javafx.scene.control.TreeItem node = + new javafx.scene.control.TreeItem<>(s); + node.setExpanded(false); + treeRoot.getChildren().add(node); + } + + contentTreeView = new javafx.scene.control.TreeView<>(treeRoot); + contentTreeView.setShowRoot(false); + contentTreeView.setPrefWidth(200); + contentTreeView.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> { + if (nv == null) return; + javafx.scene.control.TreeItem parent = nv.getParent(); + // Klick auf einen Character-Eintrag (Kind-Knoten von Characters) + if (parent != null && parent == contentCharNode) { + if (contentCharBtn != null && !contentCharBtn.isSelected()) contentCharBtn.fire(); + loadCharacterById(nv.getValue()); + return; + } + switch (nv.getValue()) { + case "Characters" -> { if (contentCharBtn != null) contentCharBtn.fire(); } + case "Quests" -> { if (contentQuestBtn != null) contentQuestBtn.fire(); } + case "Fraktionen" -> { if (contentFraktionBtn != null) contentFraktionBtn.fire(); } + case "Items" -> { if (contentItemBtn != null) contentItemBtn.fire(); } + case "Rezepte" -> { if (contentRezeptBtn != null) contentRezeptBtn.fire(); } + case "Crafting Tables" -> { if (contentCraftingBtn != null) contentCraftingBtn.fire(); } + case "Areas" -> { if (contentAreaBtn != null) contentAreaBtn.fire(); } + case "Locations" -> { if (contentLocationBtn != null) contentLocationBtn.fire(); } + } + }); + return contentTreeView; + } + + private void refreshContentCharacterTree() { + if (contentCharNode == null) return; + contentCharNode.getChildren().clear(); + Path charDir = ASSET_ROOT.resolve("character"); + if (!java.nio.file.Files.isDirectory(charDir)) return; + try (var walk = java.nio.file.Files.list(charDir)) { + walk.filter(p -> p.toString().endsWith(".character")) + .map(p -> p.getFileName().toString().replace(".character", "")) + .sorted() + .forEach(id -> contentCharNode.getChildren().add( + new javafx.scene.control.TreeItem<>(id))); + } catch (IOException ignored) {} + } + + private void loadCharacterById(String id) { + if (id == null || id.isBlank()) return; + Path charDir = ASSET_ROOT.resolve("character"); + try { + de.blight.common.model.GameCharacter c = + de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character")); + if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : ""); + if (charNameField != null) charNameField.setText(c.getName() != null ? c.getName().id() : ""); + if (charTypeCombo != null) + charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC"); + if (charModelCombo != null) charModelCombo.setValue(c.getModelPath()); + if (charAnimSetCombo != null) { + charAnimSetCombo.setValue(c.getAnimSetPath()); + updateCharActionCombosFromSet(); + } + if (charFractionCombo != null) { + de.blight.common.model.Fraction npcFraction = + (c instanceof de.blight.common.model.NPC npc) ? npc.getFraction() : null; + if (npcFraction != null && npcFraction.getFractionId() != null) { + final java.util.UUID fid = npcFraction.getFractionId(); + charFractionCombo.getItems().stream() + .filter(f -> f != null && fid.equals(f.getFractionId())) + .findFirst() + .ifPresentOrElse(charFractionCombo::setValue, () -> charFractionCombo.setValue(null)); + } else { + charFractionCombo.setValue(null); + } + } + if (c instanceof de.blight.common.model.NPC npc) { + if (charStatusCombo != null && npc.getStatus() != null) + charStatusCombo.setValue(npc.getStatus().name()); + if (charTraderCheck != null) charTraderCheck.setSelected(npc.isTrader()); + if (charTraderItemsView != null) { + charTraderItemsView.getItems().clear(); + if (npc.getItems() != null) + npc.getItems().forEach(it -> charTraderItemsView.getItems().add(it.getItemId())); + } + java.util.Map msgs = + npc.getDefaultMessages(); + if (charMsgFriendly != null) charMsgFriendly.setText(msgKey(msgs, de.blight.common.model.Status.FRIENDLY)); + if (charMsgNeutral != null) charMsgNeutral.setText(msgKey(msgs, de.blight.common.model.Status.NEUTRAL)); + if (charMsgEnraged != null) charMsgEnraged.setText(msgKey(msgs, de.blight.common.model.Status.ENRAGED)); + if (charMsgEnemy != null) charMsgEnemy.setText(msgKey(msgs, de.blight.common.model.Status.ENEMY)); + if (dialogEditorView != null) { + dialogEditorView.setDisable(false); + dialogEditorView.loadNpc(npc); + } + } else { + if (dialogEditorView != null) { + dialogEditorView.setDisable(false); + dialogEditorView.clear(); + } + } + if (c instanceof de.blight.common.model.MainCharacter mc) { + loadAbilities(mc.getAbilities()); + } + if (charEditContainer != null) charEditContainer.setDisable(false); + if (routineEditorView != null) { + routineEditorView.setDisable(false); + if (c instanceof de.blight.common.model.NPC npc) routineEditorView.loadNpc(npc); + else routineEditorView.loadNpc(new de.blight.common.model.NPC()); + } + if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id); + charDirty = true; + } catch (Exception e) { + if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage()); + } + } + + /** Ctrl+F1–F8 im Hauptfenster: ggf. in Content-Modus wechseln, dann Werkzeug aktivieren. */ + private void activateContentSection(int idx) { + if (dualWindowMode) { + if (contentEditorStage != null && !contentEditorStage.isShowing()) { + contentEditorStage.show(); + } + fireContentBtn(idx); + } else { + if (!"content".equals(currentTool)) { + switchToContentEditor(); + } + fireContentBtn(idx); + } + } + + private void fireContentBtn(int idx) { + if (contentCharBtn == null) return; + ToggleButton[] btns = { contentCharBtn, contentQuestBtn, contentFraktionBtn, + contentItemBtn, contentRezeptBtn, contentCraftingBtn, + contentAreaBtn, contentLocationBtn }; + if (idx >= 0 && idx < btns.length) btns[idx].fire(); + } + + private void restoreContentSection(String section) { + if (contentCharBtn == null) return; + switch (section) { + case "character" -> contentCharBtn.fire(); + case "quest" -> contentQuestBtn.fire(); + case "fraction" -> contentFraktionBtn.fire(); + case "item" -> contentItemBtn.fire(); + case "recipe" -> contentRezeptBtn.fire(); + case "crafting" -> contentCraftingBtn.fire(); + case "area" -> contentAreaBtn.fire(); + case "location" -> contentLocationBtn.fire(); + } + } + + private void restoreEditorPrefs() { + Path prefsPath = BlightHome.resolve("config", "editor.prefs"); + if (!java.nio.file.Files.exists(prefsPath)) return; + java.util.Properties p = new java.util.Properties(); + try (java.io.Reader r = Files.newBufferedReader(prefsPath)) { + p.load(r); + } catch (IOException e) { + log.warn("[Editor] Konnte editor.prefs nicht laden: {}", e.getMessage()); + return; + } + if ("true".equals(p.getProperty("dual.window", "false"))) { + if (dualWindowMenuItem != null) dualWindowMenuItem.setSelected(true); + enableDualWindowMode(); // setzt dualWindowMode + blendet contentSwitchBtn aus + double cx = parseDoublePref(p, "content.x", -1); + double cy = parseDoublePref(p, "content.y", -1); + double cw = parseDoublePref(p, "content.w", 1000); + double ch = parseDoublePref(p, "content.h", 700); + if (cx >= 0 && contentEditorStage != null) { + boolean onScreen = !javafx.stage.Screen + .getScreensForRectangle(cx, cy, cw, ch).isEmpty(); + if (onScreen) { + contentEditorStage.setX(cx); + contentEditorStage.setY(cy); + contentEditorStage.setWidth(cw); + contentEditorStage.setHeight(ch); + } + } + contentLastSection = p.getProperty("content.last.section", "character"); + restoreContentSection(contentLastSection); + } + } + + private double parseDoublePref(java.util.Properties p, String key, double def) { + try { return Double.parseDouble(p.getProperty(key, String.valueOf(def))); } + catch (NumberFormatException e) { return def; } + } + + private void buildCharacterViewInContent() { + onF5 = null; + dialogEditorView = new de.blight.editor.ui.DialogEditorView(); + dialogEditorView.setDisable(true); + monologueEditorView = new de.blight.editor.ui.MonologueEditorView(); + + javafx.scene.control.TabPane tabs = new javafx.scene.control.TabPane(); + tabs.setTabClosingPolicy(javafx.scene.control.TabPane.TabClosingPolicy.UNAVAILABLE); + javafx.scene.control.Tab charTab = new javafx.scene.control.Tab("Charakter", buildCharacterSettingsPane()); + javafx.scene.control.Tab dialogTab = new javafx.scene.control.Tab("Dialog", dialogEditorView); + javafx.scene.control.Tab monologTab = new javafx.scene.control.Tab("Monologe", monologueEditorView); + tabs.getTabs().addAll(charTab, dialogTab, monologTab); + tabs.getSelectionModel().select(charTab); + + contentTarget().setCenter(tabs); + contentTarget().setRight(null); + + if (contentCharNode != null) contentCharNode.setExpanded(true); + } + + private void buildQuestViewInContent() { + onF5 = null; + contentTarget().setCenter( + new de.blight.editor.ui.QuestEditorView(ASSET_ROOT.resolve("quests"))); + contentTarget().setRight(null); + } + + private void buildFractionViewInContent() { + onF5 = null; + contentTarget().setCenter( + new de.blight.editor.ui.FractionEditorView(ASSET_ROOT.resolve("fractions"))); + contentTarget().setRight(null); + } + + private void buildItemViewInContent() { + onF5 = null; + contentTarget().setCenter( + new de.blight.editor.ui.ItemEditorView(ASSET_ROOT.resolve("items"))); + contentTarget().setRight(null); + } + + private void buildRecipeViewInContent() { + onF5 = null; + contentTarget().setCenter( + new de.blight.editor.ui.RecipeEditorView(ASSET_ROOT.resolve("recipes"))); + contentTarget().setRight(null); + } + + private void buildCraftingViewInContent() { + onF5 = null; + contentTarget().setCenter( + new de.blight.editor.ui.CraftingTableEditorView(ASSET_ROOT.resolve("craftingtables"))); + contentTarget().setRight(null); + } + + private void buildAreaViewInContent() { + onF5 = null; + contentTarget().setCenter(new de.blight.editor.ui.AreaEditorView()); + contentTarget().setRight(null); + } + + private void buildLocationViewInContent() { + onF5 = null; + contentTarget().setCenter(new de.blight.editor.ui.LocationEditorView()); + contentTarget().setRight(null); + } + } diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index ec597bb..ebafe47 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -97,6 +97,13 @@ public class SharedInput { public record GrassVertexEdit(float screenX, float screenY, int action) {} public final ConcurrentLinkedQueue grassVertexEditQueue = new ConcurrentLinkedQueue<>(); + /** Globale Grasfarbe – frisches Gras (Spitze; Wurzel wird intern abgeleitet). Defaults: TIP_COLOR */ + public volatile float grassFreshR = 0.26f, grassFreshG = 0.72f, grassFreshB = 0.11f; + /** Globale Grasfarbe – vertrocknet (Spitze). Defaults: DRY_TIP_COLOR */ + public volatile float grassDryR = 0.82f, grassDryG = 0.74f, grassDryB = 0.16f; + /** true → GrassVertexState soll alle Chunks neu aufbauen */ + public volatile boolean grassColorsChanged = false; + // ── Farn-Generator ──────────────────────────────────────────────────────── public record FernGenRequest(de.blight.editor.tree.FernOptions options, boolean exportAfter) {} public final ConcurrentLinkedQueue fernGenQueue = new ConcurrentLinkedQueue<>(); @@ -276,6 +283,8 @@ public class SharedInput { public volatile boolean objectSelectionChanged = false; /** Wird von JME3 gesetzt, wenn ein Objekt gerade neu platziert wurde (nicht nur selektiert). */ public volatile boolean objectJustPlaced = false; + /** Wird von JME3 gesetzt, wenn ein Objekt im Platzierungsmodus angeklickt wurde → JFX wechselt Panel zu Edit. */ + public volatile boolean objectClickedWhileInPlaceMode = false; /** JavaFX → JME3: Modell-Pfad für nächste Platzierung (relativ zu blight-assets/src/main/resources/). */ public volatile String pendingModelPath = null; @@ -1087,6 +1096,10 @@ public class SharedInput { /** 3D-Noise-Frequenz für die Kliff-Oberfläche (höher = feiner Detail). */ public volatile float cliffRoughnessScale = 0.12f; + // ── Universelles Auswahl-Werkzeug ───────────────────────────────────────── + /** activeLayer==33 → Auswahl-Modus: kein Terrain-Editing, Klick selektiert Objekte */ + public static final int LAYER_AUSWAHL = 33; + // ── Voxel-Textur-Malen ──────────────────────────────────────────────────── /** Parallel-Queue zu textureEditQueue – wird von submitEdit(layer=4) mitbefüllt, * damit VoxelEditorState unabhängig vom Terrain seine Splatmap beschreiben kann. */ diff --git a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java index 3e84d9e..140aee4 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java @@ -91,6 +91,10 @@ public class GrassVertexState extends BaseAppState { private final Node[] chunkNodes = new Node[CHUNK_COUNT]; private final boolean[] dirtyChunks = new boolean[CHUNK_COUNT]; + // Effektive Grasfarben – werden aus SharedInput übernommen, Wurzel wird abgeleitet + private float freshTipR = TIP_COLOR.r, freshTipG = TIP_COLOR.g, freshTipB = TIP_COLOR.b; + private float dryTipR = DRY_TIP_COLOR.r, dryTipG = DRY_TIP_COLOR.g, dryTipB = DRY_TIP_COLOR.b; + public GrassVertexState(SharedInput input) { this.input = input; for (int i = 0; i < CHUNK_COUNT; i++) chunkBlades[i] = new ArrayList<>(); @@ -168,6 +172,12 @@ public class GrassVertexState extends BaseAppState { @Override public void update(float tpf) { + if (input.grassColorsChanged) { + input.grassColorsChanged = false; + freshTipR = input.grassFreshR; freshTipG = input.grassFreshG; freshTipB = input.grassFreshB; + dryTipR = input.grassDryR; dryTipG = input.grassDryG; dryTipB = input.grassDryB; + for (int ci = 0; ci < CHUNK_COUNT; ci++) dirtyChunks[ci] = !chunkBlades[ci].isEmpty(); + } processBrushEdits(); rebuildDirtyChunks(); updateChunkVisibility(); @@ -399,9 +409,15 @@ public class GrassVertexState extends BaseAppState { float[] texCoords = new float[vertCount * 2]; int[] indices = new int[indexCount]; + // Wurzelfarbe = Spitze * fester Faktor (entspricht den ursprünglichen statischen Verhältnissen) + float frR = freshTipR * 0.308f, frG = freshTipG * 0.472f, frB = freshTipB * 0.364f; + float drR = dryTipR * 0.549f, drG = dryTipG * 0.473f, drB = dryTipB * 0.500f; + int vi = 0, ii = 0; for (GrassVertexBlade blade : blades) { - buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade); + buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade, + frR, frG, frB, freshTipR, freshTipG, freshTipB, + drR, drG, drB, dryTipR, dryTipG, dryTipB); vi += BLADES_PER_TUFT * (SEGMENTS + 1) * 2; ii += BLADES_PER_TUFT * SEGMENTS * 6; } @@ -560,7 +576,11 @@ public class GrassVertexState extends BaseAppState { // ── Mesh-Generierung (gemeinsame Logik, auch von GrassVertexRenderState genutzt) ── static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex, int[] idx, - int vi, int ii, GrassVertexBlade blade) { + int vi, int ii, GrassVertexBlade blade, + float frshRootR, float frshRootG, float frshRootB, + float frshTipR, float frshTipG, float frshTipB, + float dryRootR, float dryRootG, float dryRootB, + float dryTipR, float dryTipG, float dryTipB) { float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height(); float baseHW = h * WIDTH_FACTOR * 0.5f; float tAngle = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2)); @@ -626,8 +646,12 @@ public class GrassVertexState extends BaseAppState { int svi = bladeVi + s * 2; float dry = blade.dryness(); - setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, dry); - setV(pos, nrm, col, tex, svi+1, spX + cosA * hw, spY, spZ + sinA * hw, nx, ny, nz, t, dry); + setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, dry, + frshRootR, frshRootG, frshRootB, frshTipR, frshTipG, frshTipB, + dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB); + setV(pos, nrm, col, tex, svi+1, spX + cosA * hw, spY, spZ + sinA * hw, nx, ny, nz, t, dry, + frshRootR, frshRootG, frshRootB, frshTipR, frshTipG, frshTipB, + dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB); if (s < SEGMENTS) { int sii = bladeIi + s * 6; @@ -651,7 +675,11 @@ public class GrassVertexState extends BaseAppState { private static void setV(float[] pos, float[] nrm, float[] col, float[] tex, int vi, float x, float y, float z, float nx, float ny, float nz, - float wf, float dryness) { + float wf, float dryness, + float frshRootR, float frshRootG, float frshRootB, + float frshTipR, float frshTipG, float frshTipB, + float dryRootR, float dryRootG, float dryRootB, + float dryTipR, float dryTipG, float dryTipB) { int pi = vi * 3; pos[pi] = x; pos[pi+1] = y; pos[pi+2] = z; @@ -659,16 +687,15 @@ public class GrassVertexState extends BaseAppState { nrm[ni] = nx; nrm[ni+1] = ny; nrm[ni+2] = nz; int ci = vi * 4; - float gr = ROOT_COLOR.r + (TIP_COLOR.r - ROOT_COLOR.r) * wf; - float gg = ROOT_COLOR.g + (TIP_COLOR.g - ROOT_COLOR.g) * wf; - float gb = ROOT_COLOR.b + (TIP_COLOR.b - ROOT_COLOR.b) * wf; - float dr = DRY_ROOT_COLOR.r + (DRY_TIP_COLOR.r - DRY_ROOT_COLOR.r) * wf; - float dg = DRY_ROOT_COLOR.g + (DRY_TIP_COLOR.g - DRY_ROOT_COLOR.g) * wf; - float db = DRY_ROOT_COLOR.b + (DRY_TIP_COLOR.b - DRY_ROOT_COLOR.b) * wf; + float gr = frshRootR + (frshTipR - frshRootR) * wf; + float gg = frshRootG + (frshTipG - frshRootG) * wf; + float gb = frshRootB + (frshTipB - frshRootB) * wf; + float dr = dryRootR + (dryTipR - dryRootR) * wf; + float dg = dryRootG + (dryTipG - dryRootG) * wf; + float db = dryRootB + (dryTipB - dryRootB) * wf; float vr = VERY_DRY_ROOT_COLOR.r + (VERY_DRY_TIP_COLOR.r - VERY_DRY_ROOT_COLOR.r) * wf; float vg = VERY_DRY_ROOT_COLOR.g + (VERY_DRY_TIP_COLOR.g - VERY_DRY_ROOT_COLOR.g) * wf; float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf; - // Zwei-Segment-Gradient: 0→0.5 = grün→goldgelb, 0.5→1.0 = goldgelb→dunkelbraun float fr, fg, fb; if (dryness <= 0.5f) { float t = dryness * 2f; diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlayToolState.java b/blight-editor/src/main/java/de/blight/editor/state/PlayToolState.java index 03eeb88..db1cdc5 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PlayToolState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PlayToolState.java @@ -381,10 +381,13 @@ public class PlayToolState extends BaseAppState { VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); if (ves != null) { Vector3f pt = ves.clickAt(ray); - if (pt != null && ves.terrainTypeAt(pt.x, pt.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) { - return null; + if (pt != null) { + if (ves.terrainTypeAt(pt.x, pt.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) { + return null; + } + return pt; } - return pt; + // ves hat kein Terrain-Geometry — Fallback auf Base-Terrain } if (terrain == null) return null; CollisionResults hits = new CollisionResults(); diff --git a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java index 56226e0..d74de6f 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/SceneObjectState.java @@ -395,7 +395,8 @@ public class SceneObjectState extends BaseAppState { handleBenchSitzLayer(); boolean isObjectLayer = input.activeLayer == SharedInput.LAYER_OBJECTS - || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT; + || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT + || input.activeLayer == SharedInput.LAYER_AUSWAHL; if (!isObjectLayer) return; // Animation-Clip-Zuweisung von JavaFX @@ -680,11 +681,23 @@ public class SceneObjectState extends BaseAppState { input.objectSelectionMode = SharedInput.SEL_MODE_OBJECT; if (input.objectSelectionMode != SharedInput.SEL_MODE_OBJECT) applySubSelection(closest); + if (input.activeLayer == SharedInput.LAYER_OBJECTS + || input.activeLayer == SharedInput.LAYER_AUSWAHL) { + input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT; + input.objectClickedWhileInPlaceMode = true; + } return; } } - // 3. Terrain-Treffer – Platzieren nur im Platzieren-Modus + // 3. Terrain-Treffer – im Edit-Modus: zurück zu Place-Modus, deselektieren + if (input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT) { + input.activeLayer = SharedInput.LAYER_OBJECTS; + deselectAll(); + return; + } + // Auswahl-Modus: kein Objekt angeklickt → nur deselektieren, Layer bleibt + if (input.activeLayer == SharedInput.LAYER_AUSWAHL) { deselectAll(); return; } if (input.activeLayer != SharedInput.LAYER_OBJECTS) { deselectAll(); return; } String modelPath = input.pendingModelPath; diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index 24cc7aa..b65995f 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -378,6 +378,9 @@ public class TerrainEditorState extends BaseAppState { } input.loadingStatus = "Baue Szene..."; + VoxelEditorState ves = app.getStateManager().getState(VoxelEditorState.class); + if (ves != null) ves.setTerrainNode(terrain); + PlayToolState playToolState = app.getStateManager().getState(PlayToolState.class); if (playToolState != null) playToolState.setTerrain(terrain); @@ -1394,6 +1397,7 @@ public class TerrainEditorState extends BaseAppState { || layer == SharedInput.LAYER_VOXEL || layer == SharedInput.LAYER_STONE || layer == SharedInput.LAYER_BED_LIEGE || layer == SharedInput.LAYER_BENCH_SITZ + || layer == SharedInput.LAYER_AUSWAHL || mx < 0) { brushIndicator.setCullHint(Spatial.CullHint.Always); return; diff --git a/blight-editor/src/main/java/de/blight/editor/tool/GrassVertexTool.java b/blight-editor/src/main/java/de/blight/editor/tool/GrassVertexTool.java index 1057847..b140a68 100644 --- a/blight-editor/src/main/java/de/blight/editor/tool/GrassVertexTool.java +++ b/blight-editor/src/main/java/de/blight/editor/tool/GrassVertexTool.java @@ -6,7 +6,7 @@ public class GrassVertexTool extends EditorTool { public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 50.0); public final ToolParameter bladeHeight = new ToolParameter("Halmhöhe", 0.6, 0.1, 2.0); - public final ToolParameter density = new ToolParameter("Dichte", 5.0, 1.0, 100.0); + public final ToolParameter density = new ToolParameter("Dichte", 50.0, 1.0, 100.0); public final ToolParameter dryness = new ToolParameter("Vertrocknet %", 0.0, 0.0, 100.0); /** 1.0 = exakt gleiche Höhe, 0.0 = ±25 % Zufallsvariation */ public final ToolParameter uniformity = new ToolParameter("Gleichmäßigkeit", 1.0, 0.0, 1.0); diff --git a/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java index 24f6f82..2522958 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java @@ -1,5 +1,6 @@ package de.blight.editor.ui; +import de.blight.common.MonologueIO; import de.blight.common.model.*; import de.blight.common.model.quests.Quest; import de.blight.common.model.quests.QuestIO; @@ -21,7 +22,10 @@ import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import javafx.stage.Modality; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.time.LocalDate; import java.util.*; /** @@ -41,6 +45,8 @@ public class DialogEditorView extends BorderPane { private ToggleButton listBtn; private ToggleButton graphBtn; + private Button skriptBtn; + private Button ttsBtn; // ── List-mode ───────────────────────────────────────────────────────────── @@ -105,6 +111,7 @@ public class DialogEditorView extends BorderPane { normalizeReferences(); refreshOptionList(); clearDetailForm(); + setCharacterLoaded(true); } public void clear() { @@ -112,8 +119,15 @@ public class DialogEditorView extends BorderPane { allOptions.clear(); rootIds.clear(); selectedId = null; + currentNpcId = ""; refreshOptionList(); clearDetailForm(); + setCharacterLoaded(false); + } + + public void setCharacterLoaded(boolean loaded) { + if (skriptBtn != null) skriptBtn.setDisable(!loaded); + if (ttsBtn != null) ttsBtn.setDisable(!loaded); } public void exportToNpc(NPC npc) { @@ -162,8 +176,21 @@ public class DialogEditorView extends BorderPane { Button newBtn = new Button("+ Option"); newBtn.setOnAction(e -> createOption(false)); + skriptBtn = new Button("Skript"); + skriptBtn.setStyle("-fx-background-color: #3a4a6a; -fx-text-fill: white;"); + skriptBtn.setDisable(true); + skriptBtn.setOnAction(e -> { exportNpcScript(); exportHeroScript(); }); + + ttsBtn = new Button("TTS"); + ttsBtn.setStyle("-fx-background-color: #5a3a2a; -fx-text-fill: white;"); + ttsBtn.setDisable(true); + ttsBtn.setOnAction(e -> new TtsGeneratorDialog(currentNpcId, + new LinkedHashMap<>(allOptions)).showAndWait()); + HBox bar = new HBox(8, listBtn, graphBtn, - new Separator(Orientation.VERTICAL), newRootBtn, newBtn); + new Separator(Orientation.VERTICAL), newRootBtn, newBtn, + new Separator(Orientation.VERTICAL), skriptBtn, + new Separator(Orientation.VERTICAL), ttsBtn); bar.setPadding(new Insets(8)); bar.setAlignment(Pos.CENTER_LEFT); bar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #555;" @@ -1249,6 +1276,149 @@ public class DialogEditorView extends BorderPane { return idx == 0 ? base + suffix : base + suffix + "." + idx; } + // ── Skript-Export ────────────────────────────────────────────────────────── + + private void exportNpcScript() { + saveCurrentForm(); + if (currentNpcId.isBlank() || allOptions.isEmpty()) { + Dialogs.alert(Alert.AlertType.WARNING, + "Kein NPC geladen oder keine Dialog-Optionen vorhanden.", ButtonType.OK).showAndWait(); + return; + } + Map texts = loadTexts(); + StringBuilder sb = new StringBuilder(); + sb.append("=== NPC-Skript: ").append(currentNpcId).append(" ===\n"); + sb.append("Exportiert: ").append(LocalDate.now()).append("\n\n"); + + for (Map.Entry entry : allOptions.entrySet()) { + DialogOption opt = entry.getValue(); + String optId = entry.getKey(); + String base = "dialog." + currentNpcId + "." + optId; + List npcKeys = resolveStepKeys(opt.getNpcSteps(), opt.getTextNpc(), base, ".textnpc"); + if (npcKeys.isEmpty()) continue; + + sb.append("--- ").append(optId); + if (rootIds.contains(optId)) sb.append(" [ROOT]"); + sb.append(" ---\n"); + appendStepLines(sb, npcKeys, "NPC", texts); + sb.append("\n"); + } + + writeScript(ProjectRoot.resolve("dialog_script_npc_" + currentNpcId + ".txt"), sb.toString()); + } + + private void exportHeroScript() { + Map texts = loadTexts(); + StringBuilder sb = new StringBuilder(); + sb.append("=== Held-Skript ===\n"); + sb.append("Exportiert: ").append(LocalDate.now()).append("\n\n"); + + 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; + + boolean npcHeaderWritten = false; + for (Map.Entry entry : npc.getDialogOptions().entrySet()) { + String optId = entry.getKey(); + DialogOption opt = entry.getValue(); + String base = "dialog." + npc.getCharacterId() + "." + optId; + List heroKeys = resolveStepKeys(opt.getHeroSteps(), opt.getTextHero(), base, ".textmainchar"); + if (heroKeys.isEmpty()) continue; + + if (!npcHeaderWritten) { + sb.append("=== NPC: ").append(npc.getCharacterId()).append(" ===\n\n"); + npcHeaderWritten = true; + } + sb.append("--- ").append(optId).append(" ---\n"); + appendStepLines(sb, heroKeys, "HELD", texts); + sb.append("\n"); + } + } + + List monologues = MonologueIO.load(); + if (!monologues.isEmpty()) { + sb.append("=== Monologe ===\n\n"); + for (Monologue m : monologues) { + if (m.getHeroSteps() == null || m.getHeroSteps().isEmpty()) continue; + sb.append("--- ").append(m.getId()).append(" ---\n"); + List 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"); + } + } + + writeScript(ProjectRoot.resolve("dialog_script_held.txt"), sb.toString()); + } + + private Map loadTexts() { + try { + Path msgFile = ProjectRoot.resolve( + "blight-lang", "src", "main", "resources", "lang", "messages_de.properties"); + if (Files.exists(msgFile)) return new LinkedHashMap<>(TextBundleIO.load(msgFile).getEntries()); + } catch (Exception ignored) {} + return new LinkedHashMap<>(); + } + + private static void appendStepLines(StringBuilder sb, List keys, + String label, Map 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) { + try { + Files.writeString(out, content); + Dialogs.alert(Alert.AlertType.INFORMATION, + "Skript exportiert nach:\n" + out, ButtonType.OK).showAndWait(); + } catch (IOException ex) { + Dialogs.alert(Alert.AlertType.ERROR, + "Fehler beim Schreiben: " + ex.getMessage(), ButtonType.OK).showAndWait(); + } + } + + /** Liefert die Step-Keys einer Option: erst aus heroSteps/npcSteps, dann textHero/textNpc, dann Ableitungsfall. */ + static List resolveStepKeys(List steps, TextReference legacy, + String base, String suffix) { + if (steps != null && !steps.isEmpty()) { + List keys = new ArrayList<>(); + for (DialogStep s : steps) { + if (s.getText() != null && !s.getText().id().isBlank()) keys.add(s.getText().id()); + } + if (!keys.isEmpty()) return keys; + } + if (legacy != null && !legacy.id().isBlank()) return List.of(legacy.id()); + return List.of(base + suffix); + } + + private static String audioPathForKey(String key) { + if (key == null) return "—"; + if (key.startsWith("dialog.")) { + String without = key.substring("dialog.".length()); + int dot = without.indexOf('.'); + if (dot < 0) return "—"; + return "audio/dialog/" + without.substring(0, dot) + "/" + + without.substring(dot + 1).replace('.', '_') + ".ogg"; + } + if (key.startsWith("monologue.")) { + String without = key.substring("monologue.".length()); + int dot = without.indexOf('.'); + if (dot < 0) return "audio/monologue/" + without + ".ogg"; + return "audio/monologue/" + without.substring(0, dot) + "_" + + without.substring(dot + 1).replace('.', '_') + ".ogg"; + } + return "—"; + } + + private void refreshStepKeys(String base) { if (heroStepsView == null || npcStepsView == null) { return; diff --git a/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java index 3ebd3ce..bc51be8 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/LocalizationEditorView.java @@ -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 de.blight.editor.ProjectRoot; import javafx.animation.PauseTransition; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; @@ -139,7 +140,11 @@ public class LocalizationEditorView extends BorderPane { Button sortBtn = new Button("A->Z"); sortBtn.setOnAction(e -> audioData.sort(Comparator.comparing(r -> r[0]))); - HBox subBar = new HBox(6, addKeyBtn, delKeyBtn, sortBtn); + Button fillDialogBtn = new Button("Dialog-Schluessel befuellen"); + fillDialogBtn.setStyle("-fx-background-color: #3a4a6a; -fx-text-fill: white;"); + fillDialogBtn.setOnAction(e -> autoFillDialogKeys()); + + HBox subBar = new HBox(6, addKeyBtn, delKeyBtn, sortBtn, fillDialogBtn); subBar.setPadding(new Insets(6, 12, 6, 12)); subBar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;"); @@ -177,9 +182,20 @@ public class LocalizationEditorView extends BorderPane { new FileChooser.ExtensionFilter("Audio", "*.ogg", "*.wav", "*.mp3"), new FileChooser.ExtensionFilter("Alle Dateien", "*.*") ); + Path audioRoot = ProjectRoot.resolve( + "blight-assets", "src", "main", "resources"); + File initialDir = audioRoot.resolve("audio").resolve("dialog").toFile(); + if (initialDir.isDirectory()) fc.setInitialDirectory(initialDir); File file = fc.showOpenDialog(getScene().getWindow()); if (file != null) { - audioData.get(idx)[1] = file.getAbsolutePath(); + String path; + try { + path = audioRoot.relativize(file.toPath()) + .toString().replace('\\', '/'); + } catch (IllegalArgumentException e) { + path = file.getAbsolutePath(); + } + audioData.get(idx)[1] = path; audioTable.refresh(); scheduleSave(); } @@ -349,6 +365,38 @@ public class LocalizationEditorView extends BorderPane { if (sel != null) { audioData.remove(sel); scheduleSave(); } } + private void autoFillDialogKeys() { + Set existing = new HashSet<>(); + for (String[] row : audioData) existing.add(row[0]); + + List added = new ArrayList<>(); + for (String key : TextKeyStore.getKeys()) { + if (!key.startsWith("dialog.")) continue; + if (existing.contains(key)) continue; + audioData.add(new String[]{key, keyToAudioPath(key)}); + existing.add(key); + added.add(key); + } + if (!added.isEmpty()) { + audioData.sort(Comparator.comparing(r -> r[0])); + scheduleSave(); + } + Dialogs.alert(Alert.AlertType.INFORMATION, + added.isEmpty() ? "Alle Dialog-Schluessel bereits vorhanden." + : added.size() + " Schluessel hinzugefuegt.", + ButtonType.OK).showAndWait(); + } + + private static String keyToAudioPath(String key) { + if (key == null || !key.startsWith("dialog.")) return ""; + String without = key.substring("dialog.".length()); + int dot = without.indexOf('.'); + if (dot < 0) return ""; + String npcId = without.substring(0, dot); + String rest = without.substring(dot + 1).replace('.', '_'); + return "audio/dialog/" + npcId + "/" + rest + ".ogg"; + } + // Auto-save private void scheduleSave() { diff --git a/blight-editor/src/main/java/de/blight/editor/ui/TtsGeneratorDialog.java b/blight-editor/src/main/java/de/blight/editor/ui/TtsGeneratorDialog.java new file mode 100644 index 0000000..83a6422 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/ui/TtsGeneratorDialog.java @@ -0,0 +1,424 @@ +package de.blight.editor.ui; + +import de.blight.common.BlightHome; +import de.blight.common.MonologueIO; +import de.blight.common.model.*; +import de.blight.editor.ProjectRoot; +import javafx.application.Platform; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.stage.DirectoryChooser; +import javafx.stage.Modality; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.*; +import java.util.stream.Stream; + +/** + * Generiert TTS-Platzhalter-Audio via Piper für alle Dialog- oder Monolog-Zeilen. + * + *
    + *
  • NPC-Modus: NPC-Zeilen des aktuell geladenen NPCs
  • + *
  • Held-Modus: alle Held-Zeilen aus allen NPCs + Monologe
  • + *
+ * + * Ausgabe: OGG (wenn ffmpeg verfügbar) oder WAV als Fallback. + * Pfade werden automatisch in {@code audio_de.properties} eingetragen. + */ +public class TtsGeneratorDialog extends Dialog { + + private static final Path PREFS_FILE = BlightHome.resolve("config", "editor.prefs"); + + private final String npcId; + private final Map npcOptions; + + private TextField piperField; + private TextField modelsDirField; + private ComboBox modelCombo; // zeigt Dateinamen, Wert = voller Pfad + private CheckBox overwriteCheck; + private TextArea logArea; + private Button generateBtn; + private ProgressBar progressBar; + + public TtsGeneratorDialog(String npcId, Map npcOptions) { + this.npcId = npcId != null ? npcId : ""; + this.npcOptions = npcOptions != null ? new LinkedHashMap<>(npcOptions) : Map.of(); + + setTitle("TTS-Platzhalter generieren (Piper)"); + initModality(Modality.APPLICATION_MODAL); + initOwner(Dialogs.primaryWindow()); + setResizable(true); + + buildUI(); + loadPrefs(); + + getDialogPane().getButtonTypes().add(ButtonType.CLOSE); + setResultConverter(bt -> null); + } + + // ── UI ──────────────────────────────────────────────────────────────────── + + private void buildUI() { + // Piper binary + piperField = new TextField(); + piperField.setPromptText("/pfad/zur/piper-binary oder piper"); + + // Models directory + rescan + modelsDirField = new TextField(); + modelsDirField.setPromptText("/pfad/zu/models/"); + Button dirBrowseBtn = new Button("..."); + dirBrowseBtn.setOnAction(e -> browseModelsDir()); + Button rescanBtn = new Button("⟳"); + rescanBtn.setTooltip(new Tooltip("Modelle neu einlesen")); + rescanBtn.setOnAction(e -> refreshModels()); + HBox dirRow = new HBox(4, modelsDirField, dirBrowseBtn, rescanBtn); + HBox.setHgrow(modelsDirField, Priority.ALWAYS); + + // Model chooser + modelCombo = new ComboBox<>(); + modelCombo.setMaxWidth(Double.MAX_VALUE); + modelCombo.setPromptText("Modell wählen..."); + + overwriteCheck = new CheckBox("Vorhandene Dateien überschreiben"); + + progressBar = new ProgressBar(0); + progressBar.setMaxWidth(Double.MAX_VALUE); + + logArea = new TextArea(); + logArea.setEditable(false); + logArea.setPrefHeight(200); + logArea.setStyle("-fx-control-inner-background: #1a1a2a; -fx-text-fill: #ccc;" + + " -fx-font-family: monospace; -fx-font-size: 11;"); + + generateBtn = new Button("Generieren"); + generateBtn.setStyle("-fx-background-color: #3a7a3a; -fx-text-fill: white;"); + generateBtn.setMaxWidth(Double.MAX_VALUE); + generateBtn.setOnAction(e -> startGeneration()); + + VBox content = new VBox(8); + content.setPadding(new Insets(16)); + content.setPrefWidth(580); + content.getChildren().addAll( + row("Piper-Binary:", piperField), + row("Modell-Verzeichnis:", dirRow), + row("Stimme:", modelCombo), + new Separator(), + overwriteCheck, + new Separator(), + generateBtn, + progressBar, + logArea + ); + getDialogPane().setContent(content); + + piperField.focusedProperty().addListener((obs, o, n) -> { if (!n) savePrefs(); }); + modelsDirField.focusedProperty().addListener((obs, o, n) -> { if (!n) { refreshModels(); savePrefs(); } }); + } + + private void browseModelsDir() { + DirectoryChooser dc = new DirectoryChooser(); + dc.setTitle("Piper-Modell-Verzeichnis wählen"); + String cur = modelsDirField.getText().trim(); + if (!cur.isBlank()) { + File dir = new File(cur); + if (dir.isDirectory()) dc.setInitialDirectory(dir); + } + File chosen = dc.showDialog(getDialogPane().getScene().getWindow()); + if (chosen != null) { + modelsDirField.setText(chosen.getAbsolutePath()); + refreshModels(); + savePrefs(); + } + } + + /** Scannt das Modell-Verzeichnis nach .onnx-Dateien und befüllt die ComboBox. */ + private void refreshModels() { + String dirStr = modelsDirField.getText().trim(); + if (dirStr.isBlank()) return; + Path dir = Path.of(dirStr); + if (!Files.isDirectory(dir)) return; + + String prevSelection = modelCombo.getValue(); + modelCombo.getItems().clear(); + + try (Stream s = Files.list(dir)) { + s.filter(p -> p.getFileName().toString().endsWith(".onnx") + && !p.getFileName().toString().endsWith(".onnx.1")) + .sorted() + .map(Path::toAbsolutePath) + .map(Path::toString) + .forEach(modelCombo.getItems()::add); + } catch (IOException ignored) {} + + // Zeige nur Dateinamen, interner Wert bleibt voller Pfad + modelCombo.setButtonCell(new ModelCell()); + modelCombo.setCellFactory(lv -> new ModelCell()); + + if (prevSelection != null && modelCombo.getItems().contains(prevSelection)) { + modelCombo.setValue(prevSelection); + } else if (!modelCombo.getItems().isEmpty()) { + modelCombo.setValue(modelCombo.getItems().get(0)); + } + } + + private static class ModelCell extends ListCell { + @Override + protected void updateItem(String fullPath, boolean empty) { + super.updateItem(fullPath, empty); + if (empty || fullPath == null) { setText(null); return; } + setText(Path.of(fullPath).getFileName().toString() + .replaceAll("\\.onnx$", "")); + } + } + + // ── Generierung ─────────────────────────────────────────────────────────── + + private void startGeneration() { + String piperCmd = piperField.getText().trim(); + String modelPath = modelCombo.getValue(); + boolean heroMode = this.npcId.isBlank(); + boolean overwrite = overwriteCheck.isSelected(); + + if (piperCmd.isBlank()) { appendLog("FEHLER: Piper-Binary nicht angegeben."); return; } + if (modelPath == null || modelPath.isBlank()) { appendLog("FEHLER: Kein Modell gewählt."); return; } + if (!new File(modelPath).exists()) { appendLog("FEHLER: Modell nicht gefunden:\n " + modelPath); return; } + + generateBtn.setDisable(true); + logArea.clear(); + progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + + final String piperFinal = piperCmd, modelFinal = modelPath; + Task task = new Task<>() { + @Override protected Void call() { + try { + doGenerate(piperFinal, modelFinal, heroMode, overwrite); + } catch (Exception e) { + Platform.runLater(() -> { + appendLog("FEHLER: " + e.getMessage()); + progressBar.setProgress(0); + generateBtn.setDisable(false); + }); + } + return null; + } + }; + new Thread(task, "tts-generator").start(); + } + + private void doGenerate(String piperCmd, String modelPath, + boolean heroMode, boolean overwrite) throws IOException { + + Map texts = loadTexts(); + boolean hasFfmpeg = checkFfmpeg(); + Platform.runLater(() -> appendLog(hasFfmpeg + ? "[i] ffmpeg verfügbar — Ausgabe als OGG." + : "[i] ffmpeg nicht gefunden — Ausgabe als WAV.\n")); + + List keys = heroMode ? collectHeroKeys() : collectNpcKeys(); + Platform.runLater(() -> appendLog(keys.size() + " Schlüssel zu verarbeiten.\n")); + + Path assetsRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources"); + Path langDir = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang"); + AudioBundle bundle = AudioBundleIO.loadOrEmpty("de", langDir); + Map audioEntries = new LinkedHashMap<>(bundle.getEntries()); + + int total = keys.size(), done = 0; + + for (String key : keys) { + String text = texts.getOrDefault(key, "").trim(); + if (text.isBlank()) { + Platform.runLater(() -> appendLog("SKIP (kein Text): " + key)); + done++; + final int d = done; + Platform.runLater(() -> progressBar.setProgress((double) d / total)); + continue; + } + + String basePath = audioPathForKey(key); + if (basePath.isBlank()) { done++; continue; } + String relPath = hasFfmpeg ? basePath : basePath.replaceAll("\\.ogg$", ".wav"); + Path outPath = assetsRoot.resolve(relPath); + + if (!overwrite && Files.exists(outPath)) { + Platform.runLater(() -> appendLog("SKIP (vorhanden): " + relPath)); + done++; + final int d = done; + Platform.runLater(() -> progressBar.setProgress((double) d / total)); + continue; + } + + Files.createDirectories(outPath.getParent()); + boolean ok = hasFfmpeg + ? generateOgg(piperCmd, modelPath, text, outPath) + : generateWav(piperCmd, modelPath, text, outPath); + + if (ok) { + audioEntries.put(key, relPath); + Platform.runLater(() -> appendLog("OK: " + relPath)); + } else { + Platform.runLater(() -> appendLog("FEHLER: " + relPath)); + } + done++; + final int d = done; + Platform.runLater(() -> progressBar.setProgress((double) d / total)); + } + + bundle.setEntries(audioEntries); + AudioBundleIO.save(bundle, langDir); + + Platform.runLater(() -> { + progressBar.setProgress(1.0); + appendLog("\n--- Fertig! audio_de.properties aktualisiert. ---"); + generateBtn.setDisable(false); + }); + } + + // ── Schlüssel sammeln ───────────────────────────────────────────────────── + + private List collectNpcKeys() { + List keys = new ArrayList<>(); + for (Map.Entry e : npcOptions.entrySet()) { + String base = "dialog." + npcId + "." + e.getKey(); + keys.addAll(DialogEditorView.resolveStepKeys( + e.getValue().getNpcSteps(), e.getValue().getTextNpc(), base, ".textnpc")); + } + return keys; + } + + private List collectHeroKeys() { + List keys = new ArrayList<>(); + Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character"); + for (GameCharacter gc : CharacterIO.loadAll(charDir)) { + if (!(gc instanceof NPC npc) || npc.getDialogOptions() == null) continue; + for (Map.Entry e : npc.getDialogOptions().entrySet()) { + String base = "dialog." + npc.getCharacterId() + "." + e.getKey(); + keys.addAll(DialogEditorView.resolveStepKeys( + e.getValue().getHeroSteps(), e.getValue().getTextHero(), base, ".textmainchar")); + } + } + for (Monologue m : MonologueIO.load()) { + if (m.getHeroSteps() == null) continue; + m.getHeroSteps().stream() + .filter(s -> s.getText() != null && !s.getText().id().isBlank()) + .map(s -> s.getText().id()) + .forEach(keys::add); + } + return keys; + } + + // ── Piper / ffmpeg ──────────────────────────────────────────────────────── + + private boolean generateWav(String piperCmd, String modelPath, String text, Path out) { + try { + ProcessBuilder pb = new ProcessBuilder( + piperCmd, "--model", modelPath, "--output_file", out.toString(), "--quiet"); + pb.redirectErrorStream(true); + Process p = pb.start(); + try (OutputStream os = p.getOutputStream()) { + os.write((text + "\n").getBytes(StandardCharsets.UTF_8)); + } + return p.waitFor() == 0; + } catch (Exception e) { return false; } + } + + private boolean generateOgg(String piperCmd, String modelPath, String text, Path out) { + Path tmp = out.resolveSibling(out.getFileName() + ".tmp.wav"); + try { + if (!generateWav(piperCmd, modelPath, text, tmp)) return false; + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", "-i", tmp.toString(), + "-ar", "22050", "-ac", "1", "-q:a", "4", + out.toString(), "-y", "-loglevel", "error"); + pb.redirectErrorStream(true); + return pb.start().waitFor() == 0; + } catch (Exception e) { return false; } + finally { try { Files.deleteIfExists(tmp); } catch (Exception ignored) {} } + } + + private boolean checkFfmpeg() { + try { + return new ProcessBuilder("ffmpeg", "-version") + .redirectErrorStream(true).start().waitFor() == 0; + } catch (Exception e) { return false; } + } + + // ── Hilfsmethoden ───────────────────────────────────────────────────────── + + private static Map loadTexts() { + try { + Path f = ProjectRoot.resolve( + "blight-lang", "src", "main", "resources", "lang", "messages_de.properties"); + if (Files.exists(f)) return new LinkedHashMap<>(TextBundleIO.load(f).getEntries()); + } catch (Exception ignored) {} + return Map.of(); + } + + private static String audioPathForKey(String key) { + if (key == null) return ""; + if (key.startsWith("dialog.")) { + String w = key.substring("dialog.".length()); + int dot = w.indexOf('.'); + if (dot < 0) return ""; + return "audio/dialog/" + w.substring(0, dot) + "/" + + w.substring(dot + 1).replace('.', '_') + ".ogg"; + } + if (key.startsWith("monologue.")) { + String w = key.substring("monologue.".length()); + int dot = w.indexOf('.'); + if (dot < 0) return "audio/monologue/" + w + ".ogg"; + return "audio/monologue/" + w.substring(0, dot) + "_" + + w.substring(dot + 1).replace('.', '_') + ".ogg"; + } + return ""; + } + + private void appendLog(String msg) { logArea.appendText(msg + "\n"); } + + private void loadPrefs() { + try { + if (!Files.exists(PREFS_FILE)) return; + Properties p = new Properties(); + try (InputStream is = Files.newInputStream(PREFS_FILE)) { p.load(is); } + piperField.setText(p.getProperty("piper.path", "piper")); + modelsDirField.setText(p.getProperty("piper.models.dir", "")); + refreshModels(); + String savedModel = p.getProperty("piper.model", ""); + if (!savedModel.isBlank() && modelCombo.getItems().contains(savedModel)) { + modelCombo.setValue(savedModel); + } + } catch (Exception ignored) {} + } + + private void savePrefs() { + try { + Properties p = new Properties(); + p.setProperty("piper.path", piperField.getText().trim()); + p.setProperty("piper.models.dir", modelsDirField.getText().trim()); + String sel = modelCombo.getValue(); + if (sel != null) p.setProperty("piper.model", sel); + try (OutputStream os = Files.newOutputStream(PREFS_FILE)) { + p.store(os, "Piper TTS Settings"); + } + } catch (Exception ignored) {} + } + + private static HBox row(String labelText, javafx.scene.Node ctrl) { + Label lbl = new Label(labelText); + lbl.setMinWidth(140); + HBox.setHgrow(ctrl, Priority.ALWAYS); + HBox box = new HBox(8, lbl, ctrl); + box.setAlignment(Pos.CENTER_LEFT); + return box; + } + + private static Label bold(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-weight: bold;"); + return l; + } +} diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 1fa6d92..3a09cb6 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -327,6 +327,7 @@ public class WorldScene extends BaseAppState { if (saveState != null) saveState.bind(mc, physicsChar::getPhysicsLocation); app.getStateManager().attach(new LocationState(mc, character)); + app.getStateManager().attach(new de.blight.game.state.AreaTriggerState(mc, character)); app.getStateManager().attach( new WorldItemsState(keyBindings, physicsChar, mc, playerInput)); app.getStateManager().attach( diff --git a/blight-game/src/main/java/de/blight/game/state/AreaTriggerState.java b/blight-game/src/main/java/de/blight/game/state/AreaTriggerState.java new file mode 100644 index 0000000..9ab0d65 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/AreaTriggerState.java @@ -0,0 +1,97 @@ +package de.blight.game.state; + +import com.jme3.app.Application; +import com.jme3.app.state.BaseAppState; +import com.jme3.math.Vector3f; +import com.jme3.scene.Node; +import de.blight.common.AreaIO; +import de.blight.common.PlacedArea; +import de.blight.common.model.MainCharacter; +import de.blight.common.model.trigger.Trigger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Prüft pro Frame ob der Spieler eine PlacedArea betreten hat und feuert + * ggf. die zugehörigen Trigger (Übergang outside → inside). + * + * Betretene Areas werden zusätzlich in {@code character.visitedZoneIds} + * eingetragen, damit die {@code FirstTimeCondition} funktioniert. + */ +public class AreaTriggerState extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(AreaTriggerState.class); + + private final MainCharacter character; + private final Node playerNode; + private List areas; + private final Set active = new HashSet<>(); + + public AreaTriggerState(MainCharacter character, Node playerNode) { + this.character = character; + this.playerNode = playerNode; + } + + @Override + protected void initialize(Application app) { + try { + areas = AreaIO.load(); + log.info("{} Area(s) für Trigger-Prüfung geladen.", areas.size()); + } catch (Exception e) { + log.error("Areas nicht ladbar", e); + areas = List.of(); + } + } + + @Override + public void update(float tpf) { + if (areas.isEmpty() || playerNode == null) return; + Vector3f pos = playerNode.getWorldTranslation(); + float px = pos.x, pz = pos.z; + + for (PlacedArea area : areas) { + boolean inside = pointInPolygon(px, pz, area.pointsX(), area.pointsZ()); + boolean wasInside = active.contains(area.areaId()); + + if (inside && !wasInside) { + active.add(area.areaId()); + + if (area.areaId() != null && !area.areaId().isBlank()) { + character.getVisitedZoneIds().add(area.areaId()); + } + + List triggers = area.triggers(); + if (triggers != null && !triggers.isEmpty()) { + triggers.stream() + .filter(t -> t.isTriggarable(character)) + .forEach(t -> t.fire(character)); + log.debug("Area betreten: {} – {} Trigger geprüft.", area.areaId(), triggers.size()); + } + } else if (!inside) { + active.remove(area.areaId()); + } + } + } + + /** Ray-Casting-Algorithmus für Punkt-in-Polygon-Test (XZ-Ebene). */ + private static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) { + int n = xs.length; + if (n < 3) return false; + boolean inside = false; + for (int i = 0, j = n - 1; i < n; j = i++) { + if (((zs[i] > pz) != (zs[j] > pz)) && + (px < (xs[j] - xs[i]) * (pz - zs[i]) / (zs[j] - zs[i]) + xs[i])) { + inside = !inside; + } + } + return inside; + } + + @Override protected void cleanup(Application app) {} + @Override protected void onEnable() {} + @Override protected void onDisable() {} +} diff --git a/blight-game/src/main/java/de/blight/game/state/DialogHudState.java b/blight-game/src/main/java/de/blight/game/state/DialogHudState.java index 6352d53..ee09de5 100644 --- a/blight-game/src/main/java/de/blight/game/state/DialogHudState.java +++ b/blight-game/src/main/java/de/blight/game/state/DialogHudState.java @@ -222,6 +222,17 @@ public class DialogHudState extends BaseAppState { Monologue pending = mainCharForMonologue.pollPendingMonologue(); if (pending != null) { startMonologue(pending, mainCharForMonologue); + } else { + String pendingNpcId = mainCharForMonologue.pollPendingDialog(); + if (pendingNpcId != null) { + WorldNpcsState npcs = getStateManager().getState(WorldNpcsState.class); + NPC npc = npcs != null ? npcs.findNpcById(pendingNpcId) : null; + if (npc != null) { + startDialog(npc, mainCharForMonologue, null, null); + } else { + log.warn("[DialogHud] Trigger-Dialog: NPC '{}' nicht gefunden.", pendingNpcId); + } + } } } } @@ -359,7 +370,14 @@ public class DialogHudState extends BaseAppState { playSteps(ph, speaker, steps, idx + 1, after); return; } - String audioPath = step.getAudio() != null ? AudioResolver.get().resolve(step.getAudio()) : null; + String audioPath; + if (step.getAudio() != null) { + audioPath = AudioResolver.get().resolve(step.getAudio()); + } else if (step.getText() != null && !step.getText().id().isBlank()) { + audioPath = AudioResolver.get().resolveKey(step.getText().id()); + } else { + audioPath = null; + } showText(ph, speaker, text, audioPath, () -> playSteps(ph, speaker, steps, idx + 1, after)); } diff --git a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java index ffd2fad..d4382b8 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java @@ -632,6 +632,14 @@ public class WorldNpcsState extends BaseAppState { // ── Accessoren ──────────────────────────────────────────────────────────── + /** Liefert den NPC mit der gegebenen characterId, oder null wenn nicht gefunden. */ + public NPC findNpcById(String characterId) { + if (characterId == null) return null; + return allNpcs.stream() + .filter(n -> characterId.equals(n.getCharacterId())) + .findFirst().orElse(null); + } + /** Liefert das Spatial des aktuellen Dialog-NPCs, oder null wenn kein Dialog aktiv. */ public com.jme3.scene.Spatial getDialogNpcSpatial() { return dialogNpc != null ? dialogNpc.visual() : null; diff --git a/blight-lang/src/main/resources/lang/audio_de.properties b/blight-lang/src/main/resources/lang/audio_de.properties new file mode 100644 index 0000000..570a71a --- /dev/null +++ b/blight-lang/src/main/resources/lang/audio_de.properties @@ -0,0 +1,50 @@ +dialog.silas.begruessung.textnpc=audio/dialog/silas/begruessung_textnpc.ogg +dialog.silas.antwort_sklave.textnpc=audio/dialog/silas/antwort_sklave_textnpc.ogg +dialog.silas.antwort_gedaechtnislos.textnpc=audio/dialog/silas/antwort_gedaechtnislos_textnpc.ogg +dialog.silas.antwort_gesandter.textnpc=audio/dialog/silas/antwort_gesandter_textnpc.ogg +dialog.silas.wer_bist_du.textnpc=audio/dialog/silas/wer_bist_du_textnpc.ogg +dialog.silas.wer_seid_ihr.textnpc=audio/dialog/silas/wer_seid_ihr_textnpc.ogg +dialog.silas.was_leuchten.textnpc=audio/dialog/silas/was_leuchten_textnpc.ogg +dialog.silas.ausruestung.textnpc=audio/dialog/silas/ausruestung_textnpc.ogg +dialog.silas.gibts_was_rum.textnpc=audio/dialog/silas/gibts_was_rum_textnpc.ogg +dialog.silas.tipp_rum.textnpc=audio/dialog/silas/tipp_rum_textnpc.ogg +dialog.silas.welchen_weg.textnpc=audio/dialog/silas/welchen_weg_textnpc.ogg +dialog.silas.abschluss_phase1.textnpc=audio/dialog/silas/abschluss_phase1_textnpc.ogg +dialog.silas.rueckkehr_beobachtet.textnpc=audio/dialog/silas/rueckkehr_beobachtet_textnpc.ogg +dialog.silas.ich_bin_zaeh.textnpc=audio/dialog/silas/ich_bin_zaeh_textnpc.ogg +dialog.silas.weg_ins_innere.textnpc=audio/dialog/silas/weg_ins_innere_textnpc.ogg +dialog.silas.was_ueber_insel.textnpc=audio/dialog/silas/was_ueber_insel_textnpc.ogg +dialog.silas.info_osten.textnpc=audio/dialog/silas/info_osten_textnpc.ogg +dialog.silas.info_westen.textnpc=audio/dialog/silas/info_westen_textnpc.ogg +dialog.silas.info_dorf.textnpc=audio/dialog/silas/info_dorf_textnpc.ogg +dialog.silas.info_pass.textnpc=audio/dialog/silas/info_pass_textnpc.ogg +dialog.silas.ich_weiss_genug.textnpc=audio/dialog/silas/ich_weiss_genug_textnpc.ogg +dialog.silas.karte_zeichnen.textnpc=audio/dialog/silas/karte_zeichnen_textnpc.ogg +dialog.silas.dankeschoen.textnpc=audio/dialog/silas/dankeschoen_textnpc.ogg +dialog.silas.erstaunt_ruhe.textnpc=audio/dialog/silas/erstaunt_ruhe_textnpc.ogg +dialog.silas.abschluss_letzte_worte.textnpc=audio/dialog/silas/abschluss_letzte_worte_textnpc.ogg +dialog.silas.begruessung.textmainchar=audio/dialog/silas/begruessung_textmainchar.ogg +dialog.silas.antwort_sklave.textmainchar=audio/dialog/silas/antwort_sklave_textmainchar.ogg +dialog.silas.antwort_gedaechtnislos.textmainchar=audio/dialog/silas/antwort_gedaechtnislos_textmainchar.ogg +dialog.silas.antwort_gesandter.textmainchar=audio/dialog/silas/antwort_gesandter_textmainchar.ogg +dialog.silas.wer_bist_du.textmainchar=audio/dialog/silas/wer_bist_du_textmainchar.ogg +dialog.silas.wer_seid_ihr.textmainchar=audio/dialog/silas/wer_seid_ihr_textmainchar.ogg +dialog.silas.was_leuchten.textmainchar=audio/dialog/silas/was_leuchten_textmainchar.ogg +dialog.silas.ausruestung.textmainchar=audio/dialog/silas/ausruestung_textmainchar.ogg +dialog.silas.gibts_was_rum.textmainchar=audio/dialog/silas/gibts_was_rum_textmainchar.ogg +dialog.silas.tipp_rum.textmainchar=audio/dialog/silas/tipp_rum_textmainchar.ogg +dialog.silas.welchen_weg.textmainchar=audio/dialog/silas/welchen_weg_textmainchar.ogg +dialog.silas.abschluss_phase1.textmainchar=audio/dialog/silas/abschluss_phase1_textmainchar.ogg +dialog.silas.rueckkehr_beobachtet.textmainchar=audio/dialog/silas/rueckkehr_beobachtet_textmainchar.ogg +dialog.silas.ich_bin_zaeh.textmainchar=audio/dialog/silas/ich_bin_zaeh_textmainchar.ogg +dialog.silas.weg_ins_innere.textmainchar=audio/dialog/silas/weg_ins_innere_textmainchar.ogg +dialog.silas.was_ueber_insel.textmainchar=audio/dialog/silas/was_ueber_insel_textmainchar.ogg +dialog.silas.info_osten.textmainchar=audio/dialog/silas/info_osten_textmainchar.ogg +dialog.silas.info_westen.textmainchar=audio/dialog/silas/info_westen_textmainchar.ogg +dialog.silas.info_dorf.textmainchar=audio/dialog/silas/info_dorf_textmainchar.ogg +dialog.silas.info_pass.textmainchar=audio/dialog/silas/info_pass_textmainchar.ogg +dialog.silas.ich_weiss_genug.textmainchar=audio/dialog/silas/ich_weiss_genug_textmainchar.ogg +dialog.silas.karte_zeichnen.textmainchar=audio/dialog/silas/karte_zeichnen_textmainchar.ogg +dialog.silas.dankeschoen.textmainchar=audio/dialog/silas/dankeschoen_textmainchar.ogg +dialog.silas.erstaunt_ruhe.textmainchar=audio/dialog/silas/erstaunt_ruhe_textmainchar.ogg +dialog.silas.abschluss_letzte_worte.textmainchar=audio/dialog/silas/abschluss_letzte_worte_textmainchar.ogg diff --git a/blight-lang/src/main/resources/lang/text_keys.txt b/blight-lang/src/main/resources/lang/text_keys.txt index 90591d8..6572bdf 100644 --- a/blight-lang/src/main/resources/lang/text_keys.txt +++ b/blight-lang/src/main/resources/lang/text_keys.txt @@ -95,5 +95,9 @@ erzmoss.name hero.name location.neu_1786533928723 location.neu_1786546178407 +location.neu_1786606437840 location.test silas.name +strandgut.description +strandgut.name +strandgut.successmassage diff --git a/blight-map/src/main/map/blight_areas.bla b/blight-map/src/main/map/blight_areas.bla index 8fad02b..f0335eb 100644 --- a/blight-map/src/main/map/blight_areas.bla +++ b/blight-map/src/main/map/blight_areas.bla @@ -1,2 +1,2 @@ -# polygon areaId -99.610,-1029.072;108.552,-1049.218;144.375,-1062.661;138.386,-1033.647;126.795,-1015.806;107.068,-1012.408 area.test +# polygon areaId triggersJson +99.610,-1029.072;108.552,-1049.218;144.375,-1062.661;138.386,-1033.647;126.795,-1015.806;107.068,-1012.408 area.test [] diff --git a/blight-map/src/main/map/blight_grass.blg b/blight-map/src/main/map/blight_grass.blg index 090b31d..6c8c70a 100644 Binary files a/blight-map/src/main/map/blight_grass.blg and b/blight-map/src/main/map/blight_grass.blg differ diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index 12f30bf..b8a150a 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_locations.blo b/blight-map/src/main/map/blight_locations.blo new file mode 100644 index 0000000..d91e0db --- /dev/null +++ b/blight-map/src/main/map/blight_locations.blo @@ -0,0 +1,2 @@ +# nameId centerX centerZ radius triggersJson +location.test 0.000 0.000 0.000 [] diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index f685d4f..08d4c88 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/chunks/chunk_15_05.blc b/blight-map/src/main/map/chunks/chunk_15_05.blc index c60f170..1e2619d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_05.blc and b/blight-map/src/main/map/chunks/chunk_15_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_05.blc b/blight-map/src/main/map/chunks/chunk_16_05.blc index 25e2669..d8db58d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_05.blc and b/blight-map/src/main/map/chunks/chunk_16_05.blc differ diff --git a/dialog_script_silas.txt b/dialog_script_silas.txt new file mode 100644 index 0000000..7a36b14 --- /dev/null +++ b/dialog_script_silas.txt @@ -0,0 +1,258 @@ +=== Dialog-Skript: silas === +Exportiert: 2026-08-13 + +--- begruessung [ROOT] --- + HERO: + Key: dialog.silas.begruessung.textmainchar + Text: "Hi! Ich suche nur einen Weg weg von diesem Strand." + Audio: audio/dialog/silas/begruessung_textmainchar.ogg + NPC: + Key: dialog.silas.begruessung.textnpc + Text: "Woher kommst du?" + Audio: audio/dialog/silas/begruessung_textnpc.ogg + +--- antwort_sklave --- + HERO: + Key: dialog.silas.antwort_sklave.textmainchar + Text: "Ich war Sklave des Protektorats – unser Schiff ist an den steilen Klippen gestrandet." + Audio: audio/dialog/silas/antwort_sklave_textmainchar.ogg + NPC: + Key: dialog.silas.antwort_sklave.textnpc + Text: "Ein Sklave des Protektorats? Das Meer hat heute wohl einen Sinn für Ironie. Spült dich direkt vor meine Haustür. Immerhin bist du ehrlich… und sind wir nicht alle Sklaven des Protektorats? Ich jage ihr Wild, du hast ihre Steine geschleppt. Am Ende des Tages sitzen wir beide unter demselben Stiefel. Nur dass meiner schon etwas länger drückt." + Audio: audio/dialog/silas/antwort_sklave_textnpc.ogg + +--- antwort_gedaechtnislos --- + HERO: + Key: dialog.silas.antwort_gedaechtnislos.textmainchar + Text: "Ich habe mein Gedächtnis verloren – ich bin kürzlich an dem Strand dort unten aufgewacht." + Audio: audio/dialog/silas/antwort_gedaechtnislos_textmainchar.ogg + NPC: + Key: dialog.silas.antwort_gedaechtnislos.textnpc + Text: "Du siehst aus wie ein Sklave des Protektorats! Das Meer hat heute wohl einen Sinn für Ironie. Spült dich direkt vor meine Haustür. Aber sind wir nicht alle Sklaven des Protektorats." + Audio: audio/dialog/silas/antwort_gedaechtnislos_textnpc.ogg + +--- antwort_gesandter --- + HERO: + Key: dialog.silas.antwort_gesandter.textmainchar + Text: "Ich bin Gesandter des Königs – unser Schiff ist gekentert und ich bin der einzige Überlebende." + Audio: audio/dialog/silas/antwort_gesandter_textmainchar.ogg + NPC: + Key: dialog.silas.antwort_gesandter.textnpc + Text: "Ein Gesandter des Königs? Du siehst eher aus wie ein Sklave des Protektorats! Das Meer hat heute wohl einen Sinn für Ironie. Spült dich direkt vor meine Haustür. Aber sind wir nicht alle Sklaven des Protektorats." + Audio: audio/dialog/silas/antwort_gesandter_textnpc.ogg + +--- wer_bist_du --- + HERO: + Key: dialog.silas.wer_bist_du.textmainchar + Text: "Wer bist du?" + Audio: audio/dialog/silas/wer_bist_du_textmainchar.ogg + NPC: + Key: dialog.silas.wer_bist_du.textnpc + Text: "Silas. Früher haben wir diesen Pass bewacht, damit die Guanchen und wir in Frieden tauschen konnten. Heute bin ich nur noch ein Geist, der hier oben wartet, dass die Fäule auch den letzten Rest der Welt frisst." + Audio: audio/dialog/silas/wer_bist_du_textnpc.ogg + +--- wer_seid_ihr --- + HERO: + Key: dialog.silas.wer_seid_ihr.textmainchar + Text: "Und wer seid „Ihr“?" + Audio: audio/dialog/silas/wer_seid_ihr_textmainchar.ogg + NPC: + Key: dialog.silas.wer_seid_ihr.textnpc + Text: "Wer wir sind? Eine aussterbende Art, Junge. Ich bin einer der Jäger, die diesen Pass schon bewacht haben, als das Protektorat noch nichts weiter war als ein ferner Albtraum in den Köpfen gieriger Könige. Früher war das hier ein Geben und Nehmen: Wir haben das Wild reguliert, mit den Guanchen Fleisch gegen Kräuter getauscht und die Pfade sauber gehalten. +Dann kamen die Eroberer mit ihren Vermessungsgeräten und ihren Gesetzen. Wollten mir vorschreiben, wann ich einen Hirsch erlegen darf und wie viel Steuer ich für jedes Stück Fell an Puerto Valor abtreten soll. Die meisten meiner Brüder sind geflohen – auf die Nachbarinseln oder zu den Piraten, nur um dem Joch zu entkommen. Aber ich? Ich bin zu alt, um vor ein paar aufgeblasenen Gockeln in Blechrüstungen wegzulaufen. Also sitze ich hier oben, lebe von dem, was der Wald noch hergibt, und sehe zu, wie die Fäule alles verschlingt, was mir mal etwas bedeutet hat. +Für das Protektorat bin ich ein Wilderer. Für die Piraten ein Narr. Und für die Guanchen… nun, vielleicht der letzte Außenweltler, dem sie nicht sofort einen Speer in die Kehle jagen. Such dir aus, was dir am besten gefällt." + Audio: audio/dialog/silas/wer_seid_ihr_textnpc.ogg + +--- was_leuchten --- + HERO: + Key: dialog.silas.was_leuchten.textmainchar + Text: "Was ist das für ein Zeug am Strand? Das Leuchten?" + Audio: audio/dialog/silas/was_leuchten_textmainchar.ogg + NPC: + Key: dialog.silas.was_leuchten.textnpc + Text: "Das ist der Tod, Junge. Die Blight. Die arroganten Gecken in Puerto Valor haben den Vulkan angebohrt und jetzt blutet die Insel Gift. Wenn du klug bist, drehst du um und läufst zurück ins Wasser. +Nicht alles auf dieser Insel will dich korrumpieren, Junge. Manche wollen dich einfach nur fressen. Ein wilder Hund hat keine Kristalle, die ihn schützen, aber er hat Hunger und Zähne wie Dolche. Behalt ihn im Auge, weich seinen Sprüngen aus und zeig ihm, wer der neue Herr im Dorf ist." + Audio: audio/dialog/silas/was_leuchten_textnpc.ogg + +--- ausruestung --- + HERO: + Key: dialog.silas.ausruestung.textmainchar + Text: "Ich brauche Ausrüstung. Waffen. Und Informationen." + Audio: audio/dialog/silas/ausruestung_textmainchar.ogg + NPC: + Key: dialog.silas.ausruestung.textnpc + Text: "Ausrüstung? Du siehst aus, als hätte dich das Meer einmal durch den Fleischwolf gedreht. Pass auf: Geh zurück zum Strand, dort wo du angespült wurdest. Bei diesem Sturm ist sicher nicht nur Sklavenfleisch am Riff hängen geblieben. Such im Treibgut. Mit etwas Glück findest du dort mehr als nur nasse Planken." + Audio: audio/dialog/silas/ausruestung_textnpc.ogg + +--- gibts_was_rum --- + HERO: + Key: dialog.silas.gibts_was_rum.textmainchar + Text: "Und was ist mit dir? Hast du nichts übrig?" + Audio: audio/dialog/silas/gibts_was_rum_textmainchar.ogg + NPC: + Key: dialog.silas.gibts_was_rum.textnpc + Text: "Ich bin kein Wohlfahrtsverband, Junge. Aber … du bist mir sympathisch, einen Schluck Insel-Rum kriegst du. Das Zeug brennt die Korruption aus der Kehle, zumindest für fünf Minuten. Trink." + Audio: audio/dialog/silas/gibts_was_rum_textnpc.ogg + +--- tipp_rum --- + HERO: + Key: dialog.silas.tipp_rum.textmainchar + Text: "Und was ist mit dir? Hast du nichts übrig?" + Audio: audio/dialog/silas/tipp_rum_textmainchar.ogg + NPC: + Key: dialog.silas.tipp_rum.textnpc + Text: "Ich bin kein Wohlfahrtsverband, Junge. Aber … einen Tipp gibt es umsonst: Insel-Rum ist das Beste, wenn du dich mal zu viel mit den wilden Tieren geprügelt hast." + Audio: audio/dialog/silas/tipp_rum_textnpc.ogg + +--- welchen_weg --- + HERO: + Key: dialog.silas.welchen_weg.textmainchar + Text: "Welchen Weg soll ich nehmen, um zu den Lagern zu kommen?" + Audio: audio/dialog/silas/welchen_weg_textmainchar.ogg + NPC: + Key: dialog.silas.welchen_weg.textnpc + Text: "Egal welchen Weg du nimmst – geh nicht so, wie du jetzt aussiehst. In diesen Lumpen erkennt dich jeder sofort als entflohenen Sklaven. Das Protektorat steckt dich direkt wieder in Ketten, und die Piraten… nun, die füttern die Haie mit Typen, die nichts wert sind. +Hör zu: Wenn du den Hang Richtung Strand wieder ein Stück runtergehst, zweigt ein Pfad zu einem alten Fischerdorf ab. Die Leute dort sind schon lange weg, als die Fäule kam, aber in den Hütten findest du vielleicht ein paar alte Lumpen, die weniger nach 'Gefängnis' stinken. Es ist besser als nichts." + Audio: audio/dialog/silas/welchen_weg_textnpc.ogg + +--- abschluss_phase1 --- + HERO: + Key: dialog.silas.abschluss_phase1.textmainchar + Text: "Alles klar. Ich mache mich auf den Weg." + Audio: audio/dialog/silas/abschluss_phase1_textmainchar.ogg + NPC: + Key: dialog.silas.abschluss_phase1.textnpc + Text: "Also, hier ist der Plan: Such am Strand nach Brauchbarem, schau im Fischerdorf nach Kleidung und komm dann wieder zu mir. Wenn du dann nicht mehr aussiehst wie eine wandelnde Leiche, unterhalten wir uns darüber, wie du an den Monstern am Pass vorbeikommst. Jetzt beweg dich." + Audio: audio/dialog/silas/abschluss_phase1_textnpc.ogg + +--- rueckkehr_beobachtet --- + HERO: + Key: dialog.silas.rueckkehr_beobachtet.textmainchar + Text: "(Ich kehre zu Silas zurück)" + Audio: audio/dialog/silas/rueckkehr_beobachtet_textmainchar.ogg + NPC: + Key: dialog.silas.rueckkehr_beobachtet.textnpc + Text: "Sieh mal einer an. Das Meer hat dich ausgespückt und die Insel hat dich nicht wieder verschlungen. Ich hätte meine letzte Flasche Rum darauf verwettet, dass dich die Klippenbeßer am Hang in handliche Stücke zerlegen, noch bevor du das Dorf erreichst." + Audio: audio/dialog/silas/rueckkehr_beobachtet_textnpc.ogg + +--- ich_bin_zaeh --- + HERO: + Key: dialog.silas.ich_bin_zaeh.textmainchar + Text: "Ich bin zäher, als ich aussehe." + Audio: audio/dialog/silas/ich_bin_zaeh_textmainchar.ogg + NPC: + Key: dialog.silas.ich_bin_zaeh.textnpc + Text: "Das scheinst du wohl zu sein. Zumindest stinkst du jetzt weniger nach Sklavenpferch. Damit kommst du vielleicht an den Wachen der Festung vorbei, ohne dass sie direkt die Musketen laden." + Audio: audio/dialog/silas/ich_bin_zaeh_textnpc.ogg + +--- weg_ins_innere --- + HERO: + Key: dialog.silas.weg_ins_innere.textmainchar + Text: "Du hast gesagt, wir unterhalten uns über den Weg ins Landesinnere, wenn ich bereit bin." + Audio: audio/dialog/silas/weg_ins_innere_textmainchar.ogg + NPC: + Key: dialog.silas.weg_ins_innere.textnpc + Text: "Richtig. Hör zu, Junge: Die Pfade hier draußen sind tückisch geworden. Mein wichtigster Rat? Bleib auf den Wegen. Die Blight hat die Kreaturen in den Wäldern wahnsinnig gemacht. Abseits der Pfade lauern Dinge, für die dein kleiner Roststängel da am Gürtel nur ein Zahnstocher ist. Geh nur in die tiefe Wildnis, wenn du Rüstung am Leib und Stahl in der Hand hast, der diesen Namen auch verdient." + Audio: audio/dialog/silas/weg_ins_innere_textnpc.ogg + +--- was_ueber_insel --- + HERO: + Key: dialog.silas.was_ueber_insel.textmainchar + Text: "Diese Insel… sie wirkt so fremdartig. Was kannst du mir über diesen Ort sagen?" + Audio: audio/dialog/silas/was_ueber_insel_textmainchar.ogg + NPC: + Key: dialog.silas.was_ueber_insel.textnpc + Text: "Hör mir gut zu, Junge. Aeterna ist wie eine gespaltene Münze, und der große Vulkan in der Mitte – der Monte Fuego – ist der Amboss, auf dem alles geschmiedet wird. Der Westen ist ein einziges grünes Grab, erstickt von uralten Wäldern. Der Osten dagegen, hinter dem Berg, ist karg und trocken wie ein alter Knochen." + Audio: audio/dialog/silas/was_ueber_insel_textnpc.ogg + +--- info_osten --- + HERO: + Key: dialog.silas.info_osten.textmainchar + Text: "Wer herrscht im Osten der Insel?" + Audio: audio/dialog/silas/info_osten_textmainchar.ogg + NPC: + Key: dialog.silas.info_osten.textnpc + Text: "Die Protektoren. Sie haben sich im Südosten in Puerto Valor breitgemacht. Sie haben Mauern, Ordnung und das beste Equipment – aber sie haben auch einen Stock im Hintern und verlangen Gehorsam. Wenn du dort ihren Wein trinken willst, den sie in den Sonnental-Weinbergen anbauen, musst du nach ihrer Pfeife tanzen." + Audio: audio/dialog/silas/info_osten_textnpc.ogg + +--- info_westen --- + HERO: + Key: dialog.silas.info_westen.textmainchar + Text: "Und was ist mit dem Westen?" + Audio: audio/dialog/silas/info_westen_textmainchar.ogg + NPC: + Key: dialog.silas.info_westen.textnpc + Text: "Dort regiert das Chaos oder die Natur. Im Südwesten liegt Shipwreck Bay, das Nest der Freibeuter. Da stellt keiner Fragen über deine Vergangenheit, solange du zupacken kannst. Aber pass auf deinen Geldbeutel auf – und auf deinen Rücken. Tief im Nordwesten, im Schatten der Nebelgipfel, hausen die Guanchen in ihrer Siedlung Taganana. Du musst sie erst mal finden, aber wenn du Magie suchst, sind sie deine einzige Chance." + Audio: audio/dialog/silas/info_westen_textnpc.ogg + +--- info_dorf --- + HERO: + Key: dialog.silas.info_dorf.textmainchar + Text: "Was hat es mit diesem Dorf hier auf sich?" + Audio: audio/dialog/silas/info_dorf_textmainchar.ogg + NPC: + Key: dialog.silas.info_dorf.textnpc + Text: "Das hier im Norden ist San Pedro. Früher ein reiches Fischerdorf, jetzt nur noch Schutt und Gestrüpp. Ein guter Ort, um den Kampf gegen Klippenbeßer zu üben. Ein wilder Hund hat keine Kristalle, die ihn schützen, aber er hat Hunger und Zähne wie Dolche. Weich ihren Sprüngen aus, dann gehört das Fleisch dir." + Audio: audio/dialog/silas/info_dorf_textnpc.ogg + +--- info_pass --- + HERO: + Key: dialog.silas.info_pass.textmainchar + Text: "Und der Weg durch den Pass?" + Audio: audio/dialog/silas/info_pass_textmainchar.ogg + NPC: + Key: dialog.silas.info_pass.textnpc + Text: "Hinter meiner Hütte geht es steil bergauf. Oben am Kamm haben sich ein paar Corrupted eingenistet. Sie sind langsam, aber wenn sie dich einkesseln, war es das. Nutze den Platz, den du hast, und schlag erst zu, wenn du eine Lücke siehst. Sobald du an ihnen vorbei bist, liegt Aeterna vor dir." + Audio: audio/dialog/silas/info_pass_textnpc.ogg + +--- ich_weiss_genug --- + HERO: + Key: dialog.silas.ich_weiss_genug.textmainchar + Text: "Ich glaub ich weiß alles, was ich brauche…" + Audio: audio/dialog/silas/ich_weiss_genug_textmainchar.ogg + NPC: + Key: dialog.silas.ich_weiss_genug.textnpc + Text: "Du willst also blind ins Verderben rennen? Hier, nimm das, damit deine Leiche wenigstens in der Nähe eines Pfades gefunden wird." + Audio: audio/dialog/silas/ich_weiss_genug_textnpc.ogg + +--- karte_zeichnen --- + HERO: + Key: dialog.silas.karte_zeichnen.textmainchar + Text: "(Silas zeichnet die Karte)" + Audio: audio/dialog/silas/karte_zeichnen_textmainchar.ogg + NPC: + Key: dialog.silas.karte_zeichnen.textnpc + Text: "Du stellst verdammt viele Fragen für jemanden, der kaum eine Waffe halten kann. Aber Neugier hält einen hier oben am Leben. +Komm näher. Ich zeichne dir das Ganze auf: San Pedro hier oben, Monte Fuego in der Mitte, Puerto Valor im trockenen Südosten und Shipwreck Bay im Wald des Südwestens. Der Pfad führt direkt hinauf zum Schlund… aber sag nicht, ich hätte dich nicht gewarnt. Hier… nimm die Karte." + Audio: audio/dialog/silas/karte_zeichnen_textnpc.ogg + +--- dankeschoen --- + HERO: + Key: dialog.silas.dankeschoen.textmainchar + Text: "Danke, Silas. Ich werde vorsichtig sein." + Audio: audio/dialog/silas/dankeschoen_textmainchar.ogg + NPC: + Key: dialog.silas.dankeschoen.textnpc + Text: "Das hoffe ich. Und hey… wenn du mal wieder in der Nähe bist und noch alle Gliedmaßen hast, schau auf einen Rum vorbei. Erzähl mir, was in der Welt da draußen vor sich geht. Ich will wissen, wer sich gerade gegenseitig an die Gurgel geht." + Audio: audio/dialog/silas/dankeschoen_textnpc.ogg + +--- erstaunt_ruhe --- + HERO: + Key: dialog.silas.erstaunt_ruhe.textmainchar + Text: "Ich dachte, du willst hier oben deine Ruhe haben und hast die Welt da draußen längst abgeschrieben?" + Audio: audio/dialog/silas/erstaunt_ruhe_textmainchar.ogg + NPC: + Key: dialog.silas.erstaunt_ruhe.textnpc + Text: "Vielleicht wirke ich so. Und vielleicht habe ich den Glauben an die Menschen verloren… aber ich habe diese Insel noch nicht ganz aufgegeben. Und ich hab so ein Gefühl bei dir, Junge. Nenn es Instinkt oder den Rum, aber ich glaube, du wirst auf Aeterna noch für ordentlich Wirbel sorgen. Ich will nur sichergehen, dass ich in der ersten Reihe sitze, wenn der Vorhang fällt." + Audio: audio/dialog/silas/erstaunt_ruhe_textnpc.ogg + +--- abschluss_letzte_worte --- + HERO: + Key: dialog.silas.abschluss_letzte_worte.textmainchar + Text: "(Der Held macht sich auf den Weg)" + Audio: audio/dialog/silas/abschluss_letzte_worte_textmainchar.ogg + NPC: + Key: dialog.silas.abschluss_letzte_worte.textnpc + Text: "Jetzt geh schon. Bevor ich es mir anders überlege und dich doch noch für die Steuerjagd anmelde." + Audio: audio/dialog/silas/abschluss_letzte_worte_textnpc.ogg + diff --git a/tts_prefs.properties b/tts_prefs.properties new file mode 100644 index 0000000..8d2e0ee --- /dev/null +++ b/tts_prefs.properties @@ -0,0 +1,4 @@ +#Piper TTS Settings +piper.path=/home/mario/Programme/piper-tts/piper +piper.model=/home/mario/Programme/piper-tts/models/de_DE-thorsten-high.onnx +piper.models.dir=/home/mario/Programme/piper-tts/models