Soundsystem weiter ausgebaut
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioInputStream;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.Clip;
|
||||
import javax.sound.sampled.LineEvent;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Nicht-modales Popup zum Vorhören von OGG-Dateien.
|
||||
* Nutzt javax.sound.sampled + j-ogg-vorbis SPI (bereits transitiv über jme3-jogg
|
||||
* im Classpath). Kein GStreamer, kein javafx.media nötig.
|
||||
*/
|
||||
public class AudioPreviewPopup {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AudioPreviewPopup.class);
|
||||
|
||||
private Stage stage;
|
||||
private Clip clip;
|
||||
private boolean paused = false;
|
||||
private boolean repeat = false;
|
||||
|
||||
private List<Path> playlist = Collections.emptyList();
|
||||
private int idx = 0;
|
||||
|
||||
private Label fileLabel;
|
||||
private Label folderLabel;
|
||||
private Button playPauseBtn;
|
||||
private ToggleButton repeatBtn;
|
||||
|
||||
// ── API ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Öffnet das Popup (oder bringt es in den Vordergrund) und lädt die Datei. */
|
||||
public void open(Path file) {
|
||||
if (stage == null) buildStage();
|
||||
loadFile(file);
|
||||
if (!stage.isShowing()) stage.show();
|
||||
stage.toFront();
|
||||
}
|
||||
|
||||
// ── Intern ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void loadFile(Path file) {
|
||||
buildPlaylist(file.getParent());
|
||||
idx = playlist.indexOf(file);
|
||||
if (idx < 0) idx = 0;
|
||||
playIndex(idx);
|
||||
}
|
||||
|
||||
private void buildPlaylist(Path folder) {
|
||||
if (folder == null) { playlist = Collections.emptyList(); return; }
|
||||
try (var s = Files.list(folder)) {
|
||||
playlist = s.filter(p -> Files.isRegularFile(p)
|
||||
&& p.getFileName().toString().toLowerCase().endsWith(".ogg"))
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
} catch (IOException e) {
|
||||
log.warn("[AudioPreview] Ordner nicht lesbar: {}", folder);
|
||||
playlist = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private void navigate(int delta) {
|
||||
if (playlist.isEmpty()) return;
|
||||
idx = (idx + delta + playlist.size()) % playlist.size();
|
||||
playIndex(idx);
|
||||
}
|
||||
|
||||
private void playIndex(int i) {
|
||||
if (playlist.isEmpty()) return;
|
||||
Path file = playlist.get(i);
|
||||
|
||||
stopClip();
|
||||
fileLabel.setText(file.getFileName().toString());
|
||||
folderLabel.setText(file.getParent() != null ? shortenPath(file.getParent()) : "");
|
||||
playPauseBtn.setText("⏳");
|
||||
playPauseBtn.setDisable(true);
|
||||
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
AudioInputStream raw = AudioSystem.getAudioInputStream(file.toFile());
|
||||
AudioFormat src = raw.getFormat();
|
||||
float rate = src.getSampleRate() < 0 ? 44100f : src.getSampleRate();
|
||||
int ch = src.getChannels() < 0 ? 1 : src.getChannels();
|
||||
AudioFormat pcm = new AudioFormat(
|
||||
AudioFormat.Encoding.PCM_SIGNED,
|
||||
rate, 16, ch, ch * 2, rate, false);
|
||||
AudioInputStream pcmStream = AudioSystem.getAudioInputStream(pcm, raw);
|
||||
Clip newClip = AudioSystem.getClip();
|
||||
newClip.open(pcmStream);
|
||||
|
||||
newClip.addLineListener(ev -> {
|
||||
if (ev.getType() == LineEvent.Type.STOP && !paused && !repeat) {
|
||||
Platform.runLater(() -> playPauseBtn.setText("▶"));
|
||||
}
|
||||
});
|
||||
|
||||
Platform.runLater(() -> {
|
||||
clip = newClip;
|
||||
paused = false;
|
||||
playPauseBtn.setDisable(false);
|
||||
if (repeat) {
|
||||
clip.loop(Clip.LOOP_CONTINUOUSLY);
|
||||
} else {
|
||||
clip.start();
|
||||
}
|
||||
playPauseBtn.setText("⏸");
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.warn("[AudioPreview] Nicht ladbar '{}': {}", file.getFileName(), e.getMessage());
|
||||
Platform.runLater(() -> {
|
||||
fileLabel.setText("⚠ " + file.getFileName());
|
||||
playPauseBtn.setText("▶");
|
||||
playPauseBtn.setDisable(false);
|
||||
});
|
||||
}
|
||||
}, "audio-preview-load");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
private void togglePlay() {
|
||||
if (clip == null) {
|
||||
playIndex(idx);
|
||||
return;
|
||||
}
|
||||
if (clip.isRunning()) {
|
||||
clip.stop();
|
||||
paused = true;
|
||||
playPauseBtn.setText("▶");
|
||||
} else {
|
||||
clip.start();
|
||||
paused = false;
|
||||
playPauseBtn.setText("⏸");
|
||||
}
|
||||
}
|
||||
|
||||
private void stopClip() {
|
||||
if (clip != null) {
|
||||
clip.stop();
|
||||
clip.close();
|
||||
clip = null;
|
||||
}
|
||||
paused = false;
|
||||
}
|
||||
|
||||
private void onRepeatChanged(boolean on) {
|
||||
repeat = on;
|
||||
if (clip == null) return;
|
||||
if (on) {
|
||||
clip.loop(Clip.LOOP_CONTINUOUSLY);
|
||||
} else {
|
||||
clip.loop(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── UI-Aufbau ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void buildStage() {
|
||||
stage = new Stage(StageStyle.UTILITY);
|
||||
stage.setTitle("Audio-Vorschau");
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setResizable(false);
|
||||
stage.setOnCloseRequest(e -> stopClip());
|
||||
|
||||
fileLabel = new Label("–");
|
||||
fileLabel.setStyle("-fx-font-weight:bold; -fx-text-fill:#eee; -fx-font-size:13;");
|
||||
fileLabel.setWrapText(true);
|
||||
fileLabel.setMaxWidth(340);
|
||||
|
||||
folderLabel = new Label("");
|
||||
folderLabel.setStyle("-fx-text-fill:#777; -fx-font-size:10;");
|
||||
folderLabel.setMaxWidth(340);
|
||||
folderLabel.setWrapText(true);
|
||||
|
||||
Button prevBtn = makeBtn("◀", "Vorherige Datei", () -> navigate(-1));
|
||||
playPauseBtn = makeBtn("▶", "Abspielen / Pausieren", this::togglePlay);
|
||||
Button nextBtn = makeBtn("▶▶", "Nächste Datei", () -> navigate(+1));
|
||||
|
||||
repeatBtn = new ToggleButton("🔁");
|
||||
repeatBtn.setTooltip(new Tooltip("Wiederholen"));
|
||||
styleToggle(repeatBtn, false);
|
||||
repeatBtn.selectedProperty().addListener((o, ov, nv) -> {
|
||||
styleToggle(repeatBtn, nv);
|
||||
onRepeatChanged(nv);
|
||||
});
|
||||
|
||||
HBox controls = new HBox(6, prevBtn, playPauseBtn, nextBtn, repeatBtn);
|
||||
controls.setAlignment(Pos.CENTER);
|
||||
controls.setPadding(new Insets(8, 0, 4, 0));
|
||||
|
||||
VBox root = new VBox(6, fileLabel, folderLabel, controls);
|
||||
root.setPadding(new Insets(12));
|
||||
root.setStyle("-fx-background-color:#2a2a2a;");
|
||||
root.setPrefWidth(360);
|
||||
|
||||
stage.setScene(new Scene(root));
|
||||
}
|
||||
|
||||
private Button makeBtn(String text, String tip, Runnable action) {
|
||||
Button b = new Button(text);
|
||||
b.setTooltip(new Tooltip(tip));
|
||||
styleBtn(b, false, false);
|
||||
b.setOnMouseEntered(e -> styleBtn(b, true, false));
|
||||
b.setOnMouseExited(e -> styleBtn(b, false, false));
|
||||
b.setOnMousePressed(e -> styleBtn(b, true, true));
|
||||
b.setOnMouseReleased(e -> styleBtn(b, true, false));
|
||||
b.setOnAction(e -> action.run());
|
||||
return b;
|
||||
}
|
||||
|
||||
private void styleBtn(Button b, boolean hover, boolean pressed) {
|
||||
String bg = pressed ? "#606060" : hover ? "#4d4d4d" : "#3a3a3a";
|
||||
b.setStyle("-fx-background-color:" + bg + ";" +
|
||||
"-fx-text-fill:#eee;" +
|
||||
"-fx-min-width:36; -fx-min-height:30;" +
|
||||
"-fx-border-color:#555; -fx-border-width:1; -fx-border-radius:3;" +
|
||||
"-fx-background-radius:3; -fx-cursor:hand;");
|
||||
}
|
||||
|
||||
private void styleToggle(ToggleButton b, boolean active) {
|
||||
String bg = active ? "#1565c0" : "#3a3a3a";
|
||||
String border = active ? "#1976d2" : "#555";
|
||||
b.setStyle("-fx-background-color:" + bg + ";" +
|
||||
"-fx-text-fill:#eee;" +
|
||||
"-fx-min-width:36; -fx-min-height:30;" +
|
||||
"-fx-border-color:" + border + "; -fx-border-width:1; -fx-border-radius:3;" +
|
||||
"-fx-background-radius:3; -fx-cursor:hand;");
|
||||
}
|
||||
|
||||
private String shortenPath(Path p) {
|
||||
String s = p.toString();
|
||||
int audio = s.indexOf("/audio/");
|
||||
return audio >= 0 ? s.substring(audio) : s;
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ public class EditorApp extends Application {
|
||||
private StackPane plantPreviewPanel; // einmaliges Vorschau-Panel für alle Generatoren
|
||||
private Stage primaryStage;
|
||||
private JmeEditorApp jmeApp;
|
||||
private AudioPreviewPopup audioPreviewPopup;
|
||||
|
||||
// Baum-Generator-Zustand (wird beim Preset-Wechsel neu gesetzt)
|
||||
private TreeParams treeParams = TreeParams.oak();
|
||||
@@ -155,6 +156,7 @@ public class EditorApp extends Application {
|
||||
private String animSetPendingPlayClip = null;
|
||||
private ComboBox<String> animSetModelCombo;
|
||||
private boolean animSetDirty = false;
|
||||
private boolean charDirty = false;
|
||||
private String animSetCurrentName = null;
|
||||
private Path animSetCurrentDir = null;
|
||||
// Anim-Offset-Editor (innerhalb AnimSet-Editor)
|
||||
@@ -163,6 +165,10 @@ public class EditorApp extends Application {
|
||||
javafx.collections.FXCollections.observableArrayList();
|
||||
private java.util.Map<String, de.blight.game.animation.AnimOffset>
|
||||
animSetOffsets = new java.util.LinkedHashMap<>();
|
||||
// Sub-Clip-Editor (innerhalb AnimSet-Editor)
|
||||
private ListView<String> animSetPartsListView;
|
||||
private java.util.Map<String, de.blight.game.animation.AnimSet.SubClipDef>
|
||||
editableSubClips = new java.util.LinkedHashMap<>();
|
||||
|
||||
// Character-Editor-Zustand
|
||||
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
||||
@@ -173,6 +179,11 @@ public class EditorApp extends Application {
|
||||
private javafx.scene.control.ComboBox<String> charStatusCombo;
|
||||
private javafx.scene.control.CheckBox charTraderCheck;
|
||||
private javafx.scene.control.ListView<String> charTraderItemsView;
|
||||
/** Default-Nachrichten je NPC-Status (TextReference-Schlüssel). */
|
||||
private javafx.scene.control.TextField charMsgFriendly;
|
||||
private javafx.scene.control.TextField charMsgNeutral;
|
||||
private javafx.scene.control.TextField charMsgEnraged;
|
||||
private javafx.scene.control.TextField charMsgEnemy;
|
||||
// Abilities (MainCharacter)
|
||||
private javafx.scene.control.Spinner<Integer> abMagicSpin, abStaffSpin, abSwordSpin,
|
||||
abArcherySpin, abHeavySpin, abCrossbowSpin, abThieverySpin,
|
||||
@@ -211,6 +222,11 @@ public class EditorApp extends Application {
|
||||
// Spiel-Starten-Werkzeug-Zustand
|
||||
private TextField spawnXField;
|
||||
private TextField spawnZField;
|
||||
private Label tempSpawnCoordsLabel;
|
||||
private Label permSpawnCoordsLabel;
|
||||
private Button gameNewBtn;
|
||||
private boolean launchNewGameAfterSave = false;
|
||||
private boolean pendingNewGame = false;
|
||||
|
||||
// Baum-Ordner-Modus
|
||||
private Label randomTreeStatusLabel;
|
||||
@@ -249,6 +265,7 @@ public class EditorApp extends Application {
|
||||
private Spinner<Double> modelEditorInteractableXSpin = null;
|
||||
private Spinner<Double> modelEditorInteractableYSpin = null;
|
||||
private Spinner<Double> modelEditorInteractableZSpin = null;
|
||||
private ComboBox<String> modelEditorFootstepSurfaceCB = null;
|
||||
private boolean updatingInteractableSpinnersFromJme = false;
|
||||
|
||||
// Modell-Import-Zustand
|
||||
@@ -396,6 +413,16 @@ public class EditorApp extends Application {
|
||||
stage.setMinWidth(900);
|
||||
stage.setMinHeight(600);
|
||||
stage.setOnCloseRequest(e -> {
|
||||
if (animSetDirty || charDirty) {
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
"Es gibt ungespeicherte Änderungen.\nTrotzdem beenden?",
|
||||
ButtonType.YES, ButtonType.NO);
|
||||
confirm.setHeaderText("Ungespeicherte Änderungen");
|
||||
confirm.showAndWait().ifPresent(btn -> {
|
||||
if (btn != ButtonType.YES) e.consume();
|
||||
});
|
||||
if (e.isConsumed()) return;
|
||||
}
|
||||
saveCameraPrefs();
|
||||
if (jmeApp != null) jmeApp.stop();
|
||||
Platform.exit();
|
||||
@@ -603,6 +630,11 @@ public class EditorApp extends Application {
|
||||
updateSpawnFields(input.pickedSpawnInfo);
|
||||
}
|
||||
|
||||
if (input.permSpawnChanged) {
|
||||
input.permSpawnChanged = false;
|
||||
if (permSpawnCoordsLabel != null) permSpawnCoordsLabel.setText(permSpawnCoordsText());
|
||||
}
|
||||
|
||||
// Modell-Editor: gebakte Scale aus j3o erkannt → Spinner aktualisieren
|
||||
if (input.modelEditorBakedScaleDetected) {
|
||||
input.modelEditorBakedScaleDetected = false;
|
||||
@@ -3972,6 +4004,10 @@ public class EditorApp extends Application {
|
||||
switchToAnimPreview();
|
||||
input.animPreviewLoadPath = relPath;
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…");
|
||||
} else if (cat == audioNode && relPath.endsWith(".ogg")) {
|
||||
if (audioPreviewPopup == null) audioPreviewPopup = new AudioPreviewPopup();
|
||||
audioPreviewPopup.open(p);
|
||||
setStatus("▶ " + relPath);
|
||||
} else if (relPath.endsWith(".animset.json")) {
|
||||
openAnimSetEditor(relPath, p);
|
||||
} else if (relPath.endsWith(".character")) {
|
||||
@@ -4812,7 +4848,12 @@ public class EditorApp extends Application {
|
||||
/** Liest die AnimClip-Namen aus einer J3O-Datei, ohne den JME3-Thread zu benötigen. */
|
||||
private List<String> readAnimClipNames(Path j3oPath) {
|
||||
try {
|
||||
com.jme3.asset.DesktopAssetManager assetManager = new com.jme3.asset.DesktopAssetManager(true);
|
||||
assetManager.registerLocator(
|
||||
de.blight.game.animation.AnimationLibrary.findAssetRoot().toAbsolutePath().toString(),
|
||||
com.jme3.asset.plugins.FileLocator.class);
|
||||
com.jme3.export.binary.BinaryImporter imp = new com.jme3.export.binary.BinaryImporter();
|
||||
imp.setAssetManager(assetManager);
|
||||
com.jme3.scene.Spatial s = (com.jme3.scene.Spatial) imp.load(j3oPath.toFile());
|
||||
com.jme3.anim.AnimComposer ac =
|
||||
de.blight.game.animation.RetargetingSystem.findAnimComposer(s);
|
||||
@@ -6134,6 +6175,26 @@ public class EditorApp extends Application {
|
||||
input.modelInteractableOffsetChanged = true;
|
||||
});
|
||||
|
||||
// ── Fußgeräusch-Untergrund ────────────────────────────────────────────
|
||||
Label footstepTitle = new Label("Fußgeräusch-Untergrund:");
|
||||
footstepTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||
|
||||
modelEditorFootstepSurfaceCB = new ComboBox<>();
|
||||
modelEditorFootstepSurfaceCB.getItems().add("");
|
||||
for (de.blight.game.audio.SurfaceType st : de.blight.game.audio.SurfaceType.values()) {
|
||||
modelEditorFootstepSurfaceCB.getItems().add(st.name());
|
||||
}
|
||||
String currentSurface = meta.footstepSurface() != null ? meta.footstepSurface() : "";
|
||||
modelEditorFootstepSurfaceCB.setValue(
|
||||
modelEditorFootstepSurfaceCB.getItems().contains(currentSurface) ? currentSurface : "");
|
||||
modelEditorFootstepSurfaceCB.setMaxWidth(Double.MAX_VALUE);
|
||||
modelEditorFootstepSurfaceCB.setConverter(new javafx.util.StringConverter<>() {
|
||||
@Override public String toString(String s) {
|
||||
return (s == null || s.isEmpty()) ? "(aus Textur)" : s;
|
||||
}
|
||||
@Override public String fromString(String s) { return s; }
|
||||
});
|
||||
|
||||
// ── Buttons ───────────────────────────────────────────────────────────
|
||||
Button saveBtn = new Button("💾 Speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
@@ -6172,7 +6233,10 @@ public class EditorApp extends Application {
|
||||
input.modelInteractableOffsetX,
|
||||
input.modelInteractableOffsetY,
|
||||
input.modelInteractableOffsetZ,
|
||||
input.modelInteractableRotY));
|
||||
input.modelInteractableRotY,
|
||||
modelEditorFootstepSurfaceCB != null
|
||||
? modelEditorFootstepSurfaceCB.getValue()
|
||||
: ""));
|
||||
|
||||
placeBtn.setOnAction(e -> {
|
||||
input.modelEditorCloseRequest = true;
|
||||
@@ -6215,6 +6279,8 @@ public class EditorApp extends Application {
|
||||
new Separator(),
|
||||
interactTitle, modelEditorInteractableCB, restPointBox,
|
||||
new Separator(),
|
||||
footstepTitle, modelEditorFootstepSurfaceCB,
|
||||
new Separator(),
|
||||
saveBtn, placeBtn, closeBtn
|
||||
);
|
||||
return panel;
|
||||
@@ -6667,7 +6733,8 @@ public class EditorApp extends Application {
|
||||
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters,
|
||||
de.blight.common.model.InteractableType interactableType,
|
||||
float interactableOffsetX, float interactableOffsetY,
|
||||
float interactableOffsetZ, float interactableRotY) {
|
||||
float interactableOffsetZ, float interactableRotY,
|
||||
String footstepSurface) {
|
||||
// Scale wird in j3o eingebrannt → Meta bekommt immer 1.0 (kein doppelter Scale beim Laden)
|
||||
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||
name, category, tags, 1f, 1f, 1f, uniform,
|
||||
@@ -6675,7 +6742,8 @@ public class EditorApp extends Application {
|
||||
lod1Path, lod2Path, 30f, 80f, 120f,
|
||||
lights, emitters,
|
||||
interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE,
|
||||
interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY);
|
||||
interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY,
|
||||
footstepSurface != null ? footstepSurface : "");
|
||||
|
||||
if (absolutePath == null || !absolutePath.toFile().exists()) {
|
||||
setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert");
|
||||
@@ -7008,9 +7076,15 @@ public class EditorApp extends Application {
|
||||
if (bothDown) {
|
||||
stopEditTimer();
|
||||
} else if (e.getButton() == MouseButton.PRIMARY) {
|
||||
editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
|
||||
submitEdit(editPressX, editPressY, editPressAction);
|
||||
startEditTimer();
|
||||
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 {
|
||||
editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
|
||||
submitEdit(editPressX, editPressY, editPressAction);
|
||||
startEditTimer();
|
||||
}
|
||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||
editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1;
|
||||
submitEdit(editPressX, editPressY, editPressAction);
|
||||
@@ -7030,6 +7104,15 @@ public class EditorApp extends Application {
|
||||
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();
|
||||
@@ -7041,6 +7124,9 @@ public class EditorApp extends Application {
|
||||
viewport.setOnMouseReleased(e -> {
|
||||
objDragging = false;
|
||||
if (!isObjectMode()) stopEditTimer();
|
||||
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
|
||||
input.playToolMouseUp = true;
|
||||
}
|
||||
if (input.vertexSnapEnabled
|
||||
&& input.objectSelectionMode == SharedInput.SEL_MODE_VERTEX) {
|
||||
input.vertexSnapTrigger = true;
|
||||
@@ -7141,17 +7227,28 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
private void launchGame() {
|
||||
if (launchGameAfterSave) return; // bereits ausstehend
|
||||
if (launchGameAfterSave) return;
|
||||
launchGameAfterSave = true;
|
||||
if (gamePlayBtn != null) {
|
||||
gamePlayBtn.setDisable(true);
|
||||
gamePlayBtn.setText("⏳ Startet…");
|
||||
}
|
||||
pendingNewGame = false;
|
||||
if (gamePlayBtn != null) { gamePlayBtn.setDisable(true); gamePlayBtn.setText("⏳ Startet…"); }
|
||||
if (gameNewBtn != null) gameNewBtn.setDisable(true);
|
||||
input.saveRequested = true;
|
||||
setStatus("Karte wird gespeichert, Spiel startet…");
|
||||
}
|
||||
|
||||
private void launchNewGame() {
|
||||
if (launchGameAfterSave) return;
|
||||
launchGameAfterSave = true;
|
||||
pendingNewGame = true;
|
||||
if (gameNewBtn != null) { gameNewBtn.setDisable(true); gameNewBtn.setText("⏳ Startet…"); }
|
||||
if (gamePlayBtn != null) gamePlayBtn.setDisable(true);
|
||||
input.saveRequested = true;
|
||||
setStatus("Karte wird gespeichert, Neues Spiel startet…");
|
||||
}
|
||||
|
||||
private void startGameProcess() {
|
||||
final boolean isNewGame = pendingNewGame;
|
||||
pendingNewGame = false;
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
|
||||
@@ -7178,9 +7275,12 @@ public class EditorApp extends Application {
|
||||
// Vom Editor gestartet: Hauptmenü überspringen, letzten Stand fortsetzen
|
||||
"-Dblight.autostart=true"));
|
||||
|
||||
if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) {
|
||||
if (isNewGame) {
|
||||
cmd.add("-Dblight.new.game=true");
|
||||
} else if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) {
|
||||
cmd.add("-Dblight.temp.spawn.x=" + input.tempSpawnX);
|
||||
cmd.add("-Dblight.temp.spawn.z=" + input.tempSpawnZ);
|
||||
cmd.add("-Dblight.temp.spawn.yaw=" + input.tempSpawnYaw);
|
||||
}
|
||||
|
||||
cmd.addAll(List.of("-cp", classpath, "de.blight.game.BlightGame"));
|
||||
@@ -7191,8 +7291,9 @@ public class EditorApp extends Application {
|
||||
.start();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
setStatus("Spiel gestartet");
|
||||
setStatus(isNewGame ? "Neues Spiel gestartet" : "Spiel gestartet");
|
||||
if (gamePlayBtn != null) gamePlayBtn.setText("🎮 Läuft…");
|
||||
if (gameNewBtn != null) gameNewBtn.setText("🎮 Läuft…");
|
||||
openGameConsole();
|
||||
});
|
||||
|
||||
@@ -7205,22 +7306,18 @@ public class EditorApp extends Application {
|
||||
consoleBuffer.offer(line);
|
||||
}
|
||||
}
|
||||
// Spiel beendet → Button freigeben
|
||||
// Spiel beendet → Buttons freigeben
|
||||
consoleBuffer.offer("--- Spiel beendet ---");
|
||||
Platform.runLater(() -> {
|
||||
if (gamePlayBtn != null) {
|
||||
gamePlayBtn.setText("▶ Spielen");
|
||||
gamePlayBtn.setDisable(false);
|
||||
}
|
||||
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
|
||||
if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); }
|
||||
});
|
||||
|
||||
} catch (IOException ex) {
|
||||
Platform.runLater(() -> {
|
||||
setStatus("Spielstart fehlgeschlagen: " + ex.getMessage());
|
||||
if (gamePlayBtn != null) {
|
||||
gamePlayBtn.setText("▶ Spielen");
|
||||
gamePlayBtn.setDisable(false);
|
||||
}
|
||||
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
|
||||
if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); }
|
||||
});
|
||||
}
|
||||
}, "game-launcher").start();
|
||||
@@ -7809,47 +7906,63 @@ public class EditorApp extends Application {
|
||||
private VBox buildPlayToolPanel() {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
inner.getChildren().addAll(
|
||||
sectionTitle("Spiel starten"),
|
||||
new Separator(),
|
||||
bold("Temporärer Spawnpunkt:"),
|
||||
styledHint("L-Klick im Viewport → Spawnpunkt setzen"));
|
||||
|
||||
Label coordHint = new Label("oder manuell eingeben:");
|
||||
coordHint.setStyle("-fx-text-fill: #444;");
|
||||
inner.getChildren().add(coordHint);
|
||||
// ── Abschnitt: Spawnpunkte setzen ────────────────────────────────────
|
||||
inner.getChildren().addAll(sectionTitle("Spawnpunkte"), new Separator());
|
||||
|
||||
spawnXField = new TextField(Float.isNaN(input.tempSpawnX) ? "" : String.valueOf(input.tempSpawnX));
|
||||
spawnZField = new TextField(Float.isNaN(input.tempSpawnZ) ? "" : String.valueOf(input.tempSpawnZ));
|
||||
spawnXField.setPromptText("X");
|
||||
spawnZField.setPromptText("Z");
|
||||
|
||||
Runnable applyFields = () -> {
|
||||
try {
|
||||
input.tempSpawnX = Float.parseFloat(spawnXField.getText().trim());
|
||||
input.tempSpawnZ = Float.parseFloat(spawnZField.getText().trim());
|
||||
} catch (NumberFormatException ignored2) {}
|
||||
};
|
||||
spawnXField.setOnAction(e -> applyFields.run());
|
||||
spawnZField.setOnAction(e -> applyFields.run());
|
||||
spawnXField.focusedProperty().addListener((o, ov, nv) -> { if (!nv) applyFields.run(); });
|
||||
spawnZField.focusedProperty().addListener((o, ov, nv) -> { if (!nv) applyFields.run(); });
|
||||
|
||||
HBox coordRow = new HBox(6, new Label("X:"), spawnXField, new Label("Z:"), spawnZField);
|
||||
coordRow.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(spawnXField, Priority.ALWAYS);
|
||||
HBox.setHgrow(spawnZField, Priority.ALWAYS);
|
||||
|
||||
Button clearSpawn = new Button("✕ Spawnpunkt löschen");
|
||||
clearSpawn.setMaxWidth(Double.MAX_VALUE);
|
||||
clearSpawn.setOnAction(e -> {
|
||||
input.tempSpawnX = Float.NaN;
|
||||
input.tempSpawnZ = Float.NaN;
|
||||
spawnXField.setText("");
|
||||
spawnZField.setText("");
|
||||
Button setTempBtn = new Button("📍 Temp. Spawn hier setzen");
|
||||
setTempBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
setTempBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||
"Setzt Yaw = Kamera-Blickrichtung und aktiviert Klick-Modus im Viewport"));
|
||||
setTempBtn.setOnAction(e -> {
|
||||
input.tempSpawnYaw = camYawToSpawnYaw(input.camYaw);
|
||||
input.playToolMode = SharedInput.PlayToolMode.SET_TEMP;
|
||||
setStatus("L-Klick im Viewport → temporären Spawnpunkt setzen");
|
||||
});
|
||||
|
||||
inner.getChildren().addAll(coordRow, clearSpawn, new Separator());
|
||||
Button setPermBtn = new Button("🏁 Perm. Spawn hier setzen");
|
||||
setPermBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
setPermBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||
"Setzt Yaw = Kamera-Blickrichtung und aktiviert Klick-Modus im Viewport"));
|
||||
setPermBtn.setOnAction(e -> {
|
||||
input.permSpawnYaw = camYawToSpawnYaw(input.camYaw);
|
||||
input.playToolMode = SharedInput.PlayToolMode.SET_PERM;
|
||||
setStatus("L-Klick im Viewport → permanenten Spawnpunkt setzen");
|
||||
});
|
||||
|
||||
ToggleButton editModeBtn = new ToggleButton("✎ Bearbeiten");
|
||||
editModeBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
editModeBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||
"Drag auf Marker → Position; Drag auf Pfeilspitze → Richtung drehen"));
|
||||
editModeBtn.setOnAction(e -> {
|
||||
input.playToolMode = editModeBtn.isSelected()
|
||||
? SharedInput.PlayToolMode.EDIT
|
||||
: SharedInput.PlayToolMode.NONE;
|
||||
});
|
||||
|
||||
inner.getChildren().addAll(setTempBtn, setPermBtn, editModeBtn, new Separator());
|
||||
|
||||
// ── Koordinatenanzeige ────────────────────────────────────────────────
|
||||
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("✕ Temp. Spawn löschen");
|
||||
clearTempBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
clearTempBtn.setOnAction(e -> {
|
||||
input.tempSpawnX = Float.NaN;
|
||||
input.tempSpawnZ = Float.NaN;
|
||||
if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText("(nicht gesetzt)");
|
||||
});
|
||||
|
||||
inner.getChildren().addAll(
|
||||
bold("Temp. Spawn:"), tempSpawnCoordsLabel, clearTempBtn,
|
||||
bold("Perm. Spawn:"), permSpawnCoordsLabel,
|
||||
new Separator());
|
||||
|
||||
// ── Abschnitt: Spielen ────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(sectionTitle("Spiel starten"), new Separator());
|
||||
|
||||
Button playBtn = new Button("▶ Spielen");
|
||||
gamePlayBtn = playBtn;
|
||||
@@ -7857,8 +7970,21 @@ public class EditorApp extends Application {
|
||||
playBtn.setStyle(
|
||||
"-fx-background-color: #2d8a3e; -fx-text-fill: white; " +
|
||||
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
|
||||
playBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||
"Startet das Spiel mit dem temp. Spawnpunkt (bzw. gespeicherter Position)"));
|
||||
playBtn.setOnAction(e -> launchGame());
|
||||
inner.getChildren().add(playBtn);
|
||||
|
||||
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 javafx.scene.control.Tooltip(
|
||||
"Startet ein neues Spiel am perm. Spawnpunkt mit Intro-Sequenz"));
|
||||
newGameBtn.setOnAction(e -> launchNewGame());
|
||||
|
||||
inner.getChildren().addAll(playBtn, newGameBtn);
|
||||
|
||||
ScrollPane scroll = new ScrollPane(inner);
|
||||
scroll.setFitToWidth(true);
|
||||
@@ -7871,12 +7997,28 @@ public class EditorApp extends Application {
|
||||
return panel;
|
||||
}
|
||||
|
||||
/** Konvertiert den Editor-Kamera-Yaw in den Spawnpunkt-Yaw (0=+Z, 90=+X, UZS von oben). */
|
||||
private static float camYawToSpawnYaw(float camYaw) {
|
||||
return ((180f + camYaw) % 360f + 360f) % 360f;
|
||||
}
|
||||
|
||||
private String tempSpawnCoordsText() {
|
||||
if (Float.isNaN(input.tempSpawnX) || Float.isNaN(input.tempSpawnZ)) return "(nicht gesetzt)";
|
||||
return String.format("X=%.1f Z=%.1f Yaw=%.0f°", input.tempSpawnX, input.tempSpawnZ, input.tempSpawnYaw);
|
||||
}
|
||||
|
||||
private String permSpawnCoordsText() {
|
||||
if (Float.isNaN(input.permSpawnX) || Float.isNaN(input.permSpawnZ)) return "(nicht gesetzt)";
|
||||
return String.format("X=%.1f Z=%.1f Yaw=%.0f°", input.permSpawnX, input.permSpawnZ, input.permSpawnYaw);
|
||||
}
|
||||
|
||||
private void updateSpawnFields(String info) {
|
||||
if (spawnXField == null || info == null) return;
|
||||
if (info == null) return;
|
||||
String[] p = info.split("\\|", -1);
|
||||
if (p.length < 2) return;
|
||||
spawnXField.setText(p[0]);
|
||||
spawnZField.setText(p[1]);
|
||||
if (spawnXField != null) spawnXField.setText(p[0]);
|
||||
if (spawnZField != null) spawnZField.setText(p[1]);
|
||||
if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText(tempSpawnCoordsText());
|
||||
}
|
||||
|
||||
// ── Tripo3D-Generator ────────────────────────────────────────────────────
|
||||
@@ -8280,6 +8422,7 @@ public class EditorApp extends Application {
|
||||
animSetClipListView = new ListView<>();
|
||||
animSetClipListView.getItems().addAll(animSet.getClips());
|
||||
animSetClipListView.setPrefHeight(180);
|
||||
editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips());
|
||||
|
||||
Path animRootDir = ASSET_ROOT.resolve("animations");
|
||||
|
||||
@@ -8390,6 +8533,8 @@ public class EditorApp extends Application {
|
||||
animSetClipListView.getItems().remove(sel);
|
||||
if (animSetActionListView != null)
|
||||
animSetActionListView.getItems().removeIf(it -> it.endsWith(" → " + sel));
|
||||
editableSubClips.entrySet().removeIf(en -> sel.equals(en.getValue().source));
|
||||
if (animSetPartsListView != null) animSetPartsListView.getItems().clear();
|
||||
animSetDirty = true;
|
||||
});
|
||||
|
||||
@@ -8398,6 +8543,75 @@ public class EditorApp extends Application {
|
||||
HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
|
||||
inner.getChildren().addAll(animSetClipListView, clipBtns);
|
||||
|
||||
// ── Clip-Teile (Sub-Clips) ────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator());
|
||||
|
||||
Label partsHint = new Label("Gewählten Clip in benannte Teile aufteilen. Jeder Teil erhält einen Namen und Zeitgrenzen (Sub-Clips).");
|
||||
partsHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
||||
partsHint.setWrapText(true);
|
||||
|
||||
animSetPartsListView = new ListView<>();
|
||||
animSetPartsListView.setPrefHeight(120);
|
||||
animSetPartsListView.setPlaceholder(new Label("Clip auswählen oder noch keine Teile definiert"));
|
||||
animSetPartsListView.setDisable(true);
|
||||
|
||||
Button addPartBtn = new Button("+ Teil hinzufügen…");
|
||||
Button removePartBtn = new Button("- Entfernen");
|
||||
addPartBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
removePartBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
addPartBtn.setDisable(true);
|
||||
removePartBtn.setDisable(true);
|
||||
|
||||
animSetPartsListView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, ov, nv) -> removePartBtn.setDisable(nv == null));
|
||||
|
||||
animSetPartsListView.setOnMouseClicked(ev -> {
|
||||
if (ev.getClickCount() == 2) {
|
||||
String sel = animSetPartsListView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
String selName = sel.substring(0, sel.indexOf(" ["));
|
||||
de.blight.game.animation.AnimSet.SubClipDef def = editableSubClips.get(selName);
|
||||
String source = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||
if (def != null && source != null) showPartDialog(source, selName, def);
|
||||
}
|
||||
});
|
||||
|
||||
// Teile-Liste aktualisieren wenn Clip-Auswahl ändert
|
||||
animSetClipListView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, ov, nv) -> {
|
||||
animSetPartsListView.getItems().clear();
|
||||
boolean hasClip = nv != null;
|
||||
animSetPartsListView.setDisable(!hasClip);
|
||||
addPartBtn.setDisable(!hasClip);
|
||||
if (hasClip) {
|
||||
for (var en : editableSubClips.entrySet()) {
|
||||
if (nv.equals(en.getValue().source)) {
|
||||
animSetPartsListView.getItems().add(
|
||||
formatPartEntry(en.getKey(), en.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addPartBtn.setOnAction(e -> {
|
||||
String source = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||
if (source != null) showPartDialog(source, null, null);
|
||||
});
|
||||
|
||||
removePartBtn.setOnAction(e -> {
|
||||
String sel = animSetPartsListView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
String name = sel.substring(0, sel.indexOf(" ["));
|
||||
editableSubClips.remove(name);
|
||||
animSetPartsListView.getItems().remove(sel);
|
||||
animSetDirty = true;
|
||||
});
|
||||
|
||||
HBox partsBtns = new HBox(6, addPartBtn, removePartBtn);
|
||||
HBox.setHgrow(addPartBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(removePartBtn, Priority.ALWAYS);
|
||||
inner.getChildren().addAll(partsHint, animSetPartsListView, partsBtns);
|
||||
|
||||
// ── Aktions-Zuordnung ─────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Aktions-Zuordnung"), new Separator());
|
||||
|
||||
@@ -8701,7 +8915,11 @@ public class EditorApp extends Application {
|
||||
actionCombo.getSelectionModel().selectFirst();
|
||||
|
||||
ComboBox<String> clipCombo = new ComboBox<>();
|
||||
clipCombo.getItems().addAll(animSetClipListView.getItems());
|
||||
java.util.List<String> allClipNames = new java.util.ArrayList<>(animSetClipListView.getItems());
|
||||
for (String subName : editableSubClips.keySet()) {
|
||||
if (!allClipNames.contains(subName)) allClipNames.add(subName);
|
||||
}
|
||||
clipCombo.getItems().addAll(allClipNames);
|
||||
clipCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
clipCombo.getSelectionModel().selectFirst();
|
||||
|
||||
@@ -8732,6 +8950,78 @@ public class EditorApp extends Application {
|
||||
});
|
||||
}
|
||||
|
||||
private static String formatPartEntry(String name,
|
||||
de.blight.game.animation.AnimSet.SubClipDef def) {
|
||||
return String.format("%s [%.3fs – %.3fs]", name, def.start, def.end);
|
||||
}
|
||||
|
||||
private void showPartDialog(String source, String existingName,
|
||||
de.blight.game.animation.AnimSet.SubClipDef existing) {
|
||||
boolean isEdit = existingName != null && existing != null;
|
||||
|
||||
javafx.scene.control.TextField nameField = new javafx.scene.control.TextField(
|
||||
isEdit ? existingName : "");
|
||||
nameField.setPromptText("Teil-Name (z. B. sit_down_bench)");
|
||||
nameField.setDisable(isEdit); // Name beim Bearbeiten nicht änderbar
|
||||
|
||||
Spinner<Double> startSpinner = new Spinner<>(0.0, 9999.0,
|
||||
isEdit ? existing.start : 0.0, 0.033);
|
||||
startSpinner.setEditable(true);
|
||||
startSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
Spinner<Double> endSpinner = new Spinner<>(0.0, 9999.0,
|
||||
isEdit ? existing.end : 1.0, 0.033);
|
||||
endSpinner.setEditable(true);
|
||||
endSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane();
|
||||
grid.setHgap(8); grid.setVgap(6);
|
||||
grid.add(new Label("Name:"), 0, 0); grid.add(nameField, 1, 0);
|
||||
grid.add(new Label("Start:"), 0, 1); grid.add(startSpinner, 1, 1);
|
||||
grid.add(new Label("Ende:"), 0, 2); grid.add(endSpinner, 1, 2);
|
||||
javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints();
|
||||
cc.setHgrow(Priority.ALWAYS);
|
||||
grid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), cc);
|
||||
|
||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg =
|
||||
new javafx.scene.control.Dialog<>();
|
||||
dlg.setTitle(isEdit ? "Clip-Teil bearbeiten" : "Clip-Teil hinzufügen");
|
||||
dlg.setHeaderText((isEdit ? "Bearbeiten: " : "Neuer Teil für: ") + source);
|
||||
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType(
|
||||
isEdit ? "Übernehmen" : "Hinzufügen",
|
||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL);
|
||||
dlg.getDialogPane().setContent(grid);
|
||||
|
||||
javafx.scene.Node okNode = dlg.getDialogPane().lookupButton(ok);
|
||||
Runnable validate = () -> {
|
||||
boolean valid = !nameField.getText().isBlank()
|
||||
&& endSpinner.getValue() > startSpinner.getValue();
|
||||
okNode.setDisable(!valid);
|
||||
};
|
||||
validate.run();
|
||||
nameField.textProperty().addListener((obs, ov, nv) -> validate.run());
|
||||
startSpinner.valueProperty().addListener((obs, ov, nv) -> validate.run());
|
||||
endSpinner.valueProperty().addListener((obs, ov, nv) -> validate.run());
|
||||
|
||||
dlg.showAndWait().ifPresent(bt -> {
|
||||
if (bt != ok) return;
|
||||
String name = isEdit ? existingName : nameField.getText().trim();
|
||||
if (name.isBlank()) return;
|
||||
de.blight.game.animation.AnimSet.SubClipDef def =
|
||||
new de.blight.game.animation.AnimSet.SubClipDef();
|
||||
def.source = source;
|
||||
def.start = startSpinner.getValue().floatValue();
|
||||
def.end = endSpinner.getValue().floatValue();
|
||||
editableSubClips.put(name, def);
|
||||
if (animSetPartsListView != null) {
|
||||
animSetPartsListView.getItems().removeIf(s -> s.startsWith(name + " ["));
|
||||
animSetPartsListView.getItems().add(formatPartEntry(name, def));
|
||||
}
|
||||
animSetDirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
private void saveCurrentAnimSet(String setName, Path setDir) {
|
||||
if (animSetClipListView == null) {
|
||||
return;
|
||||
@@ -8767,6 +9057,7 @@ public class EditorApp extends Application {
|
||||
}
|
||||
}
|
||||
animSet.setAnimOffsets(offsetFinal);
|
||||
animSet.setSubClips(new java.util.LinkedHashMap<>(editableSubClips));
|
||||
// Vorschau-Modell-Pfad beibehalten
|
||||
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
|
||||
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
||||
@@ -9511,10 +9802,24 @@ public class EditorApp extends Application {
|
||||
Label npcTraderLbl = new Label("Handel:");
|
||||
Label npcItemsLbl = new Label("Waren:");
|
||||
|
||||
charMsgFriendly = new javafx.scene.control.TextField();
|
||||
charMsgFriendly.setPromptText("TextReference-Schlüssel (z.B. silas.msg.friendly)");
|
||||
charMsgNeutral = new javafx.scene.control.TextField();
|
||||
charMsgNeutral.setPromptText("TextReference-Schlüssel");
|
||||
charMsgEnraged = new javafx.scene.control.TextField();
|
||||
charMsgEnraged.setPromptText("TextReference-Schlüssel");
|
||||
charMsgEnemy = new javafx.scene.control.TextField();
|
||||
charMsgEnemy.setPromptText("TextReference-Schlüssel");
|
||||
|
||||
charNpcSection = new VBox(4,
|
||||
npcStatusLbl, charStatusCombo,
|
||||
npcTraderLbl, charTraderCheck,
|
||||
npcItemsLbl, charTraderItemsView, traderBtns);
|
||||
npcItemsLbl, charTraderItemsView, traderBtns,
|
||||
sectionTitle("Standard-Nachrichten je Status"),
|
||||
new Label("Friendly:"), charMsgFriendly,
|
||||
new Label("Neutral:"), charMsgNeutral,
|
||||
new Label("Enraged:"), charMsgEnraged,
|
||||
new Label("Enemy:"), charMsgEnemy);
|
||||
boolean npcInitial = "NPC".equals(charTypeCombo.getValue());
|
||||
charNpcSection.setVisible(npcInitial);
|
||||
charNpcSection.setManaged(npcInitial);
|
||||
@@ -9666,6 +9971,10 @@ public class EditorApp extends Application {
|
||||
if (charStatusCombo != null) charStatusCombo.setValue("NEUTRAL");
|
||||
if (charTraderCheck != null) charTraderCheck.setSelected(false);
|
||||
if (charTraderItemsView != null) charTraderItemsView.getItems().clear();
|
||||
if (charMsgFriendly != null) charMsgFriendly.clear();
|
||||
if (charMsgNeutral != null) charMsgNeutral.clear();
|
||||
if (charMsgEnraged != null) charMsgEnraged.clear();
|
||||
if (charMsgEnemy != null) charMsgEnemy.clear();
|
||||
if (abMagicSpin != null) resetAbilities();
|
||||
updateCharActionCombosFromSet();
|
||||
if (charEditContainer != null) charEditContainer.setDisable(false);
|
||||
@@ -9710,6 +10019,12 @@ public class EditorApp extends Application {
|
||||
if (npc.getItems() != null)
|
||||
npc.getItems().forEach(it -> charTraderItemsView.getItems().add(it.getItemId()));
|
||||
}
|
||||
java.util.Map<de.blight.common.model.Status, de.blight.common.model.TextReference> msgs =
|
||||
npc.getDefaultMessages();
|
||||
if (charMsgFriendly != null) charMsgFriendly.setText(msgKey(msgs, de.blight.common.model.Status.FRIENDLY));
|
||||
if (charMsgNeutral != null) charMsgNeutral.setText(msgKey(msgs, de.blight.common.model.Status.NEUTRAL));
|
||||
if (charMsgEnraged != null) charMsgEnraged.setText(msgKey(msgs, de.blight.common.model.Status.ENRAGED));
|
||||
if (charMsgEnemy != null) charMsgEnemy.setText(msgKey(msgs, de.blight.common.model.Status.ENEMY));
|
||||
}
|
||||
if (c instanceof de.blight.common.model.MainCharacter mc) {
|
||||
loadAbilities(mc.getAbilities());
|
||||
@@ -9721,6 +10036,7 @@ public class EditorApp extends Application {
|
||||
else dialogEditorView.clear();
|
||||
}
|
||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id);
|
||||
charDirty = true;
|
||||
} catch (Exception e) {
|
||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage());
|
||||
}
|
||||
@@ -9757,6 +10073,14 @@ public class EditorApp extends Application {
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
if (charTraderCheck != null) npc.setTrader(charTraderCheck.isSelected());
|
||||
// Default-Nachrichten je Status
|
||||
java.util.Map<de.blight.common.model.Status, de.blight.common.model.TextReference> msgs =
|
||||
new java.util.EnumMap<>(de.blight.common.model.Status.class);
|
||||
putMsg(msgs, de.blight.common.model.Status.FRIENDLY, charMsgFriendly);
|
||||
putMsg(msgs, de.blight.common.model.Status.NEUTRAL, charMsgNeutral);
|
||||
putMsg(msgs, de.blight.common.model.Status.ENRAGED, charMsgEnraged);
|
||||
putMsg(msgs, de.blight.common.model.Status.ENEMY, charMsgEnemy);
|
||||
npc.setDefaultMessages(msgs);
|
||||
if (charTraderItemsView != null && !charTraderItemsView.getItems().isEmpty()) {
|
||||
java.util.List<de.blight.common.model.Item> items = new java.util.ArrayList<>();
|
||||
charTraderItemsView.getItems().forEach(id2 -> {
|
||||
@@ -9775,6 +10099,7 @@ public class EditorApp extends Application {
|
||||
|
||||
try {
|
||||
de.blight.common.model.CharacterIO.save(c, charDir);
|
||||
charDirty = false;
|
||||
refreshCharacterList();
|
||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id);
|
||||
} catch (Exception e) {
|
||||
@@ -9899,6 +10224,23 @@ public class EditorApp extends Application {
|
||||
return ab;
|
||||
}
|
||||
|
||||
private static String msgKey(
|
||||
java.util.Map<de.blight.common.model.Status, de.blight.common.model.TextReference> msgs,
|
||||
de.blight.common.model.Status status) {
|
||||
if (msgs == null) return "";
|
||||
de.blight.common.model.TextReference ref = msgs.get(status);
|
||||
return (ref != null && ref.id() != null) ? ref.id() : "";
|
||||
}
|
||||
|
||||
private static void putMsg(
|
||||
java.util.Map<de.blight.common.model.Status, de.blight.common.model.TextReference> msgs,
|
||||
de.blight.common.model.Status status,
|
||||
javafx.scene.control.TextField field) {
|
||||
if (field == null) return;
|
||||
String s = field.getText().trim();
|
||||
if (!s.isBlank()) msgs.put(status, new de.blight.common.model.TextReference(s));
|
||||
}
|
||||
|
||||
private void switchToLocationEditor() {
|
||||
onF5 = null;
|
||||
currentTool = "locationEditor";
|
||||
|
||||
@@ -521,9 +521,17 @@ public class SharedInput {
|
||||
public volatile boolean cancelZoneDrawing = false;
|
||||
|
||||
// ── Spiel-Starten-Werkzeug ────────────────────────────────────────────────
|
||||
/** Klick im Viewport zum Setzen des temporären Spawnpunkts. */
|
||||
/** Klick/Drag-Ereignisse im Viewport für das Play-Tool. */
|
||||
public record PlayToolClick(float screenX, float screenY) {}
|
||||
public record PlayToolDrag(float screenX, float screenY) {}
|
||||
public final ConcurrentLinkedQueue<PlayToolClick> playToolClickQueue = new ConcurrentLinkedQueue<>();
|
||||
public final ConcurrentLinkedQueue<PlayToolDrag> playToolDragQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3 → JavaFX: Maus-Taste wurde losgelassen (EDIT-Modus). */
|
||||
public volatile boolean playToolMouseUp = false;
|
||||
|
||||
/** Sub-Modus des Play-Tools. */
|
||||
public enum PlayToolMode { NONE, SET_TEMP, SET_PERM, EDIT }
|
||||
public volatile PlayToolMode playToolMode = PlayToolMode.NONE;
|
||||
|
||||
/**
|
||||
* JME → JavaFX: Terrain-Treffpunkt nach Spawn-Klick.
|
||||
@@ -531,10 +539,21 @@ public class SharedInput {
|
||||
*/
|
||||
public volatile String pickedSpawnInfo = null;
|
||||
public volatile boolean spawnPickChanged = false;
|
||||
public volatile String pickedPermSpawnInfo = null;
|
||||
public volatile boolean permSpawnChanged = false;
|
||||
|
||||
/** Temporärer Spawnpunkt (NaN = nicht gesetzt). Wird beim Spielstart als System-Property übergeben. */
|
||||
public volatile float tempSpawnX = Float.NaN;
|
||||
public volatile float tempSpawnZ = Float.NaN;
|
||||
public volatile float tempSpawnX = Float.NaN;
|
||||
public volatile float tempSpawnZ = Float.NaN;
|
||||
public volatile float tempSpawnYaw = 0f;
|
||||
|
||||
/** Permanenter Spawnpunkt (NaN = nicht gesetzt). Wird in MapData.spawnX/Z/Yaw gespeichert. */
|
||||
public volatile float permSpawnX = Float.NaN;
|
||||
public volatile float permSpawnZ = Float.NaN;
|
||||
public volatile float permSpawnYaw = 0f;
|
||||
|
||||
/** Master-Lautstärke (0=stumm, 1=voll) – wird von der Intro-Sequenz gesetzt. */
|
||||
public volatile float masterAudioVolume = 1.0f;
|
||||
|
||||
// ── Animations-Vorschau ──────────────────────────────────────────────────
|
||||
public volatile float animPreviewRotY = 0f;
|
||||
|
||||
@@ -9,12 +9,30 @@ import com.jme3.material.Material;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.shape.Cylinder;
|
||||
import com.jme3.scene.shape.*;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.editor.SharedInput;
|
||||
|
||||
/**
|
||||
* Rendert Spawn-Marker im Editor (temp=grün, perm=blau) mit Richtungspfeilen.
|
||||
*
|
||||
* Modi:
|
||||
* NONE – Marker sichtbar, aber keine Interaktion
|
||||
* SET_TEMP – Nächster Viewport-Klick setzt temp. Spawnpunkt
|
||||
* SET_PERM – Nächster Viewport-Klick setzt perm. Spawnpunkt
|
||||
* EDIT – Drag auf Marker-Körper → Positionieren;
|
||||
* Drag auf Pfeilspitze → Richtung drehen (via XZ-Projektion)
|
||||
*/
|
||||
public class PlayToolState extends BaseAppState {
|
||||
|
||||
private static final float MARKER_RADIUS = 0.5f;
|
||||
private static final float SHAFT_LEN = 2.0f;
|
||||
private static final float SHAFT_RADIUS = 0.06f;
|
||||
private static final float TIP_LEN = 0.5f;
|
||||
private static final float TIP_RADIUS = 0.18f;
|
||||
|
||||
private enum DragTarget { NONE, POS_TEMP, POS_PERM, ROT_TEMP, ROT_PERM }
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
@@ -22,7 +40,21 @@ public class PlayToolState extends BaseAppState {
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
private Geometry spawnMarker;
|
||||
// Marker-Nodes
|
||||
private Node tempMarkerNode;
|
||||
private Node permMarkerNode;
|
||||
private Node tempArrowNode;
|
||||
private Node permArrowNode;
|
||||
|
||||
// Separate Geometrien für Ray-Cast-Erkennung
|
||||
private Geometry tempBodyGeom;
|
||||
private Geometry permBodyGeom;
|
||||
private Geometry tempTipGeom;
|
||||
private Geometry permTipGeom;
|
||||
|
||||
private DragTarget dragTarget = DragTarget.NONE;
|
||||
private float lastDragX;
|
||||
private float lastDragY;
|
||||
|
||||
public PlayToolState(SharedInput input) {
|
||||
this.input = input;
|
||||
@@ -34,10 +66,25 @@ public class PlayToolState extends BaseAppState {
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
|
||||
tempMarkerNode = buildMarkerNode(new ColorRGBA(0f, 1f, 0.2f, 1f), "temp");
|
||||
permMarkerNode = buildMarkerNode(new ColorRGBA(0.2f, 0.5f, 1f, 1f), "perm");
|
||||
|
||||
tempArrowNode = (Node) tempMarkerNode.getChild("arrow");
|
||||
permArrowNode = (Node) permMarkerNode.getChild("arrow");
|
||||
tempBodyGeom = (Geometry) tempMarkerNode.getChild("body_temp");
|
||||
permBodyGeom = (Geometry) permMarkerNode.getChild("body_perm");
|
||||
tempTipGeom = (Geometry) tempArrowNode.getChild("tip_temp");
|
||||
permTipGeom = (Geometry) permArrowNode.getChild("tip_perm");
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application application) { removeMarker(); }
|
||||
@Override protected void onEnable() {}
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
detachMarker(tempMarkerNode);
|
||||
detachMarker(permMarkerNode);
|
||||
}
|
||||
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||
@@ -46,58 +93,281 @@ public class PlayToolState extends BaseAppState {
|
||||
public void update(float tpf) {
|
||||
if (input.activeLayer != SharedInput.LAYER_PLAY_TOOL) return;
|
||||
|
||||
SharedInput.PlayToolMode mode = input.playToolMode;
|
||||
|
||||
// --- Klick-Auswertung ---
|
||||
SharedInput.PlayToolClick click;
|
||||
while ((click = input.playToolClickQueue.poll()) != null) {
|
||||
handleClick(click);
|
||||
handleClick(click, mode);
|
||||
}
|
||||
|
||||
// Update marker position if spawn changed from text fields
|
||||
if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) {
|
||||
placeMarkerAt(input.tempSpawnX, input.tempSpawnZ);
|
||||
// --- Drag ---
|
||||
if (mode == SharedInput.PlayToolMode.EDIT) {
|
||||
SharedInput.PlayToolDrag drag;
|
||||
while ((drag = input.playToolDragQueue.poll()) != null) {
|
||||
handleDrag(drag);
|
||||
}
|
||||
if (input.playToolMouseUp) {
|
||||
input.playToolMouseUp = false;
|
||||
dragTarget = DragTarget.NONE;
|
||||
}
|
||||
} else {
|
||||
input.playToolDragQueue.clear();
|
||||
if (input.playToolMouseUp) input.playToolMouseUp = false;
|
||||
dragTarget = DragTarget.NONE;
|
||||
}
|
||||
|
||||
// --- Marker aktualisieren ---
|
||||
updateMarker(tempMarkerNode, tempArrowNode, input.tempSpawnX, input.tempSpawnZ, input.tempSpawnYaw);
|
||||
updateMarker(permMarkerNode, permArrowNode, input.permSpawnX, input.permSpawnZ, input.permSpawnYaw);
|
||||
}
|
||||
|
||||
private void handleClick(SharedInput.PlayToolClick click) {
|
||||
// ── Click-Handler ─────────────────────────────────────────────────────────
|
||||
|
||||
private void handleClick(SharedInput.PlayToolClick click, SharedInput.PlayToolMode mode) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector2f screen = new Vector2f(jmeX, jmeY);
|
||||
|
||||
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());
|
||||
Ray ray = new Ray(cam.getWorldCoordinates(screen, 0f),
|
||||
cam.getWorldCoordinates(screen, 1f).subtractLocal(
|
||||
cam.getWorldCoordinates(screen, 0f)).normalizeLocal());
|
||||
|
||||
if (terrain == null) return;
|
||||
if (mode == SharedInput.PlayToolMode.SET_TEMP) {
|
||||
Vector3f pt = hitTerrain(ray);
|
||||
if (pt != null) {
|
||||
input.tempSpawnX = pt.x;
|
||||
input.tempSpawnZ = pt.z;
|
||||
input.pickedSpawnInfo = pt.x + "|" + pt.z;
|
||||
input.spawnPickChanged = true;
|
||||
input.playToolMode = SharedInput.PlayToolMode.NONE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == SharedInput.PlayToolMode.SET_PERM) {
|
||||
Vector3f pt = hitTerrain(ray);
|
||||
if (pt != null) {
|
||||
input.permSpawnX = pt.x;
|
||||
input.permSpawnZ = pt.z;
|
||||
input.pickedPermSpawnInfo = pt.x + "|" + pt.z;
|
||||
input.permSpawnChanged = true;
|
||||
input.playToolMode = SharedInput.PlayToolMode.NONE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == SharedInput.PlayToolMode.EDIT) {
|
||||
lastDragX = click.screenX();
|
||||
lastDragY = click.screenY();
|
||||
dragTarget = detectDragTarget(ray);
|
||||
}
|
||||
}
|
||||
|
||||
private DragTarget detectDragTarget(Ray ray) {
|
||||
// Pfeilspitzen zuerst prüfen (kleineres Ziel, höhere Priorität)
|
||||
if (tempTipGeom != null && !Float.isNaN(input.tempSpawnX)) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
tempTipGeom.collideWith(ray, res);
|
||||
if (res.size() > 0) return DragTarget.ROT_TEMP;
|
||||
}
|
||||
if (permTipGeom != null && !Float.isNaN(input.permSpawnX)) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
permTipGeom.collideWith(ray, res);
|
||||
if (res.size() > 0) return DragTarget.ROT_PERM;
|
||||
}
|
||||
if (tempBodyGeom != null && !Float.isNaN(input.tempSpawnX)) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
tempBodyGeom.collideWith(ray, res);
|
||||
if (res.size() > 0) return DragTarget.POS_TEMP;
|
||||
}
|
||||
if (permBodyGeom != null && !Float.isNaN(input.permSpawnX)) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
permBodyGeom.collideWith(ray, res);
|
||||
if (res.size() > 0) return DragTarget.POS_PERM;
|
||||
}
|
||||
return DragTarget.NONE;
|
||||
}
|
||||
|
||||
// ── Drag-Handler ──────────────────────────────────────────────────────────
|
||||
|
||||
private void handleDrag(SharedInput.PlayToolDrag drag) {
|
||||
if (dragTarget == DragTarget.NONE) return;
|
||||
|
||||
float jmeX = drag.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
|
||||
Vector2f screen = new Vector2f(jmeX, jmeY);
|
||||
Ray ray = new Ray(cam.getWorldCoordinates(screen, 0f),
|
||||
cam.getWorldCoordinates(screen, 1f).subtractLocal(
|
||||
cam.getWorldCoordinates(screen, 0f)).normalizeLocal());
|
||||
|
||||
switch (dragTarget) {
|
||||
case POS_TEMP -> {
|
||||
Vector3f pt = hitTerrain(ray);
|
||||
if (pt != null) {
|
||||
input.tempSpawnX = pt.x;
|
||||
input.tempSpawnZ = pt.z;
|
||||
input.pickedSpawnInfo = pt.x + "|" + pt.z;
|
||||
input.spawnPickChanged = true;
|
||||
}
|
||||
}
|
||||
case POS_PERM -> {
|
||||
Vector3f pt = hitTerrain(ray);
|
||||
if (pt != null) {
|
||||
input.permSpawnX = pt.x;
|
||||
input.permSpawnZ = pt.z;
|
||||
input.pickedPermSpawnInfo = pt.x + "|" + pt.z;
|
||||
input.permSpawnChanged = true;
|
||||
}
|
||||
}
|
||||
case ROT_TEMP -> {
|
||||
float markerY = terrainY(input.tempSpawnX, input.tempSpawnZ);
|
||||
Vector3f proj = projectOnHorizontalPlane(ray, markerY);
|
||||
if (proj != null) {
|
||||
float dx = proj.x - input.tempSpawnX;
|
||||
float dz = proj.z - input.tempSpawnZ;
|
||||
if (dx * dx + dz * dz > 0.01f) {
|
||||
// spawnYaw: 0=+Z, 90=+X → atan2(dx, dz)
|
||||
float yaw = (float) Math.toDegrees(Math.atan2(dx, dz));
|
||||
input.tempSpawnYaw = ((yaw % 360f) + 360f) % 360f;
|
||||
}
|
||||
}
|
||||
}
|
||||
case ROT_PERM -> {
|
||||
float markerY = terrainY(input.permSpawnX, input.permSpawnZ);
|
||||
Vector3f proj = projectOnHorizontalPlane(ray, markerY);
|
||||
if (proj != null) {
|
||||
float dx = proj.x - input.permSpawnX;
|
||||
float dz = proj.z - input.permSpawnZ;
|
||||
if (dx * dx + dz * dz > 0.01f) {
|
||||
float yaw = (float) Math.toDegrees(Math.atan2(dx, dz));
|
||||
input.permSpawnYaw = ((yaw % 360f) + 360f) % 360f;
|
||||
input.permSpawnChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
|
||||
lastDragX = drag.screenX();
|
||||
lastDragY = drag.screenY();
|
||||
}
|
||||
|
||||
// ── Marker aufbauen ───────────────────────────────────────────────────────
|
||||
|
||||
private Node buildMarkerNode(ColorRGBA color, String id) {
|
||||
Node markerNode = new Node("marker_" + id);
|
||||
|
||||
// Basis-Scheibe: JME3-Cylinder liegt entlang Z → rotate(-90°, 0, 0) dreht Z→Y (flach in XZ-Ebene)
|
||||
Cylinder disc = new Cylinder(8, 20, MARKER_RADIUS, 0.05f, true);
|
||||
Geometry body = new Geometry("body_" + id, disc);
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
body.setMaterial(mat);
|
||||
body.rotate(-FastMath.HALF_PI, 0f, 0f);
|
||||
markerNode.attachChild(body);
|
||||
|
||||
// Pfeil-Node (wird nach Yaw rotiert; local Y → Yaw-Richtung, local Z → Terrain-Normal)
|
||||
Node arrowNode = new Node("arrow");
|
||||
markerNode.attachChild(arrowNode);
|
||||
|
||||
// Schaft: JME3-Cylinder entlang Z → rotate(-90°, 0, 0) dreht Z→arrowNode-Y (= Yaw-Richtung)
|
||||
Cylinder shaft = new Cylinder(4, 8, SHAFT_RADIUS, SHAFT_LEN, true);
|
||||
Geometry shaftGeom = new Geometry("shaft_" + id, shaft);
|
||||
shaftGeom.setMaterial(mat);
|
||||
shaftGeom.rotate(-FastMath.HALF_PI, 0f, 0f);
|
||||
shaftGeom.setLocalTranslation(0f, SHAFT_LEN * 0.5f, 0f);
|
||||
arrowNode.attachChild(shaftGeom);
|
||||
|
||||
// Pfeilspitze: Kegel mit breiter Basis bei z=-TIP_LEN/2 → nach rotate: Basis bei y=SHAFT_LEN (Schaft-Ende)
|
||||
Cylinder tip = new Cylinder(4, 8, TIP_RADIUS, 0.001f, TIP_LEN, true, false);
|
||||
Geometry tipGeom = new Geometry("tip_" + id, tip);
|
||||
tipGeom.setMaterial(mat);
|
||||
tipGeom.rotate(-FastMath.HALF_PI, 0f, 0f);
|
||||
tipGeom.setLocalTranslation(0f, SHAFT_LEN + TIP_LEN * 0.5f, 0f);
|
||||
arrowNode.attachChild(tipGeom);
|
||||
|
||||
return markerNode;
|
||||
}
|
||||
|
||||
// ── Marker-Positions- / Rotations-Update ─────────────────────────────────
|
||||
|
||||
private void updateMarker(Node markerNode, Node arrowNode, float x, float z, float yawDeg) {
|
||||
if (Float.isNaN(x) || Float.isNaN(z)) {
|
||||
if (markerNode.getParent() != null) rootNode.detachChild(markerNode);
|
||||
return;
|
||||
}
|
||||
if (markerNode.getParent() == null) rootNode.attachChild(markerNode);
|
||||
|
||||
float y = terrainY(x, z);
|
||||
markerNode.setLocalTranslation(x, y + 0.06f, z);
|
||||
|
||||
if (arrowNode == null) return;
|
||||
|
||||
// Terrain-Normale als lokale Oben-Richtung
|
||||
Vector3f up = terrainNormal(x, z);
|
||||
|
||||
// Yaw-Richtung als horizontaler Richtungsvektor (0=+Z, 90=+X)
|
||||
float rad = yawDeg * FastMath.DEG_TO_RAD;
|
||||
Vector3f flatForward = new Vector3f(FastMath.sin(rad), 0f, FastMath.cos(rad));
|
||||
|
||||
// right = up × flatForward (liegt auf der Terrain-Oberfläche, senkrecht zur Richtung)
|
||||
Vector3f right = up.cross(flatForward);
|
||||
if (right.lengthSquared() < 1e-6f) {
|
||||
// Sonderfall: Terrain fast senkrecht → horizontale Rotation als Fallback
|
||||
arrowNode.setLocalRotation(new Quaternion().fromAngles(FastMath.HALF_PI, rad, 0f));
|
||||
return;
|
||||
}
|
||||
right.normalizeLocal();
|
||||
// forward = right × up (auf Terrain-Fläche projiziertes Forward in Yaw-Richtung)
|
||||
Vector3f forward = right.cross(up).normalizeLocal();
|
||||
|
||||
// Rotationsmatrix: local X → right, local Y → forward (Pfeil), local Z → up (Terrain-Normal)
|
||||
Matrix3f rot = new Matrix3f();
|
||||
rot.setColumn(0, right);
|
||||
rot.setColumn(1, forward);
|
||||
rot.setColumn(2, up);
|
||||
arrowNode.setLocalRotation(new Quaternion().fromRotationMatrix(rot));
|
||||
}
|
||||
|
||||
// ── Terrain-Hilfsmethoden ─────────────────────────────────────────────────
|
||||
|
||||
private Vector3f hitTerrain(Ray ray) {
|
||||
if (terrain == null) return null;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
input.tempSpawnX = pt.x;
|
||||
input.tempSpawnZ = pt.z;
|
||||
input.pickedSpawnInfo = pt.x + "|" + pt.z;
|
||||
input.spawnPickChanged = true;
|
||||
placeMarkerAt(pt.x, pt.z);
|
||||
if (hits.size() == 0) return null;
|
||||
return hits.getClosestCollision().getContactPoint();
|
||||
}
|
||||
|
||||
private void placeMarkerAt(float x, float z) {
|
||||
float y = terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f;
|
||||
if (Float.isNaN(y)) y = 0f;
|
||||
|
||||
if (spawnMarker == null) {
|
||||
Cylinder cyl = new Cylinder(8, 16, 0.4f, 0.1f, true);
|
||||
spawnMarker = new Geometry("spawn_marker", cyl);
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0f, 1f, 0f, 1f));
|
||||
spawnMarker.setMaterial(mat);
|
||||
spawnMarker.rotate(FastMath.HALF_PI, 0, 0);
|
||||
rootNode.attachChild(spawnMarker);
|
||||
}
|
||||
spawnMarker.setLocalTranslation(x, y + 0.05f, z);
|
||||
private float terrainY(float x, float z) {
|
||||
if (terrain == null) return 0f;
|
||||
float h = terrain.getHeight(new Vector2f(x, z));
|
||||
return Float.isNaN(h) ? 0f : h;
|
||||
}
|
||||
|
||||
private void removeMarker() {
|
||||
if (spawnMarker != null) {
|
||||
rootNode.detachChild(spawnMarker);
|
||||
spawnMarker = null;
|
||||
/** Terrain-Normale an (x, z) durch Kreuzprodukt zweier Tangenten. */
|
||||
private Vector3f terrainNormal(float x, float z) {
|
||||
if (terrain == null) return Vector3f.UNIT_Y.clone();
|
||||
float d = 0.5f;
|
||||
float h00 = terrainY(x, z);
|
||||
Vector3f tx = new Vector3f(d, terrainY(x + d, z) - h00, 0f);
|
||||
Vector3f tz = new Vector3f(0f, terrainY(x, z + d) - h00, d);
|
||||
return tz.cross(tx).normalizeLocal();
|
||||
}
|
||||
|
||||
/** Schneidet den Kamera-Ray mit der horizontalen Ebene Y=planeY. Null wenn kein Treffer. */
|
||||
private static Vector3f projectOnHorizontalPlane(Ray ray, float planeY) {
|
||||
float dY = ray.getDirection().y;
|
||||
if (Math.abs(dY) < 1e-6f) return null;
|
||||
float t = (planeY - ray.getOrigin().y) / dY;
|
||||
if (t < 0f) return null;
|
||||
return ray.getOrigin().add(ray.getDirection().mult(t));
|
||||
}
|
||||
|
||||
private void detachMarker(Node markerNode) {
|
||||
if (markerNode != null && markerNode.getParent() != null) {
|
||||
rootNode.detachChild(markerNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,6 +458,10 @@ public class TerrainEditorState extends BaseAppState {
|
||||
input.voxelFlatSlot = loadedMapData.voxelFlatSlot;
|
||||
input.voxelSteepSlot = loadedMapData.voxelSteepSlot;
|
||||
input.voxelCeilSlot = loadedMapData.voxelCeilSlot;
|
||||
input.permSpawnX = loadedMapData.spawnX;
|
||||
input.permSpawnZ = loadedMapData.spawnZ;
|
||||
input.permSpawnYaw = loadedMapData.spawnYaw;
|
||||
input.permSpawnChanged = true;
|
||||
input.voxelTexturesChanged = true;
|
||||
// Alte Gebirge-Splatmap-Migration: R=255 überall war der Gebirge-Standard.
|
||||
// Im neuen 1-Terrain-System bedeutet das: Slot-5-Textur deckt alles ab → auf 0 setzen.
|
||||
@@ -1210,6 +1214,16 @@ public class TerrainEditorState extends BaseAppState {
|
||||
data.voxelFlatSlot = voxelFlatSlot;
|
||||
data.voxelSteepSlot = voxelSteepSlot;
|
||||
data.voxelCeilSlot = voxelCeilSlot;
|
||||
// Permanenten Spawnpunkt übernehmen (falls gesetzt), sonst aus geladenem MapData
|
||||
if (!Float.isNaN(input.permSpawnX) && !Float.isNaN(input.permSpawnZ)) {
|
||||
data.spawnX = input.permSpawnX;
|
||||
data.spawnZ = input.permSpawnZ;
|
||||
data.spawnYaw = input.permSpawnYaw;
|
||||
} else if (loadedMapData != null) {
|
||||
data.spawnX = loadedMapData.spawnX;
|
||||
data.spawnZ = loadedMapData.spawnZ;
|
||||
data.spawnYaw = loadedMapData.spawnYaw;
|
||||
}
|
||||
|
||||
if (grassData != null) {
|
||||
try { GrassTuftIO.save(grassData); }
|
||||
|
||||
@@ -12,9 +12,9 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Crafting-Table-Verwaltung: zwei identische TablePanel-Instanzen nebeneinander.
|
||||
* Crafting-Table-Verwaltung: Liste links, Formular rechts.
|
||||
* Pro CraftingTableType kann genau ein Eintrag existieren — die Liste zeigt
|
||||
* immer alle 5 Typen; nicht konfigurierte Einträge erscheinen grau.
|
||||
* immer alle Typen; nicht konfigurierte Einträge erscheinen grau.
|
||||
*/
|
||||
public class CraftingTableEditorView extends BorderPane {
|
||||
|
||||
@@ -22,21 +22,188 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
new EnumMap<>(CraftingTable.CraftingTableType.class);
|
||||
private final Path tableDir;
|
||||
|
||||
private TablePanel left;
|
||||
private TablePanel right;
|
||||
// ── List ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private ListView<CraftingTable.CraftingTableType> listView;
|
||||
private Button deleteBtn;
|
||||
private CraftingTable.CraftingTableType currentType = null;
|
||||
|
||||
// ── Form fields ───────────────────────────────────────────────────────────
|
||||
|
||||
private Label formTypeLabel;
|
||||
private TextField nameIdField;
|
||||
private TextField objectPathField;
|
||||
private VBox formContainer;
|
||||
|
||||
public CraftingTableEditorView(Path tableDir) {
|
||||
this.tableDir = tableDir;
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
reloadMap();
|
||||
|
||||
left = new TablePanel("Liste 1", shared, tableDir, this::onSaved);
|
||||
right = new TablePanel("Liste 2", shared, tableDir, this::onSaved);
|
||||
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||
split.setDividerPositions(0.28);
|
||||
setCenter(split);
|
||||
}
|
||||
|
||||
HBox panels = new HBox(1, left, right);
|
||||
HBox.setHgrow(left, Priority.ALWAYS);
|
||||
HBox.setHgrow(right, Priority.ALWAYS);
|
||||
setCenter(panels);
|
||||
// ── List panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildListPanel() {
|
||||
listView = new ListView<>();
|
||||
listView.getItems().setAll(CraftingTable.CraftingTableType.values());
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(CraftingTable.CraftingTableType type, boolean empty) {
|
||||
super.updateItem(type, empty);
|
||||
if (empty || type == null) { setText(null); setStyle(""); return; }
|
||||
boolean configured = shared.containsKey(type);
|
||||
String color = typeColor(type);
|
||||
String suffix = configured ? " ✓" : " —";
|
||||
setText(type.name() + suffix);
|
||||
String fillColor = configured ? "#dddddd" : "#666666";
|
||||
setStyle("-fx-text-fill: " + fillColor + ";"
|
||||
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
setTooltip(new Tooltip(type.name() + (configured ? " – konfiguriert" : " – nicht konfiguriert")));
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onTypeSelected(old, nw));
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
|
||||
deleteBtn = new Button("Konfiguration löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
Button refreshBtn = new Button("↺ Neu laden");
|
||||
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
refreshBtn.setOnAction(e -> { reloadMap(); refresh(); });
|
||||
|
||||
VBox panel = new VBox(6, listView, deleteBtn, refreshBtn);
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||
return panel;
|
||||
}
|
||||
|
||||
// ── Form panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private ScrollPane buildFormPanel() {
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
ScrollPane scroll = new ScrollPane(formContainer);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(12));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
formTypeLabel = new Label("—");
|
||||
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #ccddff;");
|
||||
|
||||
nameIdField = new TextField();
|
||||
nameIdField.setPromptText("Text-Referenz ID (z. B. ui.crafting.alchemy_table)");
|
||||
|
||||
objectPathField = new TextField();
|
||||
objectPathField.setPromptText("Asset-Pfad zum 3D-Objekt (z. B. Models/crafting/alchemy_table.j3o)");
|
||||
|
||||
Button saveBtn = new Button("Crafting Table speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentTable());
|
||||
|
||||
form.getChildren().addAll(
|
||||
formTypeLabel,
|
||||
new Separator(),
|
||||
sectionTitle("Bezeichnung"),
|
||||
row("Name-ID:", nameIdField),
|
||||
new Separator(),
|
||||
sectionTitle("3D-Objekt"),
|
||||
row("Pfad:", objectPathField),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onTypeSelected(CraftingTable.CraftingTableType old, CraftingTable.CraftingTableType nw) {
|
||||
currentType = nw;
|
||||
if (nw == null) {
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
clearForm();
|
||||
} else {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromType(nw);
|
||||
deleteBtn.setDisable(!shared.containsKey(nw));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromType(CraftingTable.CraftingTableType type) {
|
||||
String color = typeColor(type);
|
||||
formTypeLabel.setText(type.name());
|
||||
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: " + color + ";");
|
||||
|
||||
CraftingTable t = shared.get(type);
|
||||
if (t != null) {
|
||||
nameIdField.setText(t.getName() != null ? t.getName().id() : "");
|
||||
objectPathField.setText(t.getObject() != null ? safe(t.getObject().getPath()) : "");
|
||||
} else {
|
||||
clearFormFields();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCurrentTable() {
|
||||
if (currentType == null) return;
|
||||
CraftingTable t = shared.getOrDefault(currentType, new CraftingTable());
|
||||
t.setType(currentType);
|
||||
|
||||
String nameId = nameIdField.getText().trim();
|
||||
t.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
||||
|
||||
String objPath = objectPathField.getText().trim();
|
||||
t.setObject(objPath.isBlank() ? null : new ObjectReference(objPath));
|
||||
|
||||
try {
|
||||
CraftingTableIO.save(t, tableDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
reloadMap();
|
||||
refresh();
|
||||
listView.getSelectionModel().select(currentType);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
if (currentType == null) return;
|
||||
try {
|
||||
CraftingTableIO.delete(currentType, tableDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
reloadMap();
|
||||
refresh();
|
||||
listView.getSelectionModel().select(currentType);
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
formTypeLabel.setText("—");
|
||||
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #ccddff;");
|
||||
clearFormFields();
|
||||
}
|
||||
|
||||
private void clearFormFields() {
|
||||
nameIdField.clear();
|
||||
objectPathField.clear();
|
||||
}
|
||||
|
||||
private void reloadMap() {
|
||||
@@ -44,251 +211,44 @@ public class CraftingTableEditorView extends BorderPane {
|
||||
shared.putAll(CraftingTableIO.loadAll(tableDir));
|
||||
}
|
||||
|
||||
private void onSaved() {
|
||||
reloadMap();
|
||||
left.refresh();
|
||||
right.refresh();
|
||||
private void refresh() {
|
||||
listView.refresh();
|
||||
if (currentType != null) {
|
||||
deleteBtn.setDisable(!shared.containsKey(currentType));
|
||||
if (!formContainer.isDisable()) loadFormFromType(currentType);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Single panel ──────────────────────────────────────────────────────────
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
static class TablePanel extends VBox {
|
||||
|
||||
private final Map<CraftingTable.CraftingTableType, CraftingTable> shared;
|
||||
private final Path tableDir;
|
||||
private final Runnable onSaved;
|
||||
|
||||
private final ListView<CraftingTable.CraftingTableType> listView;
|
||||
|
||||
private CraftingTable.CraftingTableType currentType = null;
|
||||
|
||||
// Form fields
|
||||
private TextField nameIdField;
|
||||
private TextField objectPathField;
|
||||
|
||||
private VBox formContainer;
|
||||
private Button deleteBtn;
|
||||
private Label formTypeLabel;
|
||||
|
||||
TablePanel(String title,
|
||||
Map<CraftingTable.CraftingTableType, CraftingTable> shared,
|
||||
Path tableDir, Runnable onSaved) {
|
||||
this.shared = shared;
|
||||
this.tableDir = tableDir;
|
||||
this.onSaved = onSaved;
|
||||
|
||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
||||
|
||||
// ── Header ────────────────────────────────────────────────────────
|
||||
Label titleLbl = new Label(title);
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
||||
Button refreshBtn = new Button("↺");
|
||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||
refreshBtn.setOnAction(e -> onSaved.run());
|
||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
||||
header.setPadding(new Insets(8, 10, 8, 10));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;"
|
||||
+ " -fx-border-width: 0 0 1 0;");
|
||||
|
||||
// ── Type list (always 5 fixed entries) ───────────────────────────
|
||||
listView = new ListView<>();
|
||||
listView.getItems().setAll(CraftingTable.CraftingTableType.values());
|
||||
listView.setPrefHeight(160);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(CraftingTable.CraftingTableType type, boolean empty) {
|
||||
super.updateItem(type, empty);
|
||||
if (empty || type == null) { setText(null); setStyle(""); return; }
|
||||
boolean configured = shared.containsKey(type);
|
||||
String color = typeColor(type);
|
||||
String suffix = configured ? " ✓" : " —";
|
||||
setText(type.name() + suffix);
|
||||
String fillColor = configured ? "#dddddd" : "#666666";
|
||||
setStyle("-fx-text-fill: " + fillColor + ";"
|
||||
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
setTooltip(new Tooltip(type.name() + (configured ? " – konfiguriert" : " – nicht konfiguriert")));
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onTypeSelected(old, nw));
|
||||
|
||||
deleteBtn = new Button("Konfiguration löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
VBox listSection = new VBox(listView, deleteBtn);
|
||||
listSection.setPadding(new Insets(0, 0, 4, 0));
|
||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
||||
VBox.setMargin(deleteBtn, new Insets(4, 8, 4, 8));
|
||||
|
||||
// ── Form ──────────────────────────────────────────────────────────
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
|
||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
||||
formScroll.setFitToWidth(true);
|
||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
||||
|
||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
||||
}
|
||||
|
||||
// ── Form construction ─────────────────────────────────────────────────
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(10));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
formTypeLabel = new Label("—");
|
||||
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #ccddff;");
|
||||
|
||||
nameIdField = new TextField();
|
||||
nameIdField.setPromptText("Text-Referenz ID (z. B. ui.crafting.alchemy_table)");
|
||||
|
||||
objectPathField = new TextField();
|
||||
objectPathField.setPromptText("Asset-Pfad zum 3D-Objekt (z. B. Models/crafting/alchemy_table.j3o)");
|
||||
|
||||
Button saveBtn = new Button("Crafting Table speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentTable());
|
||||
|
||||
form.getChildren().addAll(
|
||||
formTypeLabel,
|
||||
new Separator(),
|
||||
sectionTitle("Bezeichnung"),
|
||||
row("Name-ID:", nameIdField),
|
||||
new Separator(),
|
||||
sectionTitle("3D-Objekt"),
|
||||
row("Pfad:", objectPathField),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────
|
||||
|
||||
private void onTypeSelected(CraftingTable.CraftingTableType old, CraftingTable.CraftingTableType nw) {
|
||||
currentType = nw;
|
||||
if (nw == null) {
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
clearForm();
|
||||
} else {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromType(nw);
|
||||
deleteBtn.setDisable(!shared.containsKey(nw));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromType(CraftingTable.CraftingTableType type) {
|
||||
String color = typeColor(type);
|
||||
formTypeLabel.setText(type.name());
|
||||
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"
|
||||
+ " -fx-text-fill: " + color + ";");
|
||||
|
||||
CraftingTable t = shared.get(type);
|
||||
if (t != null) {
|
||||
nameIdField.setText(t.getName() != null ? t.getName().id() : "");
|
||||
objectPathField.setText(t.getObject() != null ? safe(t.getObject().getPath()) : "");
|
||||
} else {
|
||||
clearFormFields();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveCurrentTable() {
|
||||
if (currentType == null) return;
|
||||
|
||||
CraftingTable t = shared.getOrDefault(currentType, new CraftingTable());
|
||||
t.setType(currentType);
|
||||
|
||||
String nameId = nameIdField.getText().trim();
|
||||
t.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
||||
|
||||
String objPath = objectPathField.getText().trim();
|
||||
t.setObject(objPath.isBlank() ? null : new ObjectReference(objPath));
|
||||
|
||||
try {
|
||||
CraftingTableIO.save(t, tableDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
onSaved.run();
|
||||
listView.getSelectionModel().select(currentType);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
if (currentType == null) return;
|
||||
try {
|
||||
CraftingTableIO.delete(currentType, tableDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
onSaved.run();
|
||||
listView.getSelectionModel().select(currentType);
|
||||
}
|
||||
|
||||
/** Called by the parent view when shared data has been reloaded. */
|
||||
void refresh() {
|
||||
listView.refresh();
|
||||
if (currentType != null) {
|
||||
deleteBtn.setDisable(!shared.containsKey(currentType));
|
||||
if (formContainer.isDisable()) return;
|
||||
loadFormFromType(currentType);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
formTypeLabel.setText("—");
|
||||
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #ccddff;");
|
||||
clearFormFields();
|
||||
}
|
||||
|
||||
private void clearFormFields() {
|
||||
nameIdField.clear();
|
||||
objectPathField.clear();
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
static String typeColor(CraftingTable.CraftingTableType type) {
|
||||
if (type == null) return "#666666";
|
||||
return switch (type) {
|
||||
case AlchemyTable -> "#44bb88";
|
||||
case EnchantmentTable -> "#aa55ee";
|
||||
case Smithy -> "#cc8833";
|
||||
case Goldsmiths -> "#ddbb22";
|
||||
case Workshop -> "#4488cc";
|
||||
case Fireplace -> "#ee6633";
|
||||
case Kitchen -> "#88aa44";
|
||||
};
|
||||
}
|
||||
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(60);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static String safe(String s) { return s != null ? s : ""; }
|
||||
static String typeColor(CraftingTable.CraftingTableType type) {
|
||||
if (type == null) return "#666666";
|
||||
return switch (type) {
|
||||
case AlchemyTable -> "#44bb88";
|
||||
case EnchantmentTable -> "#aa55ee";
|
||||
case Smithy -> "#cc8833";
|
||||
case Goldsmiths -> "#ddbb22";
|
||||
case Workshop -> "#4488cc";
|
||||
case Fireplace -> "#ee6633";
|
||||
case Kitchen -> "#88aa44";
|
||||
};
|
||||
}
|
||||
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(60);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static String safe(String s) { return s != null ? s : ""; }
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ package de.blight.editor.ui;
|
||||
|
||||
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.editor.ProjectRoot;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
@@ -16,6 +19,7 @@ import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.Modality;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -89,8 +93,14 @@ public class DialogEditorView extends BorderPane {
|
||||
collectOptions(opt);
|
||||
if (opt.getId() != null) rootIds.add(opt.getId());
|
||||
}
|
||||
normalizeReferences();
|
||||
}
|
||||
// Restore orphaned options that were preserved separately
|
||||
if (npc.getEditorOnlyOptions() != null) {
|
||||
for (DialogOption opt : npc.getEditorOnlyOptions()) {
|
||||
collectOptions(opt);
|
||||
}
|
||||
}
|
||||
normalizeReferences();
|
||||
refreshOptionList();
|
||||
clearDetailForm();
|
||||
}
|
||||
@@ -112,6 +122,26 @@ public class DialogEditorView extends BorderPane {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Top-bar ────────────────────────────────────────────────────────────────
|
||||
@@ -147,7 +177,12 @@ public class DialogEditorView extends BorderPane {
|
||||
private SplitPane buildListPane() {
|
||||
// ── Left: option list ─────────────────────────────────────────────────
|
||||
optionListView = new ListView<>();
|
||||
optionListView.setStyle("-fx-control-inner-background: #2a2a3a; -fx-text-fill: #ddd;"
|
||||
+ " -fx-selection-bar: #3a5a8a; -fx-selection-bar-text: white;");
|
||||
optionListView.setCellFactory(lv -> new ListCell<>() {
|
||||
{
|
||||
selectedProperty().addListener((obs, old, sel) -> applyStyle());
|
||||
}
|
||||
@Override protected void updateItem(String id, boolean empty) {
|
||||
super.updateItem(id, empty);
|
||||
if (empty || id == null) { setText(null); setStyle(""); return; }
|
||||
@@ -155,7 +190,14 @@ public class DialogEditorView extends BorderPane {
|
||||
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);
|
||||
setStyle("-fx-text-fill: " + (rootIds.contains(id) ? "#ffdd88" : "#cccccc") + ";");
|
||||
applyStyle();
|
||||
}
|
||||
private void applyStyle() {
|
||||
String id = getItem();
|
||||
if (id == null || isEmpty()) { setStyle(""); return; }
|
||||
String text = rootIds.contains(id) ? "#ffdd88" : "#cccccc";
|
||||
String bg = isSelected() ? "#3a5a8a" : "#2a2a3a";
|
||||
setStyle("-fx-background-color: " + bg + "; -fx-text-fill: " + text + ";");
|
||||
}
|
||||
});
|
||||
optionListView.getSelectionModel().selectedItemProperty().addListener(
|
||||
@@ -236,9 +278,16 @@ public class DialogEditorView extends BorderPane {
|
||||
statusCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
questOpenField = new TextField();
|
||||
questOpenField.setPromptText("Quest-ID");
|
||||
questOpenField.setPromptText("Quest wählen…");
|
||||
questOpenField.setEditable(false);
|
||||
questOpenField.setCursor(Cursor.HAND);
|
||||
questOpenField.setOnMouseClicked(e -> pickQuestId(questOpenField));
|
||||
|
||||
questCompleteField = new TextField();
|
||||
questCompleteField.setPromptText("Quest-ID");
|
||||
questCompleteField.setPromptText("Quest wählen…");
|
||||
questCompleteField.setEditable(false);
|
||||
questCompleteField.setCursor(Cursor.HAND);
|
||||
questCompleteField.setOnMouseClicked(e -> pickQuestId(questCompleteField));
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Voraussetzungen"),
|
||||
@@ -284,16 +333,23 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
// Quests
|
||||
recvQuestField = new TextField();
|
||||
recvQuestField.setPromptText("Quest-ID");
|
||||
recvQuestField.setPromptText("Quest wählen…");
|
||||
recvQuestField.setEditable(false);
|
||||
recvQuestField.setCursor(Cursor.HAND);
|
||||
recvQuestField.setOnMouseClicked(e -> pickQuestId(recvQuestField));
|
||||
|
||||
fulfillsQuestField = new TextField();
|
||||
fulfillsQuestField.setPromptText("Quest-ID");
|
||||
fulfillsQuestField.setPromptText("Quest wählen…");
|
||||
fulfillsQuestField.setEditable(false);
|
||||
fulfillsQuestField.setCursor(Cursor.HAND);
|
||||
fulfillsQuestField.setOnMouseClicked(e -> pickQuestId(fulfillsQuestField));
|
||||
|
||||
abortsQuestsView = new ListView<>();
|
||||
abortsQuestsView.setPrefHeight(80);
|
||||
abortsQuestsView.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
||||
Button addAbortsBtn = smallBtn("+");
|
||||
Button delAbortsBtn = smallBtn("−");
|
||||
addAbortsBtn.setOnAction(e -> promptQuestId(abortsQuestsView));
|
||||
addAbortsBtn.setOnAction(e -> pickQuestIdForList(abortsQuestsView));
|
||||
delAbortsBtn.setOnAction(e -> removeSelected(abortsQuestsView));
|
||||
|
||||
form.getChildren().addAll(
|
||||
@@ -518,7 +574,12 @@ public class DialogEditorView extends BorderPane {
|
||||
DialogOption opt = new DialogOption();
|
||||
opt.setLabel("Neue Option");
|
||||
allOptions.put(opt.getId(), opt);
|
||||
if (asRoot) rootIds.add(opt.getId());
|
||||
if (asRoot) {
|
||||
rootIds.add(opt.getId());
|
||||
} else if (selectedId != null) {
|
||||
// Auto-link to currently selected option so no orphans are created
|
||||
nextOptionsView.getItems().add(opt.getId());
|
||||
}
|
||||
refreshOptionList();
|
||||
optionListView.getSelectionModel().select(opt.getId());
|
||||
}
|
||||
@@ -580,14 +641,63 @@ public class DialogEditorView extends BorderPane {
|
||||
});
|
||||
}
|
||||
|
||||
private void promptQuestId(ListView<String> target) {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.setTitle("Quest-ID");
|
||||
dlg.setHeaderText("Quest-ID eingeben:");
|
||||
private void pickQuestId(TextField target) {
|
||||
String chosen = showQuestPickerDialog();
|
||||
if (chosen != null) target.setText(chosen);
|
||||
}
|
||||
|
||||
private void pickQuestIdForList(ListView<String> target) {
|
||||
String chosen = showQuestPickerDialog();
|
||||
if (chosen != null && !target.getItems().contains(chosen)) target.getItems().add(chosen);
|
||||
}
|
||||
|
||||
private String showQuestPickerDialog() {
|
||||
Path questDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("quests");
|
||||
List<Quest> questList = QuestIO.loadAll(questDir);
|
||||
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Quest auswählen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
dlg.showAndWait().ifPresent(id -> {
|
||||
if (!id.isBlank() && !target.getItems().contains(id)) target.getItems().add(id);
|
||||
|
||||
ListView<Quest> chooser = new ListView<>();
|
||||
chooser.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Quest q, boolean empty) {
|
||||
super.updateItem(q, empty);
|
||||
if (empty || q == null) { setText(null); return; }
|
||||
String id = q.getQuestId() != null ? q.getQuestId() : "—";
|
||||
String name = q.getText() != null ? q.getText().id() : "";
|
||||
setText(name.isBlank() ? id : id + " — " + name);
|
||||
setStyle("-fx-text-fill: #cccccc;");
|
||||
}
|
||||
});
|
||||
chooser.getItems().setAll(questList);
|
||||
chooser.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;"
|
||||
+ " -fx-selection-bar: #3a5a8a;");
|
||||
chooser.setPrefSize(380, 300);
|
||||
|
||||
if (questList.isEmpty()) {
|
||||
Label hint = new Label("Keine Quests gefunden.\nQuests im Quest-Editor anlegen.");
|
||||
hint.setStyle("-fx-text-fill: #888; -fx-font-style: italic;");
|
||||
dlg.getDialogPane().setContent(hint);
|
||||
} else {
|
||||
dlg.getDialogPane().setContent(chooser);
|
||||
}
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
dlg.getDialogPane().setStyle("-fx-background-color: #252535;");
|
||||
|
||||
Button okBtn = (Button) dlg.getDialogPane().lookupButton(ButtonType.OK);
|
||||
okBtn.setDisable(true);
|
||||
chooser.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, o, n) -> okBtn.setDisable(n == null));
|
||||
chooser.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() == 2 && !chooser.getSelectionModel().isEmpty()) okBtn.fire();
|
||||
});
|
||||
dlg.setResultConverter(bt -> {
|
||||
if (bt != ButtonType.OK) return null;
|
||||
Quest sel = chooser.getSelectionModel().getSelectedItem();
|
||||
return sel != null ? sel.getQuestId() : null;
|
||||
});
|
||||
return dlg.showAndWait().orElse(null);
|
||||
}
|
||||
|
||||
private static void removeSelected(ListView<String> list) {
|
||||
|
||||
@@ -15,288 +15,262 @@ import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Fraktions-Verwaltung: zwei identische FractionPanel-Instanzen nebeneinander.
|
||||
* Fraktions-Verwaltung: Liste links, Formular rechts.
|
||||
* Sortiert nach Name-ID, dann nach UUID.
|
||||
*/
|
||||
public class FractionEditorView extends BorderPane {
|
||||
|
||||
private final ObservableList<Fraction> sharedFractions = FXCollections.observableArrayList();
|
||||
private final ObservableList<Fraction> fractions = FXCollections.observableArrayList();
|
||||
private final Path fractionDir;
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private final SortedList<Fraction> sortedFractions;
|
||||
private ListView<Fraction> listView;
|
||||
private Button deleteBtn;
|
||||
private Fraction current = null;
|
||||
|
||||
// ── Form fields ───────────────────────────────────────────────────────────
|
||||
|
||||
private Label idLabel;
|
||||
private TextField nameField;
|
||||
private TextField maleMemberField;
|
||||
private TextField femaleMemberField;
|
||||
private TextField rank1Field;
|
||||
private TextField rank2Field;
|
||||
private TextField rank3Field;
|
||||
private VBox formContainer;
|
||||
|
||||
public FractionEditorView(Path fractionDir) {
|
||||
this.fractionDir = fractionDir;
|
||||
this.fractionDir = fractionDir;
|
||||
this.sortedFractions = new SortedList<>(fractions, FractionIO.SORT_ORDER);
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
reload();
|
||||
|
||||
FractionPanel left = new FractionPanel("Liste 1", sharedFractions, fractionDir, this::reload);
|
||||
FractionPanel right = new FractionPanel("Liste 2", sharedFractions, fractionDir, this::reload);
|
||||
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||
split.setDividerPositions(0.28);
|
||||
setCenter(split);
|
||||
}
|
||||
|
||||
HBox panels = new HBox(1, left, right);
|
||||
HBox.setHgrow(left, Priority.ALWAYS);
|
||||
HBox.setHgrow(right, Priority.ALWAYS);
|
||||
setCenter(panels);
|
||||
// ── List panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildListPanel() {
|
||||
listView = new ListView<>(sortedFractions);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Fraction f, boolean empty) {
|
||||
super.updateItem(f, empty);
|
||||
if (empty || f == null) { setText(null); setStyle(""); return; }
|
||||
String name = f.getName() != null ? f.getName().id() : "—";
|
||||
setText(name);
|
||||
setTooltip(new Tooltip("ID: " + (f.getFractionId() != null ? f.getFractionId() : "?")));
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent #6699cc;"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onFractionSelected(old, nw));
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
|
||||
Button newBtn = new Button("Neue Fraktion");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createFraction());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
Button refreshBtn = new Button("↺ Neu laden");
|
||||
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
refreshBtn.setOnAction(e -> reload());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
|
||||
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||
return panel;
|
||||
}
|
||||
|
||||
// ── Form panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private ScrollPane buildFormPanel() {
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
ScrollPane scroll = new ScrollPane(formContainer);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(12));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
idLabel = new Label("—");
|
||||
idLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-font-family: monospace;");
|
||||
|
||||
nameField = field("z. B. faction.guards");
|
||||
maleMemberField = field("z. B. faction.guards.member.male");
|
||||
femaleMemberField = field("z. B. faction.guards.member.female");
|
||||
rank1Field = field("z. B. faction.guards.rank1");
|
||||
rank2Field = field("z. B. faction.guards.rank2");
|
||||
rank3Field = field("z. B. faction.guards.rank3");
|
||||
|
||||
Button saveBtn = new Button("Fraktion speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentFraction());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Kennung"),
|
||||
new Separator(),
|
||||
row("UUID:", idLabel),
|
||||
sectionTitle("Text-Referenzen"),
|
||||
new Separator(),
|
||||
row("Name:", nameField),
|
||||
row("Mitglied (m):", maleMemberField),
|
||||
row("Mitglied (w):", femaleMemberField),
|
||||
sectionTitle("Ränge"),
|
||||
new Separator(),
|
||||
row("Rang 1:", rank1Field),
|
||||
row("Rang 2:", rank2Field),
|
||||
row("Rang 3:", rank3Field),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onFractionSelected(Fraction old, Fraction nw) {
|
||||
if (old != null) saveFormToFraction(old);
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromFraction(nw);
|
||||
} else {
|
||||
formContainer.setDisable(true);
|
||||
clearForm();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromFraction(Fraction f) {
|
||||
idLabel.setText(f.getFractionId() != null ? f.getFractionId().toString() : "—");
|
||||
nameField.setText(textId(f.getName()));
|
||||
maleMemberField.setText(textId(f.getMaleMemberName()));
|
||||
femaleMemberField.setText(textId(f.getFemaleMemberName()));
|
||||
rank1Field.setText(textId(f.getRank1Name()));
|
||||
rank2Field.setText(textId(f.getRank2Name()));
|
||||
rank3Field.setText(textId(f.getRank3Name()));
|
||||
}
|
||||
|
||||
private void saveFormToFraction(Fraction f) {
|
||||
f.setName(ref(nameField.getText()));
|
||||
f.setMaleMemberName(ref(maleMemberField.getText()));
|
||||
f.setFemaleMemberName(ref(femaleMemberField.getText()));
|
||||
f.setRank1Name(ref(rank1Field.getText()));
|
||||
f.setRank2Name(ref(rank2Field.getText()));
|
||||
f.setRank3Name(ref(rank3Field.getText()));
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idLabel.setText("—");
|
||||
nameField.clear();
|
||||
maleMemberField.clear();
|
||||
femaleMemberField.clear();
|
||||
rank1Field.clear();
|
||||
rank2Field.clear();
|
||||
rank3Field.clear();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
|
||||
private void createFraction() {
|
||||
Fraction f = new Fraction();
|
||||
f.setFractionId(UUID.randomUUID());
|
||||
fractions.add(f);
|
||||
listView.getSelectionModel().select(f);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
Fraction sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
UUID id = sel.getFractionId();
|
||||
fractions.remove(sel);
|
||||
try { FractionIO.delete(id, fractionDir); } catch (IOException ignored) {}
|
||||
current = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
reload();
|
||||
}
|
||||
|
||||
private void saveCurrentFraction() {
|
||||
if (current == null) return;
|
||||
saveFormToFraction(current);
|
||||
if (current.getFractionId() == null) {
|
||||
new Alert(Alert.AlertType.ERROR,
|
||||
"Fraktion hat keine UUID – bitte neu erstellen.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
FractionIO.save(current, fractionDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
final UUID fid = current.getFractionId();
|
||||
fractions.stream()
|
||||
.filter(f -> fid.equals(f.getFractionId()))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
sharedFractions.setAll(FractionIO.loadAll(fractionDir));
|
||||
fractions.setAll(FractionIO.loadAll(fractionDir));
|
||||
}
|
||||
|
||||
// ── Single panel ──────────────────────────────────────────────────────────
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
static class FractionPanel extends VBox {
|
||||
private static TextReference ref(String text) {
|
||||
String t = text == null ? "" : text.trim();
|
||||
return t.isBlank() ? null : new TextReference(t);
|
||||
}
|
||||
|
||||
private final ObservableList<Fraction> fractions;
|
||||
private final Path fractionDir;
|
||||
private final Runnable onSaved;
|
||||
private static String textId(TextReference r) { return r != null ? r.id() : ""; }
|
||||
|
||||
private final SortedList<Fraction> sortedFractions;
|
||||
private final ListView<Fraction> listView;
|
||||
private static TextField field(String prompt) {
|
||||
TextField tf = new TextField();
|
||||
tf.setPromptText(prompt);
|
||||
return tf;
|
||||
}
|
||||
|
||||
private Fraction current = null;
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
// Form fields
|
||||
private Label idLabel;
|
||||
private TextField nameField;
|
||||
private TextField maleMemberField;
|
||||
private TextField femaleMemberField;
|
||||
private TextField rank1Field;
|
||||
private TextField rank2Field;
|
||||
private TextField rank3Field;
|
||||
|
||||
private VBox formContainer;
|
||||
private Button deleteBtn;
|
||||
|
||||
FractionPanel(String title, ObservableList<Fraction> fractions, Path fractionDir, Runnable onSaved) {
|
||||
this.fractions = fractions;
|
||||
this.fractionDir = fractionDir;
|
||||
this.onSaved = onSaved;
|
||||
|
||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
||||
|
||||
// ── Header ────────────────────────────────────────────────────────
|
||||
Label titleLbl = new Label(title);
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
||||
Button refreshBtn = new Button("↺");
|
||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||
refreshBtn.setOnAction(e -> onSaved.run());
|
||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
||||
header.setPadding(new Insets(8, 10, 8, 10));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;"
|
||||
+ " -fx-border-width: 0 0 1 0;");
|
||||
|
||||
// ── Fraction list ─────────────────────────────────────────────────
|
||||
sortedFractions = new SortedList<>(fractions, FractionIO.SORT_ORDER);
|
||||
listView = new ListView<>(sortedFractions);
|
||||
listView.setPrefHeight(180);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Fraction f, boolean empty) {
|
||||
super.updateItem(f, empty);
|
||||
if (empty || f == null) { setText(null); setStyle(""); return; }
|
||||
String name = f.getName() != null ? f.getName().id() : "—";
|
||||
String uuid = f.getFractionId() != null ? f.getFractionId().toString().substring(0, 8) + "…" : "?";
|
||||
setText(name);
|
||||
setTooltip(new Tooltip("ID: " + (f.getFractionId() != null ? f.getFractionId() : "?")));
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent #6699cc;"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onFractionSelected(old, nw));
|
||||
|
||||
Button newBtn = new Button("Neue Fraktion");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createFraction());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
|
||||
VBox listSection = new VBox(listView, listButtons);
|
||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
||||
|
||||
// ── Form ──────────────────────────────────────────────────────────
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
|
||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
||||
formScroll.setFitToWidth(true);
|
||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
||||
|
||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
||||
}
|
||||
|
||||
// ── Form construction ─────────────────────────────────────────────────
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(10));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
idLabel = new Label("—");
|
||||
idLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-font-family: monospace;");
|
||||
|
||||
nameField = field("z. B. faction.guards");
|
||||
maleMemberField = field("z. B. faction.guards.member.male");
|
||||
femaleMemberField = field("z. B. faction.guards.member.female");
|
||||
rank1Field = field("z. B. faction.guards.rank1");
|
||||
rank2Field = field("z. B. faction.guards.rank2");
|
||||
rank3Field = field("z. B. faction.guards.rank3");
|
||||
|
||||
Button saveBtn = new Button("Fraktion speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentFraction());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Kennung"),
|
||||
new Separator(),
|
||||
row("UUID:", idLabel),
|
||||
sectionTitle("Text-Referenzen"),
|
||||
new Separator(),
|
||||
row("Name:", nameField),
|
||||
row("Mitglied (m):", maleMemberField),
|
||||
row("Mitglied (w):", femaleMemberField),
|
||||
sectionTitle("Ränge"),
|
||||
new Separator(),
|
||||
row("Rang 1:", rank1Field),
|
||||
row("Rang 2:", rank2Field),
|
||||
row("Rang 3:", rank3Field),
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────
|
||||
|
||||
private void onFractionSelected(Fraction old, Fraction nw) {
|
||||
if (old != null) saveFormToFraction(old);
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromFraction(nw);
|
||||
} else {
|
||||
formContainer.setDisable(true);
|
||||
clearForm();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromFraction(Fraction f) {
|
||||
idLabel.setText(f.getFractionId() != null ? f.getFractionId().toString() : "—");
|
||||
nameField.setText(textId(f.getName()));
|
||||
maleMemberField.setText(textId(f.getMaleMemberName()));
|
||||
femaleMemberField.setText(textId(f.getFemaleMemberName()));
|
||||
rank1Field.setText(textId(f.getRank1Name()));
|
||||
rank2Field.setText(textId(f.getRank2Name()));
|
||||
rank3Field.setText(textId(f.getRank3Name()));
|
||||
}
|
||||
|
||||
private void saveFormToFraction(Fraction f) {
|
||||
f.setName(ref(nameField.getText()));
|
||||
f.setMaleMemberName(ref(maleMemberField.getText()));
|
||||
f.setFemaleMemberName(ref(femaleMemberField.getText()));
|
||||
f.setRank1Name(ref(rank1Field.getText()));
|
||||
f.setRank2Name(ref(rank2Field.getText()));
|
||||
f.setRank3Name(ref(rank3Field.getText()));
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idLabel.setText("—");
|
||||
nameField.clear();
|
||||
maleMemberField.clear();
|
||||
femaleMemberField.clear();
|
||||
rank1Field.clear();
|
||||
rank2Field.clear();
|
||||
rank3Field.clear();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────
|
||||
|
||||
private void createFraction() {
|
||||
Fraction f = new Fraction();
|
||||
f.setFractionId(UUID.randomUUID());
|
||||
fractions.add(f);
|
||||
listView.getSelectionModel().select(f);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
Fraction sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
UUID id = sel.getFractionId();
|
||||
fractions.remove(sel);
|
||||
try { FractionIO.delete(id, fractionDir); } catch (IOException ignored) {}
|
||||
current = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
onSaved.run();
|
||||
}
|
||||
|
||||
private void saveCurrentFraction() {
|
||||
if (current == null) return;
|
||||
saveFormToFraction(current);
|
||||
|
||||
if (current.getFractionId() == null) {
|
||||
new Alert(Alert.AlertType.ERROR,
|
||||
"Fraktion hat keine UUID – bitte neu erstellen.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
FractionIO.save(current, fractionDir);
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
onSaved.run();
|
||||
final UUID fid = current.getFractionId();
|
||||
fractions.stream()
|
||||
.filter(f -> fid.equals(f.getFractionId()))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private static TextReference ref(String text) {
|
||||
String t = text == null ? "" : text.trim();
|
||||
return t.isBlank() ? null : new TextReference(t);
|
||||
}
|
||||
|
||||
private static String textId(TextReference r) { return r != null ? r.id() : ""; }
|
||||
|
||||
private static TextField field(String prompt) {
|
||||
TextField tf = new TextField();
|
||||
tf.setPromptText(prompt);
|
||||
return tf;
|
||||
}
|
||||
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(100);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(100);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,237 +15,239 @@ import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Locations-Verwaltung: zwei identische LocationPanel-Instanzen nebeneinander.
|
||||
* Locations-Verwaltung: Liste links, Formular rechts.
|
||||
* Alle Locations werden gemeinsam in einer Datei gespeichert (LocationIO).
|
||||
*/
|
||||
public class LocationEditorView extends BorderPane {
|
||||
|
||||
private final ObservableList<Location> sharedLocations = FXCollections.observableArrayList();
|
||||
private final ObservableList<Location> locations = FXCollections.observableArrayList();
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private ListView<Location> listView;
|
||||
private Button deleteBtn;
|
||||
private Location current = null;
|
||||
|
||||
// ── Form fields ───────────────────────────────────────────────────────────
|
||||
|
||||
private TextField nameIdField;
|
||||
private TextField centerXField;
|
||||
private TextField centerZField;
|
||||
private TextField radiusField;
|
||||
private TriggerListEditor triggerEditor;
|
||||
private VBox formContainer;
|
||||
|
||||
public LocationEditorView() {
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
reload();
|
||||
|
||||
LocationPanel left = new LocationPanel("Liste 1", sharedLocations, this::saveAll);
|
||||
LocationPanel right = new LocationPanel("Liste 2", sharedLocations, this::saveAll);
|
||||
|
||||
HBox panels = new HBox(1, left, right);
|
||||
HBox.setHgrow(left, Priority.ALWAYS);
|
||||
HBox.setHgrow(right, Priority.ALWAYS);
|
||||
setCenter(panels);
|
||||
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||
split.setDividerPositions(0.28);
|
||||
setCenter(split);
|
||||
}
|
||||
|
||||
private void reload() {
|
||||
try { sharedLocations.setAll(LocationIO.load()); }
|
||||
catch (IOException e) { sharedLocations.clear(); }
|
||||
// ── List panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildListPanel() {
|
||||
listView = new ListView<>(locations);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Location loc, boolean empty) {
|
||||
super.updateItem(loc, empty);
|
||||
if (empty || loc == null) { setText(null); setStyle(""); return; }
|
||||
setText(loc.getId().isBlank() ? "—" : loc.getId());
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent #66aacc;"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onSelected(old, nw));
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
|
||||
Button newBtn = new Button("Neue Location");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createLocation());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
Button refreshBtn = new Button("↺ Neu laden");
|
||||
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
refreshBtn.setOnAction(e -> reload());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
|
||||
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void saveAll() {
|
||||
try { LocationIO.save(sharedLocations); }
|
||||
catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
// ── Form panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private ScrollPane buildFormPanel() {
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
ScrollPane scroll = new ScrollPane(formContainer);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(12));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
nameIdField = field("z. B. location.village");
|
||||
centerXField = field("X-Koordinate");
|
||||
centerZField = field("Z-Koordinate");
|
||||
radiusField = field("Radius in Meter");
|
||||
|
||||
triggerEditor = new TriggerListEditor(List.of(), () -> {});
|
||||
|
||||
Button saveBtn = new Button("Location speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrent());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Kennung & Position"),
|
||||
new Separator(),
|
||||
row("Name-ID:", nameIdField),
|
||||
row("Mitte X:", centerXField),
|
||||
row("Mitte Z:", centerZField),
|
||||
row("Radius:", radiusField),
|
||||
sectionTitle("Trigger"),
|
||||
new Separator(),
|
||||
triggerEditor,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onSelected(Location old, Location nw) {
|
||||
if (old != null) saveFormToLocation(old);
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) { formContainer.setDisable(false); loadForm(nw); }
|
||||
else { formContainer.setDisable(true); clearForm(); }
|
||||
}
|
||||
|
||||
private void loadForm(Location loc) {
|
||||
nameIdField.setText(loc.getId());
|
||||
centerXField.setText(String.valueOf(loc.getCenterX()));
|
||||
centerZField.setText(String.valueOf(loc.getCenterZ()));
|
||||
radiusField.setText(String.valueOf(loc.getRadius()));
|
||||
|
||||
int idx = formContainer.getChildren().indexOf(triggerEditor);
|
||||
triggerEditor = new TriggerListEditor(
|
||||
loc.getTriggers() != null ? loc.getTriggers() : List.of(), () -> {});
|
||||
if (idx >= 0) formContainer.getChildren().set(idx, triggerEditor);
|
||||
}
|
||||
|
||||
private void saveFormToLocation(Location loc) {
|
||||
String nameId = nameIdField.getText().trim();
|
||||
loc.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
||||
loc.setCenterX(parseFloat(centerXField.getText()));
|
||||
loc.setCenterZ(parseFloat(centerZField.getText()));
|
||||
loc.setRadius(parseFloat(radiusField.getText()));
|
||||
loc.setTriggers(triggerEditor.getTriggers());
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
nameIdField.clear();
|
||||
centerXField.clear();
|
||||
centerZField.clear();
|
||||
radiusField.clear();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
|
||||
private void createLocation() {
|
||||
Location loc = new Location();
|
||||
loc.setName(new TextReference("location.neu_" + System.currentTimeMillis()));
|
||||
locations.add(loc);
|
||||
listView.getSelectionModel().select(loc);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
if (current == null) return;
|
||||
locations.remove(current);
|
||||
current = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
persist();
|
||||
reload();
|
||||
}
|
||||
|
||||
// ── Single panel ──────────────────────────────────────────────────────────
|
||||
|
||||
static class LocationPanel extends VBox {
|
||||
|
||||
private final ObservableList<Location> locations;
|
||||
private final Runnable onSaved;
|
||||
|
||||
private final ListView<Location> listView;
|
||||
private Location current = null;
|
||||
|
||||
// Form fields
|
||||
private TextField nameIdField;
|
||||
private TextField centerXField;
|
||||
private TextField centerZField;
|
||||
private TextField radiusField;
|
||||
private TriggerListEditor triggerEditor;
|
||||
|
||||
private VBox formContainer;
|
||||
private Button deleteBtn;
|
||||
|
||||
LocationPanel(String title, ObservableList<Location> locations, Runnable onSaved) {
|
||||
this.locations = locations;
|
||||
this.onSaved = onSaved;
|
||||
|
||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
||||
|
||||
Label titleLbl = new Label(title);
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
||||
Button refreshBtn = new Button("↺");
|
||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||
refreshBtn.setOnAction(e -> onSaved.run());
|
||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
||||
header.setPadding(new Insets(8, 10, 8, 10));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;");
|
||||
|
||||
listView = new ListView<>(locations);
|
||||
listView.setPrefHeight(180);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Location loc, boolean empty) {
|
||||
super.updateItem(loc, empty);
|
||||
if (empty || loc == null) { setText(null); setStyle(""); return; }
|
||||
setText(loc.getId().isBlank() ? "—" : loc.getId());
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent #66aacc;"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onSelected(old, nw));
|
||||
|
||||
Button newBtn = new Button("Neue Location");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createLocation());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
|
||||
VBox listSection = new VBox(listView, listButtons);
|
||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
||||
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
|
||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
||||
formScroll.setFitToWidth(true);
|
||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
||||
|
||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
||||
private void saveCurrent() {
|
||||
if (current == null) return;
|
||||
saveFormToLocation(current);
|
||||
if (current.getId().isBlank()) {
|
||||
new Alert(Alert.AlertType.ERROR, "Name-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
String savedId = current.getId();
|
||||
persist();
|
||||
reload();
|
||||
locations.stream()
|
||||
.filter(l -> savedId.equals(l.getId()))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(10));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
nameIdField = field("z. B. location.village");
|
||||
centerXField = field("X-Koordinate");
|
||||
centerZField = field("Z-Koordinate");
|
||||
radiusField = field("Radius in Meter");
|
||||
|
||||
// TriggerEditor — placeholder; rebuilt when item selected
|
||||
triggerEditor = new TriggerListEditor(List.of(), () -> {});
|
||||
|
||||
Button saveBtn = new Button("Location speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrent());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Kennung & Position"),
|
||||
new Separator(),
|
||||
row("Name-ID:", nameIdField),
|
||||
row("Mitte X:", centerXField),
|
||||
row("Mitte Z:", centerZField),
|
||||
row("Radius:", radiusField),
|
||||
sectionTitle("Trigger"),
|
||||
new Separator(),
|
||||
triggerEditor,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
return form;
|
||||
}
|
||||
|
||||
private void onSelected(Location old, Location nw) {
|
||||
if (old != null) saveFormToLocation(old);
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) { formContainer.setDisable(false); loadForm(nw); }
|
||||
else { formContainer.setDisable(true); clearForm(); }
|
||||
}
|
||||
|
||||
private void loadForm(Location loc) {
|
||||
nameIdField.setText(loc.getId());
|
||||
centerXField.setText(String.valueOf(loc.getCenterX()));
|
||||
centerZField.setText(String.valueOf(loc.getCenterZ()));
|
||||
radiusField.setText(String.valueOf(loc.getRadius()));
|
||||
|
||||
// Rebuild trigger editor
|
||||
int idx = formContainer.getChildren().indexOf(triggerEditor);
|
||||
triggerEditor = new TriggerListEditor(
|
||||
loc.getTriggers() != null ? loc.getTriggers() : List.of(), () -> {});
|
||||
if (idx >= 0) formContainer.getChildren().set(idx, triggerEditor);
|
||||
}
|
||||
|
||||
private void saveFormToLocation(Location loc) {
|
||||
String nameId = nameIdField.getText().trim();
|
||||
loc.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
||||
loc.setCenterX(parseFloat(centerXField.getText()));
|
||||
loc.setCenterZ(parseFloat(centerZField.getText()));
|
||||
loc.setRadius(parseFloat(radiusField.getText()));
|
||||
loc.setTriggers(triggerEditor.getTriggers());
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
nameIdField.clear(); centerXField.clear(); centerZField.clear(); radiusField.clear();
|
||||
}
|
||||
|
||||
private void createLocation() {
|
||||
Location loc = new Location();
|
||||
loc.setName(new TextReference("location.neu_" + System.currentTimeMillis()));
|
||||
locations.add(loc);
|
||||
listView.getSelectionModel().select(loc);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
if (current == null) return;
|
||||
locations.remove(current);
|
||||
current = null; clearForm(); formContainer.setDisable(true); deleteBtn.setDisable(true);
|
||||
onSaved.run();
|
||||
}
|
||||
|
||||
private void saveCurrent() {
|
||||
if (current == null) return;
|
||||
saveFormToLocation(current);
|
||||
if (current.getId().isBlank()) {
|
||||
new Alert(Alert.AlertType.ERROR, "Name-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
onSaved.run();
|
||||
listView.refresh();
|
||||
}
|
||||
|
||||
private static float parseFloat(String s) {
|
||||
try { return Float.parseFloat(s.trim().replace(',', '.')); }
|
||||
catch (NumberFormatException ignored) { return 0f; }
|
||||
}
|
||||
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(80);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static TextField field(String prompt) {
|
||||
TextField tf = new TextField(); tf.setPromptText(prompt); return tf;
|
||||
private void persist() {
|
||||
try { LocationIO.save(locations); }
|
||||
catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
try { locations.setAll(LocationIO.load()); }
|
||||
catch (IOException e) { locations.clear(); }
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static float parseFloat(String s) {
|
||||
try { return Float.parseFloat(s.trim().replace(',', '.')); }
|
||||
catch (NumberFormatException ignored) { return 0f; }
|
||||
}
|
||||
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(80);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static TextField field(String prompt) {
|
||||
TextField tf = new TextField();
|
||||
tf.setPromptText(prompt);
|
||||
return tf;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import de.blight.common.model.quests.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
@@ -21,467 +20,454 @@ import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Quest-Verwaltung: zwei identische QuestPanel-Instanzen nebeneinander.
|
||||
* Beide Panels teilen die gleiche ObservableList und speichern in dasselbe Verzeichnis.
|
||||
* Quest-Verwaltung: Liste links, Formular rechts.
|
||||
*/
|
||||
public class QuestEditorView extends BorderPane {
|
||||
|
||||
private final ObservableList<Quest> sharedQuests = FXCollections.observableArrayList();
|
||||
private final ObservableList<Quest> quests = FXCollections.observableArrayList();
|
||||
private final Path questDir;
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private ListView<Quest> listView;
|
||||
private Button deleteBtn;
|
||||
private Quest current = null;
|
||||
|
||||
// ── Common form fields ────────────────────────────────────────────────────
|
||||
|
||||
private TextField idField;
|
||||
private Spinner<Integer> xpSpinner;
|
||||
private TextField textField;
|
||||
private TextField descField;
|
||||
private TextField successField;
|
||||
private ComboBox<String> typeCombo;
|
||||
private VBox dynamicArea;
|
||||
private VBox formContainer;
|
||||
private Button saveBtn;
|
||||
|
||||
// ── Type-specific fields ──────────────────────────────────────────────────
|
||||
|
||||
private TextField f1, f2, f3;
|
||||
private Spinner<Integer> countSpinner;
|
||||
|
||||
public QuestEditorView(Path questDir) {
|
||||
this.questDir = questDir;
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
|
||||
reload();
|
||||
|
||||
QuestPanel left = new QuestPanel("Liste 1", sharedQuests, questDir, this::reload);
|
||||
QuestPanel right = new QuestPanel("Liste 2", sharedQuests, questDir, this::reload);
|
||||
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||
split.setDividerPositions(0.28);
|
||||
setCenter(split);
|
||||
}
|
||||
|
||||
HBox panels = new HBox(1, left, right);
|
||||
HBox.setHgrow(left, Priority.ALWAYS);
|
||||
HBox.setHgrow(right, Priority.ALWAYS);
|
||||
setCenter(panels);
|
||||
// ── List panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildListPanel() {
|
||||
listView = new ListView<>(quests);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Quest q, boolean empty) {
|
||||
super.updateItem(q, empty);
|
||||
if (empty || q == null) { setText(null); setStyle(""); return; }
|
||||
String id = q.getQuestId() != null ? q.getQuestId() : "—";
|
||||
String type = QuestIO.typeOf(q);
|
||||
setText("[" + type + "] " + id);
|
||||
setStyle("-fx-text-fill: #cccccc;");
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onQuestSelected(old, nw));
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
|
||||
Button newBtn = new Button("Neue Quest");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createQuest());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
Button refreshBtn = new Button("↺ Neu laden");
|
||||
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
refreshBtn.setOnAction(e -> reload());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
|
||||
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||
return panel;
|
||||
}
|
||||
|
||||
// ── Form panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private ScrollPane buildFormPanel() {
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
|
||||
ScrollPane scroll = new ScrollPane(formContainer);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(12));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
// ID field with no-space filter + auto-fill listener
|
||||
idField = new TextField();
|
||||
idField.setPromptText("eindeutige ID (keine Leerzeichen)");
|
||||
idField.setTextFormatter(new TextFormatter<>(change -> {
|
||||
change.setText(change.getText().replace(" ", ""));
|
||||
return change;
|
||||
}));
|
||||
idField.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
|
||||
if (!isFocused) autoFillTextRefs();
|
||||
});
|
||||
|
||||
xpSpinner = new Spinner<>(0, 99999, 0);
|
||||
xpSpinner.setEditable(true);
|
||||
xpSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
textField = new TextField();
|
||||
textField.setPromptText("TextReference-Schlüssel");
|
||||
descField = new TextField();
|
||||
descField.setPromptText("TextReference-Schlüssel");
|
||||
successField = new TextField();
|
||||
successField.setPromptText("TextReference-Schlüssel");
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Quest"),
|
||||
new Separator(),
|
||||
row("Quest-ID:", idField),
|
||||
row("XP:", xpSpinner),
|
||||
new Separator(),
|
||||
sectionTitle("Texte"),
|
||||
row("Text:", textField),
|
||||
row("Beschreibung:", descField),
|
||||
row("Erfolgstext:", successField),
|
||||
new Separator()
|
||||
);
|
||||
|
||||
typeCombo = new ComboBox<>();
|
||||
typeCombo.getItems().addAll("BringQuest", "FollowQuest", "InteractQuest", "ItemQuest", "TalkQuest");
|
||||
typeCombo.setPromptText("Typ auswählen…");
|
||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
typeCombo.setOnAction(e -> rebuildDynamicArea(typeCombo.getValue()));
|
||||
|
||||
dynamicArea = new VBox(6);
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Typ"),
|
||||
typeCombo,
|
||||
dynamicArea,
|
||||
new Separator()
|
||||
);
|
||||
|
||||
saveBtn = new Button("Quest speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentQuest());
|
||||
form.getChildren().add(saveBtn);
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
private void autoFillTextRefs() {
|
||||
String id = idField.getText().trim();
|
||||
if (id.isBlank()) return;
|
||||
if (textField.getText().isBlank()) textField.setText(id + ".name");
|
||||
if (descField.getText().isBlank()) descField.setText(id + ".description");
|
||||
if (successField.getText().isBlank()) successField.setText(id + ".successmassage");
|
||||
}
|
||||
|
||||
private void rebuildDynamicArea(String type) {
|
||||
dynamicArea.getChildren().clear();
|
||||
f1 = null; f2 = null; f3 = null; countSpinner = null;
|
||||
if (type == null) return;
|
||||
|
||||
switch (type) {
|
||||
case "BringQuest" -> {
|
||||
f1 = tf("NPC-ID (bringen)");
|
||||
f2 = tf("Location-ID (Ziel)");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("BringQuest"),
|
||||
row("NPC:", f1),
|
||||
row("Ziel-Location:", f2)
|
||||
);
|
||||
}
|
||||
case "FollowQuest" -> {
|
||||
f1 = tf("NPC-ID (folgen)");
|
||||
f2 = tf("Location-ID (Ziel)");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("FollowQuest"),
|
||||
row("NPC:", f1),
|
||||
row("Ziel-Location:", f2)
|
||||
);
|
||||
}
|
||||
case "InteractQuest" -> {
|
||||
f1 = tf("Interactable-ID");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("InteractQuest"),
|
||||
row("Interactable:", f1)
|
||||
);
|
||||
}
|
||||
case "ItemQuest" -> {
|
||||
f1 = tf("Item-ID");
|
||||
countSpinner = new Spinner<>(1, 9999, 1);
|
||||
countSpinner.setEditable(true);
|
||||
countSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("ItemQuest"),
|
||||
row("Item:", f1),
|
||||
row("Anzahl:", countSpinner)
|
||||
);
|
||||
}
|
||||
case "TalkQuest" -> {
|
||||
f1 = tf("NPC-ID");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("TalkQuest"),
|
||||
row("NPC:", f1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onQuestSelected(Quest old, Quest nw) {
|
||||
if (old != null) saveFormToQuest(old);
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromQuest(nw);
|
||||
} else {
|
||||
formContainer.setDisable(true);
|
||||
clearForm();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromQuest(Quest q) {
|
||||
idField.setText(safe(q.getQuestId()));
|
||||
xpSpinner.getValueFactory().setValue(q.getXp());
|
||||
textField.setText(q.getText() != null ? q.getText().id() : "");
|
||||
descField.setText(q.getDescription() != null ? q.getDescription().id() : "");
|
||||
successField.setText(q.getSuccessText() != null ? q.getSuccessText().id() : "");
|
||||
|
||||
String type = QuestIO.typeOf(q);
|
||||
typeCombo.setValue(switch (type) {
|
||||
case "BRING" -> "BringQuest";
|
||||
case "FOLLOW" -> "FollowQuest";
|
||||
case "INTERACT" -> "InteractQuest";
|
||||
case "ITEM" -> "ItemQuest";
|
||||
case "TALK" -> "TalkQuest";
|
||||
default -> null;
|
||||
});
|
||||
rebuildDynamicArea(typeCombo.getValue());
|
||||
|
||||
switch (q) {
|
||||
case BringQuest bq -> {
|
||||
if (f1 != null) f1.setText(bq.getBring() != null ? safe(bq.getBring().getCharacterId()) : "");
|
||||
if (f2 != null) f2.setText(bq.getBringTo() != null ? safe(bq.getBringTo().getId()) : "");
|
||||
}
|
||||
case FollowQuest fq -> {
|
||||
if (f1 != null) f1.setText(fq.getFollow() != null ? safe(fq.getFollow().getCharacterId()) : "");
|
||||
if (f2 != null) f2.setText(fq.getFollowTo() != null ? safe(fq.getFollowTo().getId()) : "");
|
||||
}
|
||||
case InteractQuest iq -> {
|
||||
if (f1 != null && iq.getInteractWith() instanceof InteractableRef ir)
|
||||
f1.setText(safe(ir.getId()));
|
||||
}
|
||||
case ItemQuest iq -> {
|
||||
if (f1 != null) f1.setText(iq.getItem() != null ? safe(iq.getItem().getItemId()) : "");
|
||||
if (countSpinner != null) countSpinner.getValueFactory().setValue(iq.getCount());
|
||||
}
|
||||
case TalkQuest tq -> {
|
||||
if (f1 != null) f1.setText(tq.getTalkTo() != null ? safe(tq.getTalkTo().getCharacterId()) : "");
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveFormToQuest(Quest q) {
|
||||
q.setQuestId(idField.getText().trim());
|
||||
q.setXp(xpSpinner.getValue());
|
||||
q.setText(ref(textField));
|
||||
q.setDescription(ref(descField));
|
||||
q.setSuccessText(ref(successField));
|
||||
}
|
||||
|
||||
private Quest buildQuestFromForm() {
|
||||
String type = typeCombo.getValue();
|
||||
if (type == null) return null;
|
||||
|
||||
Quest q = switch (type) {
|
||||
case "BringQuest" -> {
|
||||
BringQuest bq = new BringQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); bq.setBring(n);
|
||||
}
|
||||
if (f2 != null && !f2.getText().isBlank()) {
|
||||
Location l = new Location();
|
||||
l.setName(new TextReference(f2.getText().trim()));
|
||||
bq.setBringTo(l);
|
||||
}
|
||||
yield bq;
|
||||
}
|
||||
case "FollowQuest" -> {
|
||||
FollowQuest fq = new FollowQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); fq.setFollow(n);
|
||||
}
|
||||
if (f2 != null && !f2.getText().isBlank()) {
|
||||
Location l = new Location();
|
||||
l.setName(new TextReference(f2.getText().trim()));
|
||||
fq.setFollowTo(l);
|
||||
}
|
||||
yield fq;
|
||||
}
|
||||
case "InteractQuest" -> {
|
||||
InteractQuest iq = new InteractQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
InteractableRef ir = new InteractableRef(); ir.setId(f1.getText().trim());
|
||||
iq.setInteractWith(ir);
|
||||
}
|
||||
yield iq;
|
||||
}
|
||||
case "ItemQuest" -> {
|
||||
ItemQuest iq = new ItemQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
Item item = new Item(); item.setItemId(f1.getText().trim()); iq.setItem(item);
|
||||
}
|
||||
iq.setCount(countSpinner != null ? countSpinner.getValue() : 1);
|
||||
yield iq;
|
||||
}
|
||||
case "TalkQuest" -> {
|
||||
TalkQuest tq = new TalkQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); tq.setTalkTo(n);
|
||||
}
|
||||
yield tq;
|
||||
}
|
||||
default -> new TalkQuest();
|
||||
};
|
||||
|
||||
q.setQuestId(idField.getText().trim());
|
||||
q.setXp(xpSpinner.getValue());
|
||||
q.setText(ref(textField));
|
||||
q.setDescription(ref(descField));
|
||||
q.setSuccessText(ref(successField));
|
||||
return q;
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
xpSpinner.getValueFactory().setValue(0);
|
||||
textField.clear();
|
||||
descField.clear();
|
||||
successField.clear();
|
||||
typeCombo.setValue(null);
|
||||
dynamicArea.getChildren().clear();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
|
||||
private void createQuest() {
|
||||
TalkQuest q = new TalkQuest();
|
||||
q.setQuestId("neue_quest_" + System.currentTimeMillis());
|
||||
quests.add(q);
|
||||
listView.getSelectionModel().select(q);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
Quest sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
String qId = sel.getQuestId();
|
||||
quests.remove(sel);
|
||||
if (qId != null && !qId.isBlank()) {
|
||||
try { QuestIO.delete(qId, questDir); }
|
||||
catch (IOException e) { /* ignore */ }
|
||||
}
|
||||
current = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
reload();
|
||||
}
|
||||
|
||||
private void saveCurrentQuest() {
|
||||
Quest built = buildQuestFromForm();
|
||||
if (built == null) {
|
||||
showError("Bitte einen Typ wählen.");
|
||||
return;
|
||||
}
|
||||
if (built.getQuestId() == null || built.getQuestId().isBlank()) {
|
||||
showError("Quest-ID darf nicht leer sein.");
|
||||
return;
|
||||
}
|
||||
int idx = quests.indexOf(current);
|
||||
if (idx >= 0) quests.set(idx, built);
|
||||
else quests.add(built);
|
||||
current = built;
|
||||
listView.getSelectionModel().select(built);
|
||||
try {
|
||||
QuestIO.save(built, questDir);
|
||||
} catch (IOException e) {
|
||||
showError("Fehler beim Speichern: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
List<Quest> loaded = QuestIO.loadAll(questDir);
|
||||
sharedQuests.setAll(loaded);
|
||||
quests.setAll(loaded);
|
||||
}
|
||||
|
||||
// ── Single panel ──────────────────────────────────────────────────────────
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
static class QuestPanel extends VBox {
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private final ObservableList<Quest> quests;
|
||||
private final Path questDir;
|
||||
private final Runnable onSaved;
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(130);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private final ListView<Quest> listView;
|
||||
private Quest current = null;
|
||||
private static TextField tf(String prompt) {
|
||||
TextField f = new TextField();
|
||||
f.setPromptText(prompt);
|
||||
return f;
|
||||
}
|
||||
|
||||
// Common fields
|
||||
private TextField idField;
|
||||
private Spinner<Integer> xpSpinner;
|
||||
private TextField textField;
|
||||
private TextField descField;
|
||||
private TextField successField;
|
||||
private static String safe(String s) { return s != null ? s : ""; }
|
||||
|
||||
// Type selection
|
||||
private ComboBox<String> typeCombo;
|
||||
private static TextReference ref(TextField f) {
|
||||
String s = f.getText().trim();
|
||||
return s.isBlank() ? null : new TextReference(s);
|
||||
}
|
||||
|
||||
// Dynamic area
|
||||
private VBox dynamicArea;
|
||||
|
||||
// Type-specific fields (lazily filled)
|
||||
private TextField f1, f2, f3;
|
||||
private Spinner<Integer> countSpinner;
|
||||
|
||||
// Form container (disabled when nothing loaded)
|
||||
private VBox formContainer;
|
||||
private Button saveBtn;
|
||||
private Button deleteBtn;
|
||||
|
||||
QuestPanel(String title, ObservableList<Quest> quests, Path questDir, Runnable onSaved) {
|
||||
this.quests = quests;
|
||||
this.questDir = questDir;
|
||||
this.onSaved = onSaved;
|
||||
|
||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
||||
setSpacing(0);
|
||||
|
||||
// ── Header ────────────────────────────────────────────────────────
|
||||
Label titleLbl = new Label(title);
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
||||
Button refreshBtn = new Button("↺");
|
||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||
refreshBtn.setOnAction(e -> onSaved.run());
|
||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
||||
header.setPadding(new Insets(8, 10, 8, 10));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;"
|
||||
+ " -fx-border-width: 0 0 1 0;");
|
||||
|
||||
// ── Quest list ────────────────────────────────────────────────────
|
||||
listView = new ListView<>(quests);
|
||||
listView.setPrefHeight(160);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Quest q, boolean empty) {
|
||||
super.updateItem(q, empty);
|
||||
if (empty || q == null) { setText(null); setStyle(""); return; }
|
||||
String id = q.getQuestId() != null ? q.getQuestId() : "—";
|
||||
String type = QuestIO.typeOf(q);
|
||||
setText("[" + type + "] " + id);
|
||||
setStyle("-fx-text-fill: #cccccc;");
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onQuestSelected(old, nw));
|
||||
|
||||
Button newBtn = new Button("Neue Quest");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createQuest());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
|
||||
VBox listSection = new VBox(listView, listButtons);
|
||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
||||
|
||||
// ── Form ──────────────────────────────────────────────────────────
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
|
||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
||||
formScroll.setFitToWidth(true);
|
||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
||||
|
||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
||||
}
|
||||
|
||||
// ── Form construction ─────────────────────────────────────────────────
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(10));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
// Common fields
|
||||
idField = new TextField();
|
||||
idField.setPromptText("eindeutige ID");
|
||||
xpSpinner = new Spinner<>(0, 99999, 0);
|
||||
xpSpinner.setEditable(true);
|
||||
xpSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
textField = new TextField();
|
||||
textField.setPromptText("TextReference-Schlüssel");
|
||||
descField = new TextField();
|
||||
descField.setPromptText("TextReference-Schlüssel");
|
||||
successField = new TextField();
|
||||
successField.setPromptText("TextReference-Schlüssel");
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Quest"),
|
||||
new Separator(),
|
||||
row("Quest-ID:", idField),
|
||||
row("XP:", xpSpinner),
|
||||
new Separator(),
|
||||
sectionTitle("Texte"),
|
||||
row("Text:", textField),
|
||||
row("Beschreibung:", descField),
|
||||
row("Erfolgstext:", successField),
|
||||
new Separator()
|
||||
);
|
||||
|
||||
// Type selection
|
||||
typeCombo = new ComboBox<>();
|
||||
typeCombo.getItems().addAll("BringQuest", "FollowQuest", "InteractQuest", "ItemQuest", "TalkQuest");
|
||||
typeCombo.setPromptText("Typ auswählen…");
|
||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
typeCombo.setOnAction(e -> rebuildDynamicArea(typeCombo.getValue()));
|
||||
|
||||
dynamicArea = new VBox(6);
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Typ"),
|
||||
typeCombo,
|
||||
dynamicArea,
|
||||
new Separator()
|
||||
);
|
||||
|
||||
// Save button
|
||||
saveBtn = new Button("Quest speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentQuest());
|
||||
form.getChildren().add(saveBtn);
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
private void rebuildDynamicArea(String type) {
|
||||
dynamicArea.getChildren().clear();
|
||||
f1 = null; f2 = null; f3 = null; countSpinner = null;
|
||||
if (type == null) return;
|
||||
|
||||
switch (type) {
|
||||
case "BringQuest" -> {
|
||||
f1 = tf("NPC-ID (bringen)");
|
||||
f2 = tf("Location-ID (Ziel)");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("BringQuest"),
|
||||
row("NPC:", f1),
|
||||
row("Ziel-Location:", f2)
|
||||
);
|
||||
}
|
||||
case "FollowQuest" -> {
|
||||
f1 = tf("NPC-ID (folgen)");
|
||||
f2 = tf("Location-ID (Ziel)");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("FollowQuest"),
|
||||
row("NPC:", f1),
|
||||
row("Ziel-Location:", f2)
|
||||
);
|
||||
}
|
||||
case "InteractQuest" -> {
|
||||
f1 = tf("Interactable-ID");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("InteractQuest"),
|
||||
row("Interactable:", f1)
|
||||
);
|
||||
}
|
||||
case "ItemQuest" -> {
|
||||
f1 = tf("Item-ID");
|
||||
countSpinner = new Spinner<>(1, 9999, 1);
|
||||
countSpinner.setEditable(true);
|
||||
countSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("ItemQuest"),
|
||||
row("Item:", f1),
|
||||
row("Anzahl:", countSpinner)
|
||||
);
|
||||
}
|
||||
case "TalkQuest" -> {
|
||||
f1 = tf("NPC-ID");
|
||||
dynamicArea.getChildren().addAll(
|
||||
sectionTitle("TalkQuest"),
|
||||
row("NPC:", f1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────
|
||||
|
||||
private void onQuestSelected(Quest old, Quest nw) {
|
||||
if (old != null) saveFormToQuest(old);
|
||||
current = nw;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromQuest(nw);
|
||||
} else {
|
||||
formContainer.setDisable(true);
|
||||
clearForm();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromQuest(Quest q) {
|
||||
idField.setText(safe(q.getQuestId()));
|
||||
xpSpinner.getValueFactory().setValue(q.getXp());
|
||||
textField.setText(q.getText() != null ? q.getText().id() : "");
|
||||
descField.setText(q.getDescription() != null ? q.getDescription().id() : "");
|
||||
successField.setText(q.getSuccessText() != null ? q.getSuccessText().id() : "");
|
||||
|
||||
String type = QuestIO.typeOf(q);
|
||||
typeCombo.setValue(switch (type) {
|
||||
case "BRING" -> "BringQuest";
|
||||
case "FOLLOW" -> "FollowQuest";
|
||||
case "INTERACT" -> "InteractQuest";
|
||||
case "ITEM" -> "ItemQuest";
|
||||
case "TALK" -> "TalkQuest";
|
||||
default -> null;
|
||||
});
|
||||
rebuildDynamicArea(typeCombo.getValue());
|
||||
|
||||
// Fill type-specific fields
|
||||
switch (q) {
|
||||
case BringQuest bq -> {
|
||||
if (f1 != null) f1.setText(bq.getBring() != null ? safe(bq.getBring().getCharacterId()) : "");
|
||||
if (f2 != null) f2.setText(bq.getBringTo() != null ? safe(bq.getBringTo().getId()) : "");
|
||||
}
|
||||
case FollowQuest fq -> {
|
||||
if (f1 != null) f1.setText(fq.getFollow() != null ? safe(fq.getFollow().getCharacterId()) : "");
|
||||
if (f2 != null) f2.setText(fq.getFollowTo() != null ? safe(fq.getFollowTo().getId()) : "");
|
||||
}
|
||||
case InteractQuest iq -> {
|
||||
if (f1 != null && iq.getInteractWith() instanceof InteractableRef ir)
|
||||
f1.setText(safe(ir.getId()));
|
||||
}
|
||||
case ItemQuest iq -> {
|
||||
if (f1 != null) f1.setText(iq.getItem() != null ? safe(iq.getItem().getItemId()) : "");
|
||||
if (countSpinner != null) countSpinner.getValueFactory().setValue(iq.getCount());
|
||||
}
|
||||
case TalkQuest tq -> {
|
||||
if (f1 != null) f1.setText(tq.getTalkTo() != null ? safe(tq.getTalkTo().getCharacterId()) : "");
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveFormToQuest(Quest q) {
|
||||
q.setQuestId(idField.getText().trim());
|
||||
q.setXp(xpSpinner.getValue());
|
||||
q.setText(ref(textField));
|
||||
q.setDescription(ref(descField));
|
||||
q.setSuccessText(ref(successField));
|
||||
|
||||
// Type-specific fields written when actually saving (buildQuestFromForm)
|
||||
}
|
||||
|
||||
private Quest buildQuestFromForm() {
|
||||
String type = typeCombo.getValue();
|
||||
if (type == null) return null;
|
||||
|
||||
Quest q = switch (type) {
|
||||
case "BringQuest" -> {
|
||||
BringQuest bq = new BringQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); bq.setBring(n);
|
||||
}
|
||||
if (f2 != null && !f2.getText().isBlank()) {
|
||||
Location l = new Location(); l.setName(new de.blight.common.model.TextReference(f2.getText().trim())); bq.setBringTo(l);
|
||||
}
|
||||
yield bq;
|
||||
}
|
||||
case "FollowQuest" -> {
|
||||
FollowQuest fq = new FollowQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); fq.setFollow(n);
|
||||
}
|
||||
if (f2 != null && !f2.getText().isBlank()) {
|
||||
Location l = new Location(); l.setName(new de.blight.common.model.TextReference(f2.getText().trim())); fq.setFollowTo(l);
|
||||
}
|
||||
yield fq;
|
||||
}
|
||||
case "InteractQuest" -> {
|
||||
InteractQuest iq = new InteractQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
InteractableRef ir = new InteractableRef(); ir.setId(f1.getText().trim());
|
||||
iq.setInteractWith(ir);
|
||||
}
|
||||
yield iq;
|
||||
}
|
||||
case "ItemQuest" -> {
|
||||
ItemQuest iq = new ItemQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
Item item = new Item(); item.setItemId(f1.getText().trim()); iq.setItem(item);
|
||||
}
|
||||
iq.setCount(countSpinner != null ? countSpinner.getValue() : 1);
|
||||
yield iq;
|
||||
}
|
||||
case "TalkQuest" -> {
|
||||
TalkQuest tq = new TalkQuest();
|
||||
if (f1 != null && !f1.getText().isBlank()) {
|
||||
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); tq.setTalkTo(n);
|
||||
}
|
||||
yield tq;
|
||||
}
|
||||
default -> new TalkQuest();
|
||||
};
|
||||
|
||||
q.setQuestId(idField.getText().trim());
|
||||
q.setXp(xpSpinner.getValue());
|
||||
q.setText(ref(textField));
|
||||
q.setDescription(ref(descField));
|
||||
q.setSuccessText(ref(successField));
|
||||
return q;
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
idField.clear();
|
||||
xpSpinner.getValueFactory().setValue(0);
|
||||
textField.clear();
|
||||
descField.clear();
|
||||
successField.clear();
|
||||
typeCombo.setValue(null);
|
||||
dynamicArea.getChildren().clear();
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────
|
||||
|
||||
private void createQuest() {
|
||||
TalkQuest q = new TalkQuest();
|
||||
q.setQuestId("neue_quest_" + System.currentTimeMillis());
|
||||
quests.add(q);
|
||||
listView.getSelectionModel().select(q);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
Quest sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
String qId = sel.getQuestId();
|
||||
quests.remove(sel);
|
||||
if (qId != null && !qId.isBlank()) {
|
||||
try { QuestIO.delete(qId, questDir); }
|
||||
catch (IOException e) { /* ignore */ }
|
||||
}
|
||||
current = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
onSaved.run();
|
||||
}
|
||||
|
||||
private void saveCurrentQuest() {
|
||||
Quest built = buildQuestFromForm();
|
||||
if (built == null) {
|
||||
showError("Bitte einen Typ wählen.");
|
||||
return;
|
||||
}
|
||||
if (built.getQuestId() == null || built.getQuestId().isBlank()) {
|
||||
showError("Quest-ID darf nicht leer sein.");
|
||||
return;
|
||||
}
|
||||
// Replace or add in shared list
|
||||
int idx = quests.indexOf(current);
|
||||
if (idx >= 0) quests.set(idx, built);
|
||||
else quests.add(built);
|
||||
current = built;
|
||||
listView.getSelectionModel().select(built);
|
||||
try {
|
||||
QuestIO.save(built, questDir);
|
||||
} catch (IOException e) {
|
||||
showError("Fehler beim Speichern: " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
onSaved.run();
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(130);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static TextField tf(String prompt) {
|
||||
TextField f = new TextField();
|
||||
f.setPromptText(prompt);
|
||||
return f;
|
||||
}
|
||||
|
||||
private static String safe(String s) { return s != null ? s : ""; }
|
||||
|
||||
private static TextReference ref(TextField f) {
|
||||
String s = f.getText().trim();
|
||||
return s.isBlank() ? null : new TextReference(s);
|
||||
}
|
||||
|
||||
private void showError(String msg) {
|
||||
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
|
||||
a.showAndWait();
|
||||
}
|
||||
private void showError(String msg) {
|
||||
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
|
||||
a.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
@@ -17,492 +16,450 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Rezept-Verwaltung: zwei identische RecipePanel-Instanzen nebeneinander.
|
||||
* Rezept-Verwaltung: Liste links, Formular rechts.
|
||||
* Sortiert nach CraftingTableType, dann nach erstelltem Item-ID.
|
||||
*/
|
||||
public class RecipeEditorView extends BorderPane {
|
||||
|
||||
private final ObservableList<Recipe> sharedRecipes = FXCollections.observableArrayList();
|
||||
private static final String NO_TABLE = "— kein Tisch —";
|
||||
|
||||
private final ObservableList<Recipe> recipes = FXCollections.observableArrayList();
|
||||
private final Path recipeDir;
|
||||
|
||||
// ── List ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private final SortedList<Recipe> sortedRecipes;
|
||||
private ListView<Recipe> listView;
|
||||
private Button deleteBtn;
|
||||
private Recipe current = null;
|
||||
private String oldFileId = null;
|
||||
|
||||
// ── Form fields ───────────────────────────────────────────────────────────
|
||||
|
||||
private TextField createsField;
|
||||
private ListView<String> componentsList;
|
||||
private ComboBox<String> tableCombo;
|
||||
private HBox alchemyRow;
|
||||
private HBox enchantingRow;
|
||||
private HBox smitheryRow;
|
||||
private HBox engineeringRow;
|
||||
private Spinner<Integer> alchemySpinner;
|
||||
private Spinner<Integer> enchantingSpinner;
|
||||
private Spinner<Integer> smitherySpinner;
|
||||
private Spinner<Integer> engineeringSpinner;
|
||||
private VBox formContainer;
|
||||
|
||||
public RecipeEditorView(Path recipeDir) {
|
||||
this.recipeDir = recipeDir;
|
||||
this.recipeDir = recipeDir;
|
||||
this.sortedRecipes = new SortedList<>(recipes, RecipeIO.SORT_ORDER);
|
||||
setStyle("-fx-background-color: #1e1e2e;");
|
||||
reload();
|
||||
|
||||
RecipePanel left = new RecipePanel("Liste 1", sharedRecipes, recipeDir, this::reload);
|
||||
RecipePanel right = new RecipePanel("Liste 2", sharedRecipes, recipeDir, this::reload);
|
||||
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||
split.setDividerPositions(0.28);
|
||||
setCenter(split);
|
||||
}
|
||||
|
||||
HBox panels = new HBox(1, left, right);
|
||||
HBox.setHgrow(left, Priority.ALWAYS);
|
||||
HBox.setHgrow(right, Priority.ALWAYS);
|
||||
setCenter(panels);
|
||||
// ── List panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildListPanel() {
|
||||
listView = new ListView<>(sortedRecipes);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Recipe r, boolean empty) {
|
||||
super.updateItem(r, empty);
|
||||
if (empty || r == null) { setText(null); setStyle(""); return; }
|
||||
String creates = r.getCreates() != null ? safe(r.getCreates().getItemId()) : "—";
|
||||
String table = r.getTable() != null && r.getTable().getType() != null
|
||||
? r.getTable().getType().name() : "Handwerk";
|
||||
setText(creates);
|
||||
setTooltip(new Tooltip("[" + table + "] erstellt: " + creates));
|
||||
String color = tableColor(r.getTable());
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onRecipeSelected(old, nw));
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
|
||||
Button newBtn = new Button("Neues Rezept");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createRecipe());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
Button refreshBtn = new Button("↺ Neu laden");
|
||||
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
refreshBtn.setOnAction(e -> reload());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
|
||||
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
|
||||
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||
panel.setPadding(new Insets(8));
|
||||
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||
return panel;
|
||||
}
|
||||
|
||||
// ── Form panel ────────────────────────────────────────────────────────────
|
||||
|
||||
private ScrollPane buildFormPanel() {
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
ScrollPane scroll = new ScrollPane(formContainer);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(10));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
createsField = new TextField();
|
||||
createsField.setPromptText("Item-ID des erstellten Items");
|
||||
|
||||
componentsList = new ListView<>();
|
||||
componentsList.setPrefHeight(110);
|
||||
componentsList.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
||||
componentsList.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(String s, boolean empty) {
|
||||
super.updateItem(s, empty);
|
||||
setText(empty || s == null ? null : s);
|
||||
setStyle(empty ? "" : "-fx-text-fill: #cccccc;");
|
||||
}
|
||||
});
|
||||
Button addCompBtn = smallBtn("+");
|
||||
Button delCompBtn = smallBtn("−");
|
||||
addCompBtn.setOnAction(e -> addComponent());
|
||||
delCompBtn.setOnAction(e -> {
|
||||
String sel = componentsList.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) componentsList.getItems().remove(sel);
|
||||
});
|
||||
HBox compButtons = new HBox(4, addCompBtn, delCompBtn);
|
||||
|
||||
tableCombo = new ComboBox<>();
|
||||
tableCombo.getItems().add(NO_TABLE);
|
||||
for (CraftingTable.CraftingTableType t : CraftingTable.CraftingTableType.values()) {
|
||||
tableCombo.getItems().add(t.name());
|
||||
}
|
||||
tableCombo.setValue(NO_TABLE);
|
||||
tableCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
tableCombo.setOnAction(e -> updateRequirementRows(tableCombo.getValue()));
|
||||
|
||||
alchemySpinner = lvlSpinner();
|
||||
enchantingSpinner = lvlSpinner();
|
||||
smitherySpinner = lvlSpinner();
|
||||
engineeringSpinner = lvlSpinner();
|
||||
|
||||
alchemyRow = requirementRow("Lvl Alchemie:", alchemySpinner);
|
||||
enchantingRow = requirementRow("Lvl Verzauberung:", enchantingSpinner);
|
||||
smitheryRow = requirementRow("Lvl Schmieden:", smitherySpinner);
|
||||
engineeringRow = requirementRow("Lvl Engineering:", engineeringSpinner);
|
||||
|
||||
Button saveBtn = new Button("Rezept speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentRecipe());
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Ergebnis"),
|
||||
new Separator(),
|
||||
row("Erstellt:", createsField),
|
||||
sectionTitle("Zutaten"),
|
||||
componentsList,
|
||||
compButtons,
|
||||
new Separator(),
|
||||
sectionTitle("Crafting Table"),
|
||||
tableCombo,
|
||||
alchemyRow,
|
||||
enchantingRow,
|
||||
smitheryRow,
|
||||
engineeringRow,
|
||||
new Separator(),
|
||||
saveBtn
|
||||
);
|
||||
|
||||
updateRequirementRows(NO_TABLE);
|
||||
return form;
|
||||
}
|
||||
|
||||
private void updateRequirementRows(String tableValue) {
|
||||
boolean noTable = tableValue == null || tableValue.equals(NO_TABLE);
|
||||
setRowVisible(alchemyRow, false);
|
||||
setRowVisible(enchantingRow, false);
|
||||
setRowVisible(smitheryRow, false);
|
||||
setRowVisible(engineeringRow, false);
|
||||
if (noTable) return;
|
||||
switch (tableValue) {
|
||||
case "AlchemyTable" -> setRowVisible(alchemyRow, true);
|
||||
case "EnchantmentTable" -> setRowVisible(enchantingRow, true);
|
||||
case "Smithy",
|
||||
"Goldsmiths" -> setRowVisible(smitheryRow, true);
|
||||
case "Workshop" -> setRowVisible(engineeringRow, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setRowVisible(HBox row, boolean visible) {
|
||||
row.setVisible(visible);
|
||||
row.setManaged(visible);
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────────
|
||||
|
||||
private void onRecipeSelected(Recipe old, Recipe nw) {
|
||||
if (old != null) saveFormToRecipe(old);
|
||||
current = nw;
|
||||
oldFileId = nw != null ? RecipeIO.fileId(nw) : null;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromRecipe(nw);
|
||||
} else {
|
||||
formContainer.setDisable(true);
|
||||
clearForm();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromRecipe(Recipe r) {
|
||||
createsField.setText(r.getCreates() != null ? safe(r.getCreates().getItemId()) : "");
|
||||
|
||||
componentsList.getItems().clear();
|
||||
if (r.getComponents() != null) {
|
||||
r.getComponents().forEach((item, count) ->
|
||||
componentsList.getItems().add(item.getItemId() + " × " + count));
|
||||
}
|
||||
|
||||
String tableVal = NO_TABLE;
|
||||
if (r.getTable() != null && r.getTable().getType() != null) {
|
||||
tableVal = r.getTable().getType().name();
|
||||
}
|
||||
tableCombo.setValue(tableVal);
|
||||
updateRequirementRows(tableVal);
|
||||
|
||||
alchemySpinner.getValueFactory().setValue(
|
||||
r.getRequiresLvlAlchemy() != null ? r.getRequiresLvlAlchemy() : 1);
|
||||
enchantingSpinner.getValueFactory().setValue(
|
||||
r.getRequiresLvlEnchanting() != null ? r.getRequiresLvlEnchanting() : 1);
|
||||
smitherySpinner.getValueFactory().setValue(
|
||||
r.getRequiresLvlSmithery() != null ? r.getRequiresLvlSmithery() : 1);
|
||||
engineeringSpinner.getValueFactory().setValue(
|
||||
r.getRequiresLvlEngineering() != null ? r.getRequiresLvlEngineering() : 1);
|
||||
}
|
||||
|
||||
private void saveFormToRecipe(Recipe r) {
|
||||
String cId = createsField.getText().trim();
|
||||
if (!cId.isBlank()) {
|
||||
Item creates = r.getCreates() != null ? r.getCreates() : new Item();
|
||||
creates.setItemId(cId);
|
||||
r.setCreates(creates);
|
||||
} else {
|
||||
r.setCreates(null);
|
||||
}
|
||||
|
||||
Map<Item, Integer> comps = new LinkedHashMap<>();
|
||||
for (String entry : componentsList.getItems()) {
|
||||
int sep = entry.lastIndexOf(" × ");
|
||||
if (sep < 0) continue;
|
||||
String itemId = entry.substring(0, sep).trim();
|
||||
int count = 1;
|
||||
try { count = Integer.parseInt(entry.substring(sep + 3).trim()); }
|
||||
catch (NumberFormatException ignored) {}
|
||||
Item item = new Item(); item.setItemId(itemId);
|
||||
comps.put(item, count);
|
||||
}
|
||||
r.setComponents(comps.isEmpty() ? null : comps);
|
||||
|
||||
String tv = tableCombo.getValue();
|
||||
if (tv == null || tv.equals(NO_TABLE)) {
|
||||
r.setTable(null);
|
||||
r.setRequiresLvlAlchemy(null);
|
||||
r.setRequiresLvlEngineering(null);
|
||||
r.setRequiresLvlSmithery(null);
|
||||
r.setRequiresLvlEnchanting(null);
|
||||
} else {
|
||||
CraftingTable table = r.getTable() != null ? r.getTable() : new CraftingTable();
|
||||
table.setType(CraftingTable.CraftingTableType.valueOf(tv));
|
||||
r.setTable(table);
|
||||
r.setRequiresLvlAlchemy(null);
|
||||
r.setRequiresLvlEngineering(null);
|
||||
r.setRequiresLvlSmithery(null);
|
||||
r.setRequiresLvlEnchanting(null);
|
||||
switch (tv) {
|
||||
case "AlchemyTable" -> r.setRequiresLvlAlchemy(alchemySpinner.getValue());
|
||||
case "EnchantmentTable" -> r.setRequiresLvlEnchanting(enchantingSpinner.getValue());
|
||||
case "Smithy",
|
||||
"Goldsmiths" -> r.setRequiresLvlSmithery(smitherySpinner.getValue());
|
||||
case "Workshop" -> r.setRequiresLvlEngineering(engineeringSpinner.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
createsField.clear();
|
||||
componentsList.getItems().clear();
|
||||
tableCombo.setValue(NO_TABLE);
|
||||
updateRequirementRows(NO_TABLE);
|
||||
alchemySpinner.getValueFactory().setValue(1);
|
||||
enchantingSpinner.getValueFactory().setValue(1);
|
||||
smitherySpinner.getValueFactory().setValue(1);
|
||||
engineeringSpinner.getValueFactory().setValue(1);
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────────
|
||||
|
||||
private void createRecipe() {
|
||||
Recipe r = new Recipe();
|
||||
Item creates = new Item();
|
||||
creates.setItemId("neues_rezept_" + System.currentTimeMillis());
|
||||
r.setCreates(creates);
|
||||
recipes.add(r);
|
||||
listView.getSelectionModel().select(r);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
Recipe sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
String fid = RecipeIO.fileId(sel);
|
||||
recipes.remove(sel);
|
||||
try { RecipeIO.delete(fid, recipeDir); } catch (IOException ignored) {}
|
||||
current = null;
|
||||
oldFileId = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
reload();
|
||||
}
|
||||
|
||||
private void saveCurrentRecipe() {
|
||||
if (current == null) return;
|
||||
saveFormToRecipe(current);
|
||||
|
||||
String newFileId = RecipeIO.fileId(current);
|
||||
if (newFileId.startsWith("unbenanntes")) {
|
||||
new Alert(Alert.AlertType.ERROR,
|
||||
"Item-ID des erstellten Items darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (oldFileId != null && !oldFileId.equals(newFileId)) {
|
||||
RecipeIO.delete(oldFileId, recipeDir);
|
||||
}
|
||||
RecipeIO.save(current, recipeDir);
|
||||
oldFileId = newFileId;
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
reload();
|
||||
final String fid = newFileId;
|
||||
recipes.stream()
|
||||
.filter(r -> fid.equals(RecipeIO.fileId(r)))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
private void addComponent() {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Zutat hinzufügen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
|
||||
TextField itemIdField = new TextField();
|
||||
itemIdField.setPromptText("Item-ID");
|
||||
Spinner<Integer> countSpin = new Spinner<>(1, 9999, 1);
|
||||
countSpin.setEditable(true);
|
||||
|
||||
GridPane grid = new GridPane();
|
||||
grid.setHgap(10); grid.setVgap(8);
|
||||
grid.setPadding(new Insets(12));
|
||||
grid.add(new Label("Item-ID:"), 0, 0); grid.add(itemIdField, 1, 0);
|
||||
grid.add(new Label("Anzahl:"), 0, 1); grid.add(countSpin, 1, 1);
|
||||
GridPane.setHgrow(itemIdField, Priority.ALWAYS);
|
||||
|
||||
dlg.getDialogPane().setContent(grid);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
Button okBtn = (Button) dlg.getDialogPane().lookupButton(ButtonType.OK);
|
||||
okBtn.setDisable(true);
|
||||
itemIdField.textProperty().addListener((obs, o, n) -> okBtn.setDisable(n.isBlank()));
|
||||
|
||||
dlg.setResultConverter(bt -> bt == ButtonType.OK
|
||||
? itemIdField.getText().trim() + " × " + countSpin.getValue() : null);
|
||||
dlg.showAndWait().ifPresent(entry -> {
|
||||
if (!componentsList.getItems().contains(entry)) {
|
||||
componentsList.getItems().add(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
sharedRecipes.setAll(RecipeIO.loadAll(recipeDir));
|
||||
recipes.setAll(RecipeIO.loadAll(recipeDir));
|
||||
}
|
||||
|
||||
// ── Single panel ──────────────────────────────────────────────────────────
|
||||
|
||||
static class RecipePanel extends VBox {
|
||||
|
||||
private static final String NO_TABLE = "— kein Tisch —";
|
||||
|
||||
private final ObservableList<Recipe> recipes;
|
||||
private final Path recipeDir;
|
||||
private final Runnable onSaved;
|
||||
|
||||
private final SortedList<Recipe> sortedRecipes;
|
||||
private final ListView<Recipe> listView;
|
||||
|
||||
private Recipe current = null;
|
||||
private String oldFileId = null; // for rename-on-save detection
|
||||
|
||||
// Form fields
|
||||
private TextField createsField;
|
||||
private ListView<String> componentsList; // "itemId × count"
|
||||
private ComboBox<String> tableCombo;
|
||||
|
||||
// Level-Anforderungs-Zeilen (jeweils Label + Spinner)
|
||||
private HBox alchemyRow;
|
||||
private HBox enchantingRow;
|
||||
private HBox smitheryRow;
|
||||
private HBox engineeringRow;
|
||||
private Spinner<Integer> alchemySpinner;
|
||||
private Spinner<Integer> enchantingSpinner;
|
||||
private Spinner<Integer> smitherySpinner;
|
||||
private Spinner<Integer> engineeringSpinner;
|
||||
|
||||
private VBox formContainer;
|
||||
private Button deleteBtn;
|
||||
|
||||
RecipePanel(String title, ObservableList<Recipe> recipes, Path recipeDir, Runnable onSaved) {
|
||||
this.recipes = recipes;
|
||||
this.recipeDir = recipeDir;
|
||||
this.onSaved = onSaved;
|
||||
|
||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
||||
|
||||
// ── Header ────────────────────────────────────────────────────────
|
||||
Label titleLbl = new Label(title);
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
||||
Button refreshBtn = new Button("↺");
|
||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
||||
refreshBtn.setOnAction(e -> onSaved.run());
|
||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
||||
header.setPadding(new Insets(8, 10, 8, 10));
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;"
|
||||
+ " -fx-border-width: 0 0 1 0;");
|
||||
|
||||
// ── Recipe list ───────────────────────────────────────────────────
|
||||
sortedRecipes = new SortedList<>(recipes, RecipeIO.SORT_ORDER);
|
||||
listView = new ListView<>(sortedRecipes);
|
||||
listView.setPrefHeight(180);
|
||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||
listView.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(Recipe r, boolean empty) {
|
||||
super.updateItem(r, empty);
|
||||
if (empty || r == null) { setText(null); setStyle(""); return; }
|
||||
String creates = r.getCreates() != null ? safe(r.getCreates().getItemId()) : "—";
|
||||
String table = r.getTable() != null && r.getTable().getType() != null
|
||||
? r.getTable().getType().name() : "Handwerk";
|
||||
setText(creates);
|
||||
setTooltip(new Tooltip("[" + table + "] erstellt: " + creates));
|
||||
String color = tableColor(r.getTable());
|
||||
setStyle("-fx-text-fill: #dddddd;"
|
||||
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||
}
|
||||
});
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, old, nw) -> onRecipeSelected(old, nw));
|
||||
|
||||
Button newBtn = new Button("Neues Rezept");
|
||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||
newBtn.setOnAction(e -> createRecipe());
|
||||
|
||||
deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||
deleteBtn.setDisable(true);
|
||||
deleteBtn.setOnAction(e -> deleteSelected());
|
||||
|
||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||
|
||||
VBox listSection = new VBox(listView, listButtons);
|
||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
||||
|
||||
// ── Form ──────────────────────────────────────────────────────────
|
||||
formContainer = buildForm();
|
||||
formContainer.setDisable(true);
|
||||
|
||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
||||
formScroll.setFitToWidth(true);
|
||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
||||
|
||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
||||
}
|
||||
|
||||
// ── Form construction ─────────────────────────────────────────────────
|
||||
|
||||
private VBox buildForm() {
|
||||
VBox form = new VBox(6);
|
||||
form.setPadding(new Insets(10));
|
||||
form.setStyle("-fx-background-color: #252535;");
|
||||
|
||||
// Erstellt
|
||||
createsField = new TextField();
|
||||
createsField.setPromptText("Item-ID des erstellten Items");
|
||||
|
||||
// Zutaten
|
||||
componentsList = new ListView<>();
|
||||
componentsList.setPrefHeight(110);
|
||||
componentsList.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
||||
componentsList.setCellFactory(lv -> new ListCell<>() {
|
||||
@Override protected void updateItem(String s, boolean empty) {
|
||||
super.updateItem(s, empty);
|
||||
setText(empty || s == null ? null : s);
|
||||
setStyle(empty ? "" : "-fx-text-fill: #cccccc;");
|
||||
}
|
||||
});
|
||||
Button addCompBtn = smallBtn("+");
|
||||
Button delCompBtn = smallBtn("−");
|
||||
addCompBtn.setOnAction(e -> addComponent());
|
||||
delCompBtn.setOnAction(e -> {
|
||||
String sel = componentsList.getSelectionModel().getSelectedItem();
|
||||
if (sel != null) componentsList.getItems().remove(sel);
|
||||
});
|
||||
HBox compButtons = new HBox(4, addCompBtn, delCompBtn);
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Ergebnis"),
|
||||
new Separator(),
|
||||
row("Erstellt:", createsField),
|
||||
sectionTitle("Zutaten"),
|
||||
componentsList,
|
||||
compButtons,
|
||||
new Separator()
|
||||
);
|
||||
|
||||
// Crafting Table
|
||||
tableCombo = new ComboBox<>();
|
||||
tableCombo.getItems().add(NO_TABLE);
|
||||
for (CraftingTable.CraftingTableType t : CraftingTable.CraftingTableType.values())
|
||||
tableCombo.getItems().add(t.name());
|
||||
tableCombo.setValue(NO_TABLE);
|
||||
tableCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
tableCombo.setOnAction(e -> updateRequirementRows(tableCombo.getValue()));
|
||||
|
||||
// Level-Anforderungen (werden je nach Tischtyp aktiviert)
|
||||
alchemySpinner = lvlSpinner();
|
||||
enchantingSpinner = lvlSpinner();
|
||||
smitherySpinner = lvlSpinner();
|
||||
engineeringSpinner = lvlSpinner();
|
||||
|
||||
alchemyRow = requirementRow("Lvl Alchemie:", alchemySpinner);
|
||||
enchantingRow = requirementRow("Lvl Verzauberung:", enchantingSpinner);
|
||||
smitheryRow = requirementRow("Lvl Schmieden:", smitherySpinner);
|
||||
engineeringRow = requirementRow("Lvl Engineering:", engineeringSpinner);
|
||||
|
||||
form.getChildren().addAll(
|
||||
sectionTitle("Crafting Table"),
|
||||
tableCombo,
|
||||
alchemyRow,
|
||||
enchantingRow,
|
||||
smitheryRow,
|
||||
engineeringRow
|
||||
);
|
||||
|
||||
// Anfangs alle Anforderungen deaktiviert
|
||||
updateRequirementRows(NO_TABLE);
|
||||
|
||||
form.getChildren().add(new Separator());
|
||||
|
||||
Button saveBtn = new Button("Rezept speichern");
|
||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||
saveBtn.setOnAction(e -> saveCurrentRecipe());
|
||||
form.getChildren().add(saveBtn);
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
private void updateRequirementRows(String tableValue) {
|
||||
boolean noTable = tableValue == null || tableValue.equals(NO_TABLE);
|
||||
|
||||
// Alle ausblenden wenn kein Tisch
|
||||
setRowVisible(alchemyRow, false);
|
||||
setRowVisible(enchantingRow, false);
|
||||
setRowVisible(smitheryRow, false);
|
||||
setRowVisible(engineeringRow, false);
|
||||
|
||||
if (noTable) return;
|
||||
|
||||
switch (tableValue) {
|
||||
case "AlchemyTable" -> setRowVisible(alchemyRow, true);
|
||||
case "EnchantmentTable" -> setRowVisible(enchantingRow, true);
|
||||
case "Smithy",
|
||||
"Goldsmiths" -> setRowVisible(smitheryRow, true);
|
||||
case "Workshop" -> setRowVisible(engineeringRow, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setRowVisible(HBox row, boolean visible) {
|
||||
row.setVisible(visible);
|
||||
row.setManaged(visible);
|
||||
}
|
||||
|
||||
// ── Form load / save ──────────────────────────────────────────────────
|
||||
|
||||
private void onRecipeSelected(Recipe old, Recipe nw) {
|
||||
if (old != null) saveFormToRecipe(old);
|
||||
current = nw;
|
||||
oldFileId = nw != null ? RecipeIO.fileId(nw) : null;
|
||||
deleteBtn.setDisable(nw == null);
|
||||
if (nw != null) {
|
||||
formContainer.setDisable(false);
|
||||
loadFormFromRecipe(nw);
|
||||
} else {
|
||||
formContainer.setDisable(true);
|
||||
clearForm();
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFormFromRecipe(Recipe r) {
|
||||
createsField.setText(r.getCreates() != null ? safe(r.getCreates().getItemId()) : "");
|
||||
|
||||
componentsList.getItems().clear();
|
||||
if (r.getComponents() != null) {
|
||||
r.getComponents().forEach((item, count) ->
|
||||
componentsList.getItems().add(item.getItemId() + " × " + count));
|
||||
}
|
||||
|
||||
String tableVal = NO_TABLE;
|
||||
if (r.getTable() != null && r.getTable().getType() != null)
|
||||
tableVal = r.getTable().getType().name();
|
||||
tableCombo.setValue(tableVal);
|
||||
updateRequirementRows(tableVal);
|
||||
|
||||
alchemySpinner.getValueFactory().setValue(
|
||||
r.getRequiresLvlAlchemy() != null ? r.getRequiresLvlAlchemy() : 1);
|
||||
enchantingSpinner.getValueFactory().setValue(
|
||||
r.getRequiresLvlEnchanting() != null ? r.getRequiresLvlEnchanting() : 1);
|
||||
smitherySpinner.getValueFactory().setValue(
|
||||
r.getRequiresLvlSmithery() != null ? r.getRequiresLvlSmithery() : 1);
|
||||
engineeringSpinner.getValueFactory().setValue(
|
||||
r.getRequiresLvlEngineering() != null ? r.getRequiresLvlEngineering() : 1);
|
||||
}
|
||||
|
||||
private void saveFormToRecipe(Recipe r) {
|
||||
// creates
|
||||
String cId = createsField.getText().trim();
|
||||
if (!cId.isBlank()) {
|
||||
Item creates = r.getCreates() != null ? r.getCreates() : new Item();
|
||||
creates.setItemId(cId);
|
||||
r.setCreates(creates);
|
||||
} else {
|
||||
r.setCreates(null);
|
||||
}
|
||||
|
||||
// components
|
||||
Map<Item, Integer> comps = new LinkedHashMap<>();
|
||||
for (String entry : componentsList.getItems()) {
|
||||
int sep = entry.lastIndexOf(" × ");
|
||||
if (sep < 0) continue;
|
||||
String itemId = entry.substring(0, sep).trim();
|
||||
int count = 1;
|
||||
try { count = Integer.parseInt(entry.substring(sep + 3).trim()); }
|
||||
catch (NumberFormatException ignored) {}
|
||||
Item item = new Item(); item.setItemId(itemId);
|
||||
comps.put(item, count);
|
||||
}
|
||||
r.setComponents(comps.isEmpty() ? null : comps);
|
||||
|
||||
// table
|
||||
String tv = tableCombo.getValue();
|
||||
if (tv == null || tv.equals(NO_TABLE)) {
|
||||
r.setTable(null);
|
||||
r.setRequiresLvlAlchemy(null);
|
||||
r.setRequiresLvlEngineering(null);
|
||||
r.setRequiresLvlSmithery(null);
|
||||
r.setRequiresLvlEnchanting(null);
|
||||
} else {
|
||||
CraftingTable table = r.getTable() != null ? r.getTable() : new CraftingTable();
|
||||
table.setType(CraftingTable.CraftingTableType.valueOf(tv));
|
||||
r.setTable(table);
|
||||
// Nur das relevante Level-Feld setzen, alle anderen null
|
||||
r.setRequiresLvlAlchemy(null);
|
||||
r.setRequiresLvlEngineering(null);
|
||||
r.setRequiresLvlSmithery(null);
|
||||
r.setRequiresLvlEnchanting(null);
|
||||
switch (tv) {
|
||||
case "AlchemyTable" -> r.setRequiresLvlAlchemy(alchemySpinner.getValue());
|
||||
case "EnchantmentTable" -> r.setRequiresLvlEnchanting(enchantingSpinner.getValue());
|
||||
case "Smithy",
|
||||
"Goldsmiths" -> r.setRequiresLvlSmithery(smitherySpinner.getValue());
|
||||
case "Workshop" -> r.setRequiresLvlEngineering(engineeringSpinner.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
createsField.clear();
|
||||
componentsList.getItems().clear();
|
||||
tableCombo.setValue(NO_TABLE);
|
||||
updateRequirementRows(NO_TABLE);
|
||||
alchemySpinner.getValueFactory().setValue(1);
|
||||
enchantingSpinner.getValueFactory().setValue(1);
|
||||
smitherySpinner.getValueFactory().setValue(1);
|
||||
engineeringSpinner.getValueFactory().setValue(1);
|
||||
}
|
||||
|
||||
// ── List operations ───────────────────────────────────────────────────
|
||||
|
||||
private void createRecipe() {
|
||||
Recipe r = new Recipe();
|
||||
Item creates = new Item();
|
||||
creates.setItemId("neues_rezept_" + System.currentTimeMillis());
|
||||
r.setCreates(creates);
|
||||
recipes.add(r);
|
||||
listView.getSelectionModel().select(r);
|
||||
}
|
||||
|
||||
private void deleteSelected() {
|
||||
Recipe sel = listView.getSelectionModel().getSelectedItem();
|
||||
if (sel == null) return;
|
||||
String fid = RecipeIO.fileId(sel);
|
||||
recipes.remove(sel);
|
||||
try { RecipeIO.delete(fid, recipeDir); } catch (IOException ignored) {}
|
||||
current = null;
|
||||
oldFileId = null;
|
||||
clearForm();
|
||||
formContainer.setDisable(true);
|
||||
deleteBtn.setDisable(true);
|
||||
onSaved.run();
|
||||
}
|
||||
|
||||
private void saveCurrentRecipe() {
|
||||
if (current == null) return;
|
||||
saveFormToRecipe(current);
|
||||
|
||||
String newFileId = RecipeIO.fileId(current);
|
||||
if (newFileId.startsWith("unbenanntes")) {
|
||||
new Alert(Alert.AlertType.ERROR,
|
||||
"Item-ID des erstellten Items darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Datei umbenennen: alten Eintrag löschen wenn ID sich geändert hat
|
||||
if (oldFileId != null && !oldFileId.equals(newFileId))
|
||||
RecipeIO.delete(oldFileId, recipeDir);
|
||||
RecipeIO.save(current, recipeDir);
|
||||
oldFileId = newFileId;
|
||||
} catch (IOException e) {
|
||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
onSaved.run();
|
||||
// Re-select nach Reload
|
||||
final String fid = newFileId;
|
||||
recipes.stream()
|
||||
.filter(r -> fid.equals(RecipeIO.fileId(r)))
|
||||
.findFirst()
|
||||
.ifPresent(listView.getSelectionModel()::select);
|
||||
}
|
||||
|
||||
private void addComponent() {
|
||||
Dialog<String> dlg = new Dialog<>();
|
||||
dlg.setTitle("Zutat hinzufügen");
|
||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||
|
||||
TextField itemIdField = new TextField();
|
||||
itemIdField.setPromptText("Item-ID");
|
||||
Spinner<Integer> countSpin = new Spinner<>(1, 9999, 1);
|
||||
countSpin.setEditable(true);
|
||||
|
||||
GridPane grid = new GridPane();
|
||||
grid.setHgap(10); grid.setVgap(8);
|
||||
grid.setPadding(new Insets(12));
|
||||
grid.add(new Label("Item-ID:"), 0, 0); grid.add(itemIdField, 1, 0);
|
||||
grid.add(new Label("Anzahl:"), 0, 1); grid.add(countSpin, 1, 1);
|
||||
GridPane.setHgrow(itemIdField, Priority.ALWAYS);
|
||||
|
||||
dlg.getDialogPane().setContent(grid);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
Button okBtn = (Button) dlg.getDialogPane().lookupButton(ButtonType.OK);
|
||||
okBtn.setDisable(true);
|
||||
itemIdField.textProperty().addListener((obs, o, n) -> okBtn.setDisable(n.isBlank()));
|
||||
|
||||
dlg.setResultConverter(bt -> bt == ButtonType.OK
|
||||
? itemIdField.getText().trim() + " × " + countSpin.getValue() : null);
|
||||
dlg.showAndWait().ifPresent(entry -> {
|
||||
if (!componentsList.getItems().contains(entry))
|
||||
componentsList.getItems().add(entry);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
private static String tableColor(CraftingTable t) {
|
||||
if (t == null || t.getType() == null) return "#666666";
|
||||
return switch (t.getType()) {
|
||||
case AlchemyTable -> "#44bb88";
|
||||
case EnchantmentTable -> "#aa55ee";
|
||||
case Smithy -> "#cc8833";
|
||||
case Goldsmiths -> "#ddbb22";
|
||||
case Workshop -> "#4488cc";
|
||||
case Fireplace -> "#ee6633";
|
||||
case Kitchen -> "#88aa44";
|
||||
};
|
||||
}
|
||||
|
||||
private static HBox requirementRow(String labelText, Spinner<Integer> spinner) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(140);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
spinner.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(spinner, Priority.ALWAYS);
|
||||
HBox row = new HBox(8, lbl, spinner);
|
||||
row.setAlignment(Pos.CENTER_LEFT);
|
||||
row.setPadding(new Insets(2, 0, 2, 0));
|
||||
return row;
|
||||
}
|
||||
|
||||
private static Spinner<Integer> lvlSpinner() {
|
||||
Spinner<Integer> s = new Spinner<>(1, 100, 1);
|
||||
s.setEditable(true);
|
||||
return s;
|
||||
}
|
||||
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(80);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static Button smallBtn(String text) {
|
||||
Button b = new Button(text);
|
||||
b.setPrefWidth(28);
|
||||
return b;
|
||||
}
|
||||
|
||||
private static String safe(String s) { return s != null ? s : ""; }
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static String tableColor(CraftingTable t) {
|
||||
if (t == null || t.getType() == null) return "#666666";
|
||||
return switch (t.getType()) {
|
||||
case AlchemyTable -> "#44bb88";
|
||||
case EnchantmentTable -> "#aa55ee";
|
||||
case Smithy -> "#cc8833";
|
||||
case Goldsmiths -> "#ddbb22";
|
||||
case Workshop -> "#4488cc";
|
||||
case Fireplace -> "#ee6633";
|
||||
case Kitchen -> "#88aa44";
|
||||
};
|
||||
}
|
||||
|
||||
private static HBox requirementRow(String labelText, Spinner<Integer> spinner) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(140);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
spinner.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(spinner, Priority.ALWAYS);
|
||||
HBox row = new HBox(8, lbl, spinner);
|
||||
row.setAlignment(Pos.CENTER_LEFT);
|
||||
row.setPadding(new Insets(2, 0, 2, 0));
|
||||
return row;
|
||||
}
|
||||
|
||||
private static Spinner<Integer> lvlSpinner() {
|
||||
Spinner<Integer> s = new Spinner<>(1, 100, 1);
|
||||
s.setEditable(true);
|
||||
return s;
|
||||
}
|
||||
|
||||
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;");
|
||||
return l;
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, Node control) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(80);
|
||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||
HBox.setHgrow(control, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, control);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static Button smallBtn(String text) {
|
||||
Button b = new Button(text);
|
||||
b.setPrefWidth(28);
|
||||
return b;
|
||||
}
|
||||
|
||||
private static String safe(String s) { return s != null ? s : ""; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user