Tool und Oberflächen Konsolidierung angefangen
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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. */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -97,6 +97,13 @@ public class SharedInput {
|
||||
public record GrassVertexEdit(float screenX, float screenY, int action) {}
|
||||
public final ConcurrentLinkedQueue<GrassVertexEdit> 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<FernGenRequest> 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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String, String> texts = loadTexts();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("=== NPC-Skript: ").append(currentNpcId).append(" ===\n");
|
||||
sb.append("Exportiert: ").append(LocalDate.now()).append("\n\n");
|
||||
|
||||
for (Map.Entry<String, DialogOption> entry : allOptions.entrySet()) {
|
||||
DialogOption opt = entry.getValue();
|
||||
String optId = entry.getKey();
|
||||
String base = "dialog." + currentNpcId + "." + optId;
|
||||
List<String> npcKeys = resolveStepKeys(opt.getNpcSteps(), opt.getTextNpc(), base, ".textnpc");
|
||||
if (npcKeys.isEmpty()) continue;
|
||||
|
||||
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<String, String> 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<String, DialogOption> entry : npc.getDialogOptions().entrySet()) {
|
||||
String optId = entry.getKey();
|
||||
DialogOption opt = entry.getValue();
|
||||
String base = "dialog." + npc.getCharacterId() + "." + optId;
|
||||
List<String> 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<Monologue> 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<String> keys = m.getHeroSteps().stream()
|
||||
.filter(s -> s.getText() != null && !s.getText().id().isBlank())
|
||||
.map(s -> s.getText().id())
|
||||
.toList();
|
||||
appendStepLines(sb, keys, "HELD", texts);
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
writeScript(ProjectRoot.resolve("dialog_script_held.txt"), sb.toString());
|
||||
}
|
||||
|
||||
private Map<String, String> 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<String> keys,
|
||||
String label, Map<String, String> texts) {
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
String key = keys.get(i);
|
||||
sb.append(keys.size() > 1 ? " " + label + " " + (i + 1) + ":\n" : " " + label + ":\n");
|
||||
sb.append(" Key: ").append(key).append("\n");
|
||||
sb.append(" Text: \"").append(texts.getOrDefault(key, "(nicht übersetzt)")).append("\"\n");
|
||||
sb.append(" Audio: ").append(audioPathForKey(key)).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeScript(Path out, String content) {
|
||||
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<String> resolveStepKeys(List<DialogStep> steps, TextReference legacy,
|
||||
String base, String suffix) {
|
||||
if (steps != null && !steps.isEmpty()) {
|
||||
List<String> 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;
|
||||
|
||||
@@ -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<String> existing = new HashSet<>();
|
||||
for (String[] row : audioData) existing.add(row[0]);
|
||||
|
||||
List<String> 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() {
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <ul>
|
||||
* <li>NPC-Modus: NPC-Zeilen des aktuell geladenen NPCs</li>
|
||||
* <li>Held-Modus: alle Held-Zeilen aus allen NPCs + Monologe</li>
|
||||
* </ul>
|
||||
*
|
||||
* Ausgabe: OGG (wenn ffmpeg verfügbar) oder WAV als Fallback.
|
||||
* Pfade werden automatisch in {@code audio_de.properties} eingetragen.
|
||||
*/
|
||||
public class TtsGeneratorDialog extends Dialog<Void> {
|
||||
|
||||
private static final Path PREFS_FILE = BlightHome.resolve("config", "editor.prefs");
|
||||
|
||||
private final String npcId;
|
||||
private final Map<String, DialogOption> npcOptions;
|
||||
|
||||
private TextField piperField;
|
||||
private TextField modelsDirField;
|
||||
private ComboBox<String> modelCombo; // zeigt Dateinamen, Wert = voller Pfad
|
||||
private CheckBox overwriteCheck;
|
||||
private TextArea logArea;
|
||||
private Button generateBtn;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
public TtsGeneratorDialog(String npcId, Map<String, DialogOption> 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<Path> 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<String> {
|
||||
@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<Void> 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<String, String> 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<String> 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<String, String> 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<String> collectNpcKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (Map.Entry<String, DialogOption> 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<String> collectHeroKeys() {
|
||||
List<String> 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<String, DialogOption> 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<String, String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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<PlacedArea> areas;
|
||||
private final Set<String> 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<Trigger> 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() {}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
50
blight-lang/src/main/resources/lang/audio_de.properties
Normal file
50
blight-lang/src/main/resources/lang/audio_de.properties
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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 []
|
||||
|
||||
Binary file not shown.
Binary file not shown.
2
blight-map/src/main/map/blight_locations.blo
Normal file
2
blight-map/src/main/map/blight_locations.blo
Normal file
@@ -0,0 +1,2 @@
|
||||
# nameId centerX centerZ radius triggersJson
|
||||
location.test 0.000 0.000 0.000 []
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
258
dialog_script_silas.txt
Normal file
258
dialog_script_silas.txt
Normal file
@@ -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
|
||||
|
||||
4
tts_prefs.properties
Normal file
4
tts_prefs.properties
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user