Abschluss des großen Editor Refactorings

This commit is contained in:
2026-09-13 12:20:53 +02:00
parent feef7cab62
commit 6e2c3a2785
45 changed files with 7615 additions and 7258 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -10,22 +10,23 @@ import java.util.function.Supplier;
/**
* Shared-Dependencies-Container: wird einmalig in EditorApp erzeugt und
* an alle {@link ToolController}-Implementierungen weitergegeben.
* an alle Controller-Implementierungen weitergegeben.
*
* Ersetzt das implizite "EditorApp kennt alles" — Controller greifen nur
* über diesen Record auf Infrastruktur zu, nicht auf EditorApp direkt.
*
* @param input Gemeinsame JavaFX ↔ JME3 Datenbasis
* @param jmeApp JME3-Anwendung (für StateManager-Zugriff)
* @param stageSupplier Liefert die primäre Stage (für Dialoge)
* @param setStatus Setzt den Text im Statusbalken
* @param setCenterView Setzt den Center-Node des Haupt-BorderPane
* @param setRightPanel Setzt den Right-Node des Haupt-BorderPane
* @param setMainToolbar Ersetzt die aktive Werkzeug-Toolbar in der TopBar
* @param viewportSupplier Liefert den JME3-Viewport-StackPane
* @param assetRoot Wurzelpfad der blight-assets (für Dateidialoge / IO)
* @param switchToWorld Wechselt zurück zum Welteneditor (für "← Welteneditor"-Buttons)
* @param registry ToolRegistry (damit Controller sich selbst aktivieren können)
* @param input Gemeinsame JavaFX ↔ JME3 Datenbasis
* @param jmeApp JME3-Anwendung (für StateManager-Zugriff)
* @param stageSupplier Liefert die primäre Stage (für Dialoge)
* @param setStatus Setzt den Text im Statusbalken
* @param setCenterView Setzt den Center-Node des Haupt-BorderPane
* @param setRightPanel Setzt den Right-Node des Haupt-BorderPane
* @param setMainToolbar Ersetzt die aktive Werkzeug-Toolbar in der TopBar
* @param viewportSupplier Liefert den JME3-Viewport-StackPane
* @param assetRoot Wurzelpfad der blight-assets (für Dateidialoge / IO)
* @param switchToWorld Wechselt zurück zum Welteneditor
* @param registry ToolRegistry (damit Controller sich selbst aktivieren können)
* @param setLeftPanel Setzt den Left-Node des Haupt-BorderPane
* @param setBottomPanel Setzt den Bottom-Node des Haupt-BorderPane
* @param centerStackSupplier Liefert den persistenten 3D-Viewport-Wrapper
* @param setCurrentTool Setzt das aktive Tool-Label in EditorApp (für Keyboard-Routing)
*/
public record ToolContext(
SharedInput input,
@@ -38,5 +39,9 @@ public record ToolContext(
Supplier<StackPane> viewportSupplier,
Path assetRoot,
Runnable switchToWorld,
ToolRegistry registry
ToolRegistry registry,
Consumer<Node> setLeftPanel,
Consumer<Node> setBottomPanel,
Supplier<StackPane> centerStackSupplier,
Consumer<String> setCurrentTool
) {}

View File

@@ -40,8 +40,8 @@ public class WaterBodyState extends BaseAppState {
private static final float LINE_OFFSET = 0.1f;
private static final float POLE_HEIGHT = 5.0f;
private static final float CAP_SIZE = 1.2f;
private static final float HANDLE_PICK_PX = 24f;
private static final float CAP_SIZE = 2.0f;
private static final float HANDLE_PICK_PX = 32f;
private static final float EDGE_SPLIT_THRESHOLD = 1.5f;
private static final ColorRGBA COLOR_WATER = new ColorRGBA(0.05f, 0.25f, 0.70f, 0.50f);
@@ -53,8 +53,9 @@ public class WaterBodyState extends BaseAppState {
private static final ColorRGBA COLOR_ARROW = new ColorRGBA(1f, 0.85f, 0f, 1f);
private static final ColorRGBA COLOR_POLE_CORNER = new ColorRGBA(0f, 0.9f, 1.0f, 1f);
private static final ColorRGBA COLOR_POLE_EDGE = new ColorRGBA(0f, 0.55f, 0.75f, 1f);
private static final ColorRGBA COLOR_POLE_HOVER = new ColorRGBA(1f, 0.9f, 0f, 1f);
private static final ColorRGBA COLOR_EDGE_SPLIT = new ColorRGBA(0.1f, 1.0f, 0.4f, 1f);
private static final ColorRGBA COLOR_POLE_HOVER = new ColorRGBA(1f, 0.9f, 0f, 1f);
private static final ColorRGBA COLOR_POLE_SELECTED = new ColorRGBA(1f, 0.35f, 0.05f, 1f);
private static final ColorRGBA COLOR_EDGE_SPLIT = new ColorRGBA(0.1f, 1.0f, 0.4f, 1f);
private static final ColorRGBA PILLAR_SUB = new ColorRGBA(0.2f, 0.85f, 1.0f, 1f);
private static final ColorRGBA PILLAR_DRY = new ColorRGBA(1.0f, 0.50f, 0.1f, 1f);
@@ -90,8 +91,9 @@ public class WaterBodyState extends BaseAppState {
private final List<Geometry> handleGeos = new ArrayList<>();
private final List<Material> handleMats = new ArrayList<>();
private final List<Float> handleCapY = new ArrayList<>();
private int hoveredHandle = -1;
private int dragHandle = -1;
private int hoveredHandle = -1;
private int dragHandle = -1;
private int selectedHandle = -1;
private boolean isDragging = false;
private float[] liveXs = new float[0];
private float[] liveYs = new float[0];
@@ -187,7 +189,11 @@ public class WaterBodyState extends BaseAppState {
if (input.deleteWaterRequested) {
input.deleteWaterRequested = false;
if (selectedIdx >= 0) removeBody(selectedIdx);
if (selectedHandle >= 0 && selectedIdx >= 0 && selectedHandle < liveXs.length) {
deleteCornerPoint(selectedIdx, selectedHandle);
} else if (selectedIdx >= 0) {
removeBody(selectedIdx);
}
}
SharedInput.WaterClick click;
@@ -235,6 +241,7 @@ public class WaterBodyState extends BaseAppState {
PlacedWater b = bodies.get(selectedIdx);
int h = findHandleAt(jmeX, jmeY, b.pointsX(), b.pointsZ());
if (h >= 0) {
setSelectedHandle(h);
startDrag(h, b.pointsX(), b.pointsY(), b.pointsZ());
return;
}
@@ -569,10 +576,50 @@ public class WaterBodyState extends BaseAppState {
handleMats.clear();
handleCapY.clear();
hideSplitIndicator();
hoveredHandle = -1;
hoveredHandle = -1;
dragHandle = -1;
selectedHandle = -1;
splitHoverEdge = -1;
}
private void setSelectedHandle(int h) {
if (selectedHandle >= 0 && selectedHandle < handleMats.size() && selectedHandle != hoveredHandle) {
int n = liveXs.length;
handleMats.get(selectedHandle).setColor("Color", selectedHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE);
}
selectedHandle = h;
if (h >= 0 && h < handleMats.size() && h != hoveredHandle) {
handleMats.get(h).setColor("Color", COLOR_POLE_SELECTED);
}
}
private void deleteCornerPoint(int bodyIdx, int cornerIdx) {
int n = liveXs.length;
if (cornerIdx >= n) return; // Kantenmittelpunkte nicht löschbar
if (n <= 3) {
removeBody(bodyIdx);
return;
}
float[] newXs = new float[n - 1];
float[] newYs = new float[n - 1];
float[] newZs = new float[n - 1];
for (int i = 0, j = 0; i < n; i++) {
if (i == cornerIdx) continue;
newXs[j] = liveXs[i]; newYs[j] = liveYs[i]; newZs[j] = liveZs[i];
j++;
}
PlacedWater updated = bodies.get(bodyIdx).withPoints(newXs, newYs, newZs);
bodies.set(bodyIdx, updated);
rebuildQuadVisual(bodyIdx, newXs, newYs, newZs);
rebuildArrow(bodyIdx, updated);
liveXs = newXs.clone();
liveYs = newYs.clone();
liveZs = newZs.clone();
removeHandles();
buildHandles(newXs, newYs, newZs);
publishSelection(bodyIdx);
}
private void updateHandleHover() {
if (selectedIdx < 0) return;
PlacedWater b = bodies.get(selectedIdx);
@@ -585,7 +632,8 @@ public class WaterBodyState extends BaseAppState {
if (h != hoveredHandle) {
if (hoveredHandle >= 0 && hoveredHandle < handleMats.size()) {
ColorRGBA c = hoveredHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE;
ColorRGBA c = (hoveredHandle == selectedHandle) ? COLOR_POLE_SELECTED
: (hoveredHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE);
handleMats.get(hoveredHandle).setColor("Color", c);
}
if (h >= 0 && h < handleMats.size()) {
@@ -846,7 +894,7 @@ public class WaterBodyState extends BaseAppState {
rebuildPoleMesh(mesh, x, z, waterH, terrH);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color.clone());
mat.getAdditionalRenderState().setLineWidth(2.5f);
mat.getAdditionalRenderState().setLineWidth(4f);
Geometry geo = new Geometry(name, mesh);
geo.setMaterial(mat);
return geo;

View File

@@ -0,0 +1,91 @@
package de.blight.editor.toolset;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.*;
/** Shared static UI helpers used by multiple tool controllers. */
public final class EditorUiHelper {
private EditorUiHelper() {}
public static Label bold(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
return l;
}
public static Label styledHint(String text) {
Label l = new Label(text);
l.setStyle("-fx-text-fill: #333333; -fx-font-size: 11;");
return l;
}
public static Label sectionTitle(String text) {
Label l = new Label(text);
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #1a3a6a;");
VBox.setMargin(l, new Insets(8, 0, 0, 0));
return l;
}
public static HBox withField(Slider slider, String fmt) {
slider.setMaxWidth(Double.MAX_VALUE);
TextField tf = new TextField(String.format(fmt, slider.getValue()));
tf.setPrefWidth(62);
tf.setMaxWidth(62);
tf.setStyle("-fx-font-size: 11;");
slider.valueProperty().addListener((o, ov, nv) ->
tf.setText(String.format(fmt, nv.doubleValue())));
Runnable commit = () -> {
try {
double v = Math.max(slider.getMin(), Math.min(slider.getMax(),
Double.parseDouble(tf.getText().replace(',', '.'))));
slider.setValue(v);
} catch (NumberFormatException ignored) {
tf.setText(String.format(fmt, slider.getValue()));
}
};
tf.setOnAction(e -> commit.run());
tf.focusedProperty().addListener((o, ov, focused) -> { if (!focused) commit.run(); });
HBox row = new HBox(5, slider, tf);
HBox.setHgrow(slider, Priority.ALWAYS);
return row;
}
public static VBox paramSlider(String label, double min, double max, double init,
java.util.function.Consumer<Float> setter) {
Label name = new Label(label);
name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
Slider slider = new Slider(min, max, init);
slider.setShowTickMarks(true);
slider.setShowTickLabels(true);
slider.setMajorTickUnit((max - min) / 2.0);
slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue()));
return new VBox(3, name, withField(slider, "%.3f"));
}
public static VBox ezFloat(String label, double min, double max, double init,
java.util.function.Consumer<Float> setter) {
Label name = new Label(label);
name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
Slider slider = new Slider(min, max, Math.max(min, Math.min(max, init)));
slider.setShowTickMarks(false);
slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue()));
return new VBox(2, name, withField(slider, "%.3f"));
}
public static Spinner<Integer> intSpinner(int min, int max, int init) {
Spinner<Integer> sp = new Spinner<>(min, max, Math.max(min, Math.min(max, init)));
sp.setEditable(true);
sp.setMaxWidth(Double.MAX_VALUE);
return sp;
}
public static String pathToName(String path, String[] paths, String[] names) {
if (path == null) return names[0];
for (int i = 0; i < paths.length; i++) {
if (path.equals(paths[i])) return names[i];
}
return names[0];
}
}

View File

@@ -0,0 +1,387 @@
package de.blight.editor.toolset.game;
import de.blight.editor.ProjectRoot;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import de.blight.editor.toolset.EditorUiHelper;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
public class GameController {
private final ToolContext ctx;
// Game launch state
private boolean launchGameAfterSave = false;
private boolean pendingNewGame = false;
private boolean pendingAutostart = true;
// Play panel UI
private Button gamePlayBtn;
private Button gameNewBtn;
private Button gameFullBtn;
private CheckBox debugConsoleCB;
private Label tempSpawnCoordsLabel;
private Label permSpawnCoordsLabel;
// Game console
private Stage gameConsoleStage;
private TextArea gameConsoleArea;
private final ConcurrentLinkedQueue<String> consoleBuffer = new ConcurrentLinkedQueue<>();
public GameController(ToolContext ctx) {
this.ctx = ctx;
}
// ── Play-Tool-Panel ───────────────────────────────────────────────────────
public VBox buildPlayToolPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Spawnpunkte"), new Separator());
Button setTempBtn = new Button("Temp. Spawn hier setzen");
setTempBtn.setMaxWidth(Double.MAX_VALUE);
setTempBtn.setTooltip(new Tooltip(
"Aktiviert Klick-Modus im Viewport zum Setzen des temporären Spawnpunkts"));
setTempBtn.setOnAction(e -> {
ctx.input().playToolMode = SharedInput.PlayToolMode.SET_TEMP;
ctx.setStatus().accept("L-Klick im Viewport → temporären Spawnpunkt setzen");
});
Button setPermBtn = new Button("Perm. Spawn hier setzen");
setPermBtn.setMaxWidth(Double.MAX_VALUE);
setPermBtn.setTooltip(new Tooltip(
"Aktiviert Klick-Modus im Viewport zum Setzen des permanenten Spawnpunkts"));
setPermBtn.setOnAction(e -> {
ctx.input().playToolMode = SharedInput.PlayToolMode.SET_PERM;
ctx.setStatus().accept("L-Klick im Viewport → permanenten Spawnpunkt setzen");
});
inner.getChildren().addAll(setTempBtn, setPermBtn, new Separator());
tempSpawnCoordsLabel = new Label(tempSpawnCoordsText());
tempSpawnCoordsLabel.setStyle("-fx-font-family: monospace; -fx-font-size: 11; -fx-text-fill: #333;");
permSpawnCoordsLabel = new Label(permSpawnCoordsText());
permSpawnCoordsLabel.setStyle("-fx-font-family: monospace; -fx-font-size: 11; -fx-text-fill: #333;");
Button clearTempBtn = new Button("X Temp. Spawn loeschen");
clearTempBtn.setMaxWidth(Double.MAX_VALUE);
clearTempBtn.setOnAction(e -> {
ctx.input().tempSpawnX = Float.NaN;
ctx.input().tempSpawnZ = Float.NaN;
if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText("(nicht gesetzt)");
});
inner.getChildren().addAll(
EditorUiHelper.bold("Temp. Spawn:"), tempSpawnCoordsLabel, clearTempBtn,
EditorUiHelper.bold("Perm. Spawn:"), permSpawnCoordsLabel,
new Separator());
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Spiel starten"), new Separator());
Button playBtn = new Button("▶ Spielen");
gamePlayBtn = playBtn;
playBtn.setMaxWidth(Double.MAX_VALUE);
playBtn.setStyle("-fx-background-color: #2d8a3e; -fx-text-fill: white; " +
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
playBtn.setTooltip(new Tooltip(
"Startet das Spiel mit dem temp. Spawnpunkt (bzw. gespeicherter Position)"));
playBtn.setOnAction(e -> launchGame());
Button newGameBtn = new Button("Neues Spiel");
gameNewBtn = newGameBtn;
newGameBtn.setMaxWidth(Double.MAX_VALUE);
newGameBtn.setStyle("-fx-background-color: #7b3ea4; -fx-text-fill: white; " +
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
newGameBtn.setTooltip(new Tooltip(
"Startet ein neues Spiel am perm. Spawnpunkt mit Intro-Sequenz"));
newGameBtn.setOnAction(e -> launchNewGame());
Button fullStartBtn = new Button("▶ Vollst. Start");
gameFullBtn = fullStartBtn;
fullStartBtn.setMaxWidth(Double.MAX_VALUE);
fullStartBtn.setStyle("-fx-background-color: #1a6080; -fx-text-fill: white; " +
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
fullStartBtn.setTooltip(new Tooltip(
"Startet das Spiel wie von außen: mit OAA-Logo, JME-Logo und Hauptmenü"));
fullStartBtn.setOnAction(e -> launchFullGame());
debugConsoleCB = new CheckBox("Debug-Konsole anzeigen");
debugConsoleCB.setTooltip(new Tooltip(
"Öffnet bei Spielstart die Ausgabekonsole mit der Spielausgabe"));
inner.getChildren().addAll(playBtn, newGameBtn, fullStartBtn, debugConsoleCB);
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
// ── Launch ────────────────────────────────────────────────────────────────
public void launchGame() {
if (launchGameAfterSave) return;
launchGameAfterSave = true;
pendingNewGame = false;
pendingAutostart = true;
if (gamePlayBtn != null) { gamePlayBtn.setDisable(true); gamePlayBtn.setText("⏳ Startet…"); }
if (gameNewBtn != null) gameNewBtn.setDisable(true);
if (gameFullBtn != null) gameFullBtn.setDisable(true);
ctx.input().saveRequested = true;
ctx.setStatus().accept("Karte wird gespeichert, Spiel startet…");
}
public void launchNewGame() {
if (launchGameAfterSave) return;
launchGameAfterSave = true;
pendingNewGame = true;
pendingAutostart = true;
if (gameNewBtn != null) { gameNewBtn.setDisable(true); gameNewBtn.setText("⏳ Startet…"); }
if (gamePlayBtn != null) gamePlayBtn.setDisable(true);
if (gameFullBtn != null) gameFullBtn.setDisable(true);
ctx.input().saveRequested = true;
ctx.setStatus().accept("Karte wird gespeichert, Neues Spiel startet…");
}
public void launchFullGame() {
if (launchGameAfterSave) return;
launchGameAfterSave = true;
pendingNewGame = false;
pendingAutostart = false;
if (gameFullBtn != null) { gameFullBtn.setDisable(true); gameFullBtn.setText("⏳ Startet…"); }
if (gamePlayBtn != null) gamePlayBtn.setDisable(true);
if (gameNewBtn != null) gameNewBtn.setDisable(true);
ctx.input().saveRequested = true;
ctx.setStatus().accept("Karte wird gespeichert, vollständiger Spielstart…");
}
/** Called from EditorApp AnimationTimer after save completes. */
public void onSaveComplete() {
if (!launchGameAfterSave) return;
launchGameAfterSave = false;
startGameProcess();
}
// ── Console buffer drain — called from AnimationTimer ─────────────────────
public void flushConsoleBuffer() {
if (consoleBuffer.isEmpty()) return;
if (gameConsoleArea != null) {
StringBuilder sb = new StringBuilder();
int count = 0;
String ln;
while (count < 200 && (ln = consoleBuffer.poll()) != null) {
sb.append(ln).append('\n');
count++;
}
gameConsoleArea.appendText(sb.toString());
gameConsoleArea.setScrollTop(Double.MAX_VALUE);
} else {
consoleBuffer.clear();
}
}
// ── Spawn coords update — called from AnimationTimer ──────────────────────
public void updateSpawnFields(String info) {
if (info == null) return;
String[] p = info.split("\\|", -1);
if (p.length < 2) return;
if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText(tempSpawnCoordsText());
}
public void updatePermSpawnLabel() {
if (permSpawnCoordsLabel != null) permSpawnCoordsLabel.setText(permSpawnCoordsText());
}
// ── Internal ──────────────────────────────────────────────────────────────
private String tempSpawnCoordsText() {
SharedInput in = ctx.input();
if (Float.isNaN(in.tempSpawnX) || Float.isNaN(in.tempSpawnZ)) return "(nicht gesetzt)";
return String.format("X=%.1f Z=%.1f Yaw=%.0f°", in.tempSpawnX, in.tempSpawnZ, in.tempSpawnYaw);
}
private String permSpawnCoordsText() {
SharedInput in = ctx.input();
if (Float.isNaN(in.permSpawnX) || Float.isNaN(in.permSpawnZ)) return "(nicht gesetzt)";
return String.format("X=%.1f Z=%.1f Yaw=%.0f°", in.permSpawnX, in.permSpawnZ, in.permSpawnYaw);
}
private void startGameProcess() {
final boolean isNewGame = pendingNewGame;
final boolean isAutostart = pendingAutostart;
pendingNewGame = false;
pendingAutostart = true;
Stage primary = ctx.stageSupplier().get();
Platform.runLater(() -> { if (primary != null) primary.setIconified(true); });
SharedInput in = ctx.input();
new Thread(() -> {
try {
String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
String classpath = System.getProperty("java.class.path");
String libPath = System.getProperty("java.library.path", "");
String projRoot = ProjectRoot.PATH.toString();
Path srcMapDir = ProjectRoot.PATH.resolve(
Paths.get("blight-map", "src", "main", "map"));
Path runDir = ProjectRoot.PATH.resolve("run");
Path sessionDir = runDir.resolve("session-" + System.currentTimeMillis());
copyDirectory(srcMapDir, sessionDir);
try {
java.util.List<Path> sessions;
try (java.util.stream.Stream<Path> s = Files.list(runDir)) {
sessions = s.filter(p -> p.getFileName().toString().startsWith("session-"))
.sorted(java.util.Comparator.comparing(p -> p.getFileName().toString()))
.collect(java.util.stream.Collectors.toList());
}
int keep = 3;
for (int i = 0; i < sessions.size() - keep; i++) {
deleteDirectoryRecursive(sessions.get(i));
}
} catch (Exception e) {
// non-fatal cleanup failure
}
String sessionMapPath = sessionDir.resolve("blight_map.blm").toString();
java.util.List<String> cmd = new java.util.ArrayList<>(List.of(
javaExe,
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
"-Djava.library.path=" + libPath,
"-Dblight.project.root=" + projRoot,
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,
"-Dblight.autostart=" + isAutostart));
if (isNewGame) {
cmd.add("-Dblight.new.game=true");
} else if (!Float.isNaN(in.tempSpawnX) && !Float.isNaN(in.tempSpawnZ)) {
cmd.add("-Dblight.temp.spawn.x=" + in.tempSpawnX);
cmd.add("-Dblight.temp.spawn.z=" + in.tempSpawnZ);
cmd.add("-Dblight.temp.spawn.yaw=" + in.tempSpawnYaw);
}
cmd.addAll(List.of("-cp", classpath, "de.blight.game.BlightGame"));
Process proc = new ProcessBuilder(cmd)
.directory(ProjectRoot.PATH.toFile())
.redirectErrorStream(true)
.start();
Platform.runLater(() -> {
ctx.setStatus().accept(isNewGame ? "Neues Spiel gestartet" : "Spiel gestartet");
if (gamePlayBtn != null) gamePlayBtn.setText("Laeuft…");
if (gameNewBtn != null) gameNewBtn.setText("Laeuft…");
if (gameFullBtn != null) gameFullBtn.setText("Laeuft…");
if (debugConsoleCB != null && debugConsoleCB.isSelected()) openGameConsole();
});
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(proc.getInputStream(),
java.nio.charset.StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
consoleBuffer.offer(line);
}
}
consoleBuffer.offer("--- Spiel beendet ---");
Platform.runLater(() -> {
if (primary != null) primary.setIconified(false);
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
if (gameNewBtn != null) { gameNewBtn.setText("Neues Spiel"); gameNewBtn.setDisable(false); }
if (gameFullBtn != null) { gameFullBtn.setText("▶ Vollst. Start"); gameFullBtn.setDisable(false); }
});
} catch (IOException ex) {
Platform.runLater(() -> {
ctx.setStatus().accept("Spielstart fehlgeschlagen: " + ex.getMessage());
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
if (gameNewBtn != null) { gameNewBtn.setText("Neues Spiel"); gameNewBtn.setDisable(false); }
if (gameFullBtn != null) { gameFullBtn.setText("▶ Vollst. Start"); gameFullBtn.setDisable(false); }
});
}
}, "game-launcher").start();
}
public void openGameConsole() {
Stage primary = ctx.stageSupplier().get();
if (gameConsoleStage == null) {
gameConsoleArea = new TextArea();
gameConsoleArea.setEditable(false);
gameConsoleArea.setStyle(
"-fx-control-inner-background: #1e1e1e; -fx-text-fill: #d4d4d4;" +
" -fx-font-family: monospace; -fx-font-size: 11;");
Button clearBtn = new Button("Löschen");
clearBtn.setOnAction(e -> gameConsoleArea.clear());
HBox toolbar = new HBox(5, clearBtn);
toolbar.setPadding(new Insets(4));
VBox layout = new VBox(toolbar, gameConsoleArea);
VBox.setVgrow(gameConsoleArea, Priority.ALWAYS);
Scene scene = new Scene(layout, 800, 350);
gameConsoleStage = new Stage();
gameConsoleStage.setTitle("Spiel-Konsole");
gameConsoleStage.setScene(scene);
gameConsoleStage.initOwner(primary);
var screens = javafx.stage.Screen.getScreens();
if (screens.size() > 1) {
javafx.geometry.Rectangle2D sb = screens.get(1).getVisualBounds();
gameConsoleStage.setX(sb.getMinX());
gameConsoleStage.setY(sb.getMinY());
gameConsoleStage.setWidth(sb.getWidth());
gameConsoleStage.setHeight(sb.getHeight());
} else if (primary != null) {
gameConsoleStage.setX(primary.getX());
gameConsoleStage.setY(primary.getY() + primary.getHeight());
}
} else {
gameConsoleArea.clear();
}
gameConsoleStage.show();
gameConsoleStage.toFront();
}
private static void copyDirectory(Path src, Path dst) throws IOException {
try (java.util.stream.Stream<Path> walk = java.nio.file.Files.walk(src)) {
for (Path s : (Iterable<Path>) walk::iterator) {
Path d = dst.resolve(src.relativize(s));
if (java.nio.file.Files.isDirectory(s)) {
java.nio.file.Files.createDirectories(d);
} else {
java.nio.file.Files.copy(s, d, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
}
}
}
private static void deleteDirectoryRecursive(Path dir) throws IOException {
if (!Files.exists(dir)) return;
try (java.util.stream.Stream<Path> walk = Files.walk(dir)) {
walk.sorted(java.util.Comparator.reverseOrder())
.forEach(p -> { try { Files.delete(p); } catch (IOException ignored) {} });
}
}
}

View File

@@ -0,0 +1,335 @@
package de.blight.editor.toolset.topbar;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import javafx.geometry.Orientation;
import javafx.scene.control.*;
import javafx.scene.layout.*;
public class TopBarController {
public record Deps(
VBox toolPanel,
de.blight.editor.toolset.model.ModelEditorController modelEditorController,
de.blight.editor.toolset.asset.AssetTreeController assetController,
de.blight.editor.toolset.anim.AnimSetController animSetController,
de.blight.editor.toolset.content.ContentController contentController,
de.blight.editor.toolset.world.WorldPanelController worldPanelController,
de.blight.editor.toolset.voxel.VoxelController voxelController,
de.blight.editor.toolset.grass.GrassController grassController,
de.blight.editor.toolset.atmosphere.AtmosphereController atmosphereController,
de.blight.editor.toolset.water.WaterController waterController,
de.blight.editor.toolset.game.GameController gameController,
de.blight.editor.toolset.terrain.TerrainPanelController terrainController,
de.blight.editor.toolset.object.ObjectController objectController,
Runnable switchToVegetationGenerator,
Runnable switchToEzTree,
Runnable switchToTripo,
Runnable openCharacterEditor,
Runnable switchToThumbnailManager,
Runnable takeEditorScreenshot,
Runnable openSettings
) {}
private final SharedInput input;
// ToggleButtons – pre-created so getters work before buildTop() is called
private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl");
private final ToggleButton baseBtn = new ToggleButton("▲▼ Terrain");
private final ToggleButton voxelMergedBtn = new ToggleButton("◈ Voxel");
private final ToggleButton textureBtn = new ToggleButton(": : Textur");
private final ToggleButton grassBtn = new ToggleButton("// Gras");
private final ToggleButton stoneBtn = new ToggleButton("o Steine");
private final ToggleButton objPlaceBtn = new ToggleButton("+ Objekte");
private final ToggleButton atmosphaereBtn = new ToggleButton("* Atmosphare");
private final ToggleButton wasserMergedBtn = new ToggleButton("~ Wasser");
private final ToggleButton wegeBtn = new ToggleButton("-- Wege");
private final ToggleButton zonenBtn = new ToggleButton("[ ] Zonen");
private final ToggleButton playToolBtn = new ToggleButton("▶ Spielen");
// Menu items – pre-created
private final RadioMenuItem camOrbitItem = new RadioMenuItem("⊙ Orbit-Kamera (Alt+C)");
private final RadioMenuItem camFreeItem = new RadioMenuItem("✈ FreeFly-Kamera (Alt+C)");
private final CheckMenuItem dualWindowMenuItem = new CheckMenuItem("2-Fenster-Modus (Alt+W)");
private final CheckMenuItem showGridItem = new CheckMenuItem("1m-Gitternetz (Alt+N)");
private CheckMenuItem viewTopologyItem;
private boolean wireframeActive = false;
private ToolBar worldToolBar;
private Label worldHintLabel;
public TopBarController(SharedInput input) {
this.input = input;
// Bold style
final String BOLD = "-fx-font-weight:bold;";
auswahlBtn.setStyle(BOLD);
baseBtn.setStyle(BOLD);
voxelMergedBtn.setStyle(BOLD);
textureBtn.setStyle(BOLD);
grassBtn.setStyle(BOLD);
stoneBtn.setStyle(BOLD);
objPlaceBtn.setStyle(BOLD);
atmosphaereBtn.setStyle(BOLD);
wasserMergedBtn.setStyle(BOLD);
wegeBtn.setStyle(BOLD);
zonenBtn.setStyle(BOLD);
playToolBtn.setStyle(BOLD);
// Mutually exclusive toggle group
ToggleGroup layerGroup = new ToggleGroup();
auswahlBtn.setToggleGroup(layerGroup);
baseBtn.setToggleGroup(layerGroup);
voxelMergedBtn.setToggleGroup(layerGroup);
textureBtn.setToggleGroup(layerGroup);
grassBtn.setToggleGroup(layerGroup);
stoneBtn.setToggleGroup(layerGroup);
objPlaceBtn.setToggleGroup(layerGroup);
atmosphaereBtn.setToggleGroup(layerGroup);
wasserMergedBtn.setToggleGroup(layerGroup);
wegeBtn.setToggleGroup(layerGroup);
zonenBtn.setToggleGroup(layerGroup);
playToolBtn.setToggleGroup(layerGroup);
// Cam-Menu toggle group
ToggleGroup camMenuGroup = new ToggleGroup();
camOrbitItem.setToggleGroup(camMenuGroup);
camFreeItem.setToggleGroup(camMenuGroup);
camOrbitItem.setSelected(true);
// Grid default
showGridItem.setSelected(true);
}
// ── Getters ──────────────────────────────────────────────────────────────
public ToggleButton getAuswahlBtn() { return auswahlBtn; }
public ToggleButton getBaseBtn() { return baseBtn; }
public ToggleButton getVoxelMergedBtn() { return voxelMergedBtn; }
public ToggleButton getTextureBtn() { return textureBtn; }
public ToggleButton getGrassBtn() { return grassBtn; }
public ToggleButton getStoneBtn() { return stoneBtn; }
public ToggleButton getObjPlaceBtn() { return objPlaceBtn; }
public ToggleButton getAtmosphaereBtn() { return atmosphaereBtn; }
public ToggleButton getWasserMergedBtn(){ return wasserMergedBtn; }
public ToggleButton getWegeBtn() { return wegeBtn; }
public ToggleButton getZonenBtn() { return zonenBtn; }
public ToggleButton getPlayToolBtn() { return playToolBtn; }
public RadioMenuItem getCamOrbitItem() { return camOrbitItem; }
public RadioMenuItem getCamFreeItem() { return camFreeItem; }
public CheckMenuItem getDualWindowMenuItem() { return dualWindowMenuItem; }
public CheckMenuItem getShowGridItem() { return showGridItem; }
public CheckMenuItem getViewTopologyItem() { return viewTopologyItem; }
public ToolBar getWorldToolBar() { return worldToolBar; }
public Label getWorldHintLabel(){ return worldHintLabel; }
public boolean toggleWireframe() {
wireframeActive = !wireframeActive;
return wireframeActive;
}
public void updateCamHintLabel() {
if (worldHintLabel != null) { worldHintLabel.setText(camHintText()); }
}
public void applyShowGridPref(boolean value) {
showGridItem.setSelected(value);
}
private String camHintText() {
String mode = (input.camMode == SharedInput.CAM_FREEFLY) ? "FreeFly" : "Orbit";
return "WASD/QE: Kamera | Mitte-Drag: Drehen | Alt+C: Kamera wechseln | Kamera: " + mode;
}
// ── Build ─────────────────────────────────────────────────────────────────
public VBox buildTop(ToolContext ctx, Deps deps) {
MenuBar menuBar = new MenuBar();
// ── Datei-Menü ──────────────────────────────────────────────────────
Menu fileMenu = new Menu("Datei");
MenuItem newItem = new MenuItem("Neue Karte");
MenuItem saveItem = new MenuItem("Speichern");
saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S"));
saveItem.setOnAction(e -> { input.saveRequested = true; ctx.setStatus().accept("Speichern…"); });
MenuItem screenshotItem = new MenuItem("Screenshot");
screenshotItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+F12"));
screenshotItem.setOnAction(e -> deps.takeEditorScreenshot().run());
MenuItem settingsItem = new MenuItem("Einstellungen…");
settingsItem.setOnAction(e -> deps.openSettings().run());
MenuItem quitItem = new MenuItem("Beenden");
quitItem.setOnAction(e -> javafx.application.Platform.exit());
fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(),
screenshotItem, new SeparatorMenuItem(), settingsItem,
new SeparatorMenuItem(), quitItem);
// ── Import-Menü ─────────────────────────────────────────────────────
Menu importMenu = new Menu("Import");
MenuItem importModelLodItem = new MenuItem("Modell mit LOD-Editor…");
MenuItem importTexItem = new MenuItem("Texturen…");
MenuItem importTexZipItem = new MenuItem("Texturen-ZIP…");
MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…");
MenuItem importAudioItem = new MenuItem("Audio…");
MenuItem importAnimItem = new MenuItem("Animationen…");
MenuItem importCharModelItem = new MenuItem("Character-Modell (FBX)…");
importModelLodItem.setOnAction(e -> deps.modelEditorController().openModelImport(ctx.stageSupplier().get()));
importTexItem.setOnAction(e -> deps.assetController().handleTextureImport(ctx.stageSupplier().get()));
importTexZipItem.setOnAction(e -> deps.assetController().handleZipTextureImport(ctx.stageSupplier().get()));
importTexSetItem.setOnAction(e -> deps.assetController().openTextureSetImport(ctx.stageSupplier().get()));
importAudioItem.setOnAction(e -> deps.assetController().handleAudioImport(ctx.stageSupplier().get()));
importAnimItem.setOnAction(e -> deps.animSetController().handleAnimationImport(ctx.stageSupplier().get()));
importCharModelItem.setOnAction(e -> deps.animSetController().handleCharacterModelImport(ctx.stageSupplier().get()));
importMenu.getItems().addAll(importModelLodItem, importTexItem, importTexZipItem,
importTexSetItem, importAudioItem, importAnimItem,
new SeparatorMenuItem(), importCharModelItem);
// ── Werkzeuge-Menü ──────────────────────────────────────────────────
Menu toolsMenu = new Menu("Werkzeuge");
MenuItem vegetationsItem = new MenuItem("Vegetations Generator");
MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)");
MenuItem tripoItem = new MenuItem("AI Modell-Generator (Tripo3D)");
MenuItem animPrevItem = new MenuItem("Animationset Editor");
MenuItem objEditorItem = new MenuItem("Model Editor");
MenuItem thumbItem = new MenuItem("Thumbnail-Verwaltung");
vegetationsItem.setOnAction(e -> deps.switchToVegetationGenerator().run());
ezTreeItem.setOnAction(e -> deps.switchToEzTree().run());
tripoItem.setOnAction(e -> deps.switchToTripo().run());
animPrevItem.setOnAction(e -> deps.openCharacterEditor().run());
objEditorItem.setOnAction(e -> deps.modelEditorController().openModelEditor(null, null));
thumbItem.setOnAction(e -> deps.switchToThumbnailManager().run());
toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem,
animPrevItem, objEditorItem, new SeparatorMenuItem(), thumbItem);
// ── Ansicht-Menü ────────────────────────────────────────────────────
Menu viewMenu = new Menu("Ansicht");
MenuItem resetCam = new MenuItem("Kamera zurücksetzen (Alt+R)");
resetCam.setOnAction(e -> input.resetCameraRequested.set(true));
MenuItem viewTexture = new MenuItem("Textur (Alt+G)");
MenuItem viewWireframe = new MenuItem("Drahtgitter (Alt+G)");
viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Alt+T)");
viewTexture.setOnAction(e -> {
wireframeActive = false;
input.wireframeRequest = 2;
});
viewWireframe.setOnAction(e -> {
wireframeActive = true;
input.wireframeRequest = 1;
});
viewTopologyItem.setOnAction(e ->
input.topologyRequest = viewTopologyItem.isSelected() ? 1 : 2);
camOrbitItem.setOnAction(e -> { input.camMode = SharedInput.CAM_ORBIT; updateCamHintLabel(); });
camFreeItem.setOnAction(e -> { input.camMode = SharedInput.CAM_FREEFLY; updateCamHintLabel(); });
CheckMenuItem debugHeightItem = new CheckMenuItem("Höhen-Overlay (Alt+H)");
debugHeightItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+H"));
debugHeightItem.setOnAction(e ->
input.debugHeightOverlayEnabled = debugHeightItem.isSelected());
dualWindowMenuItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+W"));
dualWindowMenuItem.setOnAction(e -> {
if (dualWindowMenuItem.isSelected()) { deps.contentController().enableDualWindowMode(); }
else { deps.contentController().disableDualWindowMode(); }
});
deps.contentController().setDualWindowMenuItem(dualWindowMenuItem);
showGridItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+N"));
showGridItem.setOnAction(e -> input.showGrid = showGridItem.isSelected());
viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe,
new SeparatorMenuItem(), viewTopologyItem,
new SeparatorMenuItem(), debugHeightItem,
new SeparatorMenuItem(), showGridItem,
new SeparatorMenuItem(), camOrbitItem, camFreeItem);
// ── Zeit-Menü ───────────────────────────────────────────────────────
Menu zeitMenu = new Menu("Zeit");
ToggleGroup zeitGroup = new ToggleGroup();
int[] hours = {0, 3, 6, 9, 12, 15, 18, 21};
for (int h : hours) {
RadioMenuItem item = new RadioMenuItem(String.format("%02d:00", h));
item.setToggleGroup(zeitGroup);
if (h == 12) { item.setSelected(true); }
final float t = h / 24f;
item.setOnAction(e -> input.pendingTimeOfDay = t);
zeitMenu.getItems().add(item);
}
menuBar.getMenus().addAll(fileMenu, toolsMenu, importMenu, viewMenu, zeitMenu);
// ── Toolbar-Buttons ─────────────────────────────────────────────────
auswahlBtn.setSelected(true);
input.activeLayer = SharedInput.LAYER_AUSWAHL;
auswahlBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_AUSWAHL;
input.deselectAllOnAuswahl = true;
ctx.setRightPanel().accept(deps.worldPanelController().buildAuswahlPanel());
});
baseBtn.setOnAction(e -> {
input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue());
input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue());
input.heightTool.plateauHeight.setValue(input.voxelTool.plateauTarget.getValue());
input.activeLayer = 0; input.activeTool = input.heightTool;
ctx.setRightPanel().accept(deps.toolPanel());
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
});
voxelMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.voxelController().buildPanel()));
textureBtn.setOnAction(e -> {
input.activeLayer = 4; input.activeTool = input.textureTool;
ctx.setRightPanel().accept(deps.toolPanel());
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
});
grassBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.grassController().buildPanel()));
stoneBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_STONE;
input.activeTool = input.stoneTool;
ctx.setRightPanel().accept(deps.toolPanel());
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
});
objPlaceBtn.setOnAction(e -> {
deps.assetController().expandModelsNode();
deps.assetController().openOverlay();
deps.objectController().activatePlaceMode();
});
atmosphaereBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.atmosphereController().buildPanel()));
wasserMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.waterController().buildPanel()));
wegeBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_PATH_NETWORK;
ctx.setRightPanel().accept(deps.worldPanelController().buildWegePanel());
});
zonenBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_ZONEN;
ctx.setRightPanel().accept(deps.worldPanelController().buildZonenPanel());
});
playToolBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_PLAY_TOOL;
input.playToolMode = SharedInput.PlayToolMode.EDIT;
ctx.setRightPanel().accept(deps.gameController().buildPlayToolPanel());
});
worldHintLabel = new Label(camHintText());
worldHintLabel.setStyle("-fx-text-fill: #555;");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
ToolBar toolBar = new ToolBar();
toolBar.getItems().addAll(
auswahlBtn,
new Separator(Orientation.VERTICAL), baseBtn, voxelMergedBtn,
new Separator(Orientation.VERTICAL), textureBtn, grassBtn, stoneBtn,
new Separator(Orientation.VERTICAL), objPlaceBtn,
new Separator(Orientation.VERTICAL), atmosphaereBtn,
new Separator(Orientation.VERTICAL), wasserMergedBtn, wegeBtn,
new Separator(Orientation.VERTICAL), zonenBtn,
new Separator(Orientation.VERTICAL), playToolBtn,
new Separator(Orientation.VERTICAL), worldHintLabel,
new Separator(Orientation.VERTICAL), spacer,
deps.contentController().buildContentSwitchBtn());
worldToolBar = toolBar;
return new VBox(menuBar, toolBar);
}
}

View File

@@ -0,0 +1,282 @@
package de.blight.editor.toolset.tripo;
import de.blight.editor.ToolContext;
import de.blight.editor.toolset.EditorUiHelper;
import de.blight.editor.SharedInput;
import de.blight.editor.TripoGenerator;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import java.nio.file.Files;
import java.nio.file.Path;
public class TripoController {
private final ToolContext ctx;
private ImageView tripoPreviewView;
private Label tripoStatusLabel;
private ProgressBar tripoProgressBar;
private Button tripoImportBtn;
private Button tripoImportRigBtn;
private boolean tripoGenerating = false;
private String tripoLastModelUrl;
private String tripoLastTaskId;
public TripoController(ToolContext ctx) {
this.ctx = ctx;
}
public void activate() {
ctx.setCurrentTool().accept("tripo");
StackPane previewCenter = new StackPane();
previewCenter.setStyle("-fx-background-color: #1a1a2e;");
tripoPreviewView = new ImageView();
tripoPreviewView.setPreserveRatio(true);
tripoPreviewView.fitWidthProperty().bind(previewCenter.widthProperty());
tripoPreviewView.fitHeightProperty().bind(previewCenter.heightProperty());
Label placeholder = new Label("Vorschau erscheint hier nach der Generierung");
placeholder.setStyle("-fx-text-fill: #888; -fx-font-size: 14;");
previewCenter.getChildren().addAll(placeholder, tripoPreviewView);
ctx.setCenterView().accept(previewCenter);
ctx.setRightPanel().accept(buildTripoPanel());
ctx.setMainToolbar().accept(buildTripoToolBar());
}
private ToolBar buildTripoToolBar() {
Button backBtn = new Button("← Welteneditor");
backBtn.setOnAction(e -> ctx.switchToWorld().run());
Label label = new Label("AI Modell-Generator · Tripo3D");
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
ToolBar tb = new ToolBar();
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label);
return tb;
}
private VBox buildTripoPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Tripo3D API"), new Separator());
Label apiHint = EditorUiHelper.styledHint("Schlüssel unter platform.tripo3d.ai → API Keys");
apiHint.setWrapText(true);
TextField apiKeyField = new TextField(loadTripoApiKey());
apiKeyField.setPromptText("tsk_xxxx…");
apiKeyField.setStyle("-fx-font-family: monospace; -fx-font-size: 11;");
inner.getChildren().addAll(new Label("API-Schlüssel:"), apiKeyField, apiHint);
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Modell beschreiben"), new Separator());
TextArea promptArea = new TextArea();
promptArea.setPromptText("z. B. \"Ein mittelalterlicher Ritter in Ruestung\"\n"
+ "Tipp: Englische Prompts liefern meist bessere Ergebnisse.");
promptArea.setPrefRowCount(5);
promptArea.setWrapText(true);
inner.getChildren().addAll(new Label("Prompt:"), promptArea);
TextField nameField = new TextField();
nameField.setPromptText("dateiname (optional)");
inner.getChildren().addAll(new Label("Dateiname:"), nameField);
inner.getChildren().add(new Separator());
Button generateBtn = new Button("Modell generieren");
generateBtn.setMaxWidth(Double.MAX_VALUE);
generateBtn.setStyle("-fx-font-weight: bold;");
generateBtn.setDisable(tripoGenerating);
tripoProgressBar = new ProgressBar(tripoGenerating ? ProgressBar.INDETERMINATE_PROGRESS : 0);
tripoProgressBar.setMaxWidth(Double.MAX_VALUE);
tripoStatusLabel = new Label(tripoGenerating ? "Generierung läuft…" : "Bereit");
tripoStatusLabel.setWrapText(true);
tripoStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #444;");
inner.getChildren().addAll(generateBtn, tripoProgressBar, tripoStatusLabel);
inner.getChildren().addAll(EditorUiHelper.sectionTitle("Import"), new Separator());
tripoImportBtn = new Button("Importieren (GLB → j3o)");
tripoImportRigBtn = new Button("Importieren + Skelett (Humanoid)");
tripoImportBtn.setMaxWidth(Double.MAX_VALUE);
tripoImportRigBtn.setMaxWidth(Double.MAX_VALUE);
boolean hasPrev = tripoLastModelUrl != null && !tripoGenerating;
tripoImportBtn.setDisable(!hasPrev);
tripoImportRigBtn.setDisable(!hasPrev || tripoLastTaskId == null);
inner.getChildren().addAll(tripoImportBtn, tripoImportRigBtn);
inner.getChildren().add(EditorUiHelper.styledHint("Das Skelett-Modell eignet sich für Humanoid-Animationen."));
generateBtn.setOnAction(e -> {
if (tripoGenerating) return;
String apiKey = apiKeyField.getText().trim();
String prompt = promptArea.getText().trim();
if (apiKey.isEmpty()) { ctx.setStatus().accept("Bitte API-Schlüssel eingeben"); return; }
if (prompt.isEmpty()) { ctx.setStatus().accept("Bitte Prompt eingeben"); return; }
saveTripoApiKey(apiKey);
tripoGenerating = true;
generateBtn.setDisable(true);
tripoImportBtn.setDisable(true);
tripoImportRigBtn.setDisable(true);
tripoProgressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
tripoStatusLabel.setText("Erstelle Aufgabe…");
if (tripoPreviewView != null) tripoPreviewView.setImage(null);
new Thread(() -> {
try {
String taskId = TripoGenerator.createTextToModelTask(apiKey, prompt);
tripoLastTaskId = taskId;
Platform.runLater(() -> tripoStatusLabel.setText("Generiere… (ID: " + taskId + ")"));
pollUntilDone(apiKey, taskId, result -> {
tripoLastModelUrl = result.modelUrl();
if (result.previewUrl() != null && tripoPreviewView != null) {
tripoPreviewView.setImage(
new javafx.scene.image.Image(result.previewUrl(), true));
}
tripoProgressBar.setProgress(1.0);
tripoStatusLabel.setText("Fertig! Modell bereit zum Import.");
generateBtn.setDisable(false);
tripoImportBtn.setDisable(false);
tripoImportRigBtn.setDisable(false);
tripoGenerating = false;
}, err -> {
tripoProgressBar.setProgress(0);
tripoStatusLabel.setText("Fehler: " + err);
generateBtn.setDisable(false);
tripoGenerating = false;
});
} catch (Exception ex) {
Platform.runLater(() -> {
tripoProgressBar.setProgress(0);
tripoStatusLabel.setText("Fehler: " + ex.getMessage());
generateBtn.setDisable(false);
tripoGenerating = false;
});
}
}).start();
});
tripoImportBtn.setOnAction(e -> {
if (tripoLastModelUrl == null) return;
String name = nameField.getText().trim();
if (name.isEmpty()) name = "tripo_" + System.currentTimeMillis();
importTripoModel(tripoLastModelUrl, name);
});
tripoImportRigBtn.setOnAction(e -> {
if (tripoLastTaskId == null || tripoLastModelUrl == null) return;
String apiKey = apiKeyField.getText().trim();
String baseName = nameField.getText().trim();
if (baseName.isEmpty()) baseName = "tripo_" + System.currentTimeMillis();
final String rigName = baseName + "_rigged";
tripoImportRigBtn.setDisable(true);
tripoProgressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
tripoStatusLabel.setText("Erstelle Skelett…");
final String taskId = tripoLastTaskId;
new Thread(() -> {
try {
String rigTaskId = TripoGenerator.createRigTask(apiKey, taskId);
Platform.runLater(() -> tripoStatusLabel.setText(
"Rigging… (ID: " + rigTaskId + ")"));
pollUntilDone(apiKey, rigTaskId, result -> {
tripoProgressBar.setProgress(1.0);
tripoStatusLabel.setText("Skelett fertig! Importiere…");
tripoImportRigBtn.setDisable(false);
importTripoModel(result.modelUrl(), rigName);
}, err -> {
tripoProgressBar.setProgress(0);
tripoStatusLabel.setText("Skelett-Fehler: " + err);
tripoImportRigBtn.setDisable(false);
});
} catch (Exception ex) {
Platform.runLater(() -> {
tripoProgressBar.setProgress(0);
tripoStatusLabel.setText("Fehler: " + ex.getMessage());
tripoImportRigBtn.setDisable(false);
});
}
}).start();
});
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(280);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
private void pollUntilDone(String apiKey, String taskId,
java.util.function.Consumer<TripoGenerator.TaskResult> onSuccess,
java.util.function.Consumer<String> onError) {
while (true) {
try { Thread.sleep(3_000); } catch (InterruptedException ie) {
Thread.currentThread().interrupt();
Platform.runLater(() -> onError.accept("Unterbrochen"));
return;
}
try {
TripoGenerator.TaskResult r = TripoGenerator.pollTask(apiKey, taskId);
switch (r.status()) {
case "success" -> { Platform.runLater(() -> onSuccess.accept(r)); return; }
case "failed", "cancelled" -> {
Platform.runLater(() -> onError.accept(r.status())); return;
}
default -> {
int p = r.progress();
Platform.runLater(() -> {
if (tripoProgressBar != null) tripoProgressBar.setProgress(p / 100.0);
if (tripoStatusLabel != null) tripoStatusLabel.setText("Generiere… " + p + "%");
});
}
}
} catch (java.io.IOException ex) {
Platform.runLater(() -> {
if (tripoStatusLabel != null)
tripoStatusLabel.setText("Prüfe… (" + ex.getMessage() + ")");
});
}
}
}
private void importTripoModel(String url, String name) {
final String safeName = name.replaceAll("[^a-zA-Z0-9_\\-]", "_");
new Thread(() -> {
try {
Platform.runLater(() -> ctx.setStatus().accept("Lade Modell herunter: " + safeName + "…"));
Path destDir = ctx.assetRoot().resolve("Models");
Files.createDirectories(destDir);
Path destGlb = destDir.resolve(safeName + ".glb");
TripoGenerator.downloadFile(url, destGlb);
String assetPath = "Models/" + safeName + ".glb";
Path destJ3o = destDir.resolve(safeName + ".j3o");
ctx.input().modelConvertQueue.offer(
new SharedInput.ModelConvertRequest(assetPath, destJ3o, destGlb, true));
Platform.runLater(() -> ctx.setStatus().accept("Konvertiere " + safeName + ".glb → .j3o…"));
} catch (java.io.IOException ex) {
Platform.runLater(() -> ctx.setStatus().accept("Import-Fehler: " + ex.getMessage()));
}
}).start();
}
private static String loadTripoApiKey() {
return java.util.prefs.Preferences
.userNodeForPackage(TripoController.class)
.get("tripo_api_key", "");
}
private static void saveTripoApiKey(String key) {
java.util.prefs.Preferences
.userNodeForPackage(TripoController.class)
.put("tripo_api_key", key);
}
}

View File

@@ -0,0 +1,422 @@
package de.blight.editor.toolset.viewport;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import javafx.geometry.Insets;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.StackPane;
public class ViewportController {
private final SharedInput input;
private final WritableImage initialImage;
private final Runnable scheduleAutoSave;
private ImageView viewport;
private javafx.scene.canvas.Canvas compassCanvas;
private double prevDragX, prevDragY;
private double objDragPrevX, objDragPrevY;
private boolean objDragging = false;
private double editPressX, editPressY;
private int editPressAction;
private javafx.animation.Timeline editTimer;
public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) {
this.input = input;
this.initialImage = jfxImage;
this.scheduleAutoSave = scheduleAutoSave;
}
public ImageView getViewport() { return viewport; }
public void setImage(WritableImage img) {
if (viewport != null) { viewport.setImage(img); }
}
public StackPane buildViewport() {
viewport = new ImageView(initialImage);
viewport.setPreserveRatio(false);
viewport.setFocusTraversable(true);
compassCanvas = new javafx.scene.canvas.Canvas(100, 100);
compassCanvas.setMouseTransparent(true);
StackPane.setAlignment(compassCanvas, javafx.geometry.Pos.BOTTOM_RIGHT);
StackPane.setMargin(compassCanvas, new Insets(0, 10, 10, 0));
drawCompass();
StackPane pane = new StackPane(viewport, compassCanvas);
pane.setStyle("-fx-background-color: #1a1a2e;");
javafx.animation.PauseTransition resizeDebounce =
new javafx.animation.PauseTransition(javafx.util.Duration.millis(150));
resizeDebounce.setOnFinished(ev -> {
int w = (int) pane.getWidth();
int h = (int) pane.getHeight();
if (w > 0 && h > 0) { input.resizeRequest.set(new int[]{w, h}); }
});
pane.widthProperty().addListener((o, oldW, newW) -> {
viewport.setFitWidth(newW.doubleValue());
resizeDebounce.playFromStart();
});
pane.heightProperty().addListener((o, oldH, newH) -> {
viewport.setFitHeight(newH.doubleValue());
resizeDebounce.playFromStart();
});
// Camera-Rotation: Filter am StackPane (Capture-Phase) → feuert immer,
// auch wenn Overlay-Nodes (Tab-Button, Asset-Panel) das Event abfangen würden.
pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_PRESSED, e -> {
if (e.getButton() == MouseButton.MIDDLE
|| (e.isPrimaryButtonDown() && e.isSecondaryButtonDown())
|| (e.getButton() == MouseButton.PRIMARY && e.isAltDown())) {
prevDragX = e.getX(); prevDragY = e.getY();
}
});
pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_DRAGGED, e -> {
boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
boolean altLeft = e.isPrimaryButtonDown() && e.isAltDown();
if (e.isMiddleButtonDown() || bothDown || altLeft) {
double dx = e.getX() - prevDragX;
double dy = e.getY() - prevDragY;
input.addMouseDelta((int) dx, (int) dy);
prevDragX = e.getX(); prevDragY = e.getY();
if (bothDown) { stopEditTimer(); }
}
});
viewport.setOnMousePressed(e -> {
viewport.requestFocus();
boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown();
if (isObjectMode()) {
if (e.getButton() == MouseButton.PRIMARY && !bothDown) {
input.objectClickQueue.offer(
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));
objDragPrevX = e.getX(); objDragPrevY = e.getY();
}
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;
}
if (input.activeLayer == SharedInput.LAYER_WATERFALL) {
if (e.getButton() == MouseButton.PRIMARY) {
input.waterfallClickQueue.offer(
new SharedInput.WaterfallClick((float)e.getX(), (float)e.getY(), false));
} else if (e.getButton() == MouseButton.SECONDARY) {
input.waterfallClickQueue.offer(
new SharedInput.WaterfallClick((float)e.getX(), (float)e.getY(), true));
}
return;
}
if (input.activeLayer == SharedInput.LAYER_ITEMS) {
if (e.getButton() == MouseButton.PRIMARY && !bothDown) {
input.itemClickQueue.offer(
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, false));
} else if (e.getButton() == MouseButton.SECONDARY) {
input.itemClickQueue.offer(
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), true, false));
}
return;
}
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) {
if (e.getButton() == MouseButton.PRIMARY && !bothDown) {
input.pathNetworkClickQueue.offer(
new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), false));
} else if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
input.pathNetworkClickQueue.offer(
new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), true));
}
return;
}
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 && !e.isAltDown()) {
boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|| input.activeLayer == SharedInput.LAYER_ZONEN
|| input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|| input.activeLayer == SharedInput.LAYER_AREAS
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|| input.activeLayer == SharedInput.LAYER_WATER;
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
// Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam)
input.playToolClickQueue.offer(
new SharedInput.PlayToolClick((float) e.getX(), (float) e.getY()));
} else if (singleClickLayer) {
submitEdit(e.getX(), e.getY(), +1);
} else {
editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
submitEdit(editPressX, editPressY, editPressAction);
startEditTimer();
}
} else if (e.getButton() == MouseButton.SECONDARY) {
boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_ZONEN
|| input.activeLayer == SharedInput.LAYER_SOUND_AREAS
|| input.activeLayer == SharedInput.LAYER_AREAS
|| input.activeLayer == SharedInput.LAYER_LOCATION_ZONES
|| input.activeLayer == SharedInput.LAYER_WATER;
editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1;
submitEdit(editPressX, editPressY, editPressAction);
if (!singleClickLayer) { startEditTimer(); }
}
});
viewport.setOnMouseDragged(e -> {
if (isObjectMode()) {
if (objDragging && e.isPrimaryButtonDown()
&& !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) {
float dx = (float)(e.getX() - objDragPrevX);
float dy = (float)(e.getY() - objDragPrevY);
input.objectDragQueue.offer(new SharedInput.ObjectDrag(dx, dy));
objDragPrevX = e.getX(); objDragPrevY = e.getY();
} else if (e.isSecondaryButtonDown() && !e.isPrimaryButtonDown()
&& !e.isMiddleButtonDown()
&& input.activeLayer == SharedInput.LAYER_OBJECTS) {
float dx = (float)(e.getX() - objDragPrevX);
input.objectRotateDragQueue.offer(new SharedInput.ObjectRotateDrag(dx));
objDragPrevX = e.getX(); objDragPrevY = e.getY();
}
return;
}
if (input.activeLayer == SharedInput.LAYER_WATER && e.isPrimaryButtonDown()) {
input.waterDragQueue.offer(new SharedInput.WaterDrag((float) e.getX(), (float) e.getY()));
input.mouseScreenX = (float) e.getX();
input.mouseScreenY = (float) e.getY();
return;
}
if (input.activeLayer == SharedInput.LAYER_WATERFALL && e.isPrimaryButtonDown()) {
input.waterfallDragQueue.offer(new SharedInput.WaterfallDrag((float) e.getX(), (float) e.getY()));
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
&& e.isPrimaryButtonDown()) {
input.playToolDragQueue.offer(
new SharedInput.PlayToolDrag((float) e.getX(), (float) e.getY()));
return;
}
if (e.isPrimaryButtonDown() || e.isSecondaryButtonDown()) {
editPressX = e.getX();
editPressY = e.getY();
input.mouseScreenX = (float) e.getX();
input.mouseScreenY = (float) e.getY();
}
});
viewport.setOnMouseReleased(e -> {
objDragging = false;
if (!isObjectMode()) { stopEditTimer(); }
if (input.activeLayer == SharedInput.LAYER_WATER) {
input.waterDragEnd = true;
}
if (input.activeLayer == SharedInput.LAYER_WATERFALL) {
input.waterfallDragEnd = true;
}
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;
}
if (input.vertexSnapEnabled
&& input.objectSelectionMode == SharedInput.SEL_MODE_VERTEX) {
input.vertexSnapTrigger = true;
}
});
pane.setOnMouseMoved(e -> {
input.mouseScreenX = (float) e.getX();
input.mouseScreenY = (float) e.getY();
});
pane.setOnMouseExited(e -> input.mouseScreenX = -1f);
viewport.setOnScroll(e -> {
int steps = (int) Math.signum(e.getDeltaY());
if (e.isControlDown()) { steps *= 10; }
input.scrollAccum.addAndGet(steps);
});
return pane;
}
private boolean isObjectMode() {
return input.activeLayer == SharedInput.LAYER_OBJECTS
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT
|| input.activeLayer == SharedInput.LAYER_AUSWAHL;
}
private void startEditTimer() {
stopEditTimer();
editTimer = new javafx.animation.Timeline(
new javafx.animation.KeyFrame(javafx.util.Duration.millis(50),
ev -> submitEdit(editPressX, editPressY, editPressAction))
);
editTimer.setCycleCount(javafx.animation.Timeline.INDEFINITE);
editTimer.play();
}
private void stopEditTimer() {
if (editTimer != null) { editTimer.stop(); editTimer = null; }
}
private void submitEdit(double x, double y, int action) {
switch (input.activeLayer) {
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action));
case SharedInput.LAYER_GRASS_VERTEX -> {
input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
if (action < 0) { input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); }
}
case SharedInput.LAYER_TERRAIN_GRASS -> {
input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
if (action < 0) { input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); }
}
case 4 -> {
SharedInput.TextureEdit te = new SharedInput.TextureEdit((float) x, (float) y, action);
input.textureEditQueue.offer(te);
input.voxelTextureEditQueue.offer(te);
}
case SharedInput.LAYER_LIGHTS ->
input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_EMITTERS ->
input.emitterClickQueue.offer(new SharedInput.EmitterClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_WATER ->
input.waterClickQueue.offer(new SharedInput.WaterClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_SOUND_AREAS ->
input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_AREAS ->
input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_LOCATION_ZONES ->
input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_ZONEN -> {
input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0));
input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0));
input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0));
}
case SharedInput.LAYER_VOXEL_CLIFF ->
input.voxelCliffClickQueue.offer(new SharedInput.VoxelCliffClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_PLAY_TOOL -> {
if (action > 0) {
input.playToolClickQueue.offer(new SharedInput.PlayToolClick((float) x, (float) y));
}
}
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(
new SharedInput.ModelInteractableClick((float) x, (float) y));
}
}
}
scheduleAutoSave.run();
}
public void drawCompass() {
if (compassCanvas == null) { return; }
final double S = compassCanvas.getWidth(); // 100
final double cx = S / 2.0;
final double cy = S / 2.0;
final double R = cx - 3; // Radius bis zum Rand
javafx.scene.canvas.GraphicsContext gc = compassCanvas.getGraphicsContext2D();
gc.clearRect(0, 0, S, S);
// Hintergrund: dunkler Kreis
gc.setFill(javafx.scene.paint.Color.rgb(12, 12, 24, 0.82));
gc.fillOval(2, 2, S - 4, S - 4);
gc.setStroke(javafx.scene.paint.Color.rgb(100, 100, 160, 0.85));
gc.setLineWidth(1.5);
gc.strokeOval(2, 2, S - 4, S - 4);
// Rotierende Rose: -camYaw so dass die aktuelle Blickrichtung oben erscheint
gc.save();
gc.translate(cx, cy);
gc.rotate(-input.camYaw);
// Tick-Striche an 8 Positionen (45°-Schritte)
for (int i = 0; i < 8; i++) {
double a = Math.toRadians(i * 45.0);
double len = (i % 2 == 0) ? 9 : 5;
gc.setStroke(javafx.scene.paint.Color.rgb(160, 160, 200, 0.65));
gc.setLineWidth(i % 2 == 0 ? 1.5 : 1.0);
gc.strokeLine(
Math.sin(a) * (R - len), -Math.cos(a) * (R - len),
Math.sin(a) * R, -Math.cos(a) * R);
}
// Himmelsrichtungen (N/E/S/W), Text durch Gegenrotation immer lesbar
String[] dirs = {"N", "E", "S", "W"};
double[] angles = {0.0, 90.0, 180.0, 270.0};
for (int i = 0; i < 4; i++) {
double a = Math.toRadians(angles[i]);
double tx = Math.sin(a) * (R - 16);
double ty = -Math.cos(a) * (R - 16);
gc.save();
gc.translate(tx, ty);
gc.rotate(input.camYaw); // Gegenrotation zur Rose → Buchstabe immer aufrecht
gc.setFill(i == 0
? javafx.scene.paint.Color.rgb(255, 80, 80)
: javafx.scene.paint.Color.rgb(210, 210, 230));
gc.setFont(javafx.scene.text.Font.font(
"System", javafx.scene.text.FontWeight.BOLD, i == 0 ? 13 : 11));
gc.fillText(dirs[i], -4.5, 5.0);
gc.restore();
}
gc.restore(); // Ende rotierende Rose
// Fixer Richtungszeiger oben (gelbes Dreieck, zeigt immer die Blickrichtung)
gc.setFill(javafx.scene.paint.Color.rgb(255, 220, 60, 0.95));
gc.fillPolygon(
new double[]{cx, cx - 5, cx + 5},
new double[]{cy - R + 5, cy - R + 16, cy - R + 16},
3);
// Mittelpunkt
gc.setFill(javafx.scene.paint.Color.rgb(200, 200, 240, 0.85));
gc.fillOval(cx - 2.5, cy - 2.5, 5, 5);
}
}

View File

@@ -0,0 +1,511 @@
package de.blight.editor.toolset.world;
import de.blight.editor.ProjectRoot;
import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext;
import de.blight.editor.toolset.EditorUiHelper;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.*;
/**
* Owns the world-zone side panels:
* Auswahl, Wege, Zonen (unified), SoundArea, Area, LocationZone.
*/
public class WorldPanelController {
private final ToolContext ctx;
private final Runnable scheduleAutoSave;
// Dynamic VBox areas rebuilt by update*Panel methods
private VBox auswahlDynamicContent;
private VBox zonenDynamicContent;
private VBox soundAreaDynamicContent;
private VBox areaDynamicContent;
private VBox locationZoneDynamicContent;
private Runnable zonenKindStyleUpdater;
public WorldPanelController(ToolContext ctx, Runnable scheduleAutoSave) {
this.ctx = ctx;
this.scheduleAutoSave = scheduleAutoSave;
}
// ── Public update methods called from EditorApp AnimationTimer ─────────────
public void updateAuswahlItemPanel(String info) {
if (auswahlDynamicContent == null) return;
auswahlDynamicContent.getChildren().clear();
if (info == null) {
Label noSel = new Label("Kein Element ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
auswahlDynamicContent.getChildren().add(noSel);
return;
}
String[] parts = info.split("\\|");
String itemId = parts.length > 1 ? parts[1] : "?";
Label idLabel = new Label("Item: " + itemId);
idLabel.setStyle("-fx-font-weight: bold;");
Button deleteBtn = new Button("Löschen");
deleteBtn.setMaxWidth(Double.MAX_VALUE);
deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;");
deleteBtn.setOnAction(e -> ctx.input().deleteSelectedItemRequested = true);
auswahlDynamicContent.getChildren().addAll(idLabel, deleteBtn);
}
public void updateZonenPanel(String kind) {
if (zonenDynamicContent == null) return;
zonenDynamicContent.getChildren().clear();
switch (kind != null ? kind : "") {
case "area" -> {
VBox saved = areaDynamicContent;
areaDynamicContent = zonenDynamicContent;
updateAreaPanel(ctx.input().selectedAreaInfo);
areaDynamicContent = saved;
}
case "sound" -> {
VBox saved = soundAreaDynamicContent;
soundAreaDynamicContent = zonenDynamicContent;
updateSoundAreaPanel(ctx.input().selectedSoundAreaInfo);
soundAreaDynamicContent = saved;
}
case "location" -> {
VBox saved = locationZoneDynamicContent;
locationZoneDynamicContent = zonenDynamicContent;
updateLocationZonePanel(ctx.input().selectedLocationZoneInfo);
locationZoneDynamicContent = saved;
}
default -> {
Label noSel = new Label("Keine Zone ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
zonenDynamicContent.getChildren().add(noSel);
}
}
}
public void runZonenKindStyleUpdater() {
if (zonenKindStyleUpdater != null) zonenKindStyleUpdater.run();
}
public void updateSoundAreaPanel(String info) {
if (soundAreaDynamicContent == null) return;
soundAreaDynamicContent.getChildren().clear();
if (info == null) {
Label noSel = new Label("Kein Bereich ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
soundAreaDynamicContent.getChildren().add(noSel);
return;
}
String[] p = info.split("\\|", -1);
if (p.length < 4) return;
try {
int idx = Integer.parseInt(p[0]);
float volume = Float.parseFloat(p[2]);
boolean xfade = Boolean.parseBoolean(p[3]);
final String[] soundPath = {p[1]};
final float[] vol = {volume};
final boolean[] cf = {xfade};
Label soundLabel = new Label("Sound: " + (soundPath[0].isEmpty() ? "(keine)" : soundPath[0]));
soundLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
soundLabel.setWrapText(true);
Button browseBtn = new Button("Sound waehlen...");
browseBtn.setMaxWidth(Double.MAX_VALUE);
browseBtn.setOnAction(e -> {
java.nio.file.Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
new de.blight.editor.ui.SoundChooser(assetRoot, de.blight.editor.ui.SoundChooser.Mode.ALL)
.showAndWait()
.ifPresent(rel -> {
soundPath[0] = rel;
soundLabel.setText("Sound: " + rel);
sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
});
});
Label volLbl = new Label("Lautstärke");
volLbl.setStyle("-fx-font-size: 11;");
Slider volSlider = new Slider(0, 1, vol[0]);
volSlider.setShowTickLabels(false);
volSlider.valueProperty().addListener((o, ov, nv) -> {
vol[0] = nv.floatValue();
sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
});
CheckBox xfadeCB = new CheckBox("Loop-Crossfade");
xfadeCB.setSelected(cf[0]);
xfadeCB.setOnAction(e -> {
cf[0] = xfadeCB.isSelected();
sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]);
});
Button delBtn = new Button("Löschen");
delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> ctx.input().deleteSoundAreaRequested = true);
soundAreaDynamicContent.getChildren().addAll(
soundLabel, browseBtn, new Separator(),
volLbl, EditorUiHelper.withField(volSlider, "%.2f"), xfadeCB,
new Separator(), delBtn);
} catch (NumberFormatException ignored) {}
}
public void updateAreaPanel(String info) {
if (areaDynamicContent == null) return;
areaDynamicContent.getChildren().clear();
if (info == null) {
Label noSel = new Label("Keine Area ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
areaDynamicContent.getChildren().add(noSel);
return;
}
String[] p = info.split("\\|", 3);
if (p.length < 1) return;
try {
int idx = Integer.parseInt(p[0]);
String currentAreaId = p.length > 1 ? p[1] : "";
java.util.List<de.blight.common.model.trigger.Trigger> existingTriggers =
p.length > 2
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
: new java.util.ArrayList<>();
java.util.List<String> areaIds = new java.util.ArrayList<>();
areaIds.add("");
try {
de.blight.common.AreaDefinitionIO.load().stream()
.map(de.blight.common.AreaDefinition::id)
.filter(s -> !s.isBlank())
.forEach(areaIds::add);
} catch (Exception ignored) {}
Label areaLbl = new Label("Verknüpfte Area:");
areaLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
Label nameKeyLbl = new Label(currentAreaId.isBlank() ? "" : currentAreaId + ".name");
nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;");
de.blight.editor.ui.TriggerListEditor[] editorHolder = {null};
ComboBox<String> areaCombo = new ComboBox<>();
areaCombo.getItems().setAll(areaIds);
areaCombo.setValue(areaIds.contains(currentAreaId) ? currentAreaId : "");
areaCombo.setMaxWidth(Double.MAX_VALUE);
areaCombo.setPromptText("Area wählen…");
areaCombo.valueProperty().addListener((obs, o, n) -> {
String aid = n != null ? n : "";
nameKeyLbl.setText(aid.isBlank() ? "" : aid + ".name");
java.util.List<de.blight.common.model.trigger.Trigger> curTriggers =
editorHolder[0] != null
? new java.util.ArrayList<>(editorHolder[0].getTriggers())
: existingTriggers;
ctx.input().pendingArea.set(new de.blight.common.PlacedArea(
new float[0], new float[0], aid, curTriggers));
scheduleAutoSave.run();
});
Label triggerLbl = new Label("Trigger:");
triggerLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
editorHolder[0] = new de.blight.editor.ui.TriggerListEditor(existingTriggers, () -> {
String aid = areaCombo.getValue() != null ? areaCombo.getValue() : currentAreaId;
ctx.input().pendingArea.set(new de.blight.common.PlacedArea(
new float[0], new float[0], aid,
new java.util.ArrayList<>(editorHolder[0].getTriggers())));
scheduleAutoSave.run();
});
Button delBtn = new Button("Löschen");
delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> { ctx.input().deleteAreaRequested = true; scheduleAutoSave.run(); });
areaDynamicContent.getChildren().addAll(
areaLbl, areaCombo, nameKeyLbl,
new Separator(),
triggerLbl, editorHolder[0],
new Separator(),
delBtn);
} catch (NumberFormatException ignored) {}
}
public void updateLocationZonePanel(String info) {
if (locationZoneDynamicContent == null) return;
locationZoneDynamicContent.getChildren().clear();
if (info == null) {
Label noSel = new Label("Keine Location ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
locationZoneDynamicContent.getChildren().add(noSel);
return;
}
String[] p = info.split("\\|", 3);
if (p.length < 1) return;
try {
int idx = Integer.parseInt(p[0]);
String nameId = p.length > 1 ? p[1] : "";
java.util.List<de.blight.common.model.trigger.Trigger> existingTriggers =
p.length > 2
? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2])
: new java.util.ArrayList<>();
java.util.List<String> locationIds = new java.util.ArrayList<>();
locationIds.add("");
try {
de.blight.common.LocationIO.load().stream()
.map(de.blight.common.model.Location::getId)
.filter(s -> !s.isBlank())
.forEach(locationIds::add);
} catch (Exception ignored) {}
Label nameLbl = new Label("Verknüpfte Location:");
nameLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;");
Label nameKeyLbl = new Label(nameId.isBlank() ? "" : nameId + ".name");
nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;");
ComboBox<String> locationCombo = new ComboBox<>();
locationCombo.getItems().setAll(locationIds);
locationCombo.setValue(locationIds.contains(nameId) ? nameId : "");
locationCombo.setMaxWidth(Double.MAX_VALUE);
locationCombo.setPromptText("Location wählen…");
locationCombo.valueProperty().addListener((obs, o, n) -> {
String lid = n != null ? n : "";
nameKeyLbl.setText(lid.isBlank() ? "" : lid + ".name");
ctx.input().pendingLocationZone.set(new de.blight.common.PlacedLocationZone(
new float[0], new float[0], lid, existingTriggers));
scheduleAutoSave.run();
});
Button delBtn = new Button("Löschen");
delBtn.setMaxWidth(Double.MAX_VALUE);
delBtn.setStyle("-fx-text-fill: #c0392b;");
delBtn.setOnAction(e -> { ctx.input().deleteLocationZoneRequested = true; scheduleAutoSave.run(); });
locationZoneDynamicContent.getChildren().addAll(
nameLbl, locationCombo, nameKeyLbl,
new Separator(),
delBtn);
} catch (NumberFormatException ignored) {}
}
// ── Panel builders ─────────────────────────────────────────────────────────
public VBox buildAuswahlPanel() {
VBox panel = new VBox(10);
panel.setPadding(new Insets(10));
panel.setPrefWidth(260);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
Label title = new Label("Auswahl");
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;");
panel.getChildren().addAll(title, new Separator());
panel.getChildren().addAll(
EditorUiHelper.styledHint("L-Klick: Element auswählen"),
EditorUiHelper.styledHint(" → wechselt automatisch zum"),
EditorUiHelper.styledHint(" passenden Werkzeug"),
new Separator(),
EditorUiHelper.styledHint("Selektierbar:"),
EditorUiHelper.styledHint(" + Platzierte Objekte"),
EditorUiHelper.styledHint(" * Lichter / Emitter"),
EditorUiHelper.styledHint(" ~ Wasser / Wasserfall"),
EditorUiHelper.styledHint(" -- Wege / Knoten"),
EditorUiHelper.styledHint(" [ ] Zonen"),
EditorUiHelper.styledHint(" o Spawnpunkte (Items)"),
new Separator());
auswahlDynamicContent = new VBox(6);
Label noSel = new Label("Kein Element ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
auswahlDynamicContent.getChildren().add(noSel);
panel.getChildren().add(auswahlDynamicContent);
return panel;
}
public VBox buildWegePanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(
EditorUiHelper.sectionTitle("Wegnetz"),
new Separator(),
EditorUiHelper.styledHint("L-Klick: neuen Knoten setzen"),
EditorUiHelper.styledHint("L-Klick auf Knoten: auswählen"),
EditorUiHelper.styledHint(" (2 gewählte → Kante erzeugen)"),
EditorUiHelper.styledHint("L-Klick + gewählter Knoten:"),
EditorUiHelper.styledHint(" neuen Knoten + Kante"),
EditorUiHelper.styledHint("R-Klick auf Knoten: löschen"),
EditorUiHelper.styledHint("R-Klick auf Terrain: abwählen"));
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(260);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
public VBox buildZonenPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
final String STYLE_AREA_ON = "-fx-background-color: #e05050; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
final String STYLE_AREA_OFF = "-fx-background-color: #b03030; -fx-text-fill: #ddd; -fx-font-weight: bold;";
final String STYLE_SOUND_ON = "-fx-background-color: #e07818; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
final String STYLE_SOUND_OFF = "-fx-background-color: #a05010; -fx-text-fill: #ddd; -fx-font-weight: bold;";
final String STYLE_LOC_ON = "-fx-background-color: #c8a800; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;";
final String STYLE_LOC_OFF = "-fx-background-color: #8a7200; -fx-text-fill: #ddd; -fx-font-weight: bold;";
Button areaKindBtn = new Button("+ Neue Area zeichnen");
Button soundKindBtn = new Button("+ Neuen Sound zeichnen");
Button locationKindBtn = new Button("+ Neue Location zeichnen");
areaKindBtn.setMaxWidth(Double.MAX_VALUE);
soundKindBtn.setMaxWidth(Double.MAX_VALUE);
locationKindBtn.setMaxWidth(Double.MAX_VALUE);
Runnable updateKindStyles = () -> {
String k = ctx.input().zonenNewKind;
areaKindBtn.setStyle("area".equals(k) ? STYLE_AREA_ON : STYLE_AREA_OFF);
soundKindBtn.setStyle("sound".equals(k) ? STYLE_SOUND_ON : STYLE_SOUND_OFF);
locationKindBtn.setStyle("location".equals(k) ? STYLE_LOC_ON : STYLE_LOC_OFF);
};
updateKindStyles.run();
zonenKindStyleUpdater = updateKindStyles;
areaKindBtn.setOnAction(e -> { ctx.input().zonenNewKind = "area"; updateKindStyles.run(); });
soundKindBtn.setOnAction(e -> { ctx.input().zonenNewKind = "sound"; updateKindStyles.run(); });
locationKindBtn.setOnAction(e -> { ctx.input().zonenNewKind = "location"; updateKindStyles.run(); });
areaKindBtn.setTooltip(new Tooltip(
"Area: Musik-Zone mit Tag-, Nacht- und Kampf-Track.\nAreas dürfen sich nicht überschneiden."));
soundKindBtn.setTooltip(new Tooltip(
"Sound: Ambient-Soundbereich mit wählbarer Audio-Datei,\nLautstärke und Loop-Crossfade."));
locationKindBtn.setTooltip(new Tooltip(
"Location: Gameplay-Trigger-Zone, z.B. zum Starten\nvon Quests oder Dialogen beim Betreten."));
inner.getChildren().addAll(
EditorUiHelper.sectionTitle("Neue Zone"),
areaKindBtn, soundKindBtn, locationKindBtn,
new Separator(),
EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"),
EditorUiHelper.styledHint("R-Klick (beim Zeichnen) → letzten Punkt rückgängig"),
EditorUiHelper.styledHint("R-Klick (sonst) → Auswahl aufheben"),
EditorUiHelper.styledHint("ESC → Zeichnen abbrechen"),
EditorUiHelper.styledHint("Entf → Gewählte Zone löschen"),
new Separator(),
EditorUiHelper.sectionTitle("Gewählte Zone"),
new Separator());
zonenDynamicContent = new VBox(6);
Label noSel = new Label("Keine Zone ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
zonenDynamicContent.getChildren().add(noSel);
inner.getChildren().add(zonenDynamicContent);
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
public VBox buildSoundAreaPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(
EditorUiHelper.sectionTitle("Sound-Bereiche"),
EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"),
EditorUiHelper.styledHint("R-Klick → Polygon schließen / Auswahl aufheben"),
EditorUiHelper.styledHint("Entf → Bereich löschen"),
new Separator(),
EditorUiHelper.sectionTitle("Gewählter Bereich"),
new Separator());
soundAreaDynamicContent = new VBox(6);
Label noSel = new Label("Kein Bereich ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
soundAreaDynamicContent.getChildren().add(noSel);
inner.getChildren().add(soundAreaDynamicContent);
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
public VBox buildAreaPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(
EditorUiHelper.sectionTitle("Areas"),
EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"),
EditorUiHelper.styledHint("R-Klick → Polygon schließen / Auswahl aufheben"),
EditorUiHelper.styledHint("ESC → Zeichnen abbrechen"),
EditorUiHelper.styledHint("Entf → Area löschen"),
EditorUiHelper.styledHint("Areas dürfen sich nicht überschneiden"),
new Separator(),
EditorUiHelper.sectionTitle("Gewählte Area"),
new Separator());
areaDynamicContent = new VBox(6);
Label noSel = new Label("Keine Area ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
areaDynamicContent.getChildren().add(noSel);
inner.getChildren().add(areaDynamicContent);
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
public VBox buildLocationZonePanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(
EditorUiHelper.sectionTitle("Locations"),
EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"),
EditorUiHelper.styledHint("R-Klick → Polygon schließen / Auswahl aufheben"),
EditorUiHelper.styledHint("ESC → Zeichnen abbrechen"),
EditorUiHelper.styledHint("Entf → Location löschen"),
EditorUiHelper.styledHint("Locations dürfen sich überschneiden"),
new Separator(),
EditorUiHelper.sectionTitle("Gewählte Location"),
new Separator());
locationZoneDynamicContent = new VBox(6);
Label noSel = new Label("Keine Location ausgewählt");
noSel.setStyle("-fx-text-fill: #888;");
locationZoneDynamicContent.getChildren().add(noSel);
inner.getChildren().add(locationZoneDynamicContent);
ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
VBox panel = new VBox(scroll);
VBox.setVgrow(scroll, Priority.ALWAYS);
panel.setPrefWidth(270);
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
return panel;
}
// ── Internal ──────────────────────────────────────────────────────────────
private void sendSoundAreaUpdate(int idx, String soundPath, float volume, boolean crossfade) {
SharedInput in = ctx.input();
if (in.selectedSoundAreaInfo == null) return;
in.selectedSoundAreaInfo = idx + "|" + soundPath + "|" + volume + "|" + crossfade;
in.pendingSoundArea.set(new de.blight.common.PlacedSoundArea(
new float[0], new float[0], soundPath, volume, crossfade));
scheduleAutoSave.run();
}
}

View File

@@ -1 +1,2 @@
# x y z activationRadius texturePath imagesX imagesY startR startG startB startA endR endG endB endA startSize endSize velX velY velZ velVariation gravX gravY gravZ lowLife highLife maxParticles emitRate
0.93604 2.50000 -3.56553 20.00000 Effects/Explosion/flame.png 2 2 1.0000 0.7000 0.1000 1.0000 1.0000 0.1000 0.0000 0.0000 0.5000 1.2000 0.0000 2.5000 0.0000 0.5000 0.0000 -0.1000 0.0000 1.0000 3.0000 60 25.0000

View File

@@ -1 +1,3 @@
# x y z r g b intensity radius
-2.18687 3.00000 -5.43333 1.00000 1.00000 1.00000 1.00000 20.00000
0.42349 3.00000 -10.67357 1.00000 1.00000 1.00000 1.00000 20.00000

View File

@@ -1,2 +1,3 @@
# polygon_points waterHeight flowDegrees speed waveScale waveAmplitude transparency waterR waterG waterB deepR deepG deepB
155.13664,11.00000,-898.11578;120.48705,11.00000,-897.61938;107.05534,11.00000,-890.41797;96.71703,11.00000,-875.01575;98.34164,11.00000,-854.85681;182.64165,11.00000,-829.23749;195.95512,11.00000,-848.04095 11.00000 180.0 0.5000 0.008000 0.10000 0.15000 0.05000 0.25000 0.55000 0.02000 0.12000 0.30000
219.67131,1.90000,-796.04559;197.17520,1.90000,-800.89313;192.60945,1.90000,-797.65149;190.13742,1.90000,-767.83209;221.88794,1.90000,-767.83209 1.90000 0.0 0.5000 0.008000 0.10000 0.15000 0.05000 0.25000 0.55000 0.02000 0.12000 0.30000