Character und Dialog System weiter gebaut

This commit is contained in:
2026-07-06 21:06:27 +02:00
parent b05eb1055d
commit 8bcee4491e
40 changed files with 2006 additions and 250 deletions

View File

@@ -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())); }

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -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;");
}