Weiter an den Dialogen gearbeitet

This commit is contained in:
2026-07-09 21:31:37 +02:00
parent 8bcee4491e
commit f109241e1f
50 changed files with 2413 additions and 413 deletions

View File

@@ -11,12 +11,14 @@ import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils;
import de.blight.common.GrassVertexBlade;
import de.blight.common.GrassVertexIO;
@@ -26,7 +28,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
@@ -47,7 +51,7 @@ public class GrassVertexState extends BaseAppState {
// ── Geometrie ─────────────────────────────────────────────────────────────
static final int BLADES_PER_TUFT = 3; // Halme pro Büschel
static final int SEGMENTS = 5; // Segmente pro Halm (5 → 6 Reihen)
static final float WIDTH_FACTOR = 0.05f; // Basis-Halbbreite = Höhe × WIDTH_FACTOR
static final float WIDTH_FACTOR = 0.10f; // Basis-Halbbreite = Höhe × WIDTH_FACTOR
static final float BEND_FACTOR = 0.15f; // max. Krümmungsversatz an der Spitze
static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
@@ -63,6 +67,15 @@ public class GrassVertexState extends BaseAppState {
private static final float CULL_DIST = 150f;
private static final float CULL_DIST_SQ = CULL_DIST * CULL_DIST;
// ── Samen-Texturen ────────────────────────────────────────────────────────
private static final String SEED_TEX_BASE = "Textures/internal/gras/seeds/seeds";
private static final String SEED_TEX_EXT = ".png";
/** Samen-Größe relativ zur Halmhöhe */
private static final float SEED_SIZE_FACTOR = 0.45f;
/** Y-Position der Samen-Unterseite relativ zum Halmfuß (01) */
private static final float SEED_Y_FACTOR = 0.78f;
// ── Zustand ───────────────────────────────────────────────────────────────
private final SharedInput input;
private AssetManager assetManager;
@@ -70,6 +83,8 @@ public class GrassVertexState extends BaseAppState {
private TerrainQuad terrain;
private Node grassNode;
private Material material;
private Material[] seedMaterials = new Material[0];
private Material seedStalkMaterial;
@SuppressWarnings("unchecked")
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
@@ -97,7 +112,9 @@ public class GrassVertexState extends BaseAppState {
this.cam = app.getCamera();
grassNode = new Node("grassVertexNode");
((SimpleApplication) app).getRootNode().attachChild(grassNode);
material = buildMaterial();
material = buildMaterial();
seedMaterials = loadSeedMaterials();
seedStalkMaterial = buildSeedStalkMaterial();
try {
for (GrassVertexBlade b : GrassVertexIO.load()) {
@@ -109,6 +126,38 @@ public class GrassVertexState extends BaseAppState {
}
}
private Material buildSeedStalkMaterial() {
Material mat = new Material(assetManager, "MatDefs/GrassVertex.j3md");
mat.setFloat("WindSpeed", 1.0f);
mat.setFloat("WindStrength", 0.15f);
mat.setVector3("SunDir", new com.jme3.math.Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
}
private Material[] loadSeedMaterials() {
List<Material> mats = new ArrayList<>();
for (int i = 1; i <= 99; i++) {
String path = SEED_TEX_BASE + i + SEED_TEX_EXT;
try {
Texture tex = assetManager.loadTexture(path);
Material mat = new Material(assetManager, "MatDefs/GrassSeed.j3md");
mat.setTexture("ColorMap", tex);
mat.setFloat("WindSpeed", 1.0f);
mat.setFloat("WindStrength", 0.15f);
mat.setVector3("SunDir", new com.jme3.math.Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mats.add(mat);
log.info("[GrassVertexState] Samen-Textur geladen: {}", path);
} catch (Exception e) {
break;
}
}
return mats.toArray(new Material[0]);
}
@Override
protected void cleanup(Application app) {
((SimpleApplication) app).getRootNode().detachChild(grassNode);
@@ -201,7 +250,9 @@ public class GrassVertexState extends BaseAppState {
float variation = (1f - uniformity) * 0.25f; // max ±25 % bei uniformity=0
float radSq = radius * radius;
// Kleinere Rand-Halme im Pinselbereich durch größere ersetzen
float seedPct = (float) input.grassVertexTool.seedDensity.getValue() / 100f;
// Kleinere Rand-Halme im Pinselbereich durch größere ersetzen (seedIdx erhalten)
for (int ci = 0; ci < CHUNK_COUNT; ci++) {
List<GrassVertexBlade> list = chunkBlades[ci];
boolean changed = false;
@@ -215,7 +266,7 @@ public class GrassVertexState extends BaseAppState {
// Halm ist deutlich kleiner als die aktuelle Pinselposition erlaubt → ersetzen
if (b.height() < idealH * 0.88f) {
float newH = idealH * (1f + variation * (rng.nextFloat() * 2f - 1f));
list.set(i, new GrassVertexBlade(b.x(), b.y(), b.z(), Math.max(0.05f, newH), b.dryness()));
list.set(i, new GrassVertexBlade(b.x(), b.y(), b.z(), Math.max(0.05f, newH), b.dryness(), b.seedIdx()));
changed = true;
}
}
@@ -236,7 +287,11 @@ public class GrassVertexState extends BaseAppState {
h = Math.max(0.05f, h);
float bladeDryness = rng.nextFloat() < witherPct
? 0.5f + rng.nextFloat() * 0.5f : 0f;
GrassVertexBlade blade = new GrassVertexBlade(bx, by, bz, h, bladeDryness);
int seedIdx = -1;
if (seedMaterials.length > 0 && rng.nextFloat() < seedPct) {
seedIdx = rng.nextInt(seedMaterials.length);
}
GrassVertexBlade blade = new GrassVertexBlade(bx, by, bz, h, bladeDryness, seedIdx);
int ci = chunkIndex(bx, bz);
if (ci >= 0) { chunkBlades[ci].add(blade); dirtyChunks[ci] = true; }
}
@@ -254,7 +309,7 @@ public class GrassVertexState extends BaseAppState {
if (dx*dx + dz*dz > radSq) continue;
float newY = terrain.getHeight(new Vector2f(b.x(), b.z()));
if (!Float.isNaN(newY)) {
list.set(i, new GrassVertexBlade(b.x(), newY, b.z(), b.height(), b.dryness()));
list.set(i, new GrassVertexBlade(b.x(), newY, b.z(), b.height(), b.dryness(), b.seedIdx()));
changed = true;
}
}
@@ -333,10 +388,144 @@ public class GrassVertexState extends BaseAppState {
Node node = new Node("gvc_" + ci);
node.attachChild(geo);
// ── Samen-Stiele + Kreuze ─────────────────────────────────────────────
if (seedMaterials.length > 0) {
List<GrassVertexBlade> seededAll = new ArrayList<>();
Map<Integer, List<GrassVertexBlade>> byTex = new HashMap<>();
for (GrassVertexBlade b : blades) {
if (b.seedIdx() >= 0 && b.seedIdx() < seedMaterials.length) {
seededAll.add(b);
byTex.computeIfAbsent(b.seedIdx(), k -> new ArrayList<>()).add(b);
}
}
if (!seededAll.isEmpty()) {
Geometry stalkGeo = buildSeedStalkMesh("stalk_" + ci, seededAll);
stalkGeo.setMaterial(seedStalkMaterial);
node.attachChild(stalkGeo);
}
for (Map.Entry<Integer, List<GrassVertexBlade>> e : byTex.entrySet()) {
Geometry seedGeo = buildSeedCrossMesh("seed_" + ci + "_" + e.getKey(), e.getValue());
seedGeo.setMaterial(seedMaterials[e.getKey()]);
node.attachChild(seedGeo);
}
}
chunkNodes[ci] = node;
grassNode.attachChild(node);
}
private static Geometry buildSeedStalkMesh(String name, List<GrassVertexBlade> blades) {
final int SEG = 4;
final float R = 0xbb / 255f, G = 0x90 / 255f, B = 0x59 / 255f;
int n = blades.size();
int vTotal = n * (SEG + 1) * 2;
float[] pos = new float[vTotal * 3];
float[] nrm = new float[vTotal * 3];
float[] col = new float[vTotal * 4];
float[] tex = new float[vTotal * 2];
int[] idx = new int [n * SEG * 6];
int vi = 0, ii = 0;
for (GrassVertexBlade blade : blades) {
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
float hw = h * 0.018f;
float ang = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
float cA = (float) Math.cos(ang), sA = (float) Math.sin(ang);
// Normale senkrecht zur Halmbreite, 30 % Richtung Weltauf gekippt (wie Gras-Shader)
float nx = -sA, ny = 0f, nz = cA;
float blend = 0.30f;
nx *= (1f - blend); ny = blend; nz *= (1f - blend);
float nLen = (float) Math.sqrt(nx*nx + ny*ny + nz*nz);
nx /= nLen; ny /= nLen; nz /= nLen;
for (int s = 0; s <= SEG; s++) {
float t = (float) s / SEG;
float curHW = hw * (float) Math.pow(1.0 - t, 1.4);
float py = y + h * t;
int sviL = vi + s * 2;
int sviR = sviL + 1;
// Linkes Vertex
pos[sviL*3] = x - cA*curHW; pos[sviL*3+1] = py; pos[sviL*3+2] = z - sA*curHW;
nrm[sviL*3] = nx; nrm[sviL*3+1] = ny; nrm[sviL*3+2] = nz;
col[sviL*4] = R; col[sviL*4+1] = G; col[sviL*4+2] = B; col[sviL*4+3] = 1f;
tex[sviL*2] = t; tex[sviL*2+1] = 0f;
// Rechtes Vertex
pos[sviR*3] = x + cA*curHW; pos[sviR*3+1] = py; pos[sviR*3+2] = z + sA*curHW;
nrm[sviR*3] = nx; nrm[sviR*3+1] = ny; nrm[sviR*3+2] = nz;
col[sviR*4] = R; col[sviR*4+1] = G; col[sviR*4+2] = B; col[sviR*4+3] = 1f;
tex[sviR*2] = t; tex[sviR*2+1] = 0f;
}
for (int s = 0; s < SEG; s++) {
int b0 = vi + s * 2;
idx[ii] = b0; idx[ii+1] = b0+1; idx[ii+2] = b0+3;
idx[ii+3] = b0; idx[ii+4] = b0+3; idx[ii+5] = b0+2;
ii += 6;
}
vi += (SEG + 1) * 2;
}
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
m.setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(nrm));
m.setBuffer(VertexBuffer.Type.Color, 4, BufferUtils.createFloatBuffer(col));
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(tex));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
m.updateBound();
return new Geometry(name, m);
}
private static Geometry buildSeedCrossMesh(String name, List<GrassVertexBlade> blades) {
// Pro Halm: 2 Quads (X-Kreuz) × 4 Verts = 8 Verts, 2 × 6 Indices = 12
int n = blades.size();
float[] pos = new float[n * 8 * 3];
float[] tex = new float[n * 8 * 2];
int[] idx = new int [n * 12];
int vi = 0, ii = 0;
for (GrassVertexBlade b : blades) {
float x = b.x();
float yBot = b.y() + b.height() * SEED_Y_FACTOR;
float z = b.z();
float size = b.height() * SEED_SIZE_FACTOR;
float hw = size * 0.5f;
// Quad 1 entlang Welt-X
setSeedV(pos, tex, vi+0, x-hw, yBot, z, 0,0);
setSeedV(pos, tex, vi+1, x+hw, yBot, z, 1,0);
setSeedV(pos, tex, vi+2, x+hw, yBot+size, z, 1,1);
setSeedV(pos, tex, vi+3, x-hw, yBot+size, z, 0,1);
// Quad 2 entlang Welt-Z
setSeedV(pos, tex, vi+4, x, yBot, z-hw, 0,0);
setSeedV(pos, tex, vi+5, x, yBot, z+hw, 1,0);
setSeedV(pos, tex, vi+6, x, yBot+size, z+hw, 1,1);
setSeedV(pos, tex, vi+7, x, yBot+size, z-hw, 0,1);
idx[ii] = vi; idx[ii+1] = vi+1; idx[ii+2] = vi+2;
idx[ii+3] = vi; idx[ii+4] = vi+2; idx[ii+5] = vi+3;
idx[ii+6] = vi+4; idx[ii+7] = vi+5; idx[ii+8] = vi+6;
idx[ii+9] = vi+4; idx[ii+10] = vi+6; idx[ii+11] = vi+7;
vi += 8; ii += 12;
}
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(tex));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
m.updateBound();
return new Geometry(name, m);
}
private static void setSeedV(float[] pos, float[] tex, int vi,
float x, float y, float z, float u, float v) {
pos[vi*3] = x; pos[vi*3+1] = y; pos[vi*3+2] = z;
tex[vi*2] = u; tex[vi*2+1] = v;
}
// ── Mesh-Generierung (gemeinsame Logik, auch von GrassVertexRenderState genutzt) ──
static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex, int[] idx,

View File

@@ -10,6 +10,8 @@ public class GrassVertexTool extends EditorTool {
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);
/** Anteil der Halme mit Samenstand (0 = keine, 100 = alle) */
public final ToolParameter seedDensity = new ToolParameter("Samen %", 0.0, 0.0, 100.0);
@Override public String getName() { return "Gras (Vertices)"; }
@@ -17,5 +19,5 @@ public class GrassVertexTool extends EditorTool {
public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); }
@Override
public List<ToolParameter> getParameters() { return List.of(brushRadius, bladeHeight, density, dryness, uniformity); }
public List<ToolParameter> getParameters() { return List.of(brushRadius, bladeHeight, density, dryness, uniformity, seedDensity); }
}

View File

@@ -52,8 +52,8 @@ public class DialogEditorView extends BorderPane {
private TextField idField;
private Label derivedLabelKey;
private Label derivedHeroKey;
private Label derivedNpcKey;
private ListView<String> heroStepsView;
private ListView<String> npcStepsView;
private CheckBox rootCheck;
private Spinner<Integer> chapterSpinner;
private ComboBox<String> statusCombo;
@@ -70,6 +70,7 @@ public class DialogEditorView extends BorderPane {
private ListView<Trigger> triggersView;
private ListView<String> nextOptionsView;
private ListView<String> disablesOptionsView;
private ComboBox<String> npcAnimCombo;
// ── Graph-mode ────────────────────────────────────────────────────────────
@@ -252,20 +253,16 @@ public class DialogEditorView extends BorderPane {
idField.setPromptText("z. B. begruessung");
derivedLabelKey = derivedKeyLabel("");
derivedHeroKey = derivedKeyLabel("");
derivedNpcKey = derivedKeyLabel("");
idField.textProperty().addListener((obs, oldVal, newVal) -> {
String id = newVal.trim();
if (id.isBlank()) {
derivedLabelKey.setText("");
derivedHeroKey.setText("");
derivedNpcKey.setText("");
refreshStepKeys("");
} else {
String base = "dialog." + currentNpcId + "." + id;
derivedLabelKey.setText(base + ".description");
derivedHeroKey.setText(base + ".textmainchar");
derivedNpcKey.setText(base + ".textnpc");
refreshStepKeys(base);
}
});
@@ -310,12 +307,31 @@ public class DialogEditorView extends BorderPane {
new Separator()
);
heroStepsView = stepsListView();
npcStepsView = stepsListView();
Button addHeroBtn = smallBtn("+");
Button delHeroBtn = smallBtn("");
Button addNpcBtn = smallBtn("+");
Button delNpcBtn = smallBtn("");
addHeroBtn.setOnAction(e -> addStep(heroStepsView, ".textmainchar"));
delHeroBtn.setOnAction(e -> removeLastStep(heroStepsView));
addNpcBtn.setOnAction(e -> addStep(npcStepsView, ".textnpc"));
delNpcBtn.setOnAction(e -> removeLastStep(npcStepsView));
npcAnimCombo = new ComboBox<>();
npcAnimCombo.getItems().addAll("— (keine)", "talk1", "talk2", "disappoint", "secret");
npcAnimCombo.setValue("— (keine)");
npcAnimCombo.setMaxWidth(Double.MAX_VALUE);
form.getChildren().addAll(
sectionTitle("Texte"),
row("Text Held:", derivedHeroKey),
row("Text NPC:", derivedNpcKey),
row("Audio Held:", placeholder("(wird später implementiert)")),
row("Audio NPC:", placeholder("(wird später implementiert)")),
sectionTitle("Held-Steps:"),
heroStepsView,
new HBox(4, addHeroBtn, delHeroBtn),
sectionTitle("NPC-Steps:"),
npcStepsView,
new HBox(4, addNpcBtn, delNpcBtn),
row("NPC-Animation:", npcAnimCombo),
new Separator()
);
@@ -469,7 +485,26 @@ public class DialogEditorView extends BorderPane {
questCompleteField.setText(opt.getRequiresQuestComplete() != null
? safe(opt.getRequiresQuestComplete().getQuestId()) : "");
// text keys are derived from ID and shown via derivedHeroKey / derivedNpcKey labels
// Held- und NPC-Steps laden (mit Migration aus Legacy-Feldern)
String base = (!id.isBlank()) ? "dialog." + currentNpcId + "." + id : "";
heroStepsView.getItems().clear();
java.util.List<de.blight.common.model.DialogStep> hs = opt.getHeroSteps();
if (hs != null && !hs.isEmpty()) {
for (int i = 0; i < hs.size(); i++) {
heroStepsView.getItems().add(stepKey(base, ".textmainchar", i));
}
} else if (opt.getTextHero() != null && !opt.getTextHero().id().isBlank()) {
heroStepsView.getItems().add(stepKey(base, ".textmainchar", 0));
}
npcStepsView.getItems().clear();
java.util.List<de.blight.common.model.DialogStep> ns = opt.getNpcSteps();
if (ns != null && !ns.isEmpty()) {
for (int i = 0; i < ns.size(); i++) {
npcStepsView.getItems().add(stepKey(base, ".textnpc", i));
}
} else if (opt.getTextNpc() != null && !opt.getTextNpc().id().isBlank()) {
npcStepsView.getItems().add(stepKey(base, ".textnpc", 0));
}
if (opt.getRequiredItem() != null && opt.getRequiredItem().getItem() != null) {
reqItemIdField.setText(safe(opt.getRequiredItem().getItem().getItemId()));
@@ -498,6 +533,9 @@ public class DialogEditorView extends BorderPane {
.map(Quest::getQuestId)
.forEach(abortsQuestsView.getItems()::add);
String anim = opt.getNpcAnimation();
npcAnimCombo.setValue(anim != null && !anim.isBlank() ? anim : "— (keine)");
enablesTradeCheck.setSelected(opt.isEnablesTrade());
triggersView.getItems().clear();
@@ -543,8 +581,24 @@ public class DialogEditorView extends BorderPane {
if (!currentId.isBlank()) {
String base = "dialog." + currentNpcId + "." + currentId;
opt.setLabel(new TextReference(base + ".description"));
opt.setTextHero(new TextReference(base + ".textmainchar"));
opt.setTextNpc(new TextReference(base + ".textnpc"));
List<de.blight.common.model.DialogStep> heroList = new ArrayList<>();
for (int i = 0; i < heroStepsView.getItems().size(); i++) {
heroList.add(new de.blight.common.model.DialogStep(
new TextReference(stepKey(base, ".textmainchar", i)), null));
}
opt.setHeroSteps(heroList);
opt.setTextHero(heroList.isEmpty() ? null
: new TextReference(stepKey(base, ".textmainchar", 0)));
List<de.blight.common.model.DialogStep> npcList = new ArrayList<>();
for (int i = 0; i < npcStepsView.getItems().size(); i++) {
npcList.add(new de.blight.common.model.DialogStep(
new TextReference(stepKey(base, ".textnpc", i)), null));
}
opt.setNpcSteps(npcList);
opt.setTextNpc(npcList.isEmpty() ? null
: new TextReference(stepKey(base, ".textnpc", 0)));
}
if (rootCheck.isSelected()) {
@@ -578,6 +632,9 @@ public class DialogEditorView extends BorderPane {
}
opt.setAbortsQuests(aborts);
String animVal = npcAnimCombo.getValue();
opt.setNpcAnimation(animVal == null || animVal.startsWith("") ? null : animVal);
opt.setEnablesTrade(enablesTradeCheck.isSelected());
opt.setOnChosenTriggers(new ArrayList<>(triggersView.getItems()));
@@ -594,8 +651,8 @@ public class DialogEditorView extends BorderPane {
private void clearDetailForm() {
if (idField != null) idField.clear();
if (derivedLabelKey != null) derivedLabelKey.setText("");
if (derivedHeroKey != null) derivedHeroKey.setText("");
if (derivedNpcKey != null) derivedNpcKey.setText("");
if (heroStepsView != null) heroStepsView.getItems().clear();
if (npcStepsView != null) npcStepsView.getItems().clear();
if (rootCheck != null) rootCheck.setSelected(false);
if (chapterSpinner != null) chapterSpinner.getValueFactory().setValue(0);
if (statusCombo != null) statusCombo.setValue("— (keine)");
@@ -608,6 +665,7 @@ public class DialogEditorView extends BorderPane {
if (recvQuestField != null) recvQuestField.clear();
if (fulfillsQuestField != null) fulfillsQuestField.clear();
if (abortsQuestsView != null) abortsQuestsView.getItems().clear();
if (npcAnimCombo != null) npcAnimCombo.setValue("— (keine)");
if (enablesTradeCheck != null) enablesTradeCheck.setSelected(false);
if (triggersView != null) triggersView.getItems().clear();
if (nextOptionsView != null) nextOptionsView.getItems().clear();
@@ -649,6 +707,10 @@ public class DialogEditorView extends BorderPane {
opt.setLabel(new TextReference(base + ".description"));
opt.setTextHero(new TextReference(base + ".textmainchar"));
opt.setTextNpc(new TextReference(base + ".textnpc"));
opt.setHeroSteps(new ArrayList<>(List.of(new de.blight.common.model.DialogStep(
new TextReference(base + ".textmainchar"), null))));
opt.setNpcSteps(new ArrayList<>(List.of(new de.blight.common.model.DialogStep(
new TextReference(base + ".textnpc"), null))));
allOptions.put(id, opt);
if (asRoot) {
rootIds.add(id);
@@ -1145,4 +1207,56 @@ public class DialogEditorView extends BorderPane {
});
return dlg.showAndWait().orElse(null);
}
// ── Steps helpers ─────────────────────────────────────────────────────────
private static ListView<String> stepsListView() {
ListView<String> lv = new ListView<>();
lv.setPrefHeight(72);
lv.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
lv.setCellFactory(v -> new ListCell<>() {
@Override protected void updateItem(String s, boolean empty) {
super.updateItem(s, empty);
setText(empty || s == null ? null : s);
setStyle("-fx-text-fill: #aaddaa;");
}
});
return lv;
}
private void addStep(ListView<String> lv, String suffix) {
String id = idField.getText().trim();
if (id.isBlank()) {
return;
}
String base = "dialog." + currentNpcId + "." + id;
int idx = lv.getItems().size();
lv.getItems().add(stepKey(base, suffix, idx));
}
private static void removeLastStep(ListView<String> lv) {
if (!lv.getItems().isEmpty()) {
lv.getItems().remove(lv.getItems().size() - 1);
}
}
private static String stepKey(String base, String suffix, int idx) {
return idx == 0 ? base + suffix : base + suffix + "." + idx;
}
private void refreshStepKeys(String base) {
if (heroStepsView == null || npcStepsView == null) {
return;
}
refreshListKeys(heroStepsView, base, ".textmainchar");
refreshListKeys(npcStepsView, base, ".textnpc");
}
private static void refreshListKeys(ListView<String> lv, String base, String suffix) {
int count = lv.getItems().size();
lv.getItems().clear();
for (int i = 0; i < count; i++) {
lv.getItems().add(stepKey(base, suffix, i));
}
}
}