Character und Dialog System weiter gebaut
This commit is contained in:
@@ -171,7 +171,9 @@ public class EditorApp extends Application {
|
||||
editableSubClips = new java.util.LinkedHashMap<>();
|
||||
|
||||
// Character-Editor-Zustand
|
||||
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
||||
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
||||
private de.blight.editor.ui.RoutineEditorView routineEditorView;
|
||||
private StackPane routineJmeSlot;
|
||||
private javafx.scene.layout.VBox charEditContainer; // deaktiviert solange kein Character geladen
|
||||
private javafx.scene.layout.VBox charFractionSection; // sichtbar nur für NPCs
|
||||
private javafx.scene.layout.VBox charNpcSection; // sichtbar nur für NPCs
|
||||
@@ -286,6 +288,7 @@ public class EditorApp extends Application {
|
||||
private ToggleButton grassBtn;
|
||||
private ToggleButton grassVertexBtn;
|
||||
private ToggleButton textureBtn;
|
||||
private ToggleButton stoneBtn;
|
||||
private ToggleButton objPlaceBtn;
|
||||
private ToggleButton objEditBtn;
|
||||
private ToggleButton lightBtn;
|
||||
@@ -363,6 +366,10 @@ public class EditorApp extends Application {
|
||||
Stage splash = buildSplash(splashStatus);
|
||||
splash.show();
|
||||
|
||||
de.blight.common.model.TextKeyStore.init(
|
||||
de.blight.editor.ProjectRoot.resolve(
|
||||
"blight-lang", "src", "main", "resources", "lang", "text_keys.txt"));
|
||||
|
||||
jfxImage = new WritableImage(INITIAL_VP_W, INITIAL_VP_H);
|
||||
jmeApp = JmeEditorApp.launch(input, jfxImage, INITIAL_VP_W, INITIAL_VP_H);
|
||||
|
||||
@@ -773,6 +780,10 @@ public class EditorApp extends Application {
|
||||
boolean wasModelEditor = "modeleditor".equals(currentTool);
|
||||
currentTool = "world";
|
||||
topBar.getChildren().set(1, worldToolBar);
|
||||
if (routineJmeSlot != null) {
|
||||
routineJmeSlot.getChildren().remove(worldViewport);
|
||||
routineJmeSlot = null;
|
||||
}
|
||||
setCenterView(worldViewport);
|
||||
root.setRight(toolPanel);
|
||||
if (wasObjEditor && (input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||
@@ -1144,6 +1155,7 @@ public class EditorApp extends Application {
|
||||
locationZoneBtn = new ToggleButton("📍 Locations");
|
||||
playToolBtn = new ToggleButton("🎮 Spielen");
|
||||
voxelBtn = new ToggleButton("⬡ Voxel");
|
||||
stoneBtn = new ToggleButton("🪨 Steine");
|
||||
camOrbitBtn = new ToggleButton("⊙ Orbit");
|
||||
camFreeBtn = new ToggleButton("✈ FreeFly");
|
||||
baseBtn.setStyle("-fx-font-weight:bold;");
|
||||
@@ -1161,6 +1173,7 @@ public class EditorApp extends Application {
|
||||
locationZoneBtn.setStyle("-fx-font-weight:bold;");
|
||||
playToolBtn.setStyle("-fx-font-weight:bold;");
|
||||
voxelBtn.setStyle("-fx-font-weight:bold;");
|
||||
stoneBtn.setStyle("-fx-font-weight:bold;");
|
||||
camOrbitBtn.setStyle("-fx-font-weight:bold;");
|
||||
camFreeBtn.setStyle("-fx-font-weight:bold;");
|
||||
|
||||
@@ -1180,6 +1193,7 @@ public class EditorApp extends Application {
|
||||
locationZoneBtn.setToggleGroup(layerGroup);
|
||||
playToolBtn.setToggleGroup(layerGroup);
|
||||
voxelBtn.setToggleGroup(layerGroup);
|
||||
stoneBtn.setToggleGroup(layerGroup);
|
||||
baseBtn.setSelected(true);
|
||||
|
||||
baseBtn.setOnAction(e -> {
|
||||
@@ -1254,6 +1268,12 @@ public class EditorApp extends Application {
|
||||
root.setRight(toolPanel);
|
||||
showToolParameters(toolPanel, input.activeTool);
|
||||
});
|
||||
stoneBtn.setOnAction(e -> {
|
||||
input.activeLayer = SharedInput.LAYER_STONE;
|
||||
input.activeTool = input.stoneTool;
|
||||
root.setRight(toolPanel);
|
||||
showToolParameters(toolPanel, input.activeTool);
|
||||
});
|
||||
|
||||
ToggleGroup camModeGroup = new ToggleGroup();
|
||||
camOrbitBtn.setToggleGroup(camModeGroup);
|
||||
@@ -1279,6 +1299,7 @@ public class EditorApp extends Application {
|
||||
new Separator(Orientation.VERTICAL), soundAreaBtn, areaBtn, locationZoneBtn,
|
||||
new Separator(Orientation.VERTICAL), playToolBtn,
|
||||
new Separator(Orientation.VERTICAL), voxelBtn,
|
||||
new Separator(Orientation.VERTICAL), stoneBtn,
|
||||
new Separator(Orientation.VERTICAL), camOrbitBtn, camFreeBtn, camResetBtn,
|
||||
new Separator(Orientation.VERTICAL), hint);
|
||||
|
||||
@@ -1944,8 +1965,9 @@ public class EditorApp extends Application {
|
||||
inner.getChildren().add(treeFolderCB);
|
||||
inner.getChildren().add(refreshFolderBtn);
|
||||
inner.getChildren().add(randomTreeStatusLabel);
|
||||
inner.getChildren().add(styledHint("Linksklick → zufälligen Baum platzieren"));
|
||||
inner.getChildren().add(styledHint("Rechtsklick → neuer Zufallsbaum + Rotation"));
|
||||
inner.getChildren().add(styledHint("Doppelklick auf Ordner links → Ordner aktivieren"));
|
||||
inner.getChildren().add(styledHint("Linksklick → zufälliges Modell platzieren"));
|
||||
inner.getChildren().add(styledHint("Rechtsklick → neues Zufallsmodell + Rotation"));
|
||||
|
||||
// ── Modell aus Asset-Baum ─────────────────────────────────────────────
|
||||
inner.getChildren().addAll(sectionTitle("Modell"), new Separator());
|
||||
@@ -2140,11 +2162,16 @@ public class EditorApp extends Application {
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// Aktuell gesetzten Pfad in die Liste aufnehmen, auch wenn er außerhalb von trees/ liegt
|
||||
String cur = input.treeFolderPath;
|
||||
if (cur != null && !cur.isEmpty() && !items.contains(cur)) {
|
||||
items.add(cur);
|
||||
}
|
||||
|
||||
treeFolderCB.getItems().setAll(items);
|
||||
if (prev != null && items.contains(prev)) {
|
||||
treeFolderCB.setValue(prev);
|
||||
} else {
|
||||
String cur = input.treeFolderPath;
|
||||
treeFolderCB.setValue(cur != null && items.contains(cur) ? cur : "(keiner)");
|
||||
}
|
||||
}
|
||||
@@ -3965,7 +3992,26 @@ public class EditorApp extends Application {
|
||||
tree.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() != 2) return;
|
||||
TreeItem<String> sel = tree.getSelectionModel().getSelectedItem();
|
||||
if (sel == null || isAssetFolder(sel)) return;
|
||||
if (sel == null) return;
|
||||
|
||||
// Ordner-Doppelklick im Models-Baum → Zufalls-Ordner-Platzierung aktivieren
|
||||
if (isAssetFolder(sel)) {
|
||||
TreeItem<String> folderCat = getCategoryRoot(sel);
|
||||
if (folderCat == modelsNode) {
|
||||
Path folderPath = itemPaths.get(sel);
|
||||
if (folderPath != null && java.nio.file.Files.isDirectory(folderPath)) {
|
||||
String relFolder = ASSET_ROOT.relativize(folderPath).toString().replace('\\', '/');
|
||||
input.treeFolderPath = relFolder;
|
||||
input.pendingModelPath = null;
|
||||
input.activeLayer = SharedInput.LAYER_OBJECTS;
|
||||
if (objPlaceBtn != null) objPlaceBtn.setSelected(true);
|
||||
root.setRight(buildObjectPlacePanel());
|
||||
if (multiPlaceCB != null) multiPlaceCB.setSelected(true);
|
||||
setStatus("Ordner: " + relFolder + " | Linksklick → zufälliges Modell | Rechtsklick → neu würfeln");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
TreeItem<String> cat = getCategoryRoot(sel);
|
||||
Path p = itemPaths.get(sel);
|
||||
@@ -4385,6 +4431,24 @@ public class EditorApp extends Application {
|
||||
|| item == audioNode || item == animationsNode || item == itemsNode);
|
||||
Path dir = itemPaths.get(item);
|
||||
|
||||
// Ordner unter Models: Zufällig-Platzieren-Eintrag
|
||||
if (getCategoryRoot(item) == modelsNode && dir != null
|
||||
&& java.nio.file.Files.isDirectory(dir)) {
|
||||
String relFolder = ASSET_ROOT.relativize(dir).toString().replace('\\', '/');
|
||||
MenuItem placeRandom = new MenuItem("🎲 Zufällig platzieren");
|
||||
placeRandom.setOnAction(ev -> {
|
||||
input.treeFolderPath = relFolder;
|
||||
input.pendingModelPath = null;
|
||||
input.activeLayer = SharedInput.LAYER_OBJECTS;
|
||||
if (objPlaceBtn != null) objPlaceBtn.setSelected(true);
|
||||
openAssetOverlay();
|
||||
root.setRight(buildObjectPlacePanel());
|
||||
if (multiPlaceCB != null) multiPlaceCB.setSelected(true);
|
||||
setStatus("Ordner: " + relFolder + " | Linksklick → zufälliges Modell | Rechtsklick → neu würfeln");
|
||||
});
|
||||
ctx.getItems().addAll(new SeparatorMenuItem(), placeRandom);
|
||||
}
|
||||
|
||||
if (item == animationsNode) {
|
||||
MenuItem importAnim = new MenuItem("⊕ Animation importieren…");
|
||||
importAnim.setOnAction(ev -> handleAnimationImport(ctx.getOwnerWindow()));
|
||||
@@ -7031,6 +7095,20 @@ public class EditorApp extends Application {
|
||||
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, e.isShiftDown()));
|
||||
objDragPrevX = e.getX(); objDragPrevY = e.getY();
|
||||
objDragging = true;
|
||||
} else if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
|
||||
input.objectClickQueue.offer(
|
||||
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), true, false));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR) {
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
input.routinePointClickQueue.offer(
|
||||
new SharedInput.RoutinePointClick((float) e.getX(), (float) e.getY()));
|
||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||
input.routineYawDragQueue.offer(
|
||||
new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), false));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -7057,7 +7135,14 @@ public class EditorApp extends Application {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bothDown) {
|
||||
if (input.activeLayer == SharedInput.LAYER_STONE
|
||||
&& input.stoneTool.brushRadius.getValue() == 1.0) {
|
||||
// Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer
|
||||
if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
|
||||
input.stoneEditQueue.offer(
|
||||
new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), -1));
|
||||
}
|
||||
} else if (bothDown) {
|
||||
stopEditTimer();
|
||||
} else if (e.getButton() == MouseButton.PRIMARY) {
|
||||
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
|
||||
@@ -7088,6 +7173,12 @@ public class EditorApp extends Application {
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR && e.isSecondaryButtonDown()) {
|
||||
input.routineYawDragQueue.offer(
|
||||
new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), false));
|
||||
return;
|
||||
}
|
||||
|
||||
// Play-Tool EDIT: Drag-Events an JME3 weiterleiten
|
||||
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|
||||
&& input.playToolMode == SharedInput.PlayToolMode.EDIT
|
||||
@@ -7108,6 +7199,10 @@ public class EditorApp extends Application {
|
||||
viewport.setOnMouseReleased(e -> {
|
||||
objDragging = false;
|
||||
if (!isObjectMode()) stopEditTimer();
|
||||
if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR) {
|
||||
input.routineYawDragQueue.offer(
|
||||
new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), true));
|
||||
}
|
||||
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
|
||||
input.playToolMouseUp = true;
|
||||
}
|
||||
@@ -7181,6 +7276,8 @@ public class EditorApp extends Application {
|
||||
}
|
||||
case SharedInput.LAYER_VOXEL ->
|
||||
input.voxelEditQueue.offer(new SharedInput.VoxelEdit((float) x, (float) y, action));
|
||||
case SharedInput.LAYER_STONE ->
|
||||
input.stoneEditQueue.offer(new SharedInput.StoneEdit((float) x, (float) y, action));
|
||||
case SharedInput.LAYER_MODEL_INTERACTABLE -> {
|
||||
if (action > 0)
|
||||
input.modelInteractableClickQueue.offer(
|
||||
@@ -9536,7 +9633,29 @@ public class EditorApp extends Application {
|
||||
topBar.getChildren().set(1, buildCharacterEditorToolBar());
|
||||
dialogEditorView = new de.blight.editor.ui.DialogEditorView();
|
||||
dialogEditorView.setDisable(true);
|
||||
root.setCenter(dialogEditorView);
|
||||
routineEditorView = new de.blight.editor.ui.RoutineEditorView(input, ASSET_ROOT.resolve("character"));
|
||||
routineEditorView.setDisable(true);
|
||||
|
||||
// worldViewport in den Tagesabläufe-Tab verschieben; centerStack bekommt Platzhalter
|
||||
routineJmeSlot = new StackPane();
|
||||
routineJmeSlot.setMinWidth(0);
|
||||
routineJmeSlot.getChildren().add(worldViewport); // JavaFX entfernt worldViewport aus centerStack
|
||||
centerStack.getChildren().add(new StackPane()); // Platzhalter, damit setCenterView(set(0,...)) später klappt
|
||||
|
||||
javafx.scene.control.SplitPane routineSplit =
|
||||
new javafx.scene.control.SplitPane(routineEditorView, routineJmeSlot);
|
||||
routineSplit.setDividerPositions(0.2);
|
||||
routineSplit.widthProperty().addListener((obs, ov, nv) -> {
|
||||
if (nv.doubleValue() > 0) routineSplit.setDividerPositions(0.2);
|
||||
});
|
||||
|
||||
javafx.scene.control.TabPane centerTabs = new javafx.scene.control.TabPane();
|
||||
centerTabs.setTabClosingPolicy(javafx.scene.control.TabPane.TabClosingPolicy.UNAVAILABLE);
|
||||
centerTabs.getTabs().addAll(
|
||||
new javafx.scene.control.Tab("Dialog", dialogEditorView),
|
||||
new javafx.scene.control.Tab("Tagesabläufe", routineSplit)
|
||||
);
|
||||
root.setCenter(centerTabs);
|
||||
root.setRight(buildCharacterEditorPanel());
|
||||
}
|
||||
|
||||
@@ -9963,6 +10082,7 @@ public class EditorApp extends Application {
|
||||
updateCharActionCombosFromSet();
|
||||
if (charEditContainer != null) charEditContainer.setDisable(false);
|
||||
if (dialogEditorView != null) { dialogEditorView.setDisable(false); dialogEditorView.clear(); }
|
||||
if (routineEditorView != null) { routineEditorView.setDisable(false); routineEditorView.loadNpc(new de.blight.common.model.NPC()); }
|
||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Neuer Charakter");
|
||||
}
|
||||
|
||||
@@ -10019,6 +10139,11 @@ public class EditorApp extends Application {
|
||||
if (c instanceof de.blight.common.model.NPC npc) dialogEditorView.loadNpc(npc);
|
||||
else dialogEditorView.clear();
|
||||
}
|
||||
if (routineEditorView != null) {
|
||||
routineEditorView.setDisable(false);
|
||||
if (c instanceof de.blight.common.model.NPC npc) routineEditorView.loadNpc(npc);
|
||||
else routineEditorView.loadNpc(new de.blight.common.model.NPC());
|
||||
}
|
||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id);
|
||||
charDirty = true;
|
||||
} catch (Exception e) {
|
||||
@@ -10050,7 +10175,8 @@ public class EditorApp extends Application {
|
||||
c.setModelPath(charModelCombo != null ? charModelCombo.getValue() : null);
|
||||
c.setAnimSetPath(charAnimSetCombo != null ? charAnimSetCombo.getValue() : null);
|
||||
if (c instanceof de.blight.common.model.NPC npc) {
|
||||
if (dialogEditorView != null) dialogEditorView.exportToNpc(npc);
|
||||
if (dialogEditorView != null) dialogEditorView.exportToNpc(npc);
|
||||
if (routineEditorView != null) routineEditorView.exportToNpc(npc);
|
||||
npc.setFraction(charFractionCombo != null ? charFractionCombo.getValue() : null);
|
||||
if (charStatusCombo != null && charStatusCombo.getValue() != null) {
|
||||
try { npc.setStatus(de.blight.common.model.Status.valueOf(charStatusCombo.getValue())); }
|
||||
|
||||
@@ -847,6 +847,26 @@ public class SharedInput {
|
||||
public volatile String routinePickedPoint = null;
|
||||
public volatile boolean routinePickedChanged = false;
|
||||
|
||||
/**
|
||||
* JFX → JME: Marker-Punkte für die aktive Routine.
|
||||
* Jeder Eintrag: float[4] = {x, y, z, yaw}. null = Marker löschen.
|
||||
*/
|
||||
public volatile java.util.List<float[]> pendingRoutineMarkers = null;
|
||||
public volatile boolean routineMarkersChanged = false;
|
||||
|
||||
// ── Routine-Yaw-Drag ─────────────────────────────────────────────────────
|
||||
/** JFX → JME: Drag-Event zum Rotieren des aktiven Markers. released=true = Drag beendet. */
|
||||
public record RoutineYawDrag(float screenX, float screenY, boolean released) {}
|
||||
public final ConcurrentLinkedQueue<RoutineYawDrag> routineYawDragQueue =
|
||||
new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** JFX → JME: Weltposition (x, z) des aktiven Markers, als Drehpunkt für Yaw-Drag. null = kein aktiver Marker. */
|
||||
public volatile float[] routineActiveMarkerPos = null;
|
||||
|
||||
/** JME → JFX: Neue Yaw-Rotation (Radiant) nach Drag. NaN = kein Ergebnis. */
|
||||
public volatile float routineYawResult = Float.NaN;
|
||||
public volatile boolean routineYawChanged = false;
|
||||
|
||||
// ── Wegnetz-Editor ────────────────────────────────────────────────────────
|
||||
/** activeLayer==26 → Wegnetz bearbeiten */
|
||||
public static final int LAYER_PATH_NETWORK = 26;
|
||||
|
||||
@@ -733,11 +733,25 @@ public class EzTreeState extends BaseAppState {
|
||||
try {
|
||||
Path baseDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(subPath);
|
||||
Files.createDirectories(baseDir);
|
||||
File out = baseDir.resolve(fileName + ".j3o").toFile();
|
||||
Path outPath = baseDir.resolve(fileName + ".j3o");
|
||||
// Controls vor Export entfernen (nicht serialisierbar über BinaryExporter)
|
||||
while (lodRoot.getNumControls() > 0) lodRoot.removeControl(lodRoot.getControl(0));
|
||||
BinaryExporter.getInstance().save(lodRoot, out);
|
||||
log.info("[EZ-Tree] Gespeichert: {}", out.getAbsolutePath());
|
||||
BinaryExporter.getInstance().save(lodRoot, outPath.toFile());
|
||||
|
||||
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||
fileName, "trees", "",
|
||||
1f, 1f, 1f, true,
|
||||
0f, 0f,
|
||||
true, // solid
|
||||
true, true,
|
||||
1f, 1f,
|
||||
"", "", 30f, 80f, 120f,
|
||||
java.util.List.of(), java.util.List.of(),
|
||||
de.blight.common.model.InteractableType.NONE,
|
||||
0f, 0.5f, 0f, 0f, "bark");
|
||||
de.blight.common.ModelMetaIO.save(meta, outPath);
|
||||
|
||||
log.info("[EZ-Tree] Gespeichert: {}", outPath.toAbsolutePath());
|
||||
input.treeGenStatusMsg = "Gespeichert: Models/trees/" + subPath + "/" + fileName + ".j3o";
|
||||
input.refreshAssets = true;
|
||||
input.refreshTreeFolders = true;
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.jme3.material.Material;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.shape.Box;
|
||||
import com.jme3.scene.shape.Sphere;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.editor.SharedInput;
|
||||
@@ -19,18 +20,24 @@ import java.util.List;
|
||||
/**
|
||||
* JME-AppState für den Tagesablauf-Editor.
|
||||
* Verarbeitet Terrain-Klicks für die Punktauswahl und zeigt
|
||||
* Waypoint-Marker in der Szene an.
|
||||
* Waypoint-Marker (Kugel + Richtungspfeil) in der Szene an.
|
||||
* Rechts-Drag rotiert den aktiven Marker.
|
||||
*/
|
||||
public class RoutineMapState extends BaseAppState {
|
||||
|
||||
private static final float ARROW_LEN = 1.5f;
|
||||
private static final float ARROW_SHAFT_R = 0.04f;
|
||||
private static final float ARROW_TIP_HALF = 0.14f;
|
||||
|
||||
private final SharedInput input;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
private final List<Geometry> waypointMarkers = new ArrayList<>();
|
||||
private Material markerMat;
|
||||
private final List<Node> markerNodes = new ArrayList<>();
|
||||
private Material sphereMat;
|
||||
private Material arrowMat;
|
||||
|
||||
public RoutineMapState(SharedInput input) {
|
||||
this.input = input;
|
||||
@@ -43,8 +50,11 @@ public class RoutineMapState extends BaseAppState {
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
|
||||
markerMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
markerMat.setColor("Color", new ColorRGBA(1f, 0.5f, 0f, 1f));
|
||||
sphereMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
sphereMat.setColor("Color", new ColorRGBA(1f, 0.55f, 0f, 1f));
|
||||
|
||||
arrowMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
arrowMat.setColor("Color", new ColorRGBA(1f, 1f, 0.2f, 1f));
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application app) { clearMarkers(); }
|
||||
@@ -59,6 +69,21 @@ public class RoutineMapState extends BaseAppState {
|
||||
while ((click = input.routinePointClickQueue.poll()) != null) {
|
||||
handleClick(click);
|
||||
}
|
||||
|
||||
SharedInput.RoutineYawDrag drag;
|
||||
while ((drag = input.routineYawDragQueue.poll()) != null) {
|
||||
handleYawDrag(drag);
|
||||
}
|
||||
|
||||
if (input.routineMarkersChanged) {
|
||||
input.routineMarkersChanged = false;
|
||||
List<float[]> pts = input.pendingRoutineMarkers;
|
||||
if (pts != null) {
|
||||
showMarkers(pts);
|
||||
} else {
|
||||
clearMarkers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClick(SharedInput.RoutinePointClick click) {
|
||||
@@ -80,27 +105,97 @@ public class RoutineMapState extends BaseAppState {
|
||||
input.routinePickedChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert die Waypoint-Marker in der Szene.
|
||||
* Wird von JavaFX-Thread via SharedInput-Koordinaten nicht direkt aufgerufen;
|
||||
* stattdessen liest die View die Koordinaten und ruft diese Methode
|
||||
* über Platform.runLater nicht auf – die Marker werden im JME-Thread platziert.
|
||||
*
|
||||
* Format: Liste von "x|y|z" Strings.
|
||||
*/
|
||||
public void showMarkers(List<float[]> points) {
|
||||
clearMarkers();
|
||||
for (float[] p : points) {
|
||||
Geometry g = new Geometry("routine_marker", new Sphere(8, 8, 0.6f));
|
||||
g.setMaterial(markerMat);
|
||||
g.setLocalTranslation(p[0], p[1] + 0.6f, p[2]);
|
||||
rootNode.attachChild(g);
|
||||
waypointMarkers.add(g);
|
||||
private void handleYawDrag(SharedInput.RoutineYawDrag drag) {
|
||||
float[] anchor = input.routineActiveMarkerPos;
|
||||
if (anchor == null || terrain == null) return;
|
||||
|
||||
float jmeX = drag.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
|
||||
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
|
||||
Vector3f hit = hits.getClosestCollision().getContactPoint();
|
||||
float dx = hit.x - anchor[0];
|
||||
float dz = hit.z - anchor[2];
|
||||
float yaw = FastMath.atan2(dx, dz);
|
||||
|
||||
Node nearest = findNearestMarker(anchor[0], anchor[2]);
|
||||
if (nearest != null) {
|
||||
nearest.setLocalRotation(new Quaternion().fromAngleAxis(yaw, Vector3f.UNIT_Y));
|
||||
}
|
||||
|
||||
if (drag.released()) {
|
||||
input.routineYawResult = yaw;
|
||||
input.routineYawChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private Node findNearestMarker(float x, float z) {
|
||||
Node best = null;
|
||||
float bestDist = Float.MAX_VALUE;
|
||||
for (Node mn : markerNodes) {
|
||||
Vector3f pos = mn.getLocalTranslation();
|
||||
float dx = pos.x - x;
|
||||
float dz = pos.z - z;
|
||||
float d = dx * dx + dz * dz;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
best = mn;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
public void showMarkers(List<float[]> points) {
|
||||
clearMarkers();
|
||||
for (float[] p : points) {
|
||||
float x = p[0];
|
||||
float y = p[1];
|
||||
float z = p[2];
|
||||
float yaw = p.length > 3 ? p[3] : 0f;
|
||||
Node mn = buildMarkerNode(x, y, z, yaw);
|
||||
rootNode.attachChild(mn);
|
||||
markerNodes.add(mn);
|
||||
}
|
||||
}
|
||||
|
||||
private Node buildMarkerNode(float x, float y, float z, float yaw) {
|
||||
Node mn = new Node("routine_marker");
|
||||
|
||||
// Sphere
|
||||
Geometry sphere = new Geometry("sphere", new Sphere(8, 8, 0.4f));
|
||||
sphere.setMaterial(sphereMat);
|
||||
sphere.setLocalTranslation(0, 0.4f, 0);
|
||||
mn.attachChild(sphere);
|
||||
|
||||
// Arrow shaft along +Z: Box centered at (0, 0.5, halfLen)
|
||||
float halfLen = ARROW_LEN / 2f;
|
||||
Geometry shaft = new Geometry("shaft", new Box(ARROW_SHAFT_R, ARROW_SHAFT_R, halfLen));
|
||||
shaft.setMaterial(arrowMat);
|
||||
shaft.setLocalTranslation(0, 0.5f, halfLen);
|
||||
mn.attachChild(shaft);
|
||||
|
||||
// Arrowhead at tip
|
||||
Geometry tip = new Geometry("tip", new Box(ARROW_TIP_HALF, ARROW_TIP_HALF, ARROW_TIP_HALF));
|
||||
tip.setMaterial(arrowMat);
|
||||
tip.setLocalTranslation(0, 0.5f, ARROW_LEN + ARROW_TIP_HALF);
|
||||
mn.attachChild(tip);
|
||||
|
||||
mn.setLocalRotation(new Quaternion().fromAngleAxis(yaw, Vector3f.UNIT_Y));
|
||||
mn.setLocalTranslation(x, y, z);
|
||||
return mn;
|
||||
}
|
||||
|
||||
public void clearMarkers() {
|
||||
for (Geometry g : waypointMarkers) rootNode.detachChild(g);
|
||||
waypointMarkers.clear();
|
||||
for (Node mn : markerNodes) {
|
||||
rootNode.detachChild(mn);
|
||||
}
|
||||
markerNodes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,8 @@ public class StoneEditorState extends BaseAppState {
|
||||
private final Material[] slotMat = new Material[PlacedStoneIO.SLOT_COUNT];
|
||||
private Material defaultMat;
|
||||
private Geometry brushIndicator;
|
||||
private Geometry previewGeom;
|
||||
private PlacedStone pendingStone;
|
||||
|
||||
private final Random random = new Random();
|
||||
private boolean modified = false;
|
||||
@@ -94,6 +96,9 @@ public class StoneEditorState extends BaseAppState {
|
||||
|
||||
brushIndicator = buildBrushIndicator();
|
||||
rootNode.attachChild(brushIndicator);
|
||||
|
||||
previewGeom = buildPreviewGeom();
|
||||
rootNode.attachChild(previewGeom);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,6 +106,7 @@ public class StoneEditorState extends BaseAppState {
|
||||
saveIfModified();
|
||||
rootNode.detachChild(stoneRoot);
|
||||
rootNode.detachChild(brushIndicator);
|
||||
rootNode.detachChild(previewGeom);
|
||||
}
|
||||
|
||||
@Override protected void onEnable() { stoneRoot.setCullHint(Spatial.CullHint.Inherit); }
|
||||
@@ -117,6 +123,7 @@ public class StoneEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
updateBrushIndicator();
|
||||
updatePreviewStone();
|
||||
processEdits();
|
||||
processTerrainUpdates();
|
||||
rebuildDirtyChunks();
|
||||
@@ -146,11 +153,81 @@ public class StoneEditorState extends BaseAppState {
|
||||
Vector3f hit = raycastTerrain(jx, jy);
|
||||
if (hit == null) continue;
|
||||
|
||||
if (edit.action() > 0) addStonesAt(hit.x, hit.z);
|
||||
else removeStonesAt(hit.x, hit.z);
|
||||
if (isSingleStoneMode()) {
|
||||
if (edit.action() < 0) { // Rechtsklick: platzierten Stein + neuen Preview
|
||||
placePendingStoneAt(hit.x, hit.z);
|
||||
generatePendingStone();
|
||||
}
|
||||
// Linksklick ignoriert in Single-Stone-Modus
|
||||
} else {
|
||||
if (edit.action() > 0) addStonesAt(hit.x, hit.z);
|
||||
else removeStonesAt(hit.x, hit.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSingleStoneMode() {
|
||||
return input.stoneTool.brushRadius.getValue() == 1.0;
|
||||
}
|
||||
|
||||
private void generatePendingStone() {
|
||||
double minR = input.stoneTool.minSize.getValue() / 2.0;
|
||||
double maxR = input.stoneTool.maxSize.getValue() / 2.0;
|
||||
if (maxR < minR) { maxR = minR; }
|
||||
float radius = (float)(minR + random.nextDouble() * (maxR - minR));
|
||||
float rotY = random.nextFloat() * 360f;
|
||||
float sinkFrac = 0.2f + random.nextFloat() * 0.3f;
|
||||
|
||||
String[] paths = input.stoneTool.texturePaths;
|
||||
int activeSlots = 0;
|
||||
for (String p : paths) {
|
||||
if (p != null && !p.isEmpty()) { activeSlots++; }
|
||||
}
|
||||
if (activeSlots == 0) { activeSlots = 1; }
|
||||
int slot = random.nextInt(activeSlots);
|
||||
int seed = random.nextInt();
|
||||
|
||||
pendingStone = new PlacedStone(0f, 0f, radius, rotY, slot, sinkFrac, seed);
|
||||
|
||||
Mesh mesh = buildStoneMesh(radius, seed, 2);
|
||||
previewGeom.setMesh(mesh);
|
||||
previewGeom.setLocalRotation(new Quaternion().fromAngles(0f, rotY * FastMath.DEG_TO_RAD, 0f));
|
||||
}
|
||||
|
||||
private void placePendingStoneAt(float wx, float wz) {
|
||||
if (terrain == null || pendingStone == null) { return; }
|
||||
float th = terrain.getHeight(new Vector2f(wx, wz));
|
||||
if (!Float.isFinite(th)) { return; }
|
||||
PlacedStone stone = new PlacedStone(wx, wz, pendingStone.radius(), pendingStone.rotY(),
|
||||
pendingStone.textureSlot(), pendingStone.sinkFraction(), pendingStone.noiseSeed());
|
||||
int ci = chunkIndex(wx, wz);
|
||||
if (ci >= 0) {
|
||||
chunkStones[ci].add(stone);
|
||||
dirtyChunks[ci] = true;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePreviewStone() {
|
||||
if (input.activeLayer != SharedInput.LAYER_STONE || !isSingleStoneMode()
|
||||
|| input.mouseScreenX < 0) {
|
||||
previewGeom.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
if (pendingStone == null) { generatePendingStone(); }
|
||||
|
||||
float jx = input.mouseScreenX * (float) input.viewportScaleX;
|
||||
float jy = cam.getHeight() - input.mouseScreenY * (float) input.viewportScaleY;
|
||||
Vector3f hit = raycastTerrain(jx, jy);
|
||||
if (hit == null) {
|
||||
previewGeom.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
float yCenter = hit.y + pendingStone.radius() * (1f - 2f * pendingStone.sinkFraction());
|
||||
previewGeom.setLocalTranslation(hit.x, yCenter, hit.z);
|
||||
previewGeom.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
private void addStonesAt(float wx, float wz) {
|
||||
if (terrain == null) return;
|
||||
double brushR = input.stoneTool.brushRadius.getValue();
|
||||
@@ -462,7 +539,8 @@ public class StoneEditorState extends BaseAppState {
|
||||
// ── Brush-Indicator ───────────────────────────────────────────────────────
|
||||
|
||||
private void updateBrushIndicator() {
|
||||
if (input.activeLayer != SharedInput.LAYER_STONE || input.mouseScreenX < 0) {
|
||||
if (input.activeLayer != SharedInput.LAYER_STONE || input.mouseScreenX < 0
|
||||
|| isSingleStoneMode()) {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
}
|
||||
@@ -510,6 +588,19 @@ public class StoneEditorState extends BaseAppState {
|
||||
return geo;
|
||||
}
|
||||
|
||||
private Geometry buildPreviewGeom() {
|
||||
Mesh mesh = buildStoneMesh(0.5f, 0, 2);
|
||||
Geometry g = new Geometry("stonePreview", mesh);
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.75f, 0.82f, 0.95f, 0.5f));
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
g.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
g.setMaterial(mat);
|
||||
g.setCullHint(Spatial.CullHint.Always);
|
||||
return g;
|
||||
}
|
||||
|
||||
// ── Raycast ───────────────────────────────────────────────────────────────
|
||||
|
||||
private Vector3f raycastTerrain(float sx, float sy) {
|
||||
|
||||
@@ -4,6 +4,8 @@ import de.blight.common.model.*;
|
||||
import de.blight.common.model.quests.Quest;
|
||||
import de.blight.common.model.quests.QuestIO;
|
||||
import de.blight.common.model.QuestRef;
|
||||
import de.blight.common.model.trigger.*;
|
||||
import de.blight.common.model.trigger.Trigger;
|
||||
import de.blight.editor.ProjectRoot;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
@@ -32,7 +34,8 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
private final Map<String, DialogOption> allOptions = new LinkedHashMap<>();
|
||||
private final Set<String> rootIds = new LinkedHashSet<>();
|
||||
private String selectedId = null;
|
||||
private String selectedId = null;
|
||||
private String currentNpcId = "";
|
||||
|
||||
// ── Top-bar ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -47,15 +50,15 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
// ── Detail-form fields ────────────────────────────────────────────────────
|
||||
|
||||
private TextField labelField;
|
||||
private Label idLabel;
|
||||
private TextField idField;
|
||||
private Label derivedLabelKey;
|
||||
private Label derivedHeroKey;
|
||||
private Label derivedNpcKey;
|
||||
private CheckBox rootCheck;
|
||||
private Spinner<Integer> chapterSpinner;
|
||||
private ComboBox<String> statusCombo;
|
||||
private TextField questOpenField;
|
||||
private TextField questCompleteField;
|
||||
private TextField textHeroField;
|
||||
private TextField textNpcField;
|
||||
private TextField reqItemIdField;
|
||||
private Spinner<Integer> reqItemCount;
|
||||
private TextField recvItemIdField;
|
||||
@@ -63,9 +66,10 @@ public class DialogEditorView extends BorderPane {
|
||||
private TextField recvQuestField;
|
||||
private TextField fulfillsQuestField;
|
||||
private ListView<String> abortsQuestsView;
|
||||
private CheckBox enablesTradeCheck;
|
||||
private ListView<String> nextOptionsView;
|
||||
private ListView<String> disablesOptionsView;
|
||||
private CheckBox enablesTradeCheck;
|
||||
private ListView<Trigger> triggersView;
|
||||
private ListView<String> nextOptionsView;
|
||||
private ListView<String> disablesOptionsView;
|
||||
|
||||
// ── Graph-mode ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -87,19 +91,16 @@ public class DialogEditorView extends BorderPane {
|
||||
saveCurrentForm();
|
||||
allOptions.clear();
|
||||
rootIds.clear();
|
||||
selectedId = null;
|
||||
if (npc.getCurrentOptions() != null) {
|
||||
for (DialogOption opt : npc.getCurrentOptions()) {
|
||||
collectOptions(opt);
|
||||
if (opt.getId() != null) rootIds.add(opt.getId());
|
||||
}
|
||||
selectedId = null;
|
||||
currentNpcId = npc.getCharacterId() != null ? npc.getCharacterId() : "";
|
||||
if (npc.getDialogOptions() != null) {
|
||||
allOptions.putAll(npc.getDialogOptions());
|
||||
}
|
||||
// Restore orphaned options that were preserved separately
|
||||
if (npc.getEditorOnlyOptions() != null) {
|
||||
for (DialogOption opt : npc.getEditorOnlyOptions()) {
|
||||
collectOptions(opt);
|
||||
}
|
||||
if (npc.getDialogRoots() != null) {
|
||||
rootIds.addAll(npc.getDialogRoots());
|
||||
}
|
||||
// Transiente nextOptions/disablesOptions für die Editor-Formularfelder aufbauen
|
||||
npc.initRuntimeTree(npc.getDialogRoots());
|
||||
normalizeReferences();
|
||||
refreshOptionList();
|
||||
clearDetailForm();
|
||||
@@ -116,32 +117,29 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
public void exportToNpc(NPC npc) {
|
||||
saveCurrentForm();
|
||||
List<DialogOption> roots = new ArrayList<>();
|
||||
for (String id : rootIds) {
|
||||
DialogOption opt = allOptions.get(id);
|
||||
if (opt != null) roots.add(opt);
|
||||
}
|
||||
npc.setCurrentOptions(roots.isEmpty() ? null : roots);
|
||||
|
||||
// Collect orphaned options (not reachable from any root) and preserve them
|
||||
Set<String> reachable = new HashSet<>();
|
||||
for (DialogOption root : roots) collectReachable(root, reachable);
|
||||
List<DialogOption> orphans = new ArrayList<>();
|
||||
for (Map.Entry<String, DialogOption> entry : allOptions.entrySet()) {
|
||||
if (!reachable.contains(entry.getKey())) orphans.add(entry.getValue());
|
||||
}
|
||||
npc.setEditorOnlyOptions(orphans.isEmpty() ? null : orphans);
|
||||
}
|
||||
// Flache Options-Map
|
||||
npc.setDialogOptions(new LinkedHashMap<>(allOptions));
|
||||
npc.setDialogRoots(new ArrayList<>(rootIds));
|
||||
|
||||
private void collectReachable(DialogOption opt, Set<String> visited) {
|
||||
if (opt == null || opt.getId() == null || visited.contains(opt.getId())) return;
|
||||
visited.add(opt.getId());
|
||||
if (opt.getNextOptions() != null) {
|
||||
for (DialogOption next : opt.getNextOptions()) collectReachable(next, visited);
|
||||
}
|
||||
if (opt.getDisablesOptions() != null) {
|
||||
for (DialogOption dis : opt.getDisablesOptions()) collectReachable(dis, visited);
|
||||
// Baum aus transient nextOptions/disablesOptions der Options-Objekte ableiten
|
||||
Map<String, List<String>> dialogNext = new LinkedHashMap<>();
|
||||
Map<String, List<String>> dialogDisables = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, DialogOption> e : allOptions.entrySet()) {
|
||||
DialogOption opt = e.getValue();
|
||||
if (opt.getNextOptions() != null && !opt.getNextOptions().isEmpty()) {
|
||||
dialogNext.put(e.getKey(), opt.getNextOptions().stream()
|
||||
.filter(o -> o != null && o.getId() != null)
|
||||
.map(DialogOption::getId).toList());
|
||||
}
|
||||
if (opt.getDisablesOptions() != null && !opt.getDisablesOptions().isEmpty()) {
|
||||
dialogDisables.put(e.getKey(), opt.getDisablesOptions().stream()
|
||||
.filter(o -> o != null && o.getId() != null)
|
||||
.map(DialogOption::getId).toList());
|
||||
}
|
||||
}
|
||||
npc.setDialogNext(dialogNext);
|
||||
npc.setDialogDisables(dialogDisables);
|
||||
}
|
||||
|
||||
// ── Top-bar ────────────────────────────────────────────────────────────────
|
||||
@@ -186,10 +184,7 @@ public class DialogEditorView extends BorderPane {
|
||||
@Override protected void updateItem(String id, boolean empty) {
|
||||
super.updateItem(id, empty);
|
||||
if (empty || id == null) { setText(null); setStyle(""); return; }
|
||||
DialogOption opt = allOptions.get(id);
|
||||
String lbl = (opt != null && opt.getLabel() != null && !opt.getLabel().isBlank())
|
||||
? opt.getLabel() : id.substring(0, Math.min(8, id.length())) + "…";
|
||||
setText((rootIds.contains(id) ? "★ " : " ") + lbl);
|
||||
setText((rootIds.contains(id) ? "★ " : " ") + id);
|
||||
applyStyle();
|
||||
}
|
||||
private void applyStyle() {
|
||||
@@ -253,17 +248,34 @@ public class DialogEditorView extends BorderPane {
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
// Identity
|
||||
labelField = new TextField();
|
||||
labelField.setPromptText("Bezeichnung (nur im Editor)");
|
||||
idLabel = new Label("—");
|
||||
idLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #777;");
|
||||
idField = new TextField();
|
||||
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("—");
|
||||
} else {
|
||||
String base = "dialog." + currentNpcId + "." + id;
|
||||
derivedLabelKey.setText(base + ".description");
|
||||
derivedHeroKey.setText(base + ".textmainchar");
|
||||
derivedNpcKey.setText(base + ".textnpc");
|
||||
}
|
||||
});
|
||||
|
||||
rootCheck = new CheckBox("Root-Option (initial im Dialog sichtbar)");
|
||||
rootCheck.setStyle("-fx-text-fill: #ccc;");
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Option"), new Separator(),
|
||||
row("Bezeichnung:", labelField),
|
||||
row("ID:", idLabel),
|
||||
row("ID:", idField),
|
||||
row("Bezeichnung:", derivedLabelKey),
|
||||
rootCheck, new Separator()
|
||||
);
|
||||
|
||||
@@ -298,16 +310,10 @@ public class DialogEditorView extends BorderPane {
|
||||
new Separator()
|
||||
);
|
||||
|
||||
// Texte
|
||||
textHeroField = new TextField();
|
||||
textHeroField.setPromptText("TextReference-Schlüssel");
|
||||
textNpcField = new TextField();
|
||||
textNpcField.setPromptText("TextReference-Schlüssel");
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Texte"),
|
||||
row("Text Held:", textHeroField),
|
||||
row("Text NPC:", textNpcField),
|
||||
row("Text Held:", derivedHeroKey),
|
||||
row("Text NPC:", derivedNpcKey),
|
||||
row("Audio Held:", placeholder("(wird später implementiert)")),
|
||||
row("Audio NPC:", placeholder("(wird später implementiert)")),
|
||||
new Separator()
|
||||
@@ -371,6 +377,31 @@ public class DialogEditorView extends BorderPane {
|
||||
new Separator()
|
||||
);
|
||||
|
||||
// Trigger
|
||||
triggersView = new ListView<>();
|
||||
triggersView.setPrefHeight(90);
|
||||
triggersView.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
||||
triggersView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Trigger t, boolean empty) {
|
||||
super.updateItem(t, empty);
|
||||
setText(empty || t == null ? null : triggerLabel(t));
|
||||
setStyle("-fx-text-fill: #ccc;");
|
||||
}
|
||||
});
|
||||
Button addTriggerBtn = smallBtn("+");
|
||||
Button delTriggerBtn = smallBtn("−");
|
||||
addTriggerBtn.setOnAction(e -> addTrigger());
|
||||
delTriggerBtn.setOnAction(e -> {
|
||||
Trigger sel = triggersView.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) triggersView.getItems().remove(sel);
|
||||
});
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Trigger (bei Auswahl):"),
|
||||
triggersView,
|
||||
new HBox(4, addTriggerBtn, delTriggerBtn),
|
||||
new Separator()
|
||||
);
|
||||
|
||||
// Verbindungen: nextOptions
|
||||
nextOptionsView = buildRefList();
|
||||
Button addNextBtn = smallBtn("+");
|
||||
@@ -405,10 +436,7 @@ public class DialogEditorView extends BorderPane {
|
||||
@Override protected void updateItem(String id, boolean empty) {
|
||||
super.updateItem(id, empty);
|
||||
if (empty || id == null) { setText(null); return; }
|
||||
DialogOption opt = allOptions.get(id);
|
||||
String lbl = (opt != null && opt.getLabel() != null && !opt.getLabel().isBlank())
|
||||
? opt.getLabel() : id.substring(0, Math.min(8, id.length())) + "…";
|
||||
setText(lbl);
|
||||
setText(id);
|
||||
setStyle("-fx-text-fill: #cccccc;");
|
||||
}
|
||||
});
|
||||
@@ -428,8 +456,8 @@ public class DialogEditorView extends BorderPane {
|
||||
DialogOption opt = allOptions.get(id);
|
||||
if (opt == null) { clearDetailForm(); return; }
|
||||
|
||||
idLabel.setText(id);
|
||||
labelField.setText(opt.getLabel() != null ? opt.getLabel() : "");
|
||||
idField.setText(id);
|
||||
// derived keys are updated via idField listener
|
||||
rootCheck.setSelected(rootIds.contains(id));
|
||||
chapterSpinner.getValueFactory().setValue(opt.getRequiresChapter());
|
||||
|
||||
@@ -441,8 +469,7 @@ public class DialogEditorView extends BorderPane {
|
||||
questCompleteField.setText(opt.getRequiresQuestComplete() != null
|
||||
? safe(opt.getRequiresQuestComplete().getQuestId()) : "");
|
||||
|
||||
textHeroField.setText(opt.getTextHero() != null ? opt.getTextHero().id() : "");
|
||||
textNpcField.setText(opt.getTextNpc() != null ? opt.getTextNpc().id() : "");
|
||||
// text keys are derived from ID and shown via derivedHeroKey / derivedNpcKey labels
|
||||
|
||||
if (opt.getRequiredItem() != null && opt.getRequiredItem().getItem() != null) {
|
||||
reqItemIdField.setText(safe(opt.getRequiredItem().getItem().getItemId()));
|
||||
@@ -473,6 +500,11 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
enablesTradeCheck.setSelected(opt.isEnablesTrade());
|
||||
|
||||
triggersView.getItems().clear();
|
||||
if (opt.getOnChosenTriggers() != null) {
|
||||
triggersView.getItems().setAll(opt.getOnChosenTriggers());
|
||||
}
|
||||
|
||||
nextOptionsView.getItems().clear();
|
||||
if (opt.getNextOptions() != null)
|
||||
opt.getNextOptions().stream()
|
||||
@@ -492,10 +524,34 @@ public class DialogEditorView extends BorderPane {
|
||||
DialogOption opt = allOptions.get(id);
|
||||
if (opt == null) return;
|
||||
|
||||
opt.setLabel(labelField.getText());
|
||||
String newId = idField.getText().trim();
|
||||
if (!newId.isBlank() && !newId.equals(id) && !allOptions.containsKey(newId)) {
|
||||
allOptions.remove(id);
|
||||
opt.setId(newId);
|
||||
allOptions.put(newId, opt);
|
||||
int listIdx = optionListView.getItems().indexOf(id);
|
||||
if (listIdx >= 0) {
|
||||
optionListView.getItems().set(listIdx, newId);
|
||||
}
|
||||
if (rootIds.remove(id)) {
|
||||
rootIds.add(newId);
|
||||
}
|
||||
id = newId;
|
||||
}
|
||||
|
||||
if (rootCheck.isSelected()) rootIds.add(id);
|
||||
else rootIds.remove(id);
|
||||
String currentId = opt.getId();
|
||||
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"));
|
||||
}
|
||||
|
||||
if (rootCheck.isSelected()) {
|
||||
rootIds.add(id);
|
||||
} else {
|
||||
rootIds.remove(id);
|
||||
}
|
||||
|
||||
opt.setRequiresChapter(chapterSpinner.getValue());
|
||||
|
||||
@@ -510,9 +566,6 @@ public class DialogEditorView extends BorderPane {
|
||||
opt.setRequiresQuestOpen(questFromField(questOpenField));
|
||||
opt.setRequiresQuestComplete(questFromField(questCompleteField));
|
||||
|
||||
opt.setTextHero(refFromField(textHeroField));
|
||||
opt.setTextNpc(refFromField(textNpcField));
|
||||
|
||||
opt.setRequiredItem(requiredItemFromFields(reqItemIdField, reqItemCount));
|
||||
opt.setRecievesItem(recievesItemFromFields(recvItemIdField, recvItemCount));
|
||||
|
||||
@@ -526,6 +579,7 @@ public class DialogEditorView extends BorderPane {
|
||||
opt.setAbortsQuests(aborts);
|
||||
|
||||
opt.setEnablesTrade(enablesTradeCheck.isSelected());
|
||||
opt.setOnChosenTriggers(new ArrayList<>(triggersView.getItems()));
|
||||
|
||||
opt.setNextOptions(resolveRefs(nextOptionsView.getItems()));
|
||||
opt.setDisablesOptions(resolveRefs(disablesOptionsView.getItems()));
|
||||
@@ -538,15 +592,15 @@ public class DialogEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
private void clearDetailForm() {
|
||||
if (idLabel != null) idLabel.setText("—");
|
||||
if (labelField != null) labelField.clear();
|
||||
if (idField != null) idField.clear();
|
||||
if (derivedLabelKey != null) derivedLabelKey.setText("—");
|
||||
if (derivedHeroKey != null) derivedHeroKey.setText("—");
|
||||
if (derivedNpcKey != null) derivedNpcKey.setText("—");
|
||||
if (rootCheck != null) rootCheck.setSelected(false);
|
||||
if (chapterSpinner != null) chapterSpinner.getValueFactory().setValue(0);
|
||||
if (statusCombo != null) statusCombo.setValue("— (keine)");
|
||||
if (questOpenField != null) questOpenField.clear();
|
||||
if (questCompleteField != null) questCompleteField.clear();
|
||||
if (textHeroField != null) textHeroField.clear();
|
||||
if (textNpcField != null) textNpcField.clear();
|
||||
if (reqItemIdField != null) reqItemIdField.clear();
|
||||
if (reqItemCount != null) reqItemCount.getValueFactory().setValue(1);
|
||||
if (recvItemIdField != null) recvItemIdField.clear();
|
||||
@@ -555,6 +609,7 @@ public class DialogEditorView extends BorderPane {
|
||||
if (fulfillsQuestField != null) fulfillsQuestField.clear();
|
||||
if (abortsQuestsView != null) abortsQuestsView.getItems().clear();
|
||||
if (enablesTradeCheck != null) enablesTradeCheck.setSelected(false);
|
||||
if (triggersView != null) triggersView.getItems().clear();
|
||||
if (nextOptionsView != null) nextOptionsView.getItems().clear();
|
||||
if (disablesOptionsView != null) disablesOptionsView.getItems().clear();
|
||||
}
|
||||
@@ -571,17 +626,37 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
private void createOption(boolean asRoot) {
|
||||
saveCurrentForm();
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.setTitle("Dialog-Option erstellen");
|
||||
dlg.setHeaderText("ID für die neue Dialog-Option:");
|
||||
dlg.setContentText("ID (z. B. begruessung):");
|
||||
Optional<String> result = dlg.showAndWait();
|
||||
if (result.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String id = result.get().trim();
|
||||
if (id.isBlank()) {
|
||||
new Alert(Alert.AlertType.WARNING, "ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
if (allOptions.containsKey(id)) {
|
||||
new Alert(Alert.AlertType.WARNING, "ID '" + id + "' bereits vorhanden.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
DialogOption opt = new DialogOption();
|
||||
opt.setLabel("Neue Option");
|
||||
allOptions.put(opt.getId(), opt);
|
||||
opt.setId(id);
|
||||
String base = "dialog." + currentNpcId + "." + id;
|
||||
opt.setLabel(new TextReference(base + ".description"));
|
||||
opt.setTextHero(new TextReference(base + ".textmainchar"));
|
||||
opt.setTextNpc(new TextReference(base + ".textnpc"));
|
||||
allOptions.put(id, opt);
|
||||
if (asRoot) {
|
||||
rootIds.add(opt.getId());
|
||||
rootIds.add(id);
|
||||
} else if (selectedId != null) {
|
||||
// Auto-link to currently selected option so no orphans are created
|
||||
nextOptionsView.getItems().add(opt.getId());
|
||||
nextOptionsView.getItems().add(id);
|
||||
}
|
||||
refreshOptionList();
|
||||
optionListView.getSelectionModel().select(opt.getId());
|
||||
optionListView.getSelectionModel().select(id);
|
||||
}
|
||||
|
||||
private void toggleRoot() {
|
||||
@@ -616,10 +691,7 @@ public class DialogEditorView extends BorderPane {
|
||||
@Override protected void updateItem(String id, boolean empty) {
|
||||
super.updateItem(id, empty);
|
||||
if (empty || id == null) { setText(null); return; }
|
||||
DialogOption opt = allOptions.get(id);
|
||||
String lbl = (opt != null && opt.getLabel() != null && !opt.getLabel().isBlank())
|
||||
? opt.getLabel() : id.substring(0, Math.min(8, id.length())) + "…";
|
||||
setText((rootIds.contains(id) ? "★ " : " ") + lbl);
|
||||
setText((rootIds.contains(id) ? "★ " : " ") + id);
|
||||
}
|
||||
});
|
||||
chooser.getItems().addAll(allOptions.keySet());
|
||||
@@ -778,10 +850,8 @@ public class DialogEditorView extends BorderPane {
|
||||
for (Map.Entry<String, double[]> entry : pos.entrySet()) {
|
||||
String id = entry.getKey();
|
||||
double[] p = entry.getValue();
|
||||
DialogOption opt = allOptions.get(id);
|
||||
boolean isRoot = rootIds.contains(id);
|
||||
String lbl = (opt != null && opt.getLabel() != null && !opt.getLabel().isBlank())
|
||||
? opt.getLabel() : id.substring(0, Math.min(8, id.length())) + "…";
|
||||
String lbl = id;
|
||||
|
||||
Rectangle rect = new Rectangle(p[0], p[1], nodeW, nodeH);
|
||||
rect.setArcWidth(8); rect.setArcHeight(8);
|
||||
@@ -899,6 +969,15 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
// ── Widget helpers ────────────────────────────────────────────────────────
|
||||
|
||||
private static Label derivedKeyLabel(String text) {
|
||||
Label l = new Label(text);
|
||||
l.setStyle("-fx-text-fill: #999; -fx-font-family: monospace;"
|
||||
+ " -fx-background-color: #1a1a2a; -fx-padding: 3 6 3 6;"
|
||||
+ " -fx-background-radius: 3;");
|
||||
l.setMaxWidth(Double.MAX_VALUE);
|
||||
return l;
|
||||
}
|
||||
|
||||
private static Label sectionTitle(String text) {
|
||||
Label l = new Label(text);
|
||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
||||
@@ -938,4 +1017,132 @@ public class DialogEditorView extends BorderPane {
|
||||
b.setPrefWidth(28);
|
||||
return b;
|
||||
}
|
||||
|
||||
// ── Trigger-Hilfsmethoden ─────────────────────────────────────────────────
|
||||
|
||||
private static String triggerLabel(Trigger t) {
|
||||
if (t instanceof NpcStatusTrigger n) {
|
||||
return "NPC_STATUS " + n.getNpcId() + " → " + n.getTargetStatus();
|
||||
}
|
||||
if (t instanceof ChangeRoutineTrigger r) {
|
||||
return "CHANGE_ROUTINE " + r.getNpcId() + " → " + r.getRoutineName();
|
||||
}
|
||||
if (t instanceof QuestStartTrigger q) {
|
||||
return "QUEST_START " + (q.getQuest() != null ? q.getQuest().getQuestId() : "?");
|
||||
}
|
||||
if (t instanceof FractionStatusTrigger f) {
|
||||
return "FRACTION_STATUS " + f.getFractionId() + " → " + f.getTargetStatus();
|
||||
}
|
||||
return t.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
private void addTrigger() {
|
||||
// Typ-Auswahl
|
||||
List<String> types = List.of("NPC_STATUS", "CHANGE_ROUTINE", "QUEST_START", "FRACTION_STATUS");
|
||||
ChoiceDialog<String> typeDlg = new ChoiceDialog<>("NPC_STATUS", types);
|
||||
typeDlg.setTitle("Trigger hinzufügen");
|
||||
typeDlg.setHeaderText("Trigger-Typ wählen:");
|
||||
Optional<String> typeResult = typeDlg.showAndWait();
|
||||
if (typeResult.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String type = typeResult.get();
|
||||
Trigger trigger = buildTriggerDialog(type);
|
||||
if (trigger != null) {
|
||||
triggersView.getItems().add(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
private static Trigger buildTriggerDialog(String type) {
|
||||
Dialog<Trigger> dlg = new Dialog<>();
|
||||
dlg.setTitle("Trigger konfigurieren: " + type);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
GridPane grid = new GridPane();
|
||||
grid.setHgap(8);
|
||||
grid.setVgap(8);
|
||||
grid.setPadding(new Insets(12));
|
||||
|
||||
Label chapterLbl = new Label("Min. Kapitel:");
|
||||
Spinner<Integer> chapterSpinner = new Spinner<>(0, 99, 0);
|
||||
chapterSpinner.setPrefWidth(70);
|
||||
grid.add(chapterLbl, 0, 0);
|
||||
grid.add(chapterSpinner, 1, 0);
|
||||
|
||||
TextField npcIdField = new TextField();
|
||||
TextField routineField = new TextField();
|
||||
TextField questIdField = new TextField();
|
||||
TextField fractionField = new TextField();
|
||||
ComboBox<String> statusBox = new ComboBox<>();
|
||||
statusBox.getItems().addAll("FRIENDLY", "NEUTRAL", "ENRAGED", "ENEMY");
|
||||
statusBox.setValue("FRIENDLY");
|
||||
|
||||
switch (type) {
|
||||
case "NPC_STATUS" -> {
|
||||
grid.add(new Label("NPC-ID:"), 0, 1);
|
||||
grid.add(npcIdField, 1, 1);
|
||||
grid.add(new Label("Ziel-Status:"), 0, 2);
|
||||
grid.add(statusBox, 1, 2);
|
||||
}
|
||||
case "CHANGE_ROUTINE" -> {
|
||||
grid.add(new Label("NPC-ID:"), 0, 1);
|
||||
grid.add(npcIdField, 1, 1);
|
||||
grid.add(new Label("Routine-Name:"), 0, 2);
|
||||
grid.add(routineField, 1, 2);
|
||||
}
|
||||
case "QUEST_START" -> {
|
||||
grid.add(new Label("Quest-ID:"), 0, 1);
|
||||
grid.add(questIdField, 1, 1);
|
||||
}
|
||||
case "FRACTION_STATUS" -> {
|
||||
grid.add(new Label("Fraction-ID (UUID):"), 0, 1);
|
||||
grid.add(fractionField, 1, 1);
|
||||
grid.add(new Label("Ziel-Status:"), 0, 2);
|
||||
grid.add(statusBox, 1, 2);
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
|
||||
dlg.getDialogPane().setContent(grid);
|
||||
dlg.setResultConverter(btn -> {
|
||||
if (btn != ButtonType.OK) { return null; }
|
||||
int chapter = chapterSpinner.getValue();
|
||||
return switch (type) {
|
||||
case "NPC_STATUS" -> {
|
||||
NpcStatusTrigger t = new NpcStatusTrigger();
|
||||
t.setRequiresChapter(chapter);
|
||||
t.setNpcId(npcIdField.getText().trim());
|
||||
try { t.setTargetStatus(Status.valueOf(statusBox.getValue())); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
yield t;
|
||||
}
|
||||
case "CHANGE_ROUTINE" -> {
|
||||
ChangeRoutineTrigger t = new ChangeRoutineTrigger();
|
||||
t.setRequiresChapter(chapter);
|
||||
t.setNpcId(npcIdField.getText().trim());
|
||||
t.setRoutineName(routineField.getText().trim());
|
||||
yield t;
|
||||
}
|
||||
case "QUEST_START" -> {
|
||||
QuestStartTrigger t = new QuestStartTrigger();
|
||||
t.setRequiresChapter(chapter);
|
||||
QuestRef ref = new QuestRef();
|
||||
ref.setQuestId(questIdField.getText().trim());
|
||||
t.setQuest(ref);
|
||||
yield t;
|
||||
}
|
||||
case "FRACTION_STATUS" -> {
|
||||
FractionStatusTrigger t = new FractionStatusTrigger();
|
||||
t.setRequiresChapter(chapter);
|
||||
try { t.setFractionId(java.util.UUID.fromString(fractionField.getText().trim())); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
try { t.setTargetStatus(Status.valueOf(statusBox.getValue())); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
yield t;
|
||||
}
|
||||
default -> null;
|
||||
};
|
||||
});
|
||||
return dlg.showAndWait().orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package de.blight.editor.ui;
|
||||
|
||||
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 javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
@@ -16,20 +18,13 @@ import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Lokalisierungs-Editor: verwaltet Sprach-Bundles (de.json, en.json …)
|
||||
* und deren Schlüssel→Text-Einträge.
|
||||
*
|
||||
* Speicherort: {@code ASSET_ROOT/localization/<lang>.json}
|
||||
*/
|
||||
public class LocalizationEditorView extends BorderPane {
|
||||
|
||||
private final Path locDir;
|
||||
|
||||
// Sprach-Auswahl
|
||||
private final ComboBox<String> langCombo = new ComboBox<>();
|
||||
private final ObservableList<String[]> tableData = FXCollections.observableArrayList();
|
||||
private final TableView<String[]> table = new TableView<>(tableData);
|
||||
private final ComboBox<String> langCombo = new ComboBox<>();
|
||||
private final ObservableList<String[]> tableData = FXCollections.observableArrayList();
|
||||
private final TableView<String[]> table = new TableView<>(tableData);
|
||||
|
||||
private TextBundle currentBundle = null;
|
||||
|
||||
@@ -41,7 +36,6 @@ public class LocalizationEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
private void buildUI() {
|
||||
// ── Kopfzeile ─────────────────────────────────────────────────────────
|
||||
Label langLbl = new Label("Sprache:");
|
||||
langLbl.setStyle("-fx-text-fill: #aaa;");
|
||||
langCombo.setPrefWidth(120);
|
||||
@@ -59,20 +53,22 @@ public class LocalizationEditorView extends BorderPane {
|
||||
Button delKeyBtn = new Button("− Entfernen");
|
||||
delKeyBtn.setOnAction(e -> deleteKey());
|
||||
|
||||
Button sortBtn = new Button("A→Z");
|
||||
sortBtn.setOnAction(e -> tableData.sort(Comparator.comparing(r -> r[0])));
|
||||
|
||||
Button saveBtn = new Button("Speichern");
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrent());
|
||||
|
||||
HBox toolbar = new HBox(8, langLbl, langCombo, addLangBtn, delLangBtn,
|
||||
new Separator(javafx.geometry.Orientation.VERTICAL),
|
||||
addKeyBtn, delKeyBtn,
|
||||
addKeyBtn, delKeyBtn, sortBtn,
|
||||
new Separator(javafx.geometry.Orientation.VERTICAL),
|
||||
saveBtn);
|
||||
toolbar.setPadding(new Insets(8, 12, 8, 12));
|
||||
toolbar.setAlignment(Pos.CENTER_LEFT);
|
||||
toolbar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;");
|
||||
|
||||
// ── Tabelle ───────────────────────────────────────────────────────────
|
||||
table.setEditable(true);
|
||||
table.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
@@ -112,18 +108,21 @@ public class LocalizationEditorView extends BorderPane {
|
||||
private void refreshLangList() {
|
||||
String selected = langCombo.getValue();
|
||||
langCombo.getItems().setAll(TextBundleIO.availableLanguages(locDir));
|
||||
if (selected != null && langCombo.getItems().contains(selected))
|
||||
if (selected != null && langCombo.getItems().contains(selected)) {
|
||||
langCombo.setValue(selected);
|
||||
else if (!langCombo.getItems().isEmpty())
|
||||
} else if (!langCombo.getItems().isEmpty()) {
|
||||
langCombo.setValue(langCombo.getItems().get(0));
|
||||
else
|
||||
} else {
|
||||
tableData.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadLanguage(String lang) {
|
||||
if (lang == null) return;
|
||||
if (lang == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
currentBundle = TextBundleIO.load(locDir.resolve(lang + ".json"));
|
||||
currentBundle = TextBundleIO.load(locDir.resolve("messages_" + lang + ".properties"));
|
||||
} catch (IOException e) {
|
||||
currentBundle = new TextBundle(lang);
|
||||
}
|
||||
@@ -139,7 +138,9 @@ public class LocalizationEditorView extends BorderPane {
|
||||
dlg.setHeaderText("Sprach-Code (z. B. de, en, fr):");
|
||||
dlg.showAndWait().ifPresent(lang -> {
|
||||
lang = lang.trim().toLowerCase();
|
||||
if (lang.isBlank()) return;
|
||||
if (lang.isBlank()) {
|
||||
return;
|
||||
}
|
||||
TextBundle bundle = new TextBundle(lang);
|
||||
try {
|
||||
TextBundleIO.save(bundle, locDir);
|
||||
@@ -153,7 +154,9 @@ public class LocalizationEditorView extends BorderPane {
|
||||
|
||||
private void deleteLanguage() {
|
||||
String lang = langCombo.getValue();
|
||||
if (lang == null) return;
|
||||
if (lang == null) {
|
||||
return;
|
||||
}
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
"Sprache '" + lang + "' wirklich löschen?", ButtonType.YES, ButtonType.NO);
|
||||
confirm.showAndWait().ifPresent(bt -> {
|
||||
@@ -173,14 +176,60 @@ public class LocalizationEditorView extends BorderPane {
|
||||
// ── Eintrags-Verwaltung ───────────────────────────────────────────────────
|
||||
|
||||
private void addKey() {
|
||||
tableData.add(new String[]{"neuer.schluessel", ""});
|
||||
if (currentBundle == null) {
|
||||
return;
|
||||
}
|
||||
Set<String> existing = currentBundle.getEntries().keySet();
|
||||
List<String> available = new ArrayList<>(TextKeyStore.getKeys());
|
||||
available.removeAll(existing);
|
||||
|
||||
if (available.isEmpty()) {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.setTitle("Schlüssel hinzufügen");
|
||||
dlg.setHeaderText("Alle bekannten Schlüssel bereits vorhanden.\nNeuen Schlüssel eingeben:");
|
||||
dlg.showAndWait()
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isBlank())
|
||||
.ifPresent(this::insertKey);
|
||||
return;
|
||||
}
|
||||
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Schlüssel wählen");
|
||||
dlg.setHeaderText("Noch nicht übersetzte Schlüssel:");
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
ObservableList<String> items = FXCollections.observableArrayList(available);
|
||||
FilteredList<String> filtered = new FilteredList<>(items, s -> true);
|
||||
ListView<String> listView = new ListView<>(filtered);
|
||||
listView.setPrefHeight(300);
|
||||
listView.getSelectionModel().selectFirst();
|
||||
|
||||
TextField filterField = new TextField();
|
||||
filterField.setPromptText("Filtern…");
|
||||
filterField.textProperty().addListener((obs, o, n) ->
|
||||
filtered.setPredicate(s -> n.isBlank() || s.contains(n)));
|
||||
|
||||
dlg.getDialogPane().setContent(new VBox(6, filterField, listView));
|
||||
dlg.setResultConverter(bt ->
|
||||
bt == ButtonType.OK ? listView.getSelectionModel().getSelectedItem() : null);
|
||||
|
||||
dlg.showAndWait()
|
||||
.filter(k -> k != null && !k.isBlank())
|
||||
.ifPresent(this::insertKey);
|
||||
}
|
||||
|
||||
private void insertKey(String key) {
|
||||
tableData.add(new String[]{key, ""});
|
||||
table.scrollTo(tableData.size() - 1);
|
||||
table.getSelectionModel().select(tableData.size() - 1);
|
||||
}
|
||||
|
||||
private void deleteKey() {
|
||||
String[] sel = table.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) tableData.remove(sel);
|
||||
if (sel != null) {
|
||||
tableData.remove(sel);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCurrent() {
|
||||
@@ -190,7 +239,9 @@ public class LocalizationEditorView extends BorderPane {
|
||||
}
|
||||
Map<String, String> entries = new LinkedHashMap<>();
|
||||
for (String[] row : tableData) {
|
||||
if (!row[0].isBlank()) entries.put(row[0].trim(), row[1]);
|
||||
if (!row[0].isBlank()) {
|
||||
entries.put(row[0].trim(), row[1]);
|
||||
}
|
||||
}
|
||||
currentBundle.setEntries(entries);
|
||||
try {
|
||||
|
||||
@@ -56,15 +56,21 @@ public class RoutineEditorView extends VBox {
|
||||
|
||||
// Dynamische Felder je Aktivitätstyp
|
||||
private Label pointLabel; // zeigt gepickten Punkt
|
||||
private Label yawLabel; // zeigt aktuellen Yaw-Winkel
|
||||
private ComboBox<String> interactableCombo; // UUID-basiert (PlacedItems)
|
||||
private ComboBox<String> npcCombo; // für TALK
|
||||
private ListView<String> waypointList; // für PATROL
|
||||
private RadioButton rbPoint, rbInteractable; // für SIT
|
||||
|
||||
/** Aktueller Yaw-Winkel (Radiant) für den aktiven Standpunkt. */
|
||||
private float activePointYaw = 0f;
|
||||
|
||||
// Geladene Hilfsdaten
|
||||
private final List<PlacedItem> placedItems = new ArrayList<>();
|
||||
private final List<String> npcIds = new ArrayList<>();
|
||||
|
||||
private boolean updatingForm = false;
|
||||
|
||||
// ── Polling-Timer ──────────────────────────────────────────────────────────
|
||||
|
||||
private final AnimationTimer pollTimer = new AnimationTimer() {
|
||||
@@ -87,6 +93,16 @@ public class RoutineEditorView extends VBox {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (input.routineYawChanged) {
|
||||
input.routineYawChanged = false;
|
||||
float yaw = input.routineYawResult;
|
||||
if (!Float.isNaN(yaw)) {
|
||||
activePointYaw = yaw;
|
||||
updateYawLabel();
|
||||
applyActivityToBlock();
|
||||
updateMarkers();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -143,6 +159,7 @@ public class RoutineEditorView extends VBox {
|
||||
int idx = nv.intValue();
|
||||
activeRoutine = (idx >= 0 && idx < routines.size()) ? routines.get(idx) : null;
|
||||
refreshBlockList();
|
||||
updateMarkers();
|
||||
});
|
||||
|
||||
Button addR = new Button("+");
|
||||
@@ -170,7 +187,7 @@ public class RoutineEditorView extends VBox {
|
||||
blockList = new ListView<>();
|
||||
blockList.setPrefHeight(110);
|
||||
blockList.getSelectionModel().selectedIndexProperty().addListener((obs, ov, nv) -> {
|
||||
if (activeRoutine == null) return;
|
||||
if (updatingForm || activeRoutine == null) return;
|
||||
int idx = nv.intValue();
|
||||
activeBlock = (idx >= 0 && idx < activeRoutine.getBlocks().size())
|
||||
? activeRoutine.getBlocks().get(idx) : null;
|
||||
@@ -216,7 +233,7 @@ public class RoutineEditorView extends VBox {
|
||||
typeCombo.getItems().addAll(
|
||||
"Sitzen", "Stehen", "Reden", "Patrullieren", "Arbeiten", "Schlafen");
|
||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
typeCombo.setOnAction(e -> refreshDynamicFields());
|
||||
typeCombo.setOnAction(e -> { if (!updatingForm) refreshDynamicFields(); });
|
||||
|
||||
form.getChildren().addAll(timeRow, typeCombo);
|
||||
return form;
|
||||
@@ -247,19 +264,24 @@ public class RoutineEditorView extends VBox {
|
||||
rbInteractable.setToggleGroup(tg);
|
||||
rbPoint.setSelected(true);
|
||||
|
||||
pointLabel = pointLabel();
|
||||
Button pickBtn = pickButton(xyz -> setPointLabel(pointLabel, xyz));
|
||||
pointLabel = pointLabel();
|
||||
yawLabel = yawLabel();
|
||||
Button pickBtn = pickButton(xyz -> setPointLabel(pointLabel, xyz));
|
||||
|
||||
interactableCombo = itemCombo();
|
||||
|
||||
VBox pointRow = new VBox(2, pointLabel, pickBtn);
|
||||
VBox itemRow = new VBox(2, interactableCombo);
|
||||
VBox pointRow = new VBox(2, pointLabel, pickBtn, yawLabel,
|
||||
styledLabel("Rechts-Drag im Viewport → Richtung drehen", false));
|
||||
VBox itemRow = new VBox(2, interactableCombo);
|
||||
itemRow.setManaged(false); itemRow.setVisible(false);
|
||||
|
||||
tg.selectedToggleProperty().addListener((o, ov, nv) -> {
|
||||
boolean useItem = rbInteractable.isSelected();
|
||||
pointRow.setManaged(!useItem); pointRow.setVisible(!useItem);
|
||||
itemRow.setManaged(useItem); itemRow.setVisible(useItem);
|
||||
if (useItem) {
|
||||
input.routineActiveMarkerPos = null;
|
||||
}
|
||||
applyActivityToBlock();
|
||||
});
|
||||
|
||||
@@ -271,21 +293,34 @@ public class RoutineEditorView extends VBox {
|
||||
rbInteractable.setSelected(true);
|
||||
selectItemCombo(interactableCombo, act.getObjectUuid());
|
||||
} else if (act.getPosition() != null) {
|
||||
setPointLabel(pointLabel, act.getPosition());
|
||||
WorldPoint p = act.getPosition();
|
||||
setPointLabel(pointLabel, p);
|
||||
activePointYaw = p.yaw;
|
||||
updateYawLabel();
|
||||
updateActiveMarkerPos();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void buildStandFields() {
|
||||
pointLabel = pointLabel();
|
||||
yawLabel = yawLabel();
|
||||
Button pickBtn = pickButton(xyz -> setPointLabel(pointLabel, xyz));
|
||||
dynamicBox.getChildren().addAll(styledLabel("Standpunkt:", false), pointLabel, pickBtn);
|
||||
if (activeBlock.getActivity() != null && activeBlock.getActivity().getPosition() != null)
|
||||
setPointLabel(pointLabel, activeBlock.getActivity().getPosition());
|
||||
dynamicBox.getChildren().addAll(
|
||||
styledLabel("Standpunkt:", false), pointLabel, pickBtn,
|
||||
yawLabel, styledLabel("Rechts-Drag im Viewport → Richtung drehen", false));
|
||||
if (activeBlock.getActivity() != null && activeBlock.getActivity().getPosition() != null) {
|
||||
WorldPoint p = activeBlock.getActivity().getPosition();
|
||||
setPointLabel(pointLabel, p);
|
||||
activePointYaw = p.yaw;
|
||||
updateYawLabel();
|
||||
updateActiveMarkerPos();
|
||||
}
|
||||
}
|
||||
|
||||
private void buildTalkFields() {
|
||||
pointLabel = pointLabel();
|
||||
yawLabel = yawLabel();
|
||||
Button pickBtn = pickButton(xyz -> setPointLabel(pointLabel, xyz));
|
||||
npcCombo = new ComboBox<>();
|
||||
npcCombo.getItems().addAll(npcIds);
|
||||
@@ -293,11 +328,18 @@ public class RoutineEditorView extends VBox {
|
||||
npcCombo.setPromptText("NPC wählen…");
|
||||
npcCombo.setOnAction(e -> applyActivityToBlock());
|
||||
|
||||
dynamicBox.getChildren().addAll(styledLabel("Gesprächspunkt:", false), pointLabel, pickBtn,
|
||||
dynamicBox.getChildren().addAll(
|
||||
styledLabel("Gesprächspunkt:", false), pointLabel, pickBtn,
|
||||
yawLabel, styledLabel("Rechts-Drag im Viewport → Richtung drehen", false),
|
||||
styledLabel("Gesprächspartner:", false), npcCombo);
|
||||
if (activeBlock.getActivity() != null) {
|
||||
if (activeBlock.getActivity().getPosition() != null)
|
||||
setPointLabel(pointLabel, activeBlock.getActivity().getPosition());
|
||||
if (activeBlock.getActivity().getPosition() != null) {
|
||||
WorldPoint p = activeBlock.getActivity().getPosition();
|
||||
setPointLabel(pointLabel, p);
|
||||
activePointYaw = p.yaw;
|
||||
updateYawLabel();
|
||||
updateActiveMarkerPos();
|
||||
}
|
||||
if (activeBlock.getActivity().getTalkNpcId() != null)
|
||||
npcCombo.setValue(activeBlock.getActivity().getTalkNpcId());
|
||||
}
|
||||
@@ -315,6 +357,7 @@ public class RoutineEditorView extends VBox {
|
||||
activeBlock.setActivity(RoutineActivity.patrol(new ArrayList<>()));
|
||||
activeBlock.getActivity().getWaypoints().add(wp);
|
||||
refreshWaypointList();
|
||||
updateMarkers();
|
||||
}));
|
||||
|
||||
delWp.setOnAction(e -> {
|
||||
@@ -322,6 +365,7 @@ public class RoutineEditorView extends VBox {
|
||||
List<WorldPoint> wps = activeBlock.getActivity().getWaypoints();
|
||||
wps.remove(wps.size() - 1);
|
||||
refreshWaypointList();
|
||||
updateMarkers();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -352,6 +396,7 @@ public class RoutineEditorView extends VBox {
|
||||
btn.setOnAction(e -> startPick(xyz -> {
|
||||
callback.accept(xyz);
|
||||
applyActivityToBlock();
|
||||
updateMarkers();
|
||||
}));
|
||||
return btn;
|
||||
}
|
||||
@@ -359,10 +404,9 @@ public class RoutineEditorView extends VBox {
|
||||
// ── Block-Daten pflegen ────────────────────────────────────────────────────
|
||||
|
||||
private void applyBlockTimes() {
|
||||
if (activeBlock == null) return;
|
||||
if (activeBlock == null || updatingForm) return;
|
||||
activeBlock.setStartHour(startSpin.getValue());
|
||||
activeBlock.setEndHour(endSpin.getValue());
|
||||
refreshBlockList();
|
||||
refreshCoverage();
|
||||
}
|
||||
|
||||
@@ -376,17 +420,29 @@ public class RoutineEditorView extends VBox {
|
||||
activeBlock.setActivity(RoutineActivity.sitInteractable(uuid, lbl));
|
||||
} else {
|
||||
WorldPoint pt = parsedPoint(pointLabel);
|
||||
if (pt != null) {
|
||||
pt.yaw = activePointYaw;
|
||||
}
|
||||
activeBlock.setActivity(pt != null ? RoutineActivity.sit(pt) : new RoutineActivity());
|
||||
updateActiveMarkerPos();
|
||||
}
|
||||
}
|
||||
case "Stehen" -> {
|
||||
WorldPoint pt = parsedPoint(pointLabel);
|
||||
if (pt != null) {
|
||||
pt.yaw = activePointYaw;
|
||||
}
|
||||
activeBlock.setActivity(pt != null ? RoutineActivity.stand(pt) : new RoutineActivity());
|
||||
updateActiveMarkerPos();
|
||||
}
|
||||
case "Reden" -> {
|
||||
WorldPoint pt = parsedPoint(pointLabel);
|
||||
if (pt != null) {
|
||||
pt.yaw = activePointYaw;
|
||||
}
|
||||
String talkNpc = npcCombo != null ? npcCombo.getValue() : null;
|
||||
activeBlock.setActivity(RoutineActivity.talk(pt, talkNpc));
|
||||
updateActiveMarkerPos();
|
||||
}
|
||||
case "Arbeiten" -> {
|
||||
String uuid = selectedItemUuid(interactableCombo);
|
||||
@@ -400,7 +456,6 @@ public class RoutineEditorView extends VBox {
|
||||
}
|
||||
// PATROL is managed directly in buildPatrolFields
|
||||
}
|
||||
refreshBlockList();
|
||||
}
|
||||
|
||||
// ── Routine-Operationen ────────────────────────────────────────────────────
|
||||
@@ -505,10 +560,19 @@ public class RoutineEditorView extends VBox {
|
||||
dynamicBox.getChildren().clear();
|
||||
blockStatusLabel.setText(hasBlock ? "" : (activeRoutine != null ? "— Block wählen —" : "— Ablauf wählen —"));
|
||||
|
||||
if (!hasBlock) return;
|
||||
if (!hasBlock) {
|
||||
activePointYaw = 0f;
|
||||
yawLabel = null;
|
||||
input.routineActiveMarkerPos = null;
|
||||
return;
|
||||
}
|
||||
updatingForm = true;
|
||||
startSpin.getValueFactory().setValue(activeBlock.getStartHour());
|
||||
endSpin .getValueFactory().setValue(activeBlock.getEndHour());
|
||||
typeCombo.setValue(activityTypeLabel(activeBlock.getActivity()));
|
||||
updatingForm = false;
|
||||
activePointYaw = 0f;
|
||||
yawLabel = null;
|
||||
refreshDynamicFields();
|
||||
}
|
||||
|
||||
@@ -551,12 +615,54 @@ public class RoutineEditorView extends VBox {
|
||||
npc.setRoutines(new ArrayList<>(routines));
|
||||
}
|
||||
|
||||
private void updateMarkers() {
|
||||
if (activeRoutine == null) {
|
||||
input.pendingRoutineMarkers = null;
|
||||
input.routineMarkersChanged = true;
|
||||
return;
|
||||
}
|
||||
List<float[]> pts = new ArrayList<>();
|
||||
for (RoutineBlock b : activeRoutine.getBlocks()) {
|
||||
RoutineActivity act = b.getActivity();
|
||||
if (act == null) continue;
|
||||
if (act.getPosition() != null) {
|
||||
WorldPoint p = act.getPosition();
|
||||
pts.add(new float[]{p.x, p.y, p.z, p.yaw});
|
||||
}
|
||||
if (act.getWaypoints() != null) {
|
||||
for (WorldPoint wp : act.getWaypoints())
|
||||
pts.add(new float[]{wp.x, wp.y, wp.z, wp.yaw});
|
||||
}
|
||||
}
|
||||
input.pendingRoutineMarkers = pts;
|
||||
input.routineMarkersChanged = true;
|
||||
}
|
||||
|
||||
private void updateActiveMarkerPos() {
|
||||
if (activeBlock != null
|
||||
&& activeBlock.getActivity() != null
|
||||
&& activeBlock.getActivity().getPosition() != null) {
|
||||
WorldPoint p = activeBlock.getActivity().getPosition();
|
||||
input.routineActiveMarkerPos = new float[]{p.x, p.y, p.z};
|
||||
} else {
|
||||
input.routineActiveMarkerPos = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateYawLabel() {
|
||||
if (yawLabel == null) return;
|
||||
int deg = (int) Math.round(Math.toDegrees(activePointYaw));
|
||||
yawLabel.setText("Richtung: " + deg + "°");
|
||||
yawLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #fd0;");
|
||||
}
|
||||
|
||||
/** Muss aufgerufen werden, wenn die View geschlossen/versteckt wird. */
|
||||
public void onHide() {
|
||||
if (pendingPick != null) {
|
||||
pendingPick = null;
|
||||
input.activeLayer = savedLayer;
|
||||
}
|
||||
input.routineActiveMarkerPos = null;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ──────────────────────────────────────────────────────────
|
||||
@@ -574,14 +680,20 @@ public class RoutineEditorView extends VBox {
|
||||
return l;
|
||||
}
|
||||
|
||||
private Label yawLabel() {
|
||||
Label l = new Label("Richtung: 0°");
|
||||
l.setStyle("-fx-font-size: 11; -fx-text-fill: #fd0;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private void setPointLabel(Label lbl, float[] xyz) {
|
||||
lbl.setText(String.format("(%.1f, %.1f, %.1f)", xyz[0], xyz[1], xyz[2]));
|
||||
lbl.setText(String.format(java.util.Locale.US, "(%.1f, %.1f, %.1f)", xyz[0], xyz[1], xyz[2]));
|
||||
lbl.setStyle("-fx-font-size: 11; -fx-text-fill: #ddd;");
|
||||
}
|
||||
|
||||
private void setPointLabel(Label lbl, WorldPoint pt) {
|
||||
if (pt == null) return;
|
||||
lbl.setText(String.format("(%.1f, %.1f, %.1f)", pt.x, pt.y, pt.z));
|
||||
lbl.setText(String.format(java.util.Locale.US, "(%.1f, %.1f, %.1f)", pt.x, pt.y, pt.z));
|
||||
lbl.setStyle("-fx-font-size: 11; -fx-text-fill: #ddd;");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user