diff --git a/blight-assets/src/main/resources/.thumbnails/Models/imported/wolf.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/imported/wolf.j3o.thumb.png new file mode 100644 index 0000000..d63c0af Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/imported/wolf.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/northcoast/alter_steg.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/northcoast/alter_steg.j3o.thumb.png new file mode 100644 index 0000000..f71da24 Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/northcoast/alter_steg.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/northcoast/altes_ruderboot.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/northcoast/altes_ruderboot.j3o.thumb.png new file mode 100644 index 0000000..d59864d Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/northcoast/altes_ruderboot.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/northcoast/wrack1.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/northcoast/wrack1.j3o.thumb.png new file mode 100644 index 0000000..3025130 Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/northcoast/wrack1.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/imported/wolf.j3o b/blight-assets/src/main/resources/Models/imported/wolf.j3o index e4c178f..8c73407 100644 Binary files a/blight-assets/src/main/resources/Models/imported/wolf.j3o and b/blight-assets/src/main/resources/Models/imported/wolf.j3o differ diff --git a/blight-assets/src/main/resources/Models/northcoast/alter_steg.j3o b/blight-assets/src/main/resources/Models/northcoast/alter_steg.j3o index 6222679..53f8c1d 100644 Binary files a/blight-assets/src/main/resources/Models/northcoast/alter_steg.j3o and b/blight-assets/src/main/resources/Models/northcoast/alter_steg.j3o differ diff --git a/blight-assets/src/main/resources/Models/northcoast/altes_ruderboot.j3o b/blight-assets/src/main/resources/Models/northcoast/altes_ruderboot.j3o index 24efd1e..791b71e 100644 Binary files a/blight-assets/src/main/resources/Models/northcoast/altes_ruderboot.j3o and b/blight-assets/src/main/resources/Models/northcoast/altes_ruderboot.j3o differ diff --git a/blight-assets/src/main/resources/Models/northcoast/wrack1.j3o b/blight-assets/src/main/resources/Models/northcoast/wrack1.j3o index 6de5012..8366161 100644 Binary files a/blight-assets/src/main/resources/Models/northcoast/wrack1.j3o and b/blight-assets/src/main/resources/Models/northcoast/wrack1.j3o differ diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 934f301..8587ad0 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -61,67 +61,15 @@ public class EditorApp extends Application { private static final Path ASSET_ROOT = ProjectRoot.resolve("blight-assets", "src", "main", "resources"); private final SharedInput input = new SharedInput(); - private WritableImage jfxImage; - private ImageView viewport; private Label statusLabel; private Label camCoordsLabel; - private boolean launchGameAfterSave = false; private Runnable pendingAfterSave = null; - private Button gamePlayBtn; // Spielen-Button im Seitenpanel - private CheckBox debugConsoleCB; // Debug-Konsole bei Spielstart anzeigen private VBox toolPanel; private BorderPane root; - private Stage gameConsoleStage; - private TextArea gameConsoleArea; - private final java.util.concurrent.ConcurrentLinkedQueue consoleBuffer = - new java.util.concurrent.ConcurrentLinkedQueue<>(); - private VBox assetPanel; - private Tab weltkartTab; - private Tab caveMapTab; - private MapObjectsView mapObjectsView; - private de.blight.editor.ui.WorldMapView worldMapView; - private de.blight.editor.ui.CaveMapView caveMapView; private de.blight.editor.ui.ThumbnailManagerView thumbnailManagerView; private StackPane worldViewport; - private javafx.scene.canvas.Canvas compassCanvas; private VBox topBar; // MenuBar + aktuelle Toolbar - private ToolBar worldToolBar; // Welt-Editor-Toolbar (Layer-Buttons) - private Label worldHintLabel; // Kamera-Info rechts neben Spielen - - // Content Editor (Dual-Window-Modus) - private Stage contentEditorStage; - private BorderPane contentEditorRoot; - private boolean dualWindowMode = false; - private String contentLastSection = "character"; - private ToggleButton contentCharBtn, contentQuestBtn, contentFraktionBtn, - contentItemBtn, contentRezeptBtn, contentCraftingBtn, - contentAreaBtn, contentLocationBtn, contentLangBtn; - private CheckMenuItem dualWindowMenuItem; - private CheckMenuItem showGridItem; - private Button contentSwitchBtn; // "> Content" in Welt-Toolbar (nur 1-Fenster) - - // Content-Baum (Instanzfeld, damit wir Knoten gezielt expandieren/befüllen können) - private javafx.scene.control.TreeView contentTreeView; - private javafx.scene.control.TreeItem contentCharNode; - private javafx.scene.control.TreeItem contentQuestNode; - private javafx.scene.control.TreeItem contentItemNode; - private javafx.scene.control.TreeItem contentFractionNode; - private javafx.scene.control.TreeItem contentAreaNode; - private javafx.scene.control.TreeItem contentLocationNode; - private javafx.scene.control.TreeItem contentCraftingNode; - private javafx.scene.control.TreeItem contentRecipeNode; - // aktive Editor-View-Referenzen (für Baum-Selektion und Neu/Löschen) - private de.blight.editor.ui.QuestEditorView activeQuestView; - private de.blight.editor.ui.ItemEditorView activeItemView; - private de.blight.editor.ui.FractionEditorView activeFractionView; - private de.blight.editor.ui.AreaEditorView activeAreaView; - private de.blight.editor.ui.LocationEditorView activeLocationView; - private de.blight.editor.ui.CraftingTableEditorView activeCraftingView; - private de.blight.editor.ui.RecipeEditorView activeRecipeView; - private Button contentNewBtn; - private Button contentDeleteBtn; private javafx.scene.Node worldBottomBar; - private boolean contentTreeUpdating = false; private Stage primaryStage; private JmeEditorApp jmeApp; @@ -144,141 +92,21 @@ public class EditorApp extends Application { private de.blight.editor.toolset.water.WaterController waterController; private de.blight.editor.toolset.voxel.VoxelController voxelController; private de.blight.editor.toolset.grass.GrassController grassController; + private de.blight.editor.toolset.content.ContentController contentController; + private de.blight.editor.toolset.tripo.TripoController tripoController; + private de.blight.editor.toolset.game.GameController gameController; + private de.blight.editor.toolset.world.WorldPanelController worldPanelController; + private de.blight.editor.toolset.asset.AssetTreeController assetController; + private de.blight.editor.toolset.terrain.TerrainPanelController terrainController; + private de.blight.editor.toolset.viewport.ViewportController viewportController; + private de.blight.editor.toolset.topbar.TopBarController topBarController; - // Asset-Panel: Pfad-Map + DnD-Zustand - private final Map, Path> itemPaths = new HashMap<>(); - private final Map, String> jmePaths = new HashMap<>(); - private final java.util.Set> jmeFolderNodes = new java.util.HashSet<>(); - private final java.util.Set> textureSetNodes = new java.util.HashSet<>(); - private TreeItem draggedItem = null; - - - // Cache: Menge aller j3o-Pfade die ein Skelett besitzen (null = Scan noch nicht abgeschlossen) - private java.util.Set skeletalModelPaths = null; - - // Character-Editor-Zustand - private de.blight.editor.ui.DialogEditorView dialogEditorView; - private de.blight.editor.ui.RoutineEditorView routineEditorView; - private de.blight.editor.ui.MonologueEditorView monologueEditorView; - private StackPane routineJmeSlot; - private javafx.scene.layout.VBox charEditContainer; // deaktiviert solange kein Character geladen - private javafx.scene.layout.VBox charFractionSection; // sichtbar nur für NPCs - private javafx.scene.layout.VBox charNpcSection; // sichtbar nur für NPCs - private javafx.scene.layout.VBox charMainCharSection; // sichtbar nur für MainCharacter - private javafx.scene.control.ComboBox charStatusCombo; - private javafx.scene.control.CheckBox charTraderCheck; - private javafx.scene.control.ListView 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 abMagicSpin, abStaffSpin, abSwordSpin, - abArcherySpin, abHeavySpin, abCrossbowSpin, abThieverySpin, - abAlchemySpin, abEngineeringSpin, abSmitherySpin, abEnchantingSpin; - private javafx.scene.control.TextField charNameField; - private javafx.scene.control.TextField charIdField; - private javafx.scene.control.ComboBox charTypeCombo; - private javafx.scene.control.ComboBox charModelCombo; - private javafx.scene.control.ComboBox charAnimSetCombo; - private javafx.scene.control.ComboBox charFractionCombo; - private final java.util.List loadedFractions = new java.util.ArrayList<>(); - /** Read-only labels for action assignments shown in character editor. */ - private VBox charActionLabelsBox; - private Label charEditorStatusLabel; - private javafx.scene.control.ListView charListView; - - // Ansichts-Umschalter - private boolean wireframeActive = false; - private CheckMenuItem viewTopologyItem = null; - - // Sound-Bereich-Werkzeug-Zustand - private VBox soundAreaDynamicContent; - - // Bereich-Werkzeug-Zustand - private VBox areaDynamicContent; - - // Location-Zonen-Werkzeug-Zustand - private VBox locationZoneDynamicContent; - - // Zonen-Werkzeug-Zustand (vereint Sound, Area, Location) - private VBox zonenDynamicContent; - private Runnable zonenKindStyleUpdater; - - // Auswahl-Werkzeug – dynamischer Bereich für selektierte Items - private VBox auswahlDynamicContent; - - // Spiel-Starten-Werkzeug-Zustand - private TextField spawnXField; - private TextField spawnZField; - private Label tempSpawnCoordsLabel; - private Label permSpawnCoordsLabel; - private Button gameNewBtn; - private Button gameFullBtn; - private boolean launchNewGameAfterSave = false; - private boolean pendingNewGame = false; - private boolean pendingAutostart = true; - - - // Tripo3D-Generator-Zustand - private ImageView tripoPreviewView; - private Label tripoStatusLabel; - private ProgressBar tripoProgressBar; - private Button tripoImportBtn; - private Button tripoImportRigBtn; - private boolean tripoGenerating = false; - private String tripoLastModelUrl; - private String tripoLastTaskId; - - - // Asset-Overlay-Zustand - private boolean assetOverlayOpen = false; - private Button assetTabBtn; private StackPane centerStack; // persistenter Wrapper für alle 3D-Ansichten - // Toolbar-Buttons (müssen vom Status-Poller erreichbar sein) - private ToggleButton baseBtn; - private ToggleButton grassBtn; // einziger Toolbar-Button für beide Gras-Tools - private ToggleButton textureBtn; - private ToggleButton stoneBtn; - private ToggleButton objPlaceBtn; // Unified Objekte-Button (Place + Edit) - private ToggleButton wasserMergedBtn; // Haupt-Toolbar: Wasser + Wasserfall - private ToggleButton wegeBtn; // Haupt-Toolbar: Wege / Pfadnetz - private ToggleButton zonenBtn; - private ToggleButton playToolBtn; - private ToggleButton auswahlBtn; // Universelles Selektionswerkzeug (F1) - private ToggleButton atmosphaereBtn; // Merged: Licht + Emitter - private ToggleButton voxelMergedBtn; // Merged: Voxel Sculpting + Klippe - private RadioMenuItem camOrbitItem; - private RadioMenuItem camFreeItem; - - - // Drag-Tracking für Kamerarotation (mittlere Taste) - private double prevDragX, prevDragY; - - // Drag-Tracking für Gizmo im Objekt-Modus - private double objDragPrevX, objDragPrevY; - private boolean objDragging = false; - - // Edit-Tracking - private double editPressX, editPressY; - private int editPressAction; - private javafx.animation.Timeline editTimer; private final javafx.animation.PauseTransition worldAutoSave = new javafx.animation.PauseTransition(javafx.util.Duration.seconds(8)); - // Asset-Tree-Items (müssen beim Refresh-Signal erreichbar sein) - private TreeItem assetTreeRoot; - private TreeView assetTree; - private TreeItem modelsNode; - private TreeItem texturesNode; - private TreeItem audioNode; - private TreeItem jmeModelsNode; - private TreeItem jmeTexturesNode; - private TreeItem animationsNode; - private TreeItem charactersNode; - private TreeItem itemsNode; + private void scheduleAutoSave() { worldAutoSave.playFromStart(); } // ── JavaFX Entry-Point ─────────────────────────────────────────────────── @@ -318,36 +146,33 @@ public class EditorApp extends Application { de.blight.editor.ProjectRoot.resolve( "blight-lang", "src", "main", "resources", "lang", "text_keys.txt")); - jfxImage = new WritableImage(INITIAL_VP_W, INITIAL_VP_H); + WritableImage jfxImage = new WritableImage(INITIAL_VP_W, INITIAL_VP_H); jmeApp = JmeEditorApp.launch(input, jfxImage, INITIAL_VP_W, INITIAL_VP_H); - assetPanel = buildAssetPanel(); - toolPanel = buildToolPanel(); - root = new BorderPane(); - topBar = buildTop(); - root.setTop(topBar); - worldViewport = buildViewport(); + viewportController = new de.blight.editor.toolset.viewport.ViewportController(input, jfxImage, this::scheduleAutoSave); + worldViewport = viewportController.buildViewport(); centerStack = new StackPane(worldViewport); - setupAssetOverlay(); root.setCenter(centerStack); - root.setRight(buildAuswahlPanel()); worldBottomBar = buildBottomBox(); root.setBottom(worldBottomBar); + topBarController = new de.blight.editor.toolset.topbar.TopBarController(input); + // ── Tool-Controller (Phase 2-Extraktion) ────────────────────────────── ToolContext toolCtx = new ToolContext( input, jmeApp, () -> stage, this::setStatus, this::setCenterView, root::setRight, node -> topBar.getChildren().set(1, node), - () -> worldViewport, ASSET_ROOT, this::switchToWorldEditor, registry + () -> worldViewport, ASSET_ROOT, this::switchToWorldEditor, registry, + root::setLeft, root::setBottom, () -> centerStack, tool -> currentTool = tool ); vegController = new de.blight.editor.toolset.vegetation.VegetationController(toolCtx); ezTreeController = new de.blight.editor.toolset.eztree.EzTreeController(toolCtx); animPreviewController = new de.blight.editor.toolset.anim.AnimPreviewController(toolCtx); animSetController = new de.blight.editor.toolset.anim.AnimSetController(toolCtx); objectController = new de.blight.editor.toolset.object.ObjectController(toolCtx); - modelEditorController = new de.blight.editor.toolset.model.ModelEditorController(toolCtx, this::openAssetOverlay); + modelEditorController = new de.blight.editor.toolset.model.ModelEditorController(toolCtx, () -> assetController.openOverlay()); modelEditorController.setObjectController(objectController); atmosphereController = new de.blight.editor.toolset.atmosphere.AtmosphereController(toolCtx); waterController = new de.blight.editor.toolset.water.WaterController(toolCtx); @@ -358,13 +183,50 @@ public class EditorApp extends Application { input.voxelTool.plateauTarget.setValue(input.heightTool.plateauHeight.getValue()); input.activeLayer = SharedInput.LAYER_VOXEL; input.activeTool = input.voxelTool; - showToolParameters(toolPanel, input.activeTool); + terrainController.showToolParameters(toolPanel, input.activeTool); return toolPanel; }, - () -> { if (voxelMergedBtn != null) voxelMergedBtn.fire(); }); + () -> { ToggleButton vmb = topBarController.getVoxelMergedBtn(); if (vmb != null) { vmb.fire(); } }); + terrainController = new de.blight.editor.toolset.terrain.TerrainPanelController(toolCtx, this::scheduleAutoSave); grassController = new de.blight.editor.toolset.grass.GrassController(toolCtx, - viewport, this::showToolParameters, - colorAbsPath -> { de.blight.editor.TextureMapData d = loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; }); + viewportController.getViewport(), terrainController::showToolParameters, + colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; }); + terrainController.setGrassController(grassController); + contentController = new de.blight.editor.toolset.content.ContentController(toolCtx); + tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx); + gameController = new de.blight.editor.toolset.game.GameController(toolCtx); + worldPanelController = new de.blight.editor.toolset.world.WorldPanelController(toolCtx, this::scheduleAutoSave); + + toolPanel = buildToolPanel(); + + topBar = topBarController.buildTop(toolCtx, new de.blight.editor.toolset.topbar.TopBarController.Deps( + toolPanel, + modelEditorController, assetController, animSetController, + contentController, worldPanelController, voxelController, + grassController, atmosphereController, waterController, + gameController, terrainController, objectController, + this::switchToVegetationGenerator, this::switchToEzTree, + this::switchToTripo, this::openCharacterEditor, + this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings + )); + root.setTop(topBar); + root.setRight(worldPanelController.buildAuswahlPanel()); + + assetController = new de.blight.editor.toolset.asset.AssetTreeController(toolCtx, + new de.blight.editor.toolset.asset.AssetTreeController.Deps( + objectController, animPreviewController, animSetController, + modelEditorController, contentController, + atmosphereController, waterController, worldPanelController, + topBarController.getAtmosphaereBtn(), topBarController.getWasserMergedBtn(), topBarController.getZonenBtn(), + this::switchToAnimPreview, + this::activateItemPlacement, + () -> buildItemPlacePanel(null), + this::scheduleAutoSave, + terrainController::loadTexMeta, terrainController::saveTexMeta, + () -> currentTool, r -> pendingAfterSave = r + )); + assetController.buildPanel(); + assetController.setupOverlay(centerStack); Scene scene = new Scene(root, 1280, 760); java.net.URL cssUrl = getClass().getResource("/editor.css"); @@ -374,12 +236,12 @@ public class EditorApp extends Application { e.consume(); if (input.camMode == SharedInput.CAM_ORBIT) { input.camMode = SharedInput.CAM_FREEFLY; - if (camFreeItem != null) { camFreeItem.setSelected(true); } + topBarController.getCamFreeItem().setSelected(true); } else { input.camMode = SharedInput.CAM_ORBIT; - if (camOrbitItem != null) { camOrbitItem.setSelected(true); } + topBarController.getCamOrbitItem().setSelected(true); } - if (worldHintLabel != null) worldHintLabel.setText(camHintText()); + topBarController.updateCamHintLabel(); return; } if (e.getCode() == KeyCode.R && e.isAltDown()) { @@ -393,7 +255,7 @@ public class EditorApp extends Application { case F5 -> 4; case F6 -> 5; case F7 -> 6; case F8 -> 7; default -> -1; }; - if (idx >= 0) { e.consume(); activateContentSection(idx); return; } + if (idx >= 0) { e.consume(); contentController.activateContentSection(idx, currentTool); return; } } if (!e.isControlDown() && !e.isAltDown() && "content".equals(currentTool)) { boolean isFKey = switch (e.getCode()) { @@ -411,36 +273,34 @@ public class EditorApp extends Application { }); scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_RELEASED, e -> handleKeyPress(e.getCode(), false)); scene.setOnKeyTyped(e -> { - if (!input.consoleIsOpen) return; + if (!input.consoleIsOpen) { return; } String ch = e.getCharacter(); if (ch.length() == 1) { char c = ch.charAt(0); - if (c >= ' ') input.consoleChars.offer(c); + if (c >= ' ') { input.consoleChars.offer(c); } } }); primaryStage = stage; input.scanSkeletalRequested = true; // Skelett-Scan beim Start anstoßen - // Alle ungefangenen Exceptions auf dem FX-Thread loggen – hilft beim Finden des Zahnrad-Auslösers Thread.currentThread().setUncaughtExceptionHandler((t, ex) -> org.slf4j.LoggerFactory.getLogger(EditorApp.class) .error("[FX-Thread] Ungefangene Exception", ex)); - // Auch andere Threads absichern (JME3-Thread, Worker-Threads) Thread.setDefaultUncaughtExceptionHandler((t, ex) -> org.slf4j.LoggerFactory.getLogger(EditorApp.class) .error("[Thread '{}'] Ungefangene Exception", t.getName(), ex)); stage.setTitle("Blight World Editor"); try (var is = getClass().getResourceAsStream("/icon_editor.png")) { - if (is != null) stage.getIcons().add(new Image(is)); + if (is != null) { stage.getIcons().add(new Image(is)); } } catch (Exception ignored) {} stage.setScene(scene); stage.setMinWidth(900); stage.setMinHeight(600); stage.setOnCloseRequest(e -> { saveCameraPrefs(); - if (jmeApp != null) jmeApp.stop(); + if (jmeApp != null) { jmeApp.stop(); } Platform.exit(); System.exit(0); }); @@ -450,12 +310,11 @@ public class EditorApp extends Application { pollerRef[0] = new javafx.animation.Timeline( new javafx.animation.KeyFrame(javafx.util.Duration.millis(100), ev -> { boolean jmeOk = input.jmeReady; - boolean mapOk = worldMapView != null && worldMapView.isMapReady(); + boolean mapOk = assetController.isMapReady(); if (!jmeOk) { splashStatus.setText(input.loadingStatus); } else { - splashStatus.setText("Karte: " + (worldMapView != null - ? worldMapView.getLoadingStatus() : "…")); + splashStatus.setText("Karte: " + assetController.getWorldMapLoadingStatus()); } if (jmeOk && mapOk) { pollerRef[0].stop(); @@ -478,10 +337,7 @@ public class EditorApp extends Application { input.saveStatusMsg = null; setStatus(saveMsg); saveCameraPrefs(); - if (launchGameAfterSave) { - launchGameAfterSave = false; - startGameProcess(); - } + gameController.onSaveComplete(); if (pendingAfterSave != null) { Runnable r = pendingAfterSave; pendingAfterSave = null; @@ -496,92 +352,51 @@ public class EditorApp extends Application { java.util.Set skeletal = input.skeletalPaths.getAndSet(null); if (skeletal != null) { - skeletalModelPaths = skeletal; + assetController.setSkeletalModelPaths(skeletal); } if (input.animImportCompleted) { input.animImportCompleted = false; - if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion()); - if (animationsNode != null) refreshCategoryNode(animationsNode, shouldPreserveExpansion()); + boolean pe = assetController.shouldPreserveExpansion(); + assetController.refreshCharacters(pe); + assetController.refreshAnimations(pe); } SharedInput.CharModelImportResult charImport = input.charModelImportResult.getAndSet(null); if (charImport != null) { - if (charImport.error() != null) { - javafx.scene.control.Alert alert = new javafx.scene.control.Alert( - javafx.scene.control.Alert.AlertType.ERROR); - alert.initOwner(primaryStage); - alert.setTitle("Character-Import fehlgeschlagen"); - alert.setHeaderText("FBX konnte nicht als Character-Modell importiert werden"); - alert.setContentText(charImport.error()); - alert.showAndWait(); - if (charEditorStatusLabel != null) - charEditorStatusLabel.setText("Import fehlgeschlagen"); - } else { - String relPath = charImport.relPath(); - if (charModelCombo != null && !charModelCombo.getItems().contains(relPath)) { - charModelCombo.getItems().add(relPath); - } - if (charModelCombo != null) charModelCombo.setValue(relPath); - if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion()); - animSetController.onCharModelImported(relPath); - if (charEditorStatusLabel != null) - charEditorStatusLabel.setText("Modell importiert: " + relPath); - setStatus("Character-Modell importiert: " + relPath); + contentController.onCharImportResult(charImport); + if (charImport.error() == null) { + assetController.refreshCharacters(assetController.shouldPreserveExpansion()); + animSetController.onCharModelImported(charImport.relPath()); + setStatus("Character-Modell importiert: " + charImport.relPath()); } } - - String embedStatus = input.animEmbedStatus; - if (embedStatus != null) { - input.animEmbedStatus = null; - if (charEditorStatusLabel != null) charEditorStatusLabel.setText(embedStatus); - } - - String stripStatus = input.animStripStatus; - if (stripStatus != null) { - input.animStripStatus = null; - if (charEditorStatusLabel != null) charEditorStatusLabel.setText(stripStatus); - } + contentController.onFrame(); if (input.refreshAssets) { input.refreshAssets = false; - boolean pe = shouldPreserveExpansion(); - if (modelsNode != null) refreshCategoryNode(modelsNode, pe); - if (animationsNode != null) refreshCategoryNode(animationsNode, pe); - if (charactersNode != null) refreshCategoryNode(charactersNode, pe); + boolean pe = assetController.shouldPreserveExpansion(); + assetController.refreshModels(pe); + assetController.refreshAnimations(pe); + assetController.refreshCharacters(pe); } if (input.refreshWorldTree) { input.refreshWorldTree = false; - mapObjectsView.refresh(); + assetController.refreshMapObjects(); } - drawCompass(); + viewportController.drawCompass(); - // Spiel-Konsole: gepufferte Zeilen gebündelt ausgeben (max 200 auf einmal) - if (!consoleBuffer.isEmpty()) { - if (gameConsoleArea != null) { - StringBuilder sb = new StringBuilder(); - int count = 0; - String ln; - while (count < 200 && (ln = consoleBuffer.poll()) != null) { - sb.append(ln).append('\n'); - count++; - } - gameConsoleArea.appendText(sb.toString()); - gameConsoleArea.setScrollTop(Double.MAX_VALUE); - } else { - consoleBuffer.clear(); - } - } + gameController.flushConsoleBuffer(); if (input.soundAreaSelectionChanged) { input.soundAreaSelectionChanged = false; - updateSoundAreaPanel(input.selectedSoundAreaInfo); + worldPanelController.updateSoundAreaPanel(input.selectedSoundAreaInfo); } if (input.areaSelectionChanged) { input.areaSelectionChanged = false; - updateAreaPanel(input.selectedAreaInfo); + worldPanelController.updateAreaPanel(input.selectedAreaInfo); } if (input.areaOverlapRejected) { @@ -591,38 +406,34 @@ public class EditorApp extends Application { if (input.locationZoneSelectionChanged) { input.locationZoneSelectionChanged = false; - updateLocationZonePanel(input.selectedLocationZoneInfo); + worldPanelController.updateLocationZonePanel(input.selectedLocationZoneInfo); } if (input.auswahlZoneSelected) { input.auswahlZoneSelected = false; input.activeLayer = SharedInput.LAYER_ZONEN; - if (zonenBtn != null) zonenBtn.setSelected(true); - root.setRight(buildZonenPanel()); + topBarController.getZonenBtn().setSelected(true); + root.setRight(worldPanelController.buildZonenPanel()); } if (input.auswahlLightSelected) { input.auswahlLightSelected = false; - if (atmosphaereBtn != null) { - atmosphaereBtn.setSelected(true); - root.setRight(atmosphereController.buildPanel()); - atmosphereController.updateLightPanel(input.selectedLightInfo); - } + topBarController.getAtmosphaereBtn().setSelected(true); + root.setRight(atmosphereController.buildPanel()); + atmosphereController.updateLightPanel(input.selectedLightInfo); } if (input.auswahlEmitterSelected) { input.auswahlEmitterSelected = false; - if (atmosphaereBtn != null) { - atmosphaereBtn.setSelected(true); - input.activeLayer = SharedInput.LAYER_EMITTERS; - root.setRight(atmosphereController.buildPanel()); - atmosphereController.updateEmitterPanel(input.selectedEmitterInfo); - } + topBarController.getAtmosphaereBtn().setSelected(true); + input.activeLayer = SharedInput.LAYER_EMITTERS; + root.setRight(atmosphereController.buildPanel()); + atmosphereController.updateEmitterPanel(input.selectedEmitterInfo); } if (input.auswahlWaterSelected) { input.auswahlWaterSelected = false; - if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); + topBarController.getWasserMergedBtn().setSelected(true); input.activeLayer = SharedInput.LAYER_WATER; root.setRight(waterController.buildPanel()); waterController.updateWaterPanel(input.selectedWaterInfo); @@ -630,7 +441,7 @@ public class EditorApp extends Application { if (input.auswahlWaterfallSelected) { input.auswahlWaterfallSelected = false; - if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); + topBarController.getWasserMergedBtn().setSelected(true); input.activeLayer = SharedInput.LAYER_WATERFALL; root.setRight(waterController.buildPanel()); waterController.updateWaterfallPanel(input.selectedWaterfallInfo); @@ -638,23 +449,23 @@ public class EditorApp extends Application { if (input.itemSelectionChanged) { input.itemSelectionChanged = false; - updateAuswahlItemPanel(input.selectedItemInfo); + worldPanelController.updateAuswahlItemPanel(input.selectedItemInfo); } if (input.zonenSelectionChanged) { input.zonenSelectionChanged = false; - updateZonenPanel(input.zonenSelectedKind); - if (zonenKindStyleUpdater != null) zonenKindStyleUpdater.run(); + worldPanelController.updateZonenPanel(input.zonenSelectedKind); + worldPanelController.runZonenKindStyleUpdater(); } if (input.spawnPickChanged) { input.spawnPickChanged = false; - updateSpawnFields(input.pickedSpawnInfo); + gameController.updateSpawnFields(input.pickedSpawnInfo); } if (input.permSpawnChanged) { input.permSpawnChanged = false; - if (permSpawnCoordsLabel != null) permSpawnCoordsLabel.setText(permSpawnCoordsText()); + gameController.updatePermSpawnLabel(); } // Kamera-Koordinaten aktualisieren @@ -676,7 +487,7 @@ public class EditorApp extends Application { if (input.texturePathsLoaded) { input.texturePathsLoaded = false; if (input.activeLayer == 4) { - showToolParameters(toolPanel, input.activeTool); + terrainController.showToolParameters(toolPanel, input.activeTool); } } @@ -684,24 +495,23 @@ public class EditorApp extends Application { if (input.activeTool instanceof de.blight.editor.tool.HeightTool ht && ht.plateauHeightChanged) { ht.plateauHeightChanged = false; - showToolParameters(toolPanel, input.activeTool); + terrainController.showToolParameters(toolPanel, input.activeTool); } if (input.activeTool instanceof de.blight.editor.tool.VoxelTool vt && vt.plateauTargetChanged) { vt.plateauTargetChanged = false; - showToolParameters(toolPanel, input.activeTool); + terrainController.showToolParameters(toolPanel, input.activeTool); } if (input.activeTool instanceof de.blight.editor.tool.VoxelTool vt2 && vt2.modeChanged) { vt2.modeChanged = false; - showToolParameters(toolPanel, input.activeTool); + terrainController.showToolParameters(toolPanel, input.activeTool); } // Neues JME-Image nach Viewport-Resize übernehmen javafx.scene.image.WritableImage newImg = input.resizedImage.getAndSet(null); if (newImg != null) { - jfxImage = newImg; - viewport.setImage(newImg); + viewportController.setImage(newImg); input.viewportScaleX = 1.0; input.viewportScaleY = 1.0; } @@ -734,22 +544,18 @@ public class EditorApp extends Application { currentTool = "world"; if (jmeApp != null) jmeApp.setRenderThrottled(false); root.setLeft(null); // ggf. gesetzten Content-Tree entfernen - if (assetTabBtn != null) assetTabBtn.setVisible(true); // ggf. vom Thumbnail-Manager versteckt - if (wasThumbnailManager) openAssetOverlay(); - topBar.getChildren().set(1, worldToolBar); - if (routineJmeSlot != null) { - routineJmeSlot.getChildren().remove(worldViewport); - routineJmeSlot = null; - // centerStack-Kinder neu aufbauen: worldViewport könnte verschoben worden sein, - // und der Charakter-Editor-Platzhalter muss weg - centerStack.getChildren().setAll(worldViewport, assetPanel, assetTabBtn); - assetTabBtn.setTranslateX(assetPanel.getTranslateX() + 420); - if (root.getCenter() != centerStack) root.setCenter(centerStack); + assetController.showTabBtn(); + if (wasThumbnailManager) assetController.openOverlay(); + topBar.getChildren().set(1, topBarController.getWorldToolBar()); + if (contentController.hasRoutineJmeSlot()) { + contentController.detachRoutineJmeSlot(); + assetController.reattachToCenterStack(centerStack, worldViewport); + if (root.getCenter() != centerStack) { root.setCenter(centerStack); } } else { setCenterView(worldViewport); } if (input.activeLayer == SharedInput.LAYER_AUSWAHL) { - root.setRight(buildAuswahlPanel()); + root.setRight(worldPanelController.buildAuswahlPanel()); } else { root.setRight(toolPanel); } @@ -757,17 +563,16 @@ public class EditorApp extends Application { if (wasObjEditor && (input.activeLayer == SharedInput.LAYER_OBJECTS || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)) { input.activeLayer = 0; - if (baseBtn != null) baseBtn.setSelected(true); + topBarController.getBaseBtn().setSelected(true); } else if (wasModelEditor) { input.activeLayer = 0; input.modelEditorOpenPath = null; - if (baseBtn != null) baseBtn.setSelected(true); + topBarController.getBaseBtn().setSelected(true); } } - /** Im 1-Fenster-Modus zurück zum Welteneditor wechseln, wenn der Content-Editor aktiv ist. */ private void ensureNotContentEditor() { - if (!dualWindowMode && "content".equals(currentTool)) { + if (contentController.isSingleWindowContentActive(currentTool)) { switchToWorldEditor(); } } @@ -781,8 +586,8 @@ public class EditorApp extends Application { thumbnailManagerView.refresh(); } // Asset-Baum-Overlay ausblenden – wird in switchToWorldEditor() wiederhergestellt - if (assetOverlayOpen) closeAssetOverlay(); - if (assetTabBtn != null) assetTabBtn.setVisible(false); + if (assetController.isOverlayOpen()) assetController.closeOverlay(); + assetController.hideTabBtn(); topBar.getChildren().set(1, buildThumbnailManagerToolBar()); root.setLeft(null); root.setBottom(null); @@ -810,349 +615,7 @@ public class EditorApp extends Application { private void switchToTripo() { ensureNotContentEditor(); - currentTool = "tripo"; - topBar.getChildren().set(1, buildTripoToolBar()); - - // Center: large image preview (filled once generation succeeds) - StackPane previewCenter = new StackPane(); - previewCenter.setStyle("-fx-background-color: #1a1a2e;"); - tripoPreviewView = new ImageView(); - tripoPreviewView.setPreserveRatio(true); - tripoPreviewView.fitWidthProperty().bind(previewCenter.widthProperty()); - tripoPreviewView.fitHeightProperty().bind(previewCenter.heightProperty()); - Label placeholder = new Label("Vorschau erscheint hier nach der Generierung"); - placeholder.setStyle("-fx-text-fill: #888; -fx-font-size: 14;"); - previewCenter.getChildren().addAll(placeholder, tripoPreviewView); - root.setCenter(previewCenter); - - root.setRight(buildTripoPanel()); - } - - // ── Oberer Bereich: MenuBar + ToolBar ──────────────────────────────────── - - private VBox buildTop() { - MenuBar menuBar = new MenuBar(); - - Menu fileMenu = new Menu("Datei"); - MenuItem newItem = new MenuItem("Neue Karte"); - MenuItem saveItem = new MenuItem("Speichern"); - saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S")); - saveItem.setOnAction(e -> { input.saveRequested = true; setStatus("Speichern…"); }); - MenuItem screenshotItem = new MenuItem("Screenshot"); - screenshotItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+F12")); - screenshotItem.setOnAction(e -> takeEditorScreenshot()); - MenuItem settingsItem = new MenuItem("Einstellungen…"); - settingsItem.setOnAction(e -> openSettings()); - MenuItem quitItem = new MenuItem("Beenden"); - quitItem.setOnAction(e -> javafx.application.Platform.exit()); - fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(), - screenshotItem, new SeparatorMenuItem(), settingsItem, - new SeparatorMenuItem(), quitItem); - - Menu importMenu = new Menu("Import"); - MenuItem importModelLodItem = new MenuItem("Modell mit LOD-Editor…"); - MenuItem importTexItem = new MenuItem("Texturen…"); - MenuItem importTexZipItem = new MenuItem("Texturen-ZIP…"); - MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…"); - MenuItem importAudioItem = new MenuItem("Audio…"); - MenuItem importAnimItem = new MenuItem("Animationen…"); - MenuItem importCharModelItem = new MenuItem("Character-Modell (FBX)…"); - importModelLodItem.setOnAction(e -> modelEditorController.openModelImport(primaryStage)); - importTexItem.setOnAction(e -> handleTextureImport(primaryStage)); - importTexZipItem.setOnAction(e -> handleZipTextureImport(primaryStage)); - importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage)); - importAudioItem.setOnAction(e -> handleAudioImport(primaryStage)); - importAnimItem.setOnAction(e -> animSetController.handleAnimationImport(primaryStage)); - importCharModelItem.setOnAction(e -> animSetController.handleCharacterModelImport(primaryStage)); - importMenu.getItems().addAll(importModelLodItem, importTexItem, importTexZipItem, - importTexSetItem, importAudioItem, importAnimItem, - new javafx.scene.control.SeparatorMenuItem(), importCharModelItem); - - Menu toolsMenu = new Menu("Werkzeuge"); - MenuItem vegetationsItem = new MenuItem("Vegetations Generator"); - MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)"); - MenuItem tripoItem = new MenuItem("AI Modell-Generator (Tripo3D)"); - MenuItem animPrevItem = new MenuItem("Animationset Editor"); - MenuItem objEditorItem = new MenuItem("Model Editor"); - MenuItem thumbItem = new MenuItem("Thumbnail-Verwaltung"); - vegetationsItem.setOnAction(e -> switchToVegetationGenerator()); - ezTreeItem.setOnAction(e -> switchToEzTree()); - tripoItem.setOnAction(e -> switchToTripo()); - animPrevItem.setOnAction(e -> openCharacterEditor()); - objEditorItem.setOnAction(e -> modelEditorController.openModelEditor(null, null)); - thumbItem.setOnAction(e -> switchToThumbnailManager()); - toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem, - animPrevItem, objEditorItem, new SeparatorMenuItem(), thumbItem); - - Menu viewMenu = new Menu("Ansicht"); - MenuItem resetCam = new MenuItem("Kamera zurücksetzen (Alt+R)"); - resetCam.setOnAction(e -> input.resetCameraRequested.set(true)); - MenuItem viewTexture = new MenuItem("Textur (Alt+G)"); - MenuItem viewWireframe = new MenuItem("Drahtgitter (Alt+G)"); - viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Alt+T)"); - viewTexture.setOnAction(e -> { - wireframeActive = false; - input.wireframeRequest = 2; - }); - viewWireframe.setOnAction(e -> { - wireframeActive = true; - input.wireframeRequest = 1; - }); - viewTopologyItem.setOnAction(e -> - input.topologyRequest = viewTopologyItem.isSelected() ? 1 : 2); - - ToggleGroup camMenuGroup = new ToggleGroup(); - camOrbitItem = new RadioMenuItem("⊙ Orbit-Kamera (Alt+C)"); - camFreeItem = new RadioMenuItem("✈ FreeFly-Kamera (Alt+C)"); - camOrbitItem.setToggleGroup(camMenuGroup); - camFreeItem.setToggleGroup(camMenuGroup); - camOrbitItem.setSelected(true); - camOrbitItem.setOnAction(e -> { input.camMode = SharedInput.CAM_ORBIT; if (worldHintLabel != null) worldHintLabel.setText(camHintText()); }); - camFreeItem.setOnAction(e -> { input.camMode = SharedInput.CAM_FREEFLY; if (worldHintLabel != null) worldHintLabel.setText(camHintText()); }); - - CheckMenuItem debugHeightItem = new CheckMenuItem("Höhen-Overlay (Alt+H)"); - debugHeightItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+H")); - debugHeightItem.setOnAction(e -> - input.debugHeightOverlayEnabled = debugHeightItem.isSelected()); - - dualWindowMenuItem = new CheckMenuItem("2-Fenster-Modus (Alt+W)"); - dualWindowMenuItem.setAccelerator( - javafx.scene.input.KeyCombination.keyCombination("Alt+W")); - dualWindowMenuItem.setOnAction(e -> { - if (dualWindowMenuItem.isSelected()) enableDualWindowMode(); - else disableDualWindowMode(); - }); - - showGridItem = new CheckMenuItem("1m-Gitternetz (Alt+N)"); - showGridItem.setSelected(true); - showGridItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+N")); - showGridItem.setOnAction(e -> input.showGrid = showGridItem.isSelected()); - - viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe, - new SeparatorMenuItem(), viewTopologyItem, - new SeparatorMenuItem(), debugHeightItem, - new SeparatorMenuItem(), showGridItem, - new SeparatorMenuItem(), camOrbitItem, camFreeItem); - - Menu zeitMenu = new Menu("Zeit"); - ToggleGroup zeitGroup = new ToggleGroup(); - int[] hours = {0, 3, 6, 9, 12, 15, 18, 21}; - for (int h : hours) { - RadioMenuItem item = new RadioMenuItem(String.format("%02d:00", h)); - item.setToggleGroup(zeitGroup); - if (h == 12) { item.setSelected(true); } - final float t = h / 24f; - item.setOnAction(e -> input.pendingTimeOfDay = t); - zeitMenu.getItems().add(item); - } - menuBar.getMenus().addAll(fileMenu, toolsMenu, importMenu, viewMenu, zeitMenu); - - ToolBar toolBar = new ToolBar(); - auswahlBtn = new ToggleButton("[k] Auswahl"); - baseBtn = new ToggleButton("▲▼ Terrain"); - voxelMergedBtn = new ToggleButton("◈ Voxel"); - textureBtn = new ToggleButton(": : Textur"); - grassBtn = new ToggleButton("// Gras"); - stoneBtn = new ToggleButton("o Steine"); - objPlaceBtn = new ToggleButton("+ Objekte"); - atmosphaereBtn = new ToggleButton("* Atmosphare"); - wasserMergedBtn = new ToggleButton("~ Wasser"); - wegeBtn = new ToggleButton("-- Wege"); - zonenBtn = new ToggleButton("[ ] Zonen"); - playToolBtn = new ToggleButton("▶ Spielen"); - final String BOLD = "-fx-font-weight:bold;"; - auswahlBtn.setStyle(BOLD); - baseBtn.setStyle(BOLD); - voxelMergedBtn.setStyle(BOLD); - textureBtn.setStyle(BOLD); - grassBtn.setStyle(BOLD); - stoneBtn.setStyle(BOLD); - objPlaceBtn.setStyle(BOLD); - atmosphaereBtn.setStyle(BOLD); - wasserMergedBtn.setStyle(BOLD); - wegeBtn.setStyle(BOLD); - zonenBtn.setStyle(BOLD); - playToolBtn.setStyle(BOLD); - - ToggleGroup layerGroup = new ToggleGroup(); - auswahlBtn.setToggleGroup(layerGroup); - baseBtn.setToggleGroup(layerGroup); - voxelMergedBtn.setToggleGroup(layerGroup); - textureBtn.setToggleGroup(layerGroup); - grassBtn.setToggleGroup(layerGroup); - stoneBtn.setToggleGroup(layerGroup); - objPlaceBtn.setToggleGroup(layerGroup); - atmosphaereBtn.setToggleGroup(layerGroup); - wasserMergedBtn.setToggleGroup(layerGroup); - wegeBtn.setToggleGroup(layerGroup); - zonenBtn.setToggleGroup(layerGroup); - playToolBtn.setToggleGroup(layerGroup); - auswahlBtn.setSelected(true); - input.activeLayer = SharedInput.LAYER_AUSWAHL; - - auswahlBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_AUSWAHL; - input.deselectAllOnAuswahl = true; - root.setRight(buildAuswahlPanel()); - }); - baseBtn.setOnAction(e -> { - input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue()); - input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue()); - input.heightTool.plateauHeight.setValue(input.voxelTool.plateauTarget.getValue()); - input.activeLayer = 0; input.activeTool = input.heightTool; - root.setRight(toolPanel); - showToolParameters(toolPanel, input.activeTool); - }); - voxelMergedBtn.setOnAction(e -> root.setRight(voxelController.buildPanel())); - textureBtn.setOnAction(e -> { - input.activeLayer = 4; input.activeTool = input.textureTool; - root.setRight(toolPanel); - showToolParameters(toolPanel, input.activeTool); - }); - grassBtn.setOnAction(e -> root.setRight(grassController.buildPanel())); - stoneBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_STONE; - input.activeTool = input.stoneTool; - root.setRight(toolPanel); - showToolParameters(toolPanel, input.activeTool); - }); - objPlaceBtn.setOnAction(e -> { - if (modelsNode != null) modelsNode.setExpanded(true); - openAssetOverlay(); - objectController.activatePlaceMode(); - }); - atmosphaereBtn.setOnAction(e -> root.setRight(atmosphereController.buildPanel())); - wasserMergedBtn.setOnAction(e -> root.setRight(waterController.buildPanel())); - wegeBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_PATH_NETWORK; - root.setRight(buildWegePanel()); - }); - zonenBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_ZONEN; - root.setRight(buildZonenPanel()); - }); - playToolBtn.setOnAction(e -> { - input.activeLayer = SharedInput.LAYER_PLAY_TOOL; - input.playToolMode = SharedInput.PlayToolMode.EDIT; - root.setRight(buildPlayToolPanel()); - Platform.runLater(() -> { if (gamePlayBtn != null) gamePlayBtn.requestFocus(); }); - }); - - worldHintLabel = new Label(camHintText()); - worldHintLabel.setStyle("-fx-text-fill: #555;"); - - toolBar.getItems().addAll( - auswahlBtn, - new Separator(Orientation.VERTICAL), baseBtn, voxelMergedBtn, - new Separator(Orientation.VERTICAL), textureBtn, grassBtn, stoneBtn, - new Separator(Orientation.VERTICAL), objPlaceBtn, - new Separator(Orientation.VERTICAL), atmosphaereBtn, - new Separator(Orientation.VERTICAL), wasserMergedBtn, wegeBtn, - new Separator(Orientation.VERTICAL), zonenBtn, - new Separator(Orientation.VERTICAL), playToolBtn, - new Separator(Orientation.VERTICAL), worldHintLabel, - new Separator(Orientation.VERTICAL), createToolBarSpacer(), - buildContentSwitchBtn()); - - worldToolBar = toolBar; - return new VBox(menuBar, toolBar); - } - - - // ── Kleine Helpers für Baum-Parameter-Panel ────────────────────────────── - - private static Label bold(String text) { - Label l = new Label(text); - l.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); - return l; - } - - private boolean isObjectMode() { - return input.activeLayer == SharedInput.LAYER_OBJECTS - || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT - || input.activeLayer == SharedInput.LAYER_AUSWAHL; - } - - private static Label styledHint(String text) { - Label l = new Label(text); - l.setStyle("-fx-text-fill: #333333; -fx-font-size: 11;"); - return l; - } - - /** - * Wraps a Slider in an HBox with a synced TextField on the right. - * The slider's own valueProperty listeners fire normally; this just adds a text field. - */ - private static HBox withField(Slider slider, String fmt) { - slider.setMaxWidth(Double.MAX_VALUE); - TextField tf = new TextField(String.format(fmt, slider.getValue())); - tf.setPrefWidth(62); - tf.setMaxWidth(62); - tf.setStyle("-fx-font-size: 11;"); - slider.valueProperty().addListener((o, ov, nv) -> - tf.setText(String.format(fmt, nv.doubleValue()))); - Runnable commit = () -> { - try { - double v = Math.max(slider.getMin(), Math.min(slider.getMax(), - Double.parseDouble(tf.getText().replace(',', '.')))); - slider.setValue(v); - } catch (NumberFormatException ignored) { - tf.setText(String.format(fmt, slider.getValue())); - } - }; - tf.setOnAction(e -> commit.run()); - tf.focusedProperty().addListener((o, ov, focused) -> { if (!focused) commit.run(); }); - HBox row = new HBox(5, slider, tf); - HBox.setHgrow(slider, Priority.ALWAYS); - return row; - } - - private static VBox paramSlider(String label, double min, double max, double init, - Consumer setter) { - Label name = new Label(label); - name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); - Slider slider = new Slider(min, max, init); - slider.setShowTickMarks(true); - slider.setShowTickLabels(true); - slider.setMajorTickUnit((max - min) / 2.0); - slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue())); - return new VBox(3, name, withField(slider, "%.3f")); - } - - - - // ── EZ-Tree UI-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: #1a3a6a;"); - VBox.setMargin(l, new Insets(8, 0, 0, 0)); - return l; - } - - private static Spinner intSpinner(int min, int max, int init) { - Spinner sp = new Spinner<>(min, max, Math.max(min, Math.min(max, init))); - sp.setEditable(true); - sp.setMaxWidth(Double.MAX_VALUE); - return sp; - } - - private static VBox ezFloat(String label, double min, double max, double init, - java.util.function.Consumer setter) { - Label name = new Label(label); - name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); - Slider slider = new Slider(min, max, Math.max(min, Math.min(max, init))); - slider.setShowTickMarks(false); - slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue())); - return new VBox(2, name, withField(slider, "%.3f")); - } - - private static String pathToName(String path, String[] paths, String[] names) { - if (path == null) return names[0]; - for (int i = 0; i < paths.length; i++) { - if (path.equals(paths[i])) return names[i]; - } - return names[0]; + tripoController.activate(); } @@ -1162,7 +625,7 @@ public class EditorApp extends Application { String itemId = itemFile.getFileName().toString().replace(".item", ""); input.pendingItemId = itemId; input.activeLayer = SharedInput.LAYER_ITEMS; - openAssetOverlay(); + assetController.openOverlay(); root.setRight(buildItemPlacePanel(itemId)); setStatus("Item: " + itemId + " | Linksklick ins Terrain zum Platzieren | Rechtsklick = Abbrechen"); } @@ -1170,13 +633,13 @@ public class EditorApp extends Application { private VBox buildItemPlacePanel(String itemId) { VBox inner = new VBox(8); inner.setPadding(new Insets(10)); - inner.getChildren().addAll(sectionTitle("Item platzieren"), new Separator()); + inner.getChildren().addAll(de.blight.editor.toolset.UiHelper.sectionTitle("Item platzieren"), new Separator()); Label idLabel = new Label("Item-ID: " + itemId); idLabel.setWrapText(true); idLabel.setStyle("-fx-font-weight:bold; -fx-text-fill: #222;"); inner.getChildren().add(idLabel); - inner.getChildren().add(styledHint("Linksklick ins Terrain → Item platzieren")); - inner.getChildren().add(styledHint("Rechtsklick → Platzierung abbrechen")); + inner.getChildren().add(de.blight.editor.toolset.UiHelper.styledHint("Linksklick ins Terrain → Item platzieren")); + inner.getChildren().add(de.blight.editor.toolset.UiHelper.styledHint("Rechtsklick → Platzierung abbrechen")); ScrollPane sp = new ScrollPane(inner); sp.setFitToWidth(true); sp.setStyle("-fx-background-color: transparent;"); @@ -1201,3715 +664,12 @@ public class EditorApp extends Application { panel.setPadding(new Insets(10)); panel.setPrefWidth(260); panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - showToolParameters(panel, input.activeTool); + terrainController.showToolParameters(panel, input.activeTool); return panel; } // ── Kombiniertes Gras-Panel ─────────────────────────────────────────────── - private VBox buildAuswahlPanel() { - VBox panel = new VBox(10); - panel.setPadding(new Insets(10)); - panel.setPrefWidth(260); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - Label title = new Label("Auswahl"); - title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); - panel.getChildren().addAll(title, new Separator()); - panel.getChildren().addAll( - styledHint("L-Klick: Element auswählen"), - styledHint(" → wechselt automatisch zum"), - styledHint(" passenden Werkzeug"), - new Separator(), - styledHint("Selektierbar:"), - styledHint(" + Platzierte Objekte"), - styledHint(" * Lichter / Emitter"), - styledHint(" ~ Wasser / Wasserfall"), - styledHint(" -- Wege / Knoten"), - styledHint(" [ ] Zonen"), - styledHint(" o Spawnpunkte (Items)"), - new Separator()); - auswahlDynamicContent = new VBox(6); - Label noSel = new Label("Kein Element ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - auswahlDynamicContent.getChildren().add(noSel); - panel.getChildren().add(auswahlDynamicContent); - return panel; - } - - private void updateAuswahlItemPanel(String info) { - if (auswahlDynamicContent == null) return; - auswahlDynamicContent.getChildren().clear(); - if (info == null) { - Label noSel = new Label("Kein Element ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - auswahlDynamicContent.getChildren().add(noSel); - return; - } - String[] parts = info.split("\\|"); - String itemId = parts.length > 1 ? parts[1] : "?"; - Label idLabel = new Label("Item: " + itemId); - idLabel.setStyle("-fx-font-weight: bold;"); - Button deleteBtn = new Button("Löschen"); - deleteBtn.setMaxWidth(Double.MAX_VALUE); - deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;"); - deleteBtn.setOnAction(e -> input.deleteSelectedItemRequested = true); - auswahlDynamicContent.getChildren().addAll(idLabel, deleteBtn); - } - - private VBox buildWegePanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - inner.getChildren().addAll( - sectionTitle("Wegnetz"), - new Separator(), - styledHint("L-Klick: neuen Knoten setzen"), - styledHint("L-Klick auf Knoten: auswählen"), - styledHint(" (2 gewählte → Kante erzeugen)"), - styledHint("L-Klick + gewählter Knoten:"), - styledHint(" neuen Knoten + Kante"), - styledHint("R-Klick auf Knoten: löschen"), - styledHint("R-Klick auf Terrain: abwählen")); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(260); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - private void showToolParameters(VBox panel, EditorTool tool) { - panel.getChildren().clear(); - - Label toolTitle = new Label(tool.getName()); - toolTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111111;"); - panel.getChildren().addAll(toolTitle, new Separator()); - - // ── TextureTool: Radius/Stärke oben, dann 4×3-Grid + aktiver Slot ────── - if (tool instanceof de.blight.editor.tool.TextureTool tt) { - for (ToolParameter p : tt.getParameters()) { - Label name = new Label(p.getName()); - name.setStyle("-fx-text-fill: #111111;"); - Slider slider = new Slider(p.getMin(), p.getMax(), p.getValue()); - slider.setShowTickMarks(true); - slider.setShowTickLabels(true); - slider.setMajorTickUnit((p.getMax() - p.getMin()) / 2); - slider.valueProperty().addListener((obs, oldV, newV) -> p.setValue(newV.doubleValue())); - panel.getChildren().add(new VBox(3, name, withField(slider, "%.3f"))); - } - panel.getChildren().add(new Separator()); - javafx.scene.Node texUI = buildTextureChoiceWithActiveSlotUI(tt.textureIndex); - VBox.setVgrow(texUI, Priority.ALWAYS); - panel.getChildren().add(texUI); - return; - } - - for (ChoiceToolParameter param : tool.getChoiceParameters()) { - Label nameLabel = new Label(param.getName()); - nameLabel.setStyle("-fx-text-fill: #111111;"); - - if (tool instanceof de.blight.editor.tool.TextureTool - && param == ((de.blight.editor.tool.TextureTool) tool).textureIndex) { - panel.getChildren().addAll(nameLabel, buildTextureChoiceUI(param)); - } else if (tool instanceof de.blight.editor.tool.VoxelTool vt2 - && param == vt2.mode) { - panel.getChildren().addAll(nameLabel, buildVoxelModeUI(param)); - } else if (param.getImagePaths() != null) { - String[] paths = param.getImagePaths(); - String[] labels = param.getChoices(); - ToggleGroup tg = new ToggleGroup(); - ToggleButton[] buttons = new ToggleButton[paths.length]; - HBox row = new HBox(3); - - for (int j = 0; j < paths.length; j++) { - final int index = j; - ToggleButton btn = new ToggleButton(); - btn.setToggleGroup(tg); - btn.setTooltip(new Tooltip(labels[j])); - btn.setMinSize(50, 50); - btn.setMaxSize(50, 50); - - String imgUrlStr = null; - URL resUrl = EditorApp.class.getResource("/" + paths[j]); - if (resUrl != null) { - imgUrlStr = resUrl.toString(); - } else { - File imgFile = new File(paths[j]).getAbsoluteFile(); - if (imgFile.exists()) imgUrlStr = imgFile.toURI().toString(); - } - if (imgUrlStr != null) { - Image img = new Image(imgUrlStr, 38, 38, true, true); - if (!img.isError()) btn.setGraphic(new ImageView(img)); - else btn.setText(labels[j].substring(0, 2)); - } else { - btn.setText(labels[j].substring(0, 2)); - } - - boolean initially = (j == param.getSelectedIndex()); - btn.setSelected(initially); - applyModeButtonStyle(btn, initially); - btn.selectedProperty().addListener((obs, was, isNow) -> { - applyModeButtonStyle(btn, isNow); - if (isNow) param.setSelectedIndex(index); - }); - - buttons[j] = btn; - row.getChildren().add(btn); - } - - tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { - if (newT == null) tg.selectToggle(oldT); - }); - panel.getChildren().addAll(nameLabel, row); - } else { - ChoiceBox choiceBox = new ChoiceBox<>(); - choiceBox.getItems().addAll(param.getChoices()); - choiceBox.getSelectionModel().select(param.getSelectedIndex()); - choiceBox.setMaxWidth(Double.MAX_VALUE); - choiceBox.getSelectionModel().selectedIndexProperty().addListener( - (obs, oldV, newV) -> param.setSelectedIndex(newV.intValue())); - panel.getChildren().addAll(nameLabel, choiceBox); - } - } - - for (ToolParameter param : tool.getParameters()) { - Label name = new Label(param.getName()); - name.setStyle("-fx-text-fill: #111111;"); - - if (tool instanceof de.blight.editor.tool.VoxelTool vtR - && param == vtR.brushRadius) { - // Ganzzahliger Slider: snapToTicks mit majorTickUnit=1 → nur ganze Zahlen - int initR = Math.max(1, (int) Math.round(param.getValue())); - Slider slider = new Slider(1, 30, initR); - slider.setShowTickMarks(true); - slider.setShowTickLabels(true); - slider.setMajorTickUnit(5); - slider.setMinorTickCount(4); - slider.setSnapToTicks(true); - slider.setBlockIncrement(1); - slider.valueProperty().addListener((obs, oldV, newV) -> - param.setValue(Math.round(newV.doubleValue()))); - panel.getChildren().add(new VBox(3, name, withField(slider, "%.0f"))); - - } else if (tool instanceof de.blight.editor.tool.VoxelTool vtP - && param == vtP.plateauTarget) { - // x.5-gestufter Slider: min=0.5, Schritt=1 → 0.5, 1.5, 2.5, … - double initSnap = Math.round(param.getValue() - 0.5) + 0.5; - initSnap = Math.max(0.5, Math.min(49.5, initSnap)); - Slider slider = new Slider(0.5, 49.5, initSnap); - slider.setShowTickMarks(true); - slider.setShowTickLabels(true); - slider.setMajorTickUnit(5); - slider.setMinorTickCount(4); - slider.setSnapToTicks(true); - slider.setBlockIncrement(1.0); - slider.valueProperty().addListener((obs, oldV, newV) -> { - double sv = Math.round(newV.doubleValue() - 0.5) + 0.5; - param.setValue(sv); - }); - panel.getChildren().add(new VBox(3, name, withField(slider, "%.1f"))); - - } else { - Slider slider = new Slider(param.getMin(), param.getMax(), param.getValue()); - slider.setShowTickMarks(true); - slider.setShowTickLabels(true); - slider.setMajorTickUnit((param.getMax() - param.getMin()) / 2); - slider.valueProperty().addListener((obs, oldV, newV) -> param.setValue(newV.doubleValue())); - panel.getChildren().add(new VBox(3, name, withField(slider, "%.3f"))); - } - } - - // Textur-Picker (nur beim GrassTool) - if (tool instanceof GrassTool) { - javafx.scene.Node grassUI = grassController.buildGrassTextureUI(); - VBox.setVgrow(grassUI, javafx.scene.layout.Priority.ALWAYS); - panel.getChildren().addAll(new Separator(), grassUI); - } - - // Voxel-Textur-Slots (nur beim VoxelTool) - if (tool instanceof de.blight.editor.tool.VoxelTool) { - panel.getChildren().addAll(new Separator(), buildVoxelTextureSlotsUI()); - } - - // Backen-Button (nur beim VoxelTool) - if (tool instanceof de.blight.editor.tool.VoxelTool) { - Label bakeStatus = new Label(""); - bakeStatus.setWrapText(true); - bakeStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); - - javafx.scene.control.ProgressBar bakeBar = new javafx.scene.control.ProgressBar(0); - bakeBar.setMaxWidth(Double.MAX_VALUE); - bakeBar.setVisible(false); - bakeBar.setManaged(false); - - Label bakeBarLabel = new Label(""); - bakeBarLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #444;"); - bakeBarLabel.setVisible(false); - bakeBarLabel.setManaged(false); - - // ── Bake-Parameter ──────────────────────────────────────────────── - Label smoothLbl = new Label(String.format("Glättung: %.1f", input.bakeSmoothStrength)); - smoothLbl.setStyle("-fx-font-size: 11;"); - javafx.scene.control.Slider smoothSlider = - new javafx.scene.control.Slider(0, 1, input.bakeSmoothStrength); - smoothSlider.setShowTickMarks(false); - smoothSlider.valueProperty().addListener((obs, o, n) -> { - input.bakeSmoothStrength = n.floatValue(); - smoothLbl.setText(String.format("Glättung: %.1f", n.floatValue())); - }); - - Label cliffLbl = new Label(String.format("Klippen-Noise: %.1f", input.bakeCliffNoiseStrength)); - cliffLbl.setStyle("-fx-font-size: 11;"); - javafx.scene.control.Slider cliffSlider = - new javafx.scene.control.Slider(0, 1, input.bakeCliffNoiseStrength); - cliffSlider.setShowTickMarks(false); - cliffSlider.valueProperty().addListener((obs, o, n) -> { - input.bakeCliffNoiseStrength = n.floatValue(); - cliffLbl.setText(String.format("Klippen-Noise: %.1f", n.floatValue())); - }); - - Button bakeBtn = new Button("Voxels backen (J3O)"); - bakeBtn.setMaxWidth(Double.MAX_VALUE); - bakeBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;"); - bakeBtn.setOnAction(e -> { - input.bakeVoxelsRequested = true; - bakeStatus.setText(""); - bakeBtn.setDisable(true); - bakeBar.setProgress(0); - bakeBar.setVisible(true); - bakeBar.setManaged(true); - bakeBarLabel.setVisible(true); - bakeBarLabel.setManaged(true); - bakeBarLabel.setText("Starte..."); - }); - - javafx.animation.Timeline bakePoller = new javafx.animation.Timeline( - new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { - int total = input.bakeTotal; - int done = input.bakeDone; - if (total > 0) { - bakeBar.setProgress((double) done / total); - bakeBarLabel.setText(done + " / " + total + " Chunks"); - } - String msg = input.bakeStatusMsg; - if (msg != null) { - input.bakeStatusMsg = null; - input.bakeTotal = 0; - input.bakeDone = 0; - bakeStatus.setText(msg); - bakeBar.setVisible(false); - bakeBar.setManaged(false); - bakeBarLabel.setVisible(false); - bakeBarLabel.setManaged(false); - bakeBtn.setDisable(false); - } - }) - ); - bakePoller.setCycleCount(javafx.animation.Animation.INDEFINITE); - bakePoller.play(); - - // ── Selektion: Löschen / Bake rückgängig (Modus "Baked selektieren") ── - Label selInfoLabel = new Label("Keine Auswahl"); - selInfoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); - Label selHintLabel = new Label("LMK: Struktur wählen | RMK: Auswahl leeren"); - selHintLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); - selHintLabel.setWrapText(true); - - Button deleteBtn = new Button("Löschen"); - deleteBtn.setMaxWidth(Double.MAX_VALUE); - deleteBtn.setStyle("-fx-background-color: #ba4a4a; -fx-text-fill: white;"); - deleteBtn.setDisable(true); - deleteBtn.setOnAction(e -> { - int n = input.markedDeleteBakedKeys.size(); - javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( - javafx.scene.control.Alert.AlertType.CONFIRMATION, - n + " Chunk" + (n != 1 ? "s" : "") + " unwiderruflich löschen?", - javafx.scene.control.ButtonType.YES, - javafx.scene.control.ButtonType.NO); - confirm.setTitle("Gebackene Chunks löschen"); - confirm.setHeaderText(null); - confirm.initOwner(primaryStage); - confirm.showAndWait().ifPresent(btn -> { - if (btn == javafx.scene.control.ButtonType.YES) { - input.deleteMarkedBakedRequested = true; - } - }); - }); - - Button revertBtn = new Button("Bake rückgängig"); - revertBtn.setMaxWidth(Double.MAX_VALUE); - revertBtn.setStyle("-fx-background-color: #7a5a2a; -fx-text-fill: white;"); - revertBtn.setDisable(true); - revertBtn.setOnAction(e -> input.revertMarkedBakedRequested = true); - - javafx.animation.Timeline selPoller = new javafx.animation.Timeline( - new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { - int n = input.markedDeleteBakedKeys.size(); - boolean has = n > 0; - selInfoLabel.setText(has - ? n + " Chunk" + (n != 1 ? "s" : "") + " ausgewählt" - : "Keine Auswahl"); - deleteBtn.setDisable(!has); - revertBtn.setDisable(!has); - }) - ); - selPoller.setCycleCount(javafx.animation.Animation.INDEFINITE); - selPoller.play(); - - // ── Unterirdische Voxel bereinigen ────────────────────────────────── - Label cleanupStatus = new Label(""); - cleanupStatus.setWrapText(true); - cleanupStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); - - Button cleanupBtn = new Button("Unterirdische Voxel bereinigen"); - cleanupBtn.setMaxWidth(Double.MAX_VALUE); - cleanupBtn.setOnAction(e -> { - input.cleanupUndergroundStatus = null; - input.cleanupUndergroundRequested = true; - cleanupBtn.setDisable(true); - cleanupStatus.setText("Läuft..."); - }); - - javafx.animation.Timeline cleanupPoller = new javafx.animation.Timeline( - new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { - String msg = input.cleanupUndergroundStatus; - if (msg != null) { - input.cleanupUndergroundStatus = null; - cleanupStatus.setText(msg); - cleanupBtn.setDisable(false); - } - }) - ); - cleanupPoller.setCycleCount(javafx.animation.Animation.INDEFINITE); - cleanupPoller.play(); - - panel.getChildren().addAll(new Separator(), - smoothLbl, smoothSlider, cliffLbl, cliffSlider, - bakeBtn, bakeBar, bakeBarLabel, bakeStatus, - new Separator(), cleanupBtn, cleanupStatus, - new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn); - } - } - - // ── Gras-Textur-Picker (Multi-Slot) ────────────────────────────────────── - - // ── Textur-Slot-Konfigurator ────────────────────────────────────────────── - - /** Baut das 8-Slot-Thumbnail-Grid für das TextureTool. */ - private javafx.scene.Node buildTextureChoiceUI(ChoiceToolParameter param) { - String[] defaultsTerrain = { - "Textures/Terrain/splat/grass.jpg", - "Textures/Terrain/Rock2/rock.jpg", - "Textures/Terrain/splat/dirt.jpg", - "" - }; - - String[] allPaths = new String[12]; - String[] allLabels = new String[12]; - for (int i = 0; i < 4; i++) { - String p = input.terrainTexturePaths[i]; - allPaths[i] = (p != null && !p.isEmpty()) ? p : defaultsTerrain[i]; - allLabels[i] = "S" + (i + 1) + " " + labelFromPath(allPaths[i]); - } - for (int i = 0; i < 4; i++) { - String p = input.upperTexturePaths[i]; - allPaths[4 + i] = (p != null && !p.isEmpty()) ? p : ""; - allLabels[4 + i] = "S" + (i + 5) + " " + labelFromPath(allPaths[4 + i]); - } - for (int i = 0; i < 4; i++) { - String p = input.thirdTexturePaths[i]; - allPaths[8 + i] = (p != null && !p.isEmpty()) ? p : ""; - allLabels[8 + i] = "S" + (i + 9) + " " + labelFromPath(allPaths[8 + i]); - } - - ToggleGroup tg = new ToggleGroup(); - javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); - tile.setHgap(4); - tile.setVgap(4); - tile.setPrefColumns(4); - - for (int j = 0; j < 12; j++) { - final int idx = j; - String path = allPaths[j]; - - ToggleButton btn = new ToggleButton(); - btn.setToggleGroup(tg); - btn.setPrefSize(56, 76); - btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); - btn.setTooltip(new Tooltip("Slot " + (j + 1) + ": " + (path.isEmpty() ? "(leer)" : path))); - - VBox content = new VBox(2); - content.setAlignment(Pos.CENTER); - - if (!path.isEmpty()) { - String imgUrl = resolveImageUrl(path); - if (imgUrl != null) { - Image img = new Image(imgUrl, 44, 44, true, true); - ImageView iv = new ImageView(img.isError() ? null : img); - iv.setFitWidth(44); - iv.setFitHeight(44); - content.getChildren().add(img.isError() ? emptySlotPlaceholder(44) : iv); - } else { - content.getChildren().add(emptySlotPlaceholder(44)); - } - } else { - content.getChildren().add(emptySlotPlaceholder(44)); - } - - Label lbl = new Label(allLabels[j]); - lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;"); - lbl.setMaxWidth(54); - content.getChildren().add(lbl); - - btn.setGraphic(content); - boolean initially = (j == param.getSelectedIndex()); - btn.setSelected(initially); - applyModeButtonStyle(btn, initially); - btn.selectedProperty().addListener((obs, was, isNow) -> { - applyModeButtonStyle(btn, isNow); - if (isNow) param.setSelectedIndex(idx); - }); - tile.getChildren().add(btn); - } - - tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { - if (newT == null) tg.selectToggle(oldT); - }); - return tile; - } - - private javafx.scene.Node buildVoxelModeUI(ChoiceToolParameter param) { - String[] labels = param.getChoices(); - String[] imgPaths = param.getImagePaths(); - ToggleGroup tg = new ToggleGroup(); - javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); - tile.setHgap(4); - tile.setVgap(4); - tile.setPrefColumns(6); - - for (int j = 0; j < labels.length; j++) { - final int idx = j; - String path = (imgPaths != null && j < imgPaths.length) ? imgPaths[j] : null; - - ToggleButton btn = new ToggleButton(); - btn.setToggleGroup(tg); - btn.setPrefSize(44, 56); - btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); - btn.setTooltip(new Tooltip(labels[j])); - - VBox content = new VBox(2); - content.setAlignment(Pos.CENTER); - - boolean hasImage = false; - if (path != null) { - URL resUrl = EditorApp.class.getResource("/" + path); - String imgUrl = resUrl != null ? resUrl.toString() : null; - if (imgUrl == null) { - File f = new File(path).getAbsoluteFile(); - if (f.exists()) imgUrl = f.toURI().toString(); - } - if (imgUrl != null) { - Image img = new Image(imgUrl, 32, 32, true, true); - if (!img.isError()) { - ImageView iv = new ImageView(img); - iv.setFitWidth(32); iv.setFitHeight(32); - content.getChildren().add(iv); - hasImage = true; - } - } - } - if (!hasImage) { - boolean isRemove = labels[j].equals("Entfernen"); - Label icon = new Label(isRemove ? "✕" : "+"); - icon.setStyle("-fx-font-size: 16; -fx-text-fill: " + (isRemove ? "#cc2222" : "#228822") + ";"); - content.getChildren().add(icon); - } - Label lbl = new Label(labels[j]); - lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;"); - content.getChildren().add(lbl); - btn.setGraphic(content); - - boolean initially = (j == param.getSelectedIndex()); - btn.setSelected(initially); - applyModeButtonStyle(btn, initially); - btn.selectedProperty().addListener((obs, was, isNow) -> { - applyModeButtonStyle(btn, isNow); - if (isNow) param.setSelectedIndex(idx); - }); - tile.getChildren().add(btn); - } - tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { - if (newT == null) tg.selectToggle(oldT); - }); - return tile; - } - - private VBox buildVoxelTextureSlotsUI() { - VBox box = new VBox(6); - box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); - Label title = new Label("Voxel-Texturen (aus Textur-Brush)"); - title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); - box.getChildren().add(title); - - String[] roleLabels = { "Flache Flächen", "Steile Wände", "Decke / Unterseite" }; - int[] current = { input.voxelFlatSlot, input.voxelSteepSlot, input.voxelCeilSlot }; - - // Alle 12 Textur-Slots aus dem Brush zusammenbauen (Slot 0-3 unten, 4-7 oben, 8-11 dritte) - String[] slotNames = new String[13]; // Index 0 = "(keiner)", 1..12 = Slot 0..11 - slotNames[0] = "(keiner)"; - for (int s = 0; s < 4; s++) { - String p = input.terrainTexturePaths[s]; - slotNames[s + 1] = "Slot " + s + (p != null && !p.isEmpty() - ? ": " + java.nio.file.Paths.get(p).getFileName() : " (leer)"); - } - for (int s = 0; s < 4; s++) { - String p = input.upperTexturePaths[s]; - slotNames[s + 5] = "Slot " + (s + 4) + (p != null && !p.isEmpty() - ? ": " + java.nio.file.Paths.get(p).getFileName() : " (leer)"); - } - for (int s = 0; s < 4; s++) { - String p = input.thirdTexturePaths[s]; - slotNames[s + 9] = "Slot " + (s + 8) + (p != null && !p.isEmpty() - ? ": " + java.nio.file.Paths.get(p).getFileName() : " (leer)"); - } - - for (int i = 0; i < 3; i++) { - final int roleIdx = i; - Label roleLbl = new Label(roleLabels[i]); - roleLbl.setStyle("-fx-text-fill: #444; -fx-font-size: 11;"); - - javafx.scene.control.ComboBox combo = new javafx.scene.control.ComboBox<>(); - combo.getItems().addAll(slotNames); - // current[i] ist -1..7 → ComboBox-Index 0..(current+1) - combo.getSelectionModel().select(Math.max(0, current[i] + 1)); - combo.setMaxWidth(Double.MAX_VALUE); - combo.setOnAction(ev -> { - int chosen = combo.getSelectionModel().getSelectedIndex() - 1; // -1 = keiner - switch (roleIdx) { - case 0 -> input.voxelFlatSlot = chosen; - case 1 -> input.voxelSteepSlot = chosen; - case 2 -> input.voxelCeilSlot = chosen; - } - input.voxelTexturesChanged = true; - }); - - VBox roleBox = new VBox(2, roleLbl, combo); - box.getChildren().add(roleBox); - } - return box; - } - - private javafx.scene.Node buildVoxelSplatSectionUI() { - VBox box = new VBox(4); - Label title = new Label("Voxel-Splatmap (Slots 2–4)"); - title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111; -fx-font-size: 11;"); - Label hint = new Label("Slot 1 (Basis) = Voxel-Flat-Slot oben"); - hint.setStyle("-fx-text-fill: #888; -fx-font-size: 10;"); - box.getChildren().addAll(title, hint); - - String[] slotLabels = {"Slot 2 (G)", "Slot 3 (B)", "Slot 4 (A)"}; - for (int i = 1; i <= 3; i++) { - final int si = i; - String[] paths = input.voxelSplatTexturePaths; - String cur = (paths != null && i < paths.length && paths[i] != null) ? paths[i] : ""; - Label nameLbl = new Label(slotLabels[i - 1] + ": " + labelFromPath(cur)); - nameLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #444;"); - nameLbl.setMaxWidth(180); - Button chooseBtn = new Button("..."); - chooseBtn.setOnAction(e -> { - de.blight.editor.ui.TextureChooser chooser = - new de.blight.editor.ui.TextureChooser(ASSET_ROOT, false); - chooser.showAndWait().ifPresent(chosenPath -> { - String[] copy = input.voxelSplatTexturePaths.clone(); - copy[si] = chosenPath; - input.voxelSplatTexturePaths = copy; - input.voxelSplatTexturesChanged = true; - nameLbl.setText(slotLabels[si - 1] + ": " + labelFromPath(chosenPath)); - }); - }); - box.getChildren().add(new HBox(4, nameLbl, chooseBtn)); - } - return box; - } - - private javafx.scene.Node buildVoxelTextureChoiceUI(ChoiceToolParameter param) { - ToggleGroup tg = new ToggleGroup(); - javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); - tile.setHgap(4); - tile.setVgap(4); - tile.setPrefColumns(4); - - for (int j = 0; j < 4; j++) { - final int idx = j; - String path = (j < input.terrainTexturePaths.length - && input.terrainTexturePaths[j] != null - && !input.terrainTexturePaths[j].isEmpty()) - ? input.terrainTexturePaths[j] : ""; - - ToggleButton btn = new ToggleButton(); - btn.setToggleGroup(tg); - btn.setPrefSize(56, 76); - btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); - btn.setTooltip(new Tooltip("Slot " + (j + 1) + ": " + (path.isEmpty() ? "(leer)" : path))); - - VBox content = new VBox(2); - content.setAlignment(Pos.CENTER); - - if (!path.isEmpty()) { - String imgUrl = resolveImageUrl(path); - if (imgUrl != null) { - Image img = new Image(imgUrl, 44, 44, true, true); - ImageView iv = new ImageView(img.isError() ? null : img); - iv.setFitWidth(44); - iv.setFitHeight(44); - content.getChildren().add(img.isError() ? emptySlotPlaceholder(44) : iv); - } else { - content.getChildren().add(emptySlotPlaceholder(44)); - } - } else { - content.getChildren().add(emptySlotPlaceholder(44)); - } - - Label lbl = new Label("S" + (j + 1) + " " + labelFromPath(path)); - lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;"); - lbl.setMaxWidth(54); - content.getChildren().add(lbl); - - btn.setGraphic(content); - boolean initially = (j == param.getSelectedIndex()); - btn.setSelected(initially); - applyModeButtonStyle(btn, initially); - btn.selectedProperty().addListener((obs, was, isNow) -> { - applyModeButtonStyle(btn, isNow); - if (isNow) param.setSelectedIndex(idx); - }); - tile.getChildren().add(btn); - } - - tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { - if (newT == null) tg.selectToggle(oldT); - }); - return tile; - } - - private javafx.scene.layout.Region emptySlotPlaceholder(double size) { - javafx.scene.layout.Region r = new javafx.scene.layout.Region(); - r.setPrefSize(size, size); - r.setMaxSize(size, size); - r.setStyle("-fx-background-color: #cccccc; -fx-border-color: #999; -fx-border-width: 1;"); - return r; - } - - private String resolveImageUrl(String path) { - if (path == null || path.isEmpty()) return null; - URL res = EditorApp.class.getResource("/" + path); - if (res != null) return res.toString(); - File f = ASSET_ROOT.resolve(path).toFile(); - if (f.exists()) return f.toURI().toString(); - return null; - } - - private static String labelFromPath(String path) { - if (path == null || path.isEmpty()) return "(leer)"; - Path p = ASSET_ROOT.resolve(path.replace('/', java.io.File.separatorChar)); - // Wenn die Datei in einem Textur-Set-Ordner liegt, den Ordnernamen verwenden - Path parent = p.getParent(); - if (parent != null && Files.exists(parent.resolve("textureset.json"))) { - return parent.getFileName().toString(); - } - String name = p.getFileName().toString(); - int dot = name.lastIndexOf('.'); - return dot > 0 ? name.substring(0, dot) : name; - } - - /** Baut die 4-Slot-Auswahl für Terrain (isUpper=false) oder Gebirge (isUpper=true). */ - private VBox buildTextureSlotsUI(boolean isUpper) { - VBox box = new VBox(5); - box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); - Label title = new Label(isUpper ? "Texturen Slots 5-8" : "Texturen Slots 1-4"); - title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); - box.getChildren().add(title); - - String[] paths = isUpper ? input.upperTexturePaths : input.terrainTexturePaths; - String[] defaults = isUpper - ? new String[]{"Textures/Terrain/Rock2/rock.jpg","","",""} - : new String[]{"Textures/Terrain/splat/grass.jpg","Textures/Terrain/Rock2/rock.jpg","Textures/Terrain/splat/dirt.jpg",""}; - - final int group = isUpper ? 1 : 0; - - for (int i = 0; i < 4; i++) { - final int slot = i; - final int globalIdx = (isUpper ? 4 : 0) + i; - - // ── Diffuse-Zeile ───────────────────────────────────────────────── - String current = (paths[i] != null && !paths[i].isEmpty()) - ? labelFromPath(paths[i]) - : (defaults[i].isEmpty() ? "(kein)" : labelFromPath(defaults[i]) + " (std)"); - - Label slotLbl = new Label("Slot " + (isUpper ? i + 5 : i + 1) + ": " + current); - slotLbl.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); - slotLbl.setMaxWidth(Double.MAX_VALUE); - slotLbl.setMinWidth(0); - slotLbl.setEllipsisString("…"); - - javafx.scene.control.Button chooseBtn = new javafx.scene.control.Button("Wählen"); - chooseBtn.setOnAction(ev -> openTexturePicker(slotLbl, slot, isUpper)); - - javafx.scene.control.Button clearBtn = new javafx.scene.control.Button("✕"); - clearBtn.setTooltip(new Tooltip("Auf Standard zurücksetzen")); - clearBtn.setOnAction(ev -> { - String[] p = isUpper ? input.upperTexturePaths : input.terrainTexturePaths; - p[slot] = ""; - if (isUpper) input.upperTexturesChanged = true; - else input.terrainTexturesChanged = true; - showToolParameters(toolPanel, input.activeTool); - }); - - HBox row = new HBox(4, slotLbl, chooseBtn, clearBtn); - HBox.setHgrow(slotLbl, Priority.ALWAYS); - row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - row.setOnContextMenuRequested(ev -> { - String tex = (isUpper ? input.upperTexturePaths : input.terrainTexturePaths)[slot]; - if (tex != null && !tex.isEmpty()) - showTextureSlotDetails(tex, slot, group, row, ev.getScreenX(), ev.getScreenY()); - ev.consume(); - }); - - box.getChildren().add(row); - } - return box; - } - - /** Öffnet den TextureChooser-Dialog und setzt den gewählten Pfad für den Slot. */ - private void openTexturePicker(Label slotLabel, int slot, boolean isUpper) { - TextureChooser chooser = new TextureChooser(ASSET_ROOT, true); - if (primaryStage != null) chooser.initOwner(primaryStage); - chooser.showAndWait().ifPresent(path -> { - String[] paths = isUpper ? input.upperTexturePaths : input.terrainTexturePaths; - paths[slot] = path; - if (isUpper) input.upperTexturesChanged = true; - else input.terrainTexturesChanged = true; - applyMetaToSlot(path, slot, isUpper ? 1 : 0); - showToolParameters(toolPanel, input.activeTool); - }); - } - - /** - * Liest die textureset.json für texRelPath und trägt Normal- und Displacement-Map - * automatisch in den entsprechenden Slot ein. - * group: 0 = terrain, 1 = upper, 2 = third - */ - private void applyMetaToSlot(String texRelPath, int slot, int group) { - if (texRelPath == null || texRelPath.isEmpty()) return; - Path absPath = ASSET_ROOT.resolve(texRelPath.replace('/', java.io.File.separatorChar)); - TextureMapData meta = loadTexMeta(absPath); - boolean changed = false; - - if (!meta.normalMap.isEmpty()) { - switch (group) { - case 0 -> { input.terrainNormalMapPaths[slot] = meta.normalMap; input.terrainNormalMapsChanged = true; } - case 1 -> { input.upperNormalMapPaths[slot] = meta.normalMap; input.upperNormalMapsChanged = true; } - case 2 -> { input.thirdNormalMapPaths[slot] = meta.normalMap; input.thirdNormalMapsChanged = true; } - } - changed = true; - } - if (!meta.displacementMap.isEmpty()) { - switch (group) { - case 0 -> input.terrainDisplacementMapPaths[slot] = meta.displacementMap; - case 1 -> input.upperDisplacementMapPaths[slot] = meta.displacementMap; - case 2 -> input.thirdDisplacementMapPaths[slot] = meta.displacementMap; - } - changed = true; - } - if (changed) input.voxelTexturesChanged = true; - } - - /** Baut die 4-Slot-Auswahl für die dritte Gruppe (Slots 9-12). */ - private VBox buildThirdTextureSlotsUI() { - VBox box = new VBox(5); - box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); - Label title = new Label("Texturen Slots 9-12"); - title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); - box.getChildren().add(title); - - String[] paths = input.thirdTexturePaths; - - for (int i = 0; i < 4; i++) { - final int slot = i; - final int globalIdx = 8 + i; - - String current = (paths[i] != null && !paths[i].isEmpty()) - ? labelFromPath(paths[i]) - : "(kein)"; - - Label slotLbl = new Label("Slot " + (i + 9) + ": " + current); - slotLbl.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); - slotLbl.setMaxWidth(Double.MAX_VALUE); - slotLbl.setMinWidth(0); - slotLbl.setEllipsisString("…"); - - javafx.scene.control.Button chooseBtn = new javafx.scene.control.Button("Wählen"); - chooseBtn.setOnAction(ev -> { - TextureChooser chooser = new TextureChooser(ASSET_ROOT, true); - if (primaryStage != null) chooser.initOwner(primaryStage); - chooser.showAndWait().ifPresent(p -> { - input.thirdTexturePaths[slot] = p; - input.thirdTexturesChanged = true; - applyMetaToSlot(p, slot, 2); - showToolParameters(toolPanel, input.activeTool); - }); - }); - - javafx.scene.control.Button clearBtn = new javafx.scene.control.Button("✕"); - clearBtn.setTooltip(new Tooltip("Auf Standard zurücksetzen")); - clearBtn.setOnAction(ev -> { - input.thirdTexturePaths[slot] = ""; - input.thirdTexturesChanged = true; - showToolParameters(toolPanel, input.activeTool); - }); - - HBox row = new HBox(4, slotLbl, chooseBtn, clearBtn); - HBox.setHgrow(slotLbl, Priority.ALWAYS); - row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - row.setOnContextMenuRequested(ev -> { - String tex = input.thirdTexturePaths[slot]; - if (tex != null && !tex.isEmpty()) - showTextureSlotDetails(tex, slot, 2, row, ev.getScreenX(), ev.getScreenY()); - ev.consume(); - }); - box.getChildren().add(row); - } - return box; - } - - private String texPathForSlot(int globalIdx) { - if (globalIdx < 4) return input.terrainTexturePaths[globalIdx]; - else if (globalIdx < 8) return input.upperTexturePaths[globalIdx - 4]; - else return input.thirdTexturePaths[globalIdx - 8]; - } - - private void setTexPathForSlot(int globalIdx, String path) { - if (globalIdx < 4) { - input.terrainTexturePaths[globalIdx] = path; - input.terrainTexturesChanged = true; - } else if (globalIdx < 8) { - input.upperTexturePaths[globalIdx - 4] = path; - input.upperTexturesChanged = true; - } else { - input.thirdTexturePaths[globalIdx - 8] = path; - input.thirdTexturesChanged = true; - } - } - - /** 4×3-Textur-Grid + darunter UV-Scale und Textur-Button für den aktiven Slot. */ - private javafx.scene.Node buildTextureChoiceWithActiveSlotUI(ChoiceToolParameter param) { - VBox outer = new VBox(6); - - // Aktiver-Slot-Bereich (unten) — wird bei Slot-Wechsel neu befüllt - VBox activeBox = new VBox(4); - activeBox.setPadding(new Insets(4, 0, 0, 0)); - - Runnable[] refresh = { null }; - refresh[0] = () -> { - activeBox.getChildren().clear(); - int sel = param.getSelectedIndex(); - String path = texPathForSlot(sel); - boolean hasTexture = path != null && !path.isEmpty(); - int group = sel < 4 ? 0 : sel < 8 ? 1 : 2; - int slotInGrp = sel % 4; - - Label scaleLbl = new Label("UV-Scale Slot " + (sel + 1) + " (m):"); - scaleLbl.setStyle("-fx-font-size: 11; -fx-text-fill: #333;"); - float curScale = (input.diffuseScales != null && sel < input.diffuseScales.length) - ? input.diffuseScales[sel] : 8f; - javafx.scene.control.Spinner scaleSpin = - new javafx.scene.control.Spinner<>(0.5, 1024.0, curScale, 0.5); - scaleSpin.setEditable(true); - scaleSpin.setMaxWidth(Double.MAX_VALUE); - scaleSpin.setDisable(!hasTexture); - scaleSpin.valueProperty().addListener((ob, ov, nv) -> { - if (nv == null) return; - input.diffuseScales[sel] = nv.floatValue(); - input.diffuseScalesChanged = true; - }); - - Button chooseBtn = new Button("Textur ändern..."); - chooseBtn.setMaxWidth(Double.MAX_VALUE); - chooseBtn.setDisable(!hasTexture); - chooseBtn.setOnAction(ev -> { - TextureChooser chooser = new TextureChooser(ASSET_ROOT, true); - if (primaryStage != null) chooser.initOwner(primaryStage); - chooser.showAndWait().ifPresent(p -> { - setTexPathForSlot(sel, p); - applyMetaToSlot(p, slotInGrp, group); - param.setSelectedIndex(sel); - showToolParameters(toolPanel, input.activeTool); - }); - }); - - activeBox.getChildren().addAll(new Separator(), scaleLbl, scaleSpin, chooseBtn); - }; - - // 4×3 Grid (wie buildTextureChoiceUI, aber mit Refresh-Hook) - String[] defaultsTerrain = { - "Textures/Terrain/splat/grass.jpg", "Textures/Terrain/Rock2/rock.jpg", - "Textures/Terrain/splat/dirt.jpg", "" - }; - String[] allPaths = new String[12]; - String[] allLabels = new String[12]; - for (int i = 0; i < 4; i++) { - String p = input.terrainTexturePaths[i]; - allPaths[i] = (p != null && !p.isEmpty()) ? p : defaultsTerrain[i]; - allLabels[i] = "S" + (i + 1) + " " + labelFromPath(allPaths[i]); - } - for (int i = 0; i < 4; i++) { - String p = input.upperTexturePaths[i]; - allPaths[4 + i] = (p != null && !p.isEmpty()) ? p : ""; - allLabels[4 + i] = "S" + (i + 5) + " " + labelFromPath(allPaths[4 + i]); - } - for (int i = 0; i < 4; i++) { - String p = input.thirdTexturePaths[i]; - allPaths[8 + i] = (p != null && !p.isEmpty()) ? p : ""; - allLabels[8 + i] = "S" + (i + 9) + " " + labelFromPath(allPaths[8 + i]); - } - - ToggleGroup tg = new ToggleGroup(); - javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); - tile.setHgap(4); tile.setVgap(4); tile.setPrefColumns(4); - - for (int j = 0; j < 12; j++) { - final int idx = j; - String path = allPaths[j]; - ToggleButton btn = new ToggleButton(); - btn.setToggleGroup(tg); - btn.setPrefSize(56, 76); - btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); - btn.setTooltip(new Tooltip("Slot " + (j + 1) + ": " + (path.isEmpty() ? "(leer)" : path))); - VBox content = new VBox(2); - content.setAlignment(Pos.CENTER); - if (!path.isEmpty()) { - String imgUrl = resolveImageUrl(path); - if (imgUrl != null) { - Image img = new Image(imgUrl, 44, 44, true, true); - ImageView iv = new ImageView(img.isError() ? null : img); - iv.setFitWidth(44); iv.setFitHeight(44); - content.getChildren().add(img.isError() ? emptySlotPlaceholder(44) : iv); - } else { - content.getChildren().add(emptySlotPlaceholder(44)); - } - } else { - content.getChildren().add(emptySlotPlaceholder(44)); - } - Label lbl = new Label(allLabels[j]); - lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;"); - lbl.setMaxWidth(54); - content.getChildren().add(lbl); - btn.setGraphic(content); - boolean initially = (j == param.getSelectedIndex()); - btn.setSelected(initially); - applyModeButtonStyle(btn, initially); - btn.selectedProperty().addListener((obs, was, isNow) -> { - applyModeButtonStyle(btn, isNow); - if (isNow) { param.setSelectedIndex(idx); refresh[0].run(); } - }); - tile.getChildren().add(btn); - } - tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { - if (newT == null) tg.selectToggle(oldT); - }); - - refresh[0].run(); - outer.getChildren().addAll(tile, activeBox); - return outer; - } - - /** - * Normal-Map-Prioritätsliste: zeigt alle 12 Slots, lässt den Nutzer die Reihenfolge - * per ↑/↓ ändern. Slots ohne Normal-Map sind ausgegraut; ein Budget-Hinweis zeigt, - * wie viele Normal Maps tatsächlich an die GPU gebunden werden. - */ - private VBox buildNormalMapPriorityUI() { - VBox box = new VBox(5); - box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); - - Label title = new Label("Normal Map Priorität"); - title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); - - // Budget-Hinweis (dynamisch berechnet) - Label budgetLbl = new Label(); - budgetLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #555;"); - budgetLbl.setWrapText(true); - - // Alle 12 Normalpfade (flat, Index 0-11 = Slot 1-12) - java.util.function.Supplier normPaths = () -> { - String[] p = new String[12]; - System.arraycopy(input.terrainNormalMapPaths, 0, p, 0, 4); - System.arraycopy(input.upperNormalMapPaths, 0, p, 4, 4); - System.arraycopy(input.thirdNormalMapPaths, 0, p, 8, 4); - return p; - }; - - // Slot-Labels (Slot 1-12) - java.util.function.Function slotLabel = idx -> "S" + (idx + 1); - - // Observable-Liste der globalen Slot-Indizes in Prioritätsreihenfolge - javafx.collections.ObservableList items = - javafx.collections.FXCollections.observableArrayList(); - for (int v : input.normalMapOrder) items.add(v); - - // Budget neu berechnen und Label aktualisieren - Runnable refreshBudget = () -> { - String[] np = normPaths.get(); - long withNorm = Arrays.stream(np).filter(s -> s != null && !s.isEmpty()).count(); - boolean hasUpper = Arrays.stream(input.upperTexturePaths).anyMatch(s -> s != null && !s.isEmpty()); - boolean hasThird = Arrays.stream(input.thirdTexturePaths).anyMatch(s -> s != null && !s.isEmpty()); - int alphaCost = 1 + (hasUpper ? 1 : 0) + (hasThird ? 1 : 0); - long diffCount = Arrays.stream(input.terrainTexturePaths).filter(s -> s != null && !s.isEmpty()).count() - + Arrays.stream(input.upperTexturePaths).filter(s -> s != null && !s.isEmpty()).count() - + Arrays.stream(input.thirdTexturePaths).filter(s -> s != null && !s.isEmpty()).count(); - int normBudget = Math.max(0, 16 - alphaCost - (int) diffCount); - budgetLbl.setText("Budget: " + normBudget + " Normal Map" + (normBudget != 1 ? "s" : "") - + " (von " + withNorm + " konfiguriert). Oben = höchste Prio."); - }; - refreshBudget.run(); - - // ListView - javafx.scene.control.ListView list = new javafx.scene.control.ListView<>(items); - list.setPrefHeight(200); - list.setMaxHeight(240); - list.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { - @Override - protected void updateItem(Integer idx, boolean empty) { - super.updateItem(idx, empty); - if (empty || idx == null) { setText(null); setStyle(""); return; } - String[] np = normPaths.get(); - String nm = (np[idx] != null && !np[idx].isEmpty()) ? labelFromPath(np[idx]) : "–"; - setText(slotLabel.apply(idx) + " → " + nm); - boolean hasNorm = np[idx] != null && !np[idx].isEmpty(); - setStyle(hasNorm ? "-fx-text-fill: #111;" : "-fx-text-fill: #aaa;"); - } - }); - - // Schreibe Reihenfolge zurück nach SharedInput - Runnable commit = () -> { - int[] order = new int[12]; - for (int i = 0; i < 12; i++) order[i] = items.get(i); - input.normalMapOrder = order; - input.normalMapOrderChanged = true; - refreshBudget.run(); - list.refresh(); - }; - - // Auf Standardreihenfolge zurücksetzen - javafx.scene.control.Button resetBtn = new javafx.scene.control.Button("Reset"); - resetBtn.setTooltip(new Tooltip("Reihenfolge auf 1–12 zurücksetzen")); - resetBtn.setOnAction(e -> { - items.setAll(0,1,2,3,4,5,6,7,8,9,10,11); - commit.run(); - }); - - // ↑ / ↓ Buttons - javafx.scene.control.Button upBtn = new javafx.scene.control.Button("↑"); - javafx.scene.control.Button downBtn = new javafx.scene.control.Button("↓"); - - upBtn.setOnAction(e -> { - int sel = list.getSelectionModel().getSelectedIndex(); - if (sel <= 0) return; - Integer tmp = items.get(sel - 1); - items.set(sel - 1, items.get(sel)); - items.set(sel, tmp); - list.getSelectionModel().select(sel - 1); - commit.run(); - }); - downBtn.setOnAction(e -> { - int sel = list.getSelectionModel().getSelectedIndex(); - if (sel < 0 || sel >= items.size() - 1) return; - Integer tmp = items.get(sel + 1); - items.set(sel + 1, items.get(sel)); - items.set(sel, tmp); - list.getSelectionModel().select(sel + 1); - commit.run(); - }); - - HBox btnRow = new HBox(4, upBtn, downBtn, resetBtn); - btnRow.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - - box.getChildren().addAll(title, budgetLbl, list, btnRow); - return box; - } - - // ── Linke Seite: Asset-Panel (Welteneditor) ────────────────────────────── - - private VBox buildAssetPanel() { - // ── Tab 1: Asset-Baum ───────────────────────────────────────────────── - VBox assetContent = new VBox(6); - assetContent.setPadding(new Insets(8)); - assetContent.setStyle("-fx-background-color: #f0f0f0;"); - - Label title = new Label("Assets"); - title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - - HBox titleBar = new HBox(4, title); - titleBar.setAlignment(Pos.CENTER_LEFT); - - assetTreeRoot = new TreeItem<>("Projekt"); - assetTreeRoot.setExpanded(true); - populateAssetTree(assetTreeRoot); - - TreeView tree = new TreeView<>(assetTreeRoot); - assetTree = tree; - tree.setShowRoot(false); - VBox.setVgrow(tree, Priority.ALWAYS); - tree.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); - tree.setCellFactory(tv -> buildAssetCell()); - - // F2 → Umbenennen - tree.setOnKeyPressed(e -> { - if (e.getCode() != KeyCode.F2) return; - TreeItem sel = tree.getSelectionModel().getSelectedItem(); - if (sel == null || sel == modelsNode || sel == texturesNode - || sel == audioNode || sel == animationsNode || sel == itemsNode - || sel == charactersNode) return; - renameAsset(sel); - e.consume(); - }); - - // Doppelklick auf Modell / Textur / Animation - tree.setOnMouseClicked(e -> { - if (e.getClickCount() != 2) return; - TreeItem sel = tree.getSelectionModel().getSelectedItem(); - if (sel == null) return; - - // Ordner-Doppelklick im Models-Baum → Zufalls-Ordner-Platzierung aktivieren - if (isAssetFolder(sel)) { - TreeItem folderCat = getCategoryRoot(sel); - if (folderCat == modelsNode) { - Path folderPath = itemPaths.get(sel); - if (folderPath != null && java.nio.file.Files.isDirectory(folderPath)) { - String relFolder = ASSET_ROOT.relativize(folderPath).toString().replace('\\', '/'); - input.treeFolderPath = relFolder; - input.pendingModelPath = null; - objectController.activatePlaceMode(); - objectController.enableMultiPlace(); - setStatus("Ordner: " + relFolder + " | Linksklick → zufälliges Modell | Rechtsklick → neu würfeln"); - } - } - return; - } - - TreeItem cat = getCategoryRoot(sel); - Path p = itemPaths.get(sel); - String relPath = (p != null) - ? ASSET_ROOT.relativize(p).toString().replace('\\', '/') - : jmePaths.get(sel); - if (relPath == null) return; - - boolean isSkeletal = skeletalModelPaths == null /* noch nicht gescannt – zulassen */ - || skeletalModelPaths.contains(relPath); - - // Im Animations Viewer: j3o direkt laden - if ("animpreview".equals(currentTool) - && (cat == modelsNode || cat == animationsNode || cat == charactersNode) - && relPath.endsWith(".j3o")) { - if (!isSkeletal) { setStatus("Kein Skelett: " + relPath); return; } - input.animPreviewLoadPath = relPath; - animPreviewController.setStatusText("Lade…"); - animPreviewController.clearClipList(); - return; - } - - if (cat == itemsNode && relPath.endsWith(".item")) { - activateItemPlacement(p); - return; - } - - if (cat == modelsNode) { - if ("modeleditor".equals(registry.activeToolId()) && relPath.endsWith(".j3o")) { - modelEditorController.openModelEditor(relPath, p); - return; - } - input.pendingModelPath = relPath; - objectController.activatePlaceMode(); - setStatus("Modell: " + relPath + " | Linksklick ins Terrain zum Platzieren"); - } else if (cat == texturesNode) { - input.pendingTexturePath = relPath; - objectController.activatePlaceMode(); - setStatus("Textur: " + relPath + " | Wird auf platzierte Primitive angewendet"); - } else if ((cat == animationsNode || cat == charactersNode) && relPath.endsWith(".j3o") && isSkeletal) { - switchToAnimPreview(); - input.animPreviewLoadPath = relPath; - animPreviewController.setStatusText("Lade…"); - } else if (cat == audioNode && relPath.endsWith(".ogg")) { - try { - new ProcessBuilder("xdg-open", p.toAbsolutePath().toString()) - .start(); - } catch (Exception ex) { - log.warn("[Audio] xdg-open fehlgeschlagen: {}", ex.getMessage()); - } - setStatus("▶ " + relPath); - } else if (relPath.endsWith(".animset.json")) { - animSetController.openWithSet(relPath, p); registry.activate(animSetController); - } else if (relPath.endsWith(".character")) { - String charName = p.getFileName().toString().replace(".character", ""); - switchToCharacterEditor(); - loadCharacterById(charName); - } - }); - - Button importBtn = new Button("⊕ Import…"); - importBtn.setMaxWidth(Double.MAX_VALUE); - importBtn.setOnAction(e -> { - ContextMenu popup = new ContextMenu(); - MenuItem miModel = new MenuItem("Modell mit LOD-Editor…"); - MenuItem miTex = new MenuItem("Texturen…"); - MenuItem miTexZip = new MenuItem("Texturen-ZIP…"); - MenuItem miAudio = new MenuItem("Audio…"); - MenuItem miAnim = new MenuItem("Animationen…"); - miModel.setOnAction(ev -> modelEditorController.openModelImport(importBtn.getScene().getWindow())); - miTex.setOnAction(ev -> handleTextureImport(importBtn.getScene().getWindow())); - miTexZip.setOnAction(ev -> handleZipTextureImport(importBtn.getScene().getWindow())); - miAudio.setOnAction(ev -> handleAudioImport(importBtn.getScene().getWindow())); - miAnim.setOnAction(ev -> animSetController.handleAnimationImport(importBtn.getScene().getWindow())); - MenuItem miCharModel = new MenuItem("Character-Modell (FBX)…"); - miCharModel.setOnAction(ev -> animSetController.handleCharacterModelImport(importBtn.getScene().getWindow())); - popup.getItems().addAll(miModel, miTex, miTexZip, miAudio, miAnim, - new javafx.scene.control.SeparatorMenuItem(), miCharModel); - popup.show(importBtn, javafx.geometry.Side.TOP, 0, 0); - }); - - Button refreshBtn = new Button("↻"); - refreshBtn.setTooltip(new javafx.scene.control.Tooltip("Asset-Baum aktualisieren")); - refreshBtn.setOnAction(e -> refreshAllAssets(shouldPreserveExpansion())); - - HBox bottomBar = new HBox(4, importBtn, refreshBtn); - HBox.setHgrow(importBtn, Priority.ALWAYS); - bottomBar.setMaxWidth(Double.MAX_VALUE); - - assetContent.getChildren().addAll(titleBar, tree, bottomBar); - - // ── Tab 2: Karte (platzierte Objekte) ───────────────────────────────── - mapObjectsView = new MapObjectsView( - (x, y, z, toolHint) -> { - input.pendingGotoX = x; - input.pendingGotoY = y; - input.pendingGotoZ = z; - input.pendingGotoPitch = (float) (-Math.PI / 3); - switch (toolHint) { - case "model" -> { objectController.activateEditMode(); } - case "item" -> { input.activeLayer = SharedInput.LAYER_ITEMS; root.setRight(buildItemPlacePanel(null)); } - case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; if (atmosphaereBtn != null) atmosphaereBtn.setSelected(true); root.setRight(atmosphereController.buildPanel()); } - case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; if (atmosphaereBtn != null) atmosphaereBtn.setSelected(true); root.setRight(atmosphereController.buildPanel()); } - case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); root.setRight(waterController.buildPanel()); } - case "soundarea" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "sound"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); } - case "area" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "area"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); } - case "locationzone" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "location"; root.setRight(buildZonenPanel()); zonenBtn.setSelected(true); } - case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; if (wasserMergedBtn != null) wasserMergedBtn.setSelected(true); root.setRight(waterController.buildPanel()); } - } - }, - (obj, index, toolHint) -> { - switch (toolHint) { - case "model" -> { - java.util.List list = new java.util.ArrayList<>(de.blight.common.PlacedModelIO.load()); - if (index >= 0 && index < list.size()) list.remove(index); - de.blight.common.PlacedModelIO.save(list); - input.reloadPlacedModels = true; - } - case "item" -> { - de.blight.common.PlacedItem pi = (de.blight.common.PlacedItem) obj; - java.util.List list = new java.util.ArrayList<>(de.blight.common.PlacedItemIO.load()); - list.removeIf(it -> it.uuid().equals(pi.uuid())); - de.blight.common.PlacedItemIO.save(list); - input.reloadPlacedItems = true; - } - case "light" -> { - java.util.List list = new java.util.ArrayList<>(de.blight.common.LightIO.load()); - if (index >= 0 && index < list.size()) list.remove(index); - de.blight.common.LightIO.save(list); - input.reloadPlacedOther = true; - } - case "emitter" -> { - java.util.List list = new java.util.ArrayList<>(de.blight.common.EmitterIO.load()); - if (index >= 0 && index < list.size()) list.remove(index); - de.blight.common.EmitterIO.save(list); - input.reloadPlacedOther = true; - } - case "water" -> { - java.util.List list = new java.util.ArrayList<>(de.blight.common.WaterBodyIO.load()); - if (index >= 0 && index < list.size()) list.remove(index); - de.blight.common.WaterBodyIO.save(list); - input.reloadPlacedOther = true; - } - case "soundarea" -> { - java.util.List list = new java.util.ArrayList<>(de.blight.common.SoundAreaIO.load()); - if (index >= 0 && index < list.size()) list.remove(index); - de.blight.common.SoundAreaIO.save(list); - input.reloadPlacedOther = true; - } - case "area" -> { - java.util.List list = new java.util.ArrayList<>(de.blight.common.AreaIO.load()); - if (index >= 0 && index < list.size()) list.remove(index); - de.blight.common.AreaIO.save(list); - input.reloadPlacedOther = true; - } - case "locationzone" -> { - java.util.List list = new java.util.ArrayList<>(de.blight.common.LocationZoneIO.load()); - if (index >= 0 && index < list.size()) list.remove(index); - de.blight.common.LocationZoneIO.save(list); - input.reloadPlacedOther = true; - } - case "waterfall" -> { - java.util.List list = new java.util.ArrayList<>(de.blight.common.WaterfallIO.load()); - if (index >= 0 && index < list.size()) list.remove(index); - de.blight.common.WaterfallIO.save(list); - input.reloadPlacedOther = true; - } - case "river" -> { - java.util.List> list = new java.util.ArrayList<>(de.blight.common.RiverIO.load()); - if (index >= 0 && index < list.size()) list.remove(index); - de.blight.common.RiverIO.save(list); - input.reloadPlacedOther = true; - } - } - scheduleAutoSave(); - } - ); - - // ── TabPane ─────────────────────────────────────────────────────────── - Tab assetsTab = new Tab("Assets", assetContent); - assetsTab.setClosable(false); - - Tab karteTab = new Tab("Karte", mapObjectsView); - karteTab.setClosable(false); - karteTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { - if (isSelected) mapObjectsView.refresh(); - }); - - if (worldMapView == null) { - worldMapView = new de.blight.editor.ui.WorldMapView(() -> primaryStage); - worldMapView.setVoxelChunkSupplier(() -> { - de.blight.editor.state.VoxelEditorState ves = jmeApp == null ? null - : jmeApp.getStateManager().getState(de.blight.editor.state.VoxelEditorState.class); - return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll(); - }); - worldMapView.setCameraInfoSupplier(() -> - new de.blight.editor.ui.WorldMapView.CameraInfo( - input.camX, input.camY, input.camZ, input.camYaw)); - worldMapView.setTeleportCallback(pos -> { - input.pendingGotoX = pos[0]; - input.pendingGotoY = pos[1]; - input.pendingGotoZ = pos[2]; - }); - worldMapView.setRefreshAction(() -> { - input.saveRequested = true; - pendingAfterSave = worldMapView::loadAndRender; - setStatus("Speichere vor Weltkarten-Refresh…"); - }); - worldMapView.loadAndRender(); // Vorladen beim Start, nicht erst beim Tab-Öffnen - } - - weltkartTab = new Tab("Weltkarte", worldMapView); - weltkartTab.setClosable(false); - weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { - if (!isSelected) return; - if (!worldMapView.isLoaded()) { - worldMapView.loadAndRender(); - } else if (caveMapView != null && caveMapView.isLoaded() && caveMapView.isViewInitialized()) { - final double s = caveMapView.getScale(), px = caveMapView.getPanX(), py = caveMapView.getPanY(); - javafx.application.Platform.runLater(() -> worldMapView.setView(s, px, py)); - } else { - worldMapView.ensureFitToView(); - } - }); - - caveMapView = new de.blight.editor.ui.CaveMapView(() -> primaryStage); - caveMapTab = new Tab("Höhlenkarte", caveMapView); - caveMapTab.setClosable(false); - caveMapTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { - if (!isSelected) return; - if (!caveMapView.isLoaded()) { - caveMapView.loadAndRender(); - } else if (worldMapView.isMapReady() && worldMapView.isViewInitialized()) { - final double s = worldMapView.getScale(), px = worldMapView.getPanX(), py = worldMapView.getPanY(); - javafx.application.Platform.runLater(() -> caveMapView.setView(s, px, py)); - } else { - caveMapView.ensureFitToView(); - } - }); - caveMapView.setFullscreenCallbacks( - () -> { - final javafx.scene.control.Label placeholder = new javafx.scene.control.Label(); - caveMapTab.setContent(placeholder); - assetTabBtn.setVisible(false); - javafx.application.Platform.runLater(() -> { - caveMapView.setMaxWidth(Double.MAX_VALUE); - javafx.scene.layout.StackPane.setAlignment(caveMapView, null); - caveMapView.setTranslateX(0); - try { - centerStack.getChildren().add(caveMapView); - } catch (Exception ex) { - log.error("[FS-cave-enter] add failed", ex); - return; - } - javafx.application.Platform.runLater(() -> { - double w = caveMapView.getWidth(); - caveMapView.fitCanvas(); - caveMapView.setTranslateX(-w); - javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( - javafx.util.Duration.millis(300), caveMapView); - tt.setToX(0); - tt.setInterpolator(javafx.animation.Interpolator.EASE_OUT); - tt.play(); - }); - }); - }, - () -> { - double w = caveMapView.getWidth(); - javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( - javafx.util.Duration.millis(220), caveMapView); - tt.setToX(-w); - tt.setInterpolator(javafx.animation.Interpolator.EASE_IN); - tt.setOnFinished(ev -> { - centerStack.getChildren().remove(caveMapView); - caveMapView.setTranslateX(0); - caveMapTab.setContent(caveMapView); - assetTabBtn.setVisible(true); - }); - tt.play(); - }); - caveMapView.loadAndRender(); // Vorladen beim Start, nicht erst beim Tab-Öffnen - worldMapView.setFullscreenCallbacks( - () -> { - // Placeholder ersetzt worldMapView im Tab → sauberes Detach - final javafx.scene.control.Label placeholder = new javafx.scene.control.Label(); - weltkartTab.setContent(placeholder); - assetTabBtn.setVisible(false); - // Nach einem Pulse hat der TabPane-Skin den Swap verarbeitet - javafx.application.Platform.runLater(() -> { - log.debug("[FS-enter] parent after detach: {}", worldMapView.getParent()); - worldMapView.setMaxWidth(Double.MAX_VALUE); - // KEIN explizites Alignment → null = CENTER → fillWidth=true → View füllt volle Breite - StackPane.setAlignment(worldMapView, null); - worldMapView.setTranslateX(0); - try { - centerStack.getChildren().add(worldMapView); - } catch (Exception ex) { - log.error("[FS-enter] add failed", ex); - return; - } - // Nach nächstem Layout-Pass hat centerStack die View korrekt dimensioniert - javafx.application.Platform.runLater(() -> { - double w = worldMapView.getWidth(); - log.debug("[FS-enter] post-layout: w={} h={}", w, worldMapView.getHeight()); - // Karte auf volle Canvas-Größe einpassen (scale/pan), dann animieren - worldMapView.fitCanvas(); - worldMapView.setTranslateX(-w); - javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( - javafx.util.Duration.millis(300), worldMapView); - tt.setToX(0); - tt.setInterpolator(javafx.animation.Interpolator.EASE_OUT); - tt.play(); - }); - }); - }, - () -> { - // Zurück nach links schieben, dann in Tab zurücklegen - double w = worldMapView.getWidth(); - javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( - javafx.util.Duration.millis(220), worldMapView); - tt.setToX(-w); - tt.setInterpolator(javafx.animation.Interpolator.EASE_IN); - tt.setOnFinished(ev -> { - centerStack.getChildren().remove(worldMapView); - worldMapView.setTranslateX(0); - weltkartTab.setContent(worldMapView); - assetTabBtn.setVisible(true); - }); - tt.play(); - }); - - TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab, caveMapTab); - tabPane.setStyle("-fx-background-color: #e8e8e8;"); - VBox.setVgrow(tabPane, Priority.ALWAYS); - - VBox panel = new VBox(tabPane); - panel.setPrefWidth(420); - panel.setStyle("-fx-background-color: #e8e8e8;"); - VBox.setVgrow(tabPane, Priority.ALWAYS); - return panel; - } - - // ── Zentralansicht wechseln (Asset-Overlay bleibt sichtbar) ────────────── - - private void setCenterView(javafx.scene.Node view) { - centerStack.getChildren().set(0, view); - if (root.getCenter() != centerStack) root.setCenter(centerStack); - } - - // ── Asset-Overlay ───────────────────────────────────────────────────────── - - private void setupAssetOverlay() { - assetPanel.setMaxWidth(420); - assetPanel.setMaxHeight(Double.MAX_VALUE); - assetPanel.setTranslateX(0); - StackPane.setAlignment(assetPanel, Pos.TOP_LEFT); - assetPanel.setEffect(new javafx.scene.effect.DropShadow( - 14, 5, 0, javafx.scene.paint.Color.rgb(0, 0, 0, 0.55))); - - assetTabBtn = new Button("›"); - assetTabBtn.setPrefSize(26, 80); - assetTabBtn.setStyle( - "-fx-background-color: #4a4a5a; -fx-text-fill: #ddd; -fx-font-size: 18; " + - "-fx-background-radius: 0 6 6 0; -fx-border-width: 0; -fx-cursor: hand; -fx-padding: 0;"); - StackPane.setAlignment(assetTabBtn, Pos.CENTER_LEFT); - - assetPanel.translateXProperty().addListener((obs, old, nv) -> - assetTabBtn.setTranslateX(nv.doubleValue() + 420)); - - assetTabBtn.setOnMouseClicked(e -> { - if (assetOverlayOpen) closeAssetOverlay(); - else openAssetOverlay(); - }); - - // Panel startet ausgeklappt - assetOverlayOpen = true; - assetTabBtn.setText("‹"); - assetTabBtn.setTranslateX(420); - - centerStack.getChildren().addAll(assetPanel, assetTabBtn); - } - - private void openAssetOverlay() { - if (assetOverlayOpen) return; - assetOverlayOpen = true; - assetTabBtn.setText("‹"); - javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( - javafx.util.Duration.millis(200), assetPanel); - tt.setToX(0); - tt.play(); - } - - private void closeAssetOverlay() { - if (!assetOverlayOpen) return; - assetOverlayOpen = false; - assetTabBtn.setText("›"); - javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( - javafx.util.Duration.millis(150), assetPanel); - tt.setToX(-420); - tt.play(); - } - - // ── Asset-Tree-Cell (DnD + Kontextmenü) ────────────────────────────────── - - private TreeCell buildAssetCell() { - TreeCell cell = new TreeCell<>() { - @Override - protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { setText(null); setGraphic(null); setStyle(""); return; } - javafx.scene.shape.Rectangle swatch = null; - if (textureSetNodes.contains(getTreeItem())) { - swatch = new javafx.scene.shape.Rectangle(10, 10); - swatch.setFill(javafx.scene.paint.Color.web("#8b5cf6")); - } else { - Path tp = itemPaths.get(getTreeItem()); - if (tp != null && getCategoryRoot(getTreeItem()) == texturesNode) { - String lo = tp.getFileName().toString().toLowerCase(); - if (lo.endsWith(".png") || lo.endsWith(".jpg") - || lo.endsWith(".jpeg") || lo.endsWith(".dds")) { - swatch = new javafx.scene.shape.Rectangle(10, 10); - swatch.setFill(javafx.scene.paint.Color.web("#22c55e")); - } - } - } - if (swatch != null) { - swatch.setArcWidth(3); swatch.setArcHeight(3); - setGraphic(swatch); - } else { - setGraphic(null); - } - setText(item); - setCellStyle(this, false); - } - }; - - // ── Drag-Quelle: Dateien + Unterordner (keine Kategorie-Wurzeln, keine JME-Einträge) ─ - cell.setOnDragDetected(e -> { - TreeItem item = cell.getTreeItem(); - if (item == null) return; - if (jmePaths.containsKey(item) || jmeFolderNodes.contains(item)) return; - // Kategorie-Wurzeln (Models, Textures, …) nicht verschiebbar - if (item == modelsNode || item == texturesNode || item == audioNode - || item == animationsNode || item == itemsNode || item == charactersNode) return; - draggedItem = item; - Dragboard db = cell.startDragAndDrop(TransferMode.MOVE); - ClipboardContent cc = new ClipboardContent(); - cc.putString(item.getValue()); - db.setContent(cc); - e.consume(); - }); - - // ── Drag-Ziel: nur Ordner der gleichen Kategorie ───────────────────── - cell.setOnDragOver(e -> { - if (e.getGestureSource() != cell && isValidDropTarget(cell.getTreeItem())) - e.acceptTransferModes(TransferMode.MOVE); - e.consume(); - }); - - cell.setOnDragEntered(e -> { - if (e.getGestureSource() != cell && isValidDropTarget(cell.getTreeItem())) - setCellStyle(cell, true); - e.consume(); - }); - - cell.setOnDragExited(e -> { setCellStyle(cell, false); e.consume(); }); - - cell.setOnDragDropped(e -> { - boolean ok = false; - TreeItem target = cell.getTreeItem(); - if (draggedItem != null && target != null && isValidDropTarget(target)) { - Path src = itemPaths.get(draggedItem); - Path destDir = itemPaths.get(target); - Path dest = destDir.resolve(src.getFileName()); - if (!src.equals(dest)) { - if (Files.exists(dest)) { - setStatus("Fehler: " + src.getFileName() + " existiert bereits im Zielordner."); - } else { - // isDirectory VOR dem Verschieben prüfen (danach ist src weg) - boolean srcIsFolder = Files.isDirectory(src); - try { - Files.move(src, dest); - draggedItem.getParent().getChildren().remove(draggedItem); - if (srcIsFolder) { - // Ordner: TreeItem mit gesamtem Subtree reparentieren, - // alle itemPaths im Subtree auf neuen Pfad umbiegen - updateItemPathsUnder(draggedItem, src, dest); - itemPaths.put(draggedItem, dest); - } else { - // Datei: TreeItem neu anlegen; Set-Markierung übertragen - boolean wasSet = textureSetNodes.remove(draggedItem); - itemPaths.remove(draggedItem); - draggedItem = new TreeItem<>(src.getFileName().toString()); - itemPaths.put(draggedItem, dest); - if (wasSet) textureSetNodes.add(draggedItem); - } - target.getChildren().add(draggedItem); - target.setExpanded(true); - int updated = srcIsFolder - ? renameAssetPathsForFolder(src, dest) - : renameAssetPaths(src, dest); - String suffix = updated > 0 ? " (" + updated + " Referenz" + (updated == 1 ? "" : "en") + " aktualisiert)" : ""; - setStatus("Verschoben: " + src.getFileName() + suffix); - ok = true; - } catch (IOException ex) { - setStatus("Verschieben fehlgeschlagen: " + ex.getMessage()); - } - } - } - } - draggedItem = null; - e.setDropCompleted(ok); - e.consume(); - }); - - cell.setOnDragDone(e -> { draggedItem = null; e.consume(); }); - - // ── Kontextmenü ────────────────────────────────────────────────────── - cell.setOnContextMenuRequested(e -> { - TreeItem item = cell.getTreeItem(); - if (item == null) return; - if (jmeFolderNodes.contains(item) || jmePaths.containsKey(item)) return; - ContextMenu ctx = new ContextMenu(); - - // ── Multi-Select: Auswahl löschen ──────────────────────────────── - var selectedItems = assetTree.getSelectionModel().getSelectedItems(); - java.util.List> deletableSelection = selectedItems.stream() - .filter(si -> si != null && !isAssetFolder(si) - && itemPaths.containsKey(si) - && !jmePaths.containsKey(si) - && !jmeFolderNodes.contains(si)) - .collect(java.util.stream.Collectors.toList()); - if (deletableSelection.size() > 1) { - MenuItem multiDel = new MenuItem("🗑 Auswahl löschen (" + deletableSelection.size() + " Dateien)"); - multiDel.setStyle("-fx-text-fill: #c0392b; -fx-font-weight: bold;"); - multiDel.setOnAction(ev -> { - StringBuilder names = new StringBuilder(); - for (int i = 0; i < Math.min(5, deletableSelection.size()); i++) { - names.append(" ").append(deletableSelection.get(i).getValue()).append('\n'); - } - if (deletableSelection.size() > 5) - names.append(" … und ").append(deletableSelection.size() - 5).append(" weitere"); - Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, - deletableSelection.size() + " Dateien löschen?\n\n" + names, - ButtonType.OK, ButtonType.CANCEL); - confirm.initOwner(primaryStage); - confirm.setHeaderText(null); - confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { - int deleted = 0; - for (TreeItem si : new java.util.ArrayList<>(deletableSelection)) { - Path p = itemPaths.get(si); - if (p == null) continue; - try { - Files.deleteIfExists(p); - if (p.toString().endsWith(".j3o")) { - deleteJ3oSideFiles(p); - String rel = ASSET_ROOT.relativize(p).toString().replace('\\', '/'); - if (rel.startsWith("Characters/clips/")) { - String clipName = p.getFileName().toString().replaceFirst("\\.j3o$", ""); - removeClipFromAllAnimSets(clipName); - } - } - si.getParent().getChildren().remove(si); - itemPaths.remove(si); - deleted++; - } catch (IOException ex) { - setStatus("Fehler beim Löschen: " + p.getFileName()); - } - } - setStatus(deleted + " Datei(en) gelöscht"); - }); - }); - ctx.getItems().addAll(multiDel, new SeparatorMenuItem()); - ctx.show(cell, e.getScreenX(), e.getScreenY()); - e.consume(); - return; - } - - // Texture-Set-Knoten: eigenes Menü - if (textureSetNodes.contains(item)) { - Path setDir = itemPaths.get(item); - boolean setReadOnly = isInternalTexture(setDir); - MenuItem details = new MenuItem("Details…"); - details.setOnAction(ev -> { - TextureDetailDialog dlg = TextureDetailDialog.forSet(ASSET_ROOT, setDir, setReadOnly); - dlg.initOwner(ctx.getOwnerWindow()); - dlg.showAndWait().filter(Boolean.TRUE::equals).ifPresent(ok -> - refreshCategoryNode(texturesNode, true)); - }); - MenuItem reveal = new MenuItem("Im Dateisystem anzeigen"); - reveal.setOnAction(ev -> { - try { Runtime.getRuntime().exec(new String[]{"xdg-open", setDir.toString()}); } - catch (IOException ignored) {} - }); - MenuItem del = new MenuItem("🗑 Löschen"); - del.setStyle("-fx-text-fill: #c0392b;"); - del.setDisable(setReadOnly); - del.setOnAction(ev -> { - Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, - "Textur-Set wirklich löschen?\n" + setDir.getFileName() - + "\n(Alle enthaltenen Dateien werden gelöscht!)", - ButtonType.OK, ButtonType.CANCEL); - confirm.initOwner(primaryStage); - confirm.setHeaderText(null); - confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { - try { - deleteDirectoryRecursive(setDir); - item.getParent().getChildren().remove(item); - itemPaths.remove(item); - textureSetNodes.remove(item); - setStatus("Textur-Set gelöscht: " + setDir.getFileName()); - } catch (IOException ex) { - setStatus("Fehler: " + ex.getMessage()); - } - }); - }); - ctx.getItems().addAll(details, new SeparatorMenuItem(), reveal, new SeparatorMenuItem(), del); - ctx.show(cell, e.getScreenX(), e.getScreenY()); - e.consume(); - return; - } - - if (isAssetFolder(item)) { - MenuItem newSub = new MenuItem("📁 Neuer Unterordner…"); - newSub.setOnAction(ev -> createSubfolder(item)); - ctx.getItems().add(newSub); - - boolean isCatRoot = (item == modelsNode || item == texturesNode - || item == audioNode || item == animationsNode || item == itemsNode - || item == charactersNode); - Path dir = itemPaths.get(item); - - // Ordner unter Models: Zufällig-Platzieren-Eintrag - if (getCategoryRoot(item) == modelsNode && dir != null - && java.nio.file.Files.isDirectory(dir)) { - String relFolder = ASSET_ROOT.relativize(dir).toString().replace('\\', '/'); - MenuItem placeRandom = new MenuItem("🎲 Zufällig platzieren"); - placeRandom.setOnAction(ev -> { - input.treeFolderPath = relFolder; - input.pendingModelPath = null; - objectController.activatePlaceMode(); - objectController.enableMultiPlace(); - setStatus("Ordner: " + relFolder + " | Linksklick → zufälliges Modell | Rechtsklick → neu würfeln"); - }); - ctx.getItems().addAll(new SeparatorMenuItem(), placeRandom); - } - - if (item == animationsNode) { - MenuItem importAnim = new MenuItem("⊕ Animation importieren…"); - importAnim.setOnAction(ev -> animSetController.handleAnimationImport(ctx.getOwnerWindow())); - ctx.getItems().add(importAnim); - } - - if (item == charactersNode) { - MenuItem importChar = new MenuItem("⊕ Character-Modell importieren (FBX)…"); - importChar.setOnAction(ev -> animSetController.handleCharacterModelImport(ctx.getOwnerWindow())); - MenuItem importAnim = new MenuItem("⊕ Animation hinzufügen…"); - importAnim.setOnAction(ev -> animSetController.handleAnimationImport(ctx.getOwnerWindow())); - ctx.getItems().addAll(importChar, importAnim); - } - - Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets"); - if (dir != null && setsDir.equals(dir)) { - MenuItem newSet = new MenuItem("➕ Neues Set…"); - newSet.setOnAction(ev -> animSetController.createNewAnimSet()); - ctx.getItems().add(newSet); - } - - if (!isCatRoot && dir != null) { - MenuItem rename = new MenuItem("✏ Umbenennen…"); - rename.setOnAction(ev -> renameAsset(item)); - ctx.getItems().add(rename); - - // Ordner löschen nur wenn leer - boolean empty; - try (var s = Files.list(dir)) { empty = s.findFirst().isEmpty(); } - catch (IOException ex) { empty = false; } - MenuItem del = new MenuItem("🗑 Ordner löschen"); - del.setDisable(!empty); - if (!empty) del.setStyle("-fx-text-fill: grey;"); - del.setOnAction(ev -> { - try { - Files.deleteIfExists(dir); - item.getParent().getChildren().remove(item); - itemPaths.remove(item); - setStatus("Ordner gelöscht: " + dir.getFileName()); - } catch (IOException ex) { - setStatus("Fehler: " + ex.getMessage()); - } - }); - ctx.getItems().addAll(new SeparatorMenuItem(), del); - } - } else { - // Datei: Umbenennen + Im Animationseditor öffnen (j3o) + Im Dateisystem anzeigen + Löschen - Path p = itemPaths.get(item); - if (p != null) { - MenuItem rename = new MenuItem("✏ Umbenennen…"); - rename.setOnAction(ev -> renameAsset(item)); - - String pStr = p.toString(); - if (pStr.endsWith(".j3o")) { - String relPath = ASSET_ROOT.relativize(p).toString().replace('\\', '/'); - - MenuItem placeModel = new MenuItem("📦 Platzieren"); - placeModel.setOnAction(ev -> { - input.pendingModelPath = relPath; - objectController.activatePlaceMode(); - setStatus("Modell: " + relPath + " | Linksklick ins Terrain zum Platzieren"); - }); - MenuItem openEditor = new MenuItem("🔧 Im Model Editor öffnen"); - openEditor.setOnAction(ev -> modelEditorController.openModelEditor(relPath, p)); - ctx.getItems().addAll(placeModel, openEditor, new SeparatorMenuItem()); - - boolean skeletal = skeletalModelPaths == null - || skeletalModelPaths.contains(relPath); - if (skeletal) { - MenuItem openAnim = new MenuItem("▶ Im Animationseditor öffnen"); - openAnim.setOnAction(ev -> { - switchToAnimPreview(); - input.animPreviewLoadPath = relPath; - animPreviewController.setStatusText("Lade…"); - animPreviewController.clearClipList(); - }); - ctx.getItems().addAll(openAnim, new SeparatorMenuItem()); - } - } - - if (pStr.endsWith(".animset.json")) { - String animSetRel = ASSET_ROOT.relativize(p).toString().replace('\\', '/'); - MenuItem editSet = new MenuItem("✏ AnimSet bearbeiten"); - editSet.setOnAction(ev -> { animSetController.openWithSet(animSetRel, p); registry.activate(animSetController); }); - ctx.getItems().addAll(editSet, new SeparatorMenuItem()); - } - - if (pStr.endsWith(".item")) { - MenuItem placeItem = new MenuItem("📍 Platzieren"); - placeItem.setOnAction(ev -> activateItemPlacement(p)); - MenuItem editItem = new MenuItem("✏ Im Item-Editor öffnen"); - editItem.setOnAction(ev -> { - String itemId = p.getFileName().toString().replace(".item", ""); - switchToItemEditor(itemId); - }); - ctx.getItems().addAll(placeItem, editItem, new SeparatorMenuItem()); - } - - // Textur-Map-Zuordnung (nur für Bilddateien im Textures-Zweig) - String loName = pStr.toLowerCase(); - boolean isImage = loName.endsWith(".png") || loName.endsWith(".jpg") - || loName.endsWith(".jpeg") || loName.endsWith(".dds"); - if (isImage && getCategoryRoot(item) == texturesNode && !jmePaths.containsKey(item)) { - boolean imgReadOnly = isInternalTexture(p); - MenuItem imgDetails = new MenuItem("Details…"); - imgDetails.setOnAction(ev -> { - TextureDetailDialog dlg = TextureDetailDialog.forFile(ASSET_ROOT, p, imgReadOnly); - dlg.initOwner(ctx.getOwnerWindow()); - dlg.setOnConvertedToSet(newSetDir -> - Platform.runLater(() -> refreshCategoryNode(texturesNode, true))); - dlg.showAndWait().filter(Boolean.TRUE::equals).ifPresent(ok -> - refreshCategoryNode(texturesNode, true)); - }); - ctx.getItems().addAll(imgDetails, new SeparatorMenuItem()); - } - - MenuItem reveal = new MenuItem("Im Dateisystem anzeigen"); - reveal.setOnAction(ev -> { - try { Runtime.getRuntime().exec( - new String[]{"xdg-open", p.getParent().toString()}); - } catch (IOException ignored) {} - }); - MenuItem del = new MenuItem("🗑 Löschen"); - del.setStyle("-fx-text-fill: #c0392b;"); - del.setOnAction(ev -> { - Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, - "Datei wirklich löschen?\n" + p.getFileName(), - ButtonType.OK, ButtonType.CANCEL); - confirm.initOwner(primaryStage); - confirm.setHeaderText(null); - confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { - try { - Files.deleteIfExists(p); - item.getParent().getChildren().remove(item); - itemPaths.remove(item); - if (pStr.endsWith(".j3o")) { - deleteJ3oSideFiles(p); - String rel = ASSET_ROOT.relativize(p).toString().replace('\\', '/'); - if (rel.startsWith("Characters/clips/")) { - String clipName = p.getFileName().toString().replaceFirst("\\.j3o$", ""); - removeClipFromAllAnimSets(clipName); - } - } - setStatus("Gelöscht: " + p.getFileName()); - } catch (IOException ex) { - setStatus("Fehler: " + ex.getMessage()); - } - }); - }); - ctx.getItems().addAll(rename, reveal, new SeparatorMenuItem(), del); - } - } - - if (!ctx.getItems().isEmpty()) - ctx.show(cell, e.getScreenX(), e.getScreenY()); - e.consume(); - }); - - return cell; - } - - private void setCellStyle(TreeCell cell, boolean highlighted) { - TreeItem ti = cell.getTreeItem(); - boolean isBold = isAssetFolder(ti) && !textureSetNodes.contains(ti); - String base = isBold ? "-fx-font-weight:bold;" : ""; - cell.setStyle(highlighted ? base + "-fx-background-color:#cce5ff;" : base); - } - - // ── Asset-Baum Hilfsmethoden ────────────────────────────────────────────── - - /** Gibt true zurück wenn das Item ein Verzeichnis ist (inkl. Kategoriewurzeln). */ - private boolean isAssetFolder(TreeItem item) { - if (item == null) return false; - if (jmeFolderNodes.contains(item)) return true; - Path p = itemPaths.get(item); - return p != null && Files.isDirectory(p); - } - - /** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode / animationsNode / itemsNode / charactersNode). */ - private TreeItem getCategoryRoot(TreeItem item) { - for (TreeItem cur = item; cur != null; cur = cur.getParent()) - if (cur == modelsNode || cur == texturesNode || cur == audioNode - || cur == animationsNode || cur == itemsNode || cur == charactersNode) return cur; - return null; - } - - /** Gibt true zurück wenn der Pfad unter Textures/internal/ liegt (schreibgeschützt). */ - private static boolean isInternalTexture(Path p) { - if (p == null) return false; - for (Path part : p) { - if (part.toString().equals("internal")) return true; - } - return false; - } - - /** Gibt true zurück wenn draggedItem auf dieses target verschoben werden darf. */ - private boolean isValidDropTarget(TreeItem target) { - if (draggedItem == null || target == null) return false; - if (!isAssetFolder(target)) return false; - if (jmeFolderNodes.contains(target)) return false; - if (textureSetNodes.contains(target)) return false; // Set-Ordner sind keine Drop-Ziele - if (target == draggedItem.getParent()) return false; - if (target == draggedItem) return false; - // Ordner darf nicht in einen eigenen Nachfolger gezogen werden - if (isAssetFolder(draggedItem) && isDescendantOf(draggedItem, target)) return false; - TreeItem dragCat = getCategoryRoot(draggedItem); - TreeItem dropCat = getCategoryRoot(target); - return dragCat != null && dragCat == dropCat; - } - - private static boolean isDescendantOf(TreeItem ancestor, TreeItem item) { - TreeItem cur = item.getParent(); - while (cur != null) { - if (cur == ancestor) return true; - cur = cur.getParent(); - } - return false; - } - - /** Aktualisiert rekursiv alle itemPaths-Einträge unter einem verschobenen Ordner. */ - private void updateItemPathsUnder(TreeItem node, Path oldBase, Path newBase) { - for (TreeItem child : node.getChildren()) { - Path old = itemPaths.get(child); - if (old != null) { - Path relative = oldBase.relativize(old); - itemPaths.put(child, newBase.resolve(relative)); - } - if (!child.getChildren().isEmpty()) { - updateItemPathsUnder(child, oldBase, newBase); - } - } - } - - /** Erstellt einen neuen Unterordner unter parentItem (Dialog + Filesystem). */ - private void createSubfolder(TreeItem parentItem) { - Path parentPath = itemPaths.get(parentItem); - if (parentPath == null) return; - TextInputDialog dlg = new TextInputDialog("Neuer Ordner"); - dlg.initOwner(primaryStage); - dlg.setTitle("Ordner erstellen"); - dlg.setHeaderText("Neuen Unterordner in " + parentPath.getFileName() + " erstellen:"); - dlg.setContentText("Name:"); - dlg.showAndWait().ifPresent(raw -> { - String name = raw.trim(); - if (name.isEmpty()) return; - Path newDir = parentPath.resolve(name); - try { - Files.createDirectories(newDir); - TreeItem newItem = new TreeItem<>(name); - itemPaths.put(newItem, newDir); - parentItem.getChildren().add(0, newItem); // Ordner an erster Stelle - parentItem.setExpanded(true); - setStatus("Ordner erstellt: " + name); - } catch (IOException ex) { - setStatus("Fehler beim Erstellen: " + ex.getMessage()); - } - }); - } - - /** Benennt eine Datei oder einen Ordner um (Filesystem + TreeItem + itemPaths). */ - private void renameAsset(TreeItem item) { - Path oldPath = itemPaths.get(item); - if (oldPath == null) return; - - boolean isFile = !Files.isDirectory(oldPath); - String oldFileName = oldPath.getFileName().toString(); - - // For files: extract extension and pre-fill dialog with base name only - String ext = ""; - String baseName = oldFileName; - if (isFile) { - int dot = oldFileName.lastIndexOf('.'); - if (dot > 0) { - ext = oldFileName.substring(dot); // e.g. ".j3o" - baseName = oldFileName.substring(0, dot); - } - } - - String headerExt = ext.isEmpty() ? "" : " (Endung " + ext + " wird beibehalten)"; - TextInputDialog dlg = new TextInputDialog(baseName); - dlg.initOwner(primaryStage); - dlg.setTitle("Umbenennen"); - dlg.setHeaderText(oldFileName + " umbenennen:" + headerExt); - dlg.setContentText("Neuer Name:"); - - final String fixedExt = ext; - dlg.showAndWait().ifPresent(raw -> { - String newBase = raw.trim(); - if (newBase.isEmpty()) return; - // Append original extension for files - String newName = isFile ? newBase + fixedExt : newBase; - if (newName.equals(oldFileName)) return; - if (newName.contains("/") || newName.contains("\\")) { - setStatus("Ungültiger Name: keine Pfadtrennzeichen erlaubt."); - return; - } - Path newPath = oldPath.getParent().resolve(newName); - if (Files.exists(newPath)) { - setStatus("Fehler: " + newName + " existiert bereits."); - return; - } - try { - Files.move(oldPath, newPath); - itemPaths.put(item, newPath); - // Bei Ordnern alle Kind-Pfade anpassen - if (Files.isDirectory(newPath)) { - updateDescendantPaths(item, oldPath, newPath); - } - item.setValue(newName); - int updated = Files.isDirectory(newPath) - ? renameAssetPathsForFolder(oldPath, newPath) - : renameAssetPaths(oldPath, newPath); - String suffix = updated > 0 ? " (" + updated + " Referenz" + (updated == 1 ? "" : "en") + " aktualisiert)" : ""; - setStatus("Umbenannt: " + oldFileName + " → " + newName + suffix); - } catch (IOException ex) { - setStatus("Umbenennen fehlgeschlagen: " + ex.getMessage()); - } - }); - } - - /** Aktualisiert itemPaths aller Kinder nach einer Ordner-Umbenennung. */ - private void updateDescendantPaths(TreeItem parent, Path oldBase, Path newBase) { - for (TreeItem child : parent.getChildren()) { - Path p = itemPaths.get(child); - if (p != null) itemPaths.put(child, newBase.resolve(oldBase.relativize(p))); - updateDescendantPaths(child, oldBase, newBase); - } - } - - // ── Asset-Pfad-Umbenennung bei Move/Rename ──────────────────────────────── - - /** - * Aktualisiert alle in SharedInput gespeicherten Asset-Pfade wenn eine einzelne Datei - * von oldAbs nach newAbs verschoben/umbenannt wurde. - * Gibt die Anzahl aktualisierter Referenzen zurück. - */ - private int renameAssetPaths(Path oldAbs, Path newAbs) { - if (!oldAbs.startsWith(ASSET_ROOT) || !newAbs.startsWith(ASSET_ROOT)) return 0; - String oldRel = ASSET_ROOT.relativize(oldAbs).toString().replace('\\', '/'); - String newRel = ASSET_ROOT.relativize(newAbs).toString().replace('\\', '/'); - return applyPathRenameToSharedInput(oldRel, newRel); - } - - /** - * Aktualisiert alle Pfade wenn ein Ordner verschoben/umbenannt wurde: - * Walk durch den neuen Ordner, für jede Datei alte/neue rel. Pfade berechnen. - */ - private int renameAssetPathsForFolder(Path oldFolderAbs, Path newFolderAbs) { - int total = 0; - try (java.util.stream.Stream stream = Files.walk(newFolderAbs)) { - for (Path newFile : (Iterable) stream.filter(Files::isRegularFile)::iterator) { - Path relToNew = newFolderAbs.relativize(newFile); - Path oldFile = oldFolderAbs.resolve(relToNew); - total += renameAssetPaths(oldFile, newFile); - } - } catch (IOException ex) { - log.warn("[Asset] Ordner-Pfad-Update fehlgeschlagen: {}", ex.getMessage()); - } - return total; - } - - /** Wendet ein (oldRel→newRel)-Rename auf alle SharedInput-Pfad-Arrays an. */ - private int applyPathRenameToSharedInput(String oldRel, String newRel) { - int count = 0; - - count += replaceInPaths(input.terrainTexturePaths, oldRel, newRel, - () -> input.terrainTexturesChanged = true); - count += replaceInPaths(input.terrainNormalMapPaths, oldRel, newRel, - () -> input.terrainNormalMapsChanged = true); - count += replaceInPaths(input.terrainDisplacementMapPaths, oldRel, newRel, null); - - count += replaceInPaths(input.upperTexturePaths, oldRel, newRel, - () -> input.upperTexturesChanged = true); - count += replaceInPaths(input.upperNormalMapPaths, oldRel, newRel, - () -> input.upperNormalMapsChanged = true); - count += replaceInPaths(input.upperDisplacementMapPaths, oldRel, newRel, null); - - count += replaceInPaths(input.thirdTexturePaths, oldRel, newRel, - () -> input.thirdTexturesChanged = true); - count += replaceInPaths(input.thirdNormalMapPaths, oldRel, newRel, - () -> input.thirdNormalMapsChanged = true); - count += replaceInPaths(input.thirdDisplacementMapPaths, oldRel, newRel, null); - - count += replaceInPaths(input.voxelSplatTexturePaths, oldRel, newRel, - () -> input.voxelSplatTexturesChanged = true); - - if (oldRel.equals(input.grassTexturePath)) { - input.grassTexturePath = newRel; - input.grassSlotsChanged = true; - count++; - } - count += replaceInPaths(input.grassTextureSlots, oldRel, newRel, - () -> input.grassSlotsChanged = true); - count += replaceInPaths(input.grassNormalMapPaths, oldRel, newRel, - () -> input.grassSlotsChanged = true); - - // JME3-States (SceneObjects, Emitter) über Queue informieren - SharedInput.PathRename rename = new SharedInput.PathRename(oldRel, newRel); - input.sceneObjectPathRenames.offer(rename); - input.emitterPathRenames.offer(rename); - - return count; - } - - /** Ersetzt oldRel durch newRel in einem String-Array; ruft onChange einmalig wenn geändert. */ - private static int replaceInPaths(String[] arr, String oldRel, String newRel, Runnable onChange) { - int count = 0; - for (int i = 0; i < arr.length; i++) { - if (oldRel.equals(arr[i])) { - arr[i] = newRel; - count++; - } - } - if (count > 0 && onChange != null) onChange.run(); - return count; - } - - /** Scannt alle JME-JARs im Classpath und baut den Textur-Teilbaum unter root auf. */ - private void loadJmeTexturesInto(TreeItem root) { - String[] texExts = {".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds"}; - java.util.Set seen = new java.util.HashSet<>(); - java.util.List collected = new java.util.ArrayList<>(); - - String cp = System.getProperty("java.class.path", ""); - for (String entry : cp.split(File.pathSeparator)) { - if (!entry.toLowerCase().contains("jme")) continue; - File f = new File(entry); - if (!f.exists() || !f.getName().endsWith(".jar")) continue; - try (java.util.jar.JarFile jar = new java.util.jar.JarFile(f)) { - jar.stream() - .filter(je -> !je.isDirectory()) - .map(java.util.jar.JarEntry::getName) - .filter(name -> name.startsWith("Textures/")) - .filter(name -> { - String lo = name.toLowerCase(); - for (String ext : texExts) if (lo.endsWith(ext)) return true; - return false; - }) - .filter(seen::add) - .forEach(collected::add); - } catch (IOException ignored) {} - } - - java.util.Collections.sort(collected); - for (String path : collected) addJmeTreeEntry(root, path); - } - - private void addJmeTreeEntry(TreeItem root, String fullPath) { - String[] parts = fullPath.split("/"); - TreeItem cur = root; - for (int i = 0; i < parts.length - 1; i++) { - final String dirName = parts[i]; - TreeItem found = null; - for (TreeItem child : cur.getChildren()) { - if (child.getValue().equals(dirName) && jmeFolderNodes.contains(child)) { - found = child; - break; - } - } - if (found == null) { - found = new TreeItem<>(dirName); - jmeFolderNodes.add(found); - cur.getChildren().add(found); - } - cur = found; - } - TreeItem file = new TreeItem<>(parts[parts.length - 1]); - jmePaths.put(file, fullPath); - cur.getChildren().add(file); - } - - /** - * Scannt alle JME-JARs im Classpath und baut den Modell-Teilbaum unter root auf. - * Nur Einträge unter "Models/" werden berücksichtigt; der Präfix wird für die - * Anzeige entfernt, der vollständige Classpath-Pfad in jmePaths hinterlegt. - */ - private void loadJmeModelsInto(TreeItem root) { - // .mesh.xml (OGRE-Format) braucht jme3-plugins, das nicht auf dem Classpath ist - String[] modelExts = {".j3o", ".obj", ".gltf", ".glb"}; - java.util.Set seen = new java.util.HashSet<>(); - java.util.List collected = new java.util.ArrayList<>(); - - String cp = System.getProperty("java.class.path", ""); - for (String entry : cp.split(File.pathSeparator)) { - if (!entry.toLowerCase().contains("jme")) continue; - File f = new File(entry); - if (!f.exists() || !f.getName().endsWith(".jar")) continue; - try (java.util.jar.JarFile jar = new java.util.jar.JarFile(f)) { - jar.stream() - .filter(je -> !je.isDirectory()) - .map(java.util.jar.JarEntry::getName) - .filter(name -> name.startsWith("Models/")) - .filter(name -> { - String lo = name.toLowerCase(); - for (String ext : modelExts) if (lo.endsWith(ext)) return true; - return false; - }) - .filter(seen::add) - .forEach(collected::add); - } catch (IOException ignored) {} - } - - java.util.Collections.sort(collected); - for (String fullPath : collected) { - // Präfix "Models/" für Anzeige entfernen; jmePaths speichert den vollen Pfad - String displayPath = fullPath.substring("Models/".length()); - addJmeModelEntry(root, displayPath, fullPath); - } - } - - /** Fügt einen JME-Modell-Eintrag ein (displayPath für Baum, fullPath für AssetManager). */ - private void addJmeModelEntry(TreeItem root, String displayPath, String fullPath) { - String[] parts = displayPath.split("/"); - TreeItem cur = root; - for (int i = 0; i < parts.length - 1; i++) { - final String dirName = parts[i]; - TreeItem found = null; - for (TreeItem child : cur.getChildren()) { - if (child.getValue().equals(dirName) && jmeFolderNodes.contains(child)) { - found = child; break; - } - } - if (found == null) { - found = new TreeItem<>(dirName); - jmeFolderNodes.add(found); - cur.getChildren().add(found); - } - cur = found; - } - TreeItem file = new TreeItem<>(parts[parts.length - 1]); - jmePaths.put(file, fullPath); - cur.getChildren().add(file); - } - - /** Lädt Assets rekursiv: Ordner zuerst (alphabetisch), dann Dateien. Keine exts = alles. */ - private void loadAssetsRecursive(TreeItem parent, Path dir, String... exts) { - if (!Files.exists(dir)) return; - try (var stream = Files.list(dir)) { - List paths = stream - .sorted(Comparator.comparing((Path p) -> Files.isDirectory(p) ? 0 : 1) - .thenComparing(p -> p.getFileName().toString().toLowerCase())) - .toList(); - for (Path p : paths) { - String name = p.getFileName().toString(); - if (Files.isDirectory(p)) { - // Texture-Set-Ordner: enthält textureset.json → als einzelner Blattknoten - if (Files.exists(p.resolve("textureset.json"))) { - TreeItem sub = new TreeItem<>(name); - itemPaths.put(sub, p); - textureSetNodes.add(sub); - parent.getChildren().add(sub); - } else { - TreeItem sub = new TreeItem<>(name); - itemPaths.put(sub, p); - parent.getChildren().add(sub); - loadAssetsRecursive(sub, p, exts); - } - } else { - String lo = name.toLowerCase(); - if (lo.endsWith(".meta")) continue; - boolean include = exts.length == 0 - || java.util.Arrays.stream(exts).anyMatch(lo::endsWith); - if (include) { - TreeItem file = new TreeItem<>(name); - itemPaths.put(file, p); - parent.getChildren().add(file); - } - } - } - } catch (IOException ignored) {} - } - - /** Liest die AnimClip-Namen aus einer J3O-Datei, ohne den JME3-Thread zu benötigen. */ - private List 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); - if (ac == null) return List.of(); - List names = new ArrayList<>(ac.getAnimClipsNames()); - names.sort(String.CASE_INSENSITIVE_ORDER); - return names; - } catch (Exception e) { - return List.of(); - } - } - - /** Hängt Clip-Namen als Unterknoten unter jeden J3O-Eintrag im Animations-Teilbaum. */ - private void addAnimClipSubNodes(TreeItem node) { - for (TreeItem child : node.getChildren()) { - Path p = itemPaths.get(child); - if (p != null && !Files.isDirectory(p) - && p.getFileName().toString().toLowerCase().endsWith(".j3o")) { - child.getChildren().clear(); - for (String clip : readAnimClipNames(p)) { - child.getChildren().add(new TreeItem<>(clip)); - } - } else if (p != null && Files.isDirectory(p)) { - addAnimClipSubNodes(child); - } - } - } - - /** Löscht Thumbnail, das zu einer .j3o-Datei gehört. */ - private void deleteJ3oSideFiles(Path j3oPath) { - try { - Files.deleteIfExists( - de.blight.editor.state.ThumbnailRenderer.sidecarPath(j3oPath, ASSET_ROOT)); - } catch (IOException ignored) {} - } - - /** - * Entfernt einen Animations-Clip aus allen AnimSet-Dateien in Characters/sets/. - * Wird aufgerufen wenn eine .j3o-Datei aus Characters/clips/ gelöscht wird. - */ - private void removeClipFromAllAnimSets(String clipName) { - Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets"); - if (!Files.isDirectory(setsDir)) return; - try (var walk = Files.walk(setsDir, 1)) { - walk.filter(p -> p.toString().endsWith(".animset.json")).forEach(setFile -> { - try { - String json = Files.readString(setFile); - com.google.gson.JsonObject root = com.google.gson.JsonParser.parseString(json).getAsJsonObject(); - com.google.gson.JsonArray clips = root.has("clips") ? root.getAsJsonArray("clips") : null; - if (clips == null) return; - boolean changed = false; - com.google.gson.JsonArray updated = new com.google.gson.JsonArray(); - for (com.google.gson.JsonElement el : clips) { - if (el.isJsonPrimitive() && el.getAsString().equals(clipName)) { - changed = true; - } else { - updated.add(el); - } - } - if (!changed) return; - root.add("clips", updated); - String pretty = new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(root); - Files.writeString(setFile, pretty); - log.info("[AssetTree] Clip '{}' aus AnimSet '{}' entfernt", clipName, setFile.getFileName()); - } catch (Exception ex) { - log.warn("[AssetTree] Fehler beim Bereinigen von {}: {}", setFile.getFileName(), ex.getMessage()); - } - }); - } catch (IOException ignored) {} - } - - /** - * Befüllt den Asset-Baum vollständig aus dem Dateisystem. - * Bekannte Verzeichnisse werden den Kategorie-Feldern zugewiesen; - * alle anderen erscheinen als generische Knoten. - */ - private void populateAssetTree(TreeItem root) { - modelsNode = null; texturesNode = null; - audioNode = null; animationsNode = null; itemsNode = null; - charactersNode = null; - jmeModelsNode = null; jmeTexturesNode = null; - jmeFolderNodes.clear(); - textureSetNodes.clear(); - - List topDirs; - try (var s = Files.list(ASSET_ROOT)) { - topDirs = s.filter(Files::isDirectory) - .filter(p -> { - String n = p.getFileName().toString(); - return !n.equals(de.blight.editor.state.ThumbnailRenderer.THUMB_DIR); - }) - .sorted(Comparator.comparing(p -> p.getFileName().toString().toLowerCase())) - .collect(java.util.stream.Collectors.toList()); - } catch (IOException e) { topDirs = List.of(); } - - for (Path dir : topDirs) { - String name = dir.getFileName().toString(); - TreeItem node = new TreeItem<>(name); - itemPaths.put(node, dir); - root.getChildren().add(node); - loadAssetsRecursive(node, dir); - - switch (name.toLowerCase()) { - case "models" -> { - modelsNode = node; - jmeModelsNode = new TreeItem<>("JME"); - jmeFolderNodes.add(jmeModelsNode); - node.getChildren().add(jmeModelsNode); - loadJmeModelsInto(jmeModelsNode); - } - case "textures" -> { - texturesNode = node; - jmeTexturesNode = new TreeItem<>("JME"); - jmeFolderNodes.add(jmeTexturesNode); - node.getChildren().add(jmeTexturesNode); - loadJmeTexturesInto(jmeTexturesNode); - } - case "audio" -> audioNode = node; - case "animations" -> { - animationsNode = node; - addAnimClipSubNodes(node); - } - case "characters" -> charactersNode = node; - case "items" -> itemsNode = node; - } - } - } - - /** Baut den Teilbaum einer Kategorie komplett neu auf. - * @param preserveExpansion true → aufgeklappte Knoten bleiben nach dem Rebuild erhalten */ - private void refreshCategoryNode(TreeItem catNode, boolean preserveExpansion) { - java.util.Set expanded = preserveExpansion ? collectExpandedPaths() : java.util.Set.of(); - clearItemPathsFor(catNode); - catNode.getChildren().clear(); - Path dir = itemPaths.get(catNode); - if (dir != null) loadAssetsRecursive(catNode, dir); - if (catNode == animationsNode) addAnimClipSubNodes(catNode); - if (catNode == modelsNode && jmeModelsNode != null) - catNode.getChildren().add(jmeModelsNode); - if (catNode == texturesNode && jmeTexturesNode != null) - catNode.getChildren().add(jmeTexturesNode); - if (preserveExpansion) { - restoreExpansion(expanded); - catNode.setExpanded(true); - } - } - - /** Baut den gesamten Asset-Baum neu aus dem Dateisystem auf. - * @param preserveExpansion true → aufgeklappte Knoten bleiben nach dem Rebuild erhalten */ - private void refreshAllAssets(boolean preserveExpansion) { - if (assetTreeRoot == null) return; - java.util.Set expanded = preserveExpansion ? collectExpandedPaths() : java.util.Set.of(); - itemPaths.clear(); - jmePaths.clear(); - assetTreeRoot.getChildren().clear(); - populateAssetTree(assetTreeRoot); - if (preserveExpansion) restoreExpansion(expanded); - } - - /** Gibt zurück, ob das aktive Tool den aufgeklappten Zustand des Asset-Baums behalten soll. */ - private boolean shouldPreserveExpansion() { - return "vegetation".equals(currentTool) || "eztree".equals(currentTool) - || "objecteditor".equals(registry.activeToolId()) || "modeleditor".equals(registry.activeToolId()) - || ("world".equals(currentTool) - && (input.activeLayer == SharedInput.LAYER_OBJECTS - || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)); - } - - /** Sammelt die Pfade (Name-Kette mit '/') aller aufgeklappten TreeItems. */ - private java.util.Set collectExpandedPaths() { - java.util.Set result = new java.util.HashSet<>(); - collectExpandedPaths(assetTreeRoot, "", result); - return result; - } - - private void collectExpandedPaths(TreeItem node, String prefix, java.util.Set result) { - for (TreeItem child : node.getChildren()) { - String path = prefix.isEmpty() ? child.getValue() : prefix + "/" + child.getValue(); - if (child.isExpanded()) result.add(path); - collectExpandedPaths(child, path, result); - } - } - - /** Klappt alle TreeItems wieder auf, deren Pfad in {@code expanded} enthalten ist. */ - private void restoreExpansion(java.util.Set expanded) { - if (expanded.isEmpty()) return; - restoreExpansion(assetTreeRoot, "", expanded); - } - - private void restoreExpansion(TreeItem node, String prefix, java.util.Set expanded) { - for (TreeItem child : node.getChildren()) { - String path = prefix.isEmpty() ? child.getValue() : prefix + "/" + child.getValue(); - if (expanded.contains(path)) child.setExpanded(true); - restoreExpansion(child, path, expanded); - } - } - - private void clearItemPathsFor(TreeItem item) { - for (TreeItem child : item.getChildren()) clearItemPathsFor(child); - if (item != modelsNode && item != texturesNode && item != audioNode && item != animationsNode && item != charactersNode) - itemPaths.remove(item); - textureSetNodes.remove(item); - } - - /** - * Kopiert MTL-Dateien und darin referenzierte Texturen aus dem Quell-Ordner - * der OBJ-Datei in das Ziel-Verzeichnis (blight-assets/Models/). - */ - private static void copyObjCompanions(File objFile, Path destDir) { - Path srcDir = objFile.toPath().getParent(); - - // MTL-Dateinamen aus dem OBJ selbst lesen (mtllib-Direktiven) - java.util.Set mtlNames = new java.util.LinkedHashSet<>(); - String baseName = objFile.getName().replaceFirst("\\.[^.]+$", ""); - mtlNames.add(baseName + ".mtl"); // Standardname - mtlNames.add(baseName + ".MTL"); - try (java.io.BufferedReader br = java.nio.file.Files.newBufferedReader(objFile.toPath())) { - String line; - while ((line = br.readLine()) != null) { - if (line.startsWith("mtllib ")) mtlNames.add(line.substring(7).trim()); - } - } catch (IOException ignored) {} - - for (String mtlName : mtlNames) { - Path mtlSrc = srcDir.resolve(mtlName); - if (!java.nio.file.Files.exists(mtlSrc)) continue; - copyQuiet(mtlSrc, destDir.resolve(mtlSrc.getFileName())); - copyMtlTextures(mtlSrc, srcDir, destDir); - } - } - - /** Kopiert Texturdateien, die in einer MTL-Datei referenziert werden. */ - private static void copyMtlTextures(Path mtlFile, Path srcDir, Path destDir) { - try (java.io.BufferedReader br = java.nio.file.Files.newBufferedReader(mtlFile)) { - String line; - while ((line = br.readLine()) != null) { - String lo = line.strip().toLowerCase(); - if (!lo.startsWith("map_")) continue; - int sp = line.indexOf(' '); - if (sp < 0) continue; - String ref = line.substring(sp + 1).strip(); - // Optionale -Flags überspringen (z.B. "map_Kd -s 1 1 1 texture.png") - String[] parts = ref.split("\\s+"); - String texName = parts[parts.length - 1]; - if (texName.isEmpty()) continue; - Path texSrc = srcDir.resolve(texName); - if (java.nio.file.Files.exists(texSrc)) { - copyQuiet(texSrc, destDir.resolve(texSrc.getFileName())); - } - } - } catch (IOException ignored) {} - } - - private static void copyQuiet(Path src, Path dst) { - try { Files.copy(src, dst, java.nio.file.StandardCopyOption.REPLACE_EXISTING); } - catch (IOException ignored) {} - } - - /** - * Konvertiert / normalisiert eine Audiodatei zu OGG Vorbis bei 48000 Hz. - * Das Ziel-Format stimmt mit PipeWire/PulseAudio überein, sodass OpenALSofts - * Spatial-Mixer kein On-the-fly-Resampling durchführen muss (kein Knistern). - */ - private static Path convertToOgg(Path src, Path destOgg) throws IOException { - try { - Process proc = new ProcessBuilder( - "ffmpeg", "-i", src.toString(), - "-ar", "48000", - "-q:a", "4", - destOgg.toString(), "-y") - .redirectErrorStream(true) - .start(); - proc.getInputStream().transferTo(java.io.OutputStream.nullOutputStream()); - int exit = proc.waitFor(); - if (exit != 0) throw new IOException("ffmpeg fehlgeschlagen (exit " + exit + ")"); - return destOgg; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IOException("Konvertierung unterbrochen", e); - } - } - - /** Stellt sicher, dass eine Audiodatei als OGG vorliegt. - * Ist sie bereits OGG, wird sie einfach kopiert. Andernfalls konvertiert. - * Gibt den relativen Asset-Pfad zurück. */ - private static String ensureOgg(File chosen, Path assetRoot) throws IOException { - Path src = chosen.toPath(); - String baseName = chosen.getName().replaceFirst("\\.[^.]+$", ""); - boolean alreadyOgg = chosen.getName().toLowerCase().endsWith(".ogg"); - Path destOgg = src.resolveSibling(baseName + ".ogg"); - if (!alreadyOgg) convertToOgg(src, destOgg); - else if (!src.equals(destOgg)) Files.copy(src, destOgg, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - return assetRoot.relativize(destOgg).toString().replace('\\', '/'); - } - - private void handleImport(javafx.stage.Window owner) { - FileChooser fc = new FileChooser(); - fc.setTitle("Assets importieren"); - fc.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("Alle unterstützten Dateien", - "*.j3o","*.obj","*.gltf","*.glb", - "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds", - "*.ogg","*.wav","*.mp3"), - new FileChooser.ExtensionFilter("Modelle", "*.j3o","*.obj","*.gltf","*.glb"), - new FileChooser.ExtensionFilter("Texturen", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"), - new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg","*.wav","*.mp3") - ); - var files = fc.showOpenMultipleDialog(owner); - if (files == null) return; - - for (File file : files) { - String name = file.getName().toLowerCase(); - boolean isNativeModel = name.matches(".*\\.(obj|fbx|gltf|glb)"); - boolean isJ3o = name.endsWith(".j3o"); - boolean isAudio = name.matches(".*\\.(ogg|wav|mp3)"); - boolean isModel = isNativeModel || isJ3o; - - String subDir = isModel ? "Models" : isAudio ? "audio" : "Textures"; - TreeItem parent = isModel ? modelsNode : isAudio ? audioNode : texturesNode; - archiveOriginal(file, isModel ? "models" : isAudio ? "audio" : "textures"); - - try { - Path destDir = ASSET_ROOT.resolve(subDir); - Files.createDirectories(destDir); - - if (isAudio) { - String baseName = file.getName().replaceFirst("\\.[^.]+$", ""); - Path destOgg = destDir.resolve(baseName + ".ogg"); - setStatus("Normalisiere " + file.getName() + " → 48 kHz OGG …"); - convertToOgg(file.toPath(), destOgg); - String finalName = baseName + ".ogg"; - TreeItem newItem = new TreeItem<>(finalName); - itemPaths.put(newItem, destOgg); - parent.getChildren().add(newItem); - parent.setExpanded(true); - setStatus("Importiert: " + finalName); - } else { - Path destFile = destDir.resolve(file.getName()); - Files.copy(file.toPath(), destFile, StandardCopyOption.REPLACE_EXISTING); - if (isNativeModel) { - if (name.endsWith(".obj")) copyObjCompanions(file, destDir); - String assetPath = subDir + "/" + file.getName(); - String baseName = file.getName().replaceFirst("\\.[^.]+$", ""); - Path destJ3o = destDir.resolve(baseName + ".j3o"); - boolean keepCtrls = name.matches(".*\\.(gltf|glb)"); - input.modelConvertQueue.offer( - new SharedInput.ModelConvertRequest(assetPath, destJ3o, destFile, keepCtrls)); - setStatus("Konvertiere " + file.getName() + " → .j3o …"); - } else { - TreeItem newItem = new TreeItem<>(file.getName()); - itemPaths.put(newItem, destFile); - parent.getChildren().add(newItem); - parent.setExpanded(true); - setStatus("Importiert: " + file.getName()); - } - } - } catch (IOException ex) { - setStatus("Fehler beim Import: " + ex.getMessage()); - } - } - } - - private void takeEditorScreenshot() { - playScreenshotClick(); - WritableImage image = primaryStage.getScene().snapshot(null); - String timestamp = java.time.LocalDateTime.now() - .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")); - Path dir = BlightHome.resolve("screenshots"); - try { - Files.createDirectories(dir); - File out = dir.resolve("editor_" + timestamp + ".png").toFile(); - ImageIO.write(SwingFXUtils.fromFXImage(image, null), "PNG", out); - setStatus("Screenshot gespeichert: " + out.getPath()); - } catch (IOException ex) { - setStatus("Screenshot fehlgeschlagen: " + ex.getMessage()); - } - } - - private void playScreenshotClick() { - Thread t = new Thread(() -> { - try { - int sr = 44100; - int frames = (int) (sr * 0.08); - byte[] buf = new byte[frames * 2]; - java.util.Random rng = new java.util.Random(); - for (int i = 0; i < frames; i++) { - double env = Math.exp(-i / (sr * 0.012)); - double s = (rng.nextGaussian() * 0.55 + Math.sin(2 * Math.PI * 3200.0 * i / sr) * 0.45) * env * 0.85; - short val = (short) Math.max(-32767, Math.min(32767, s * 32767)); - buf[i * 2] = (byte) (val & 0xff); - buf[i * 2 + 1] = (byte) ((val >> 8) & 0xff); - } - javax.sound.sampled.AudioFormat fmt = new javax.sound.sampled.AudioFormat(sr, 16, 1, true, false); - javax.sound.sampled.DataLine.Info info = new javax.sound.sampled.DataLine.Info(javax.sound.sampled.SourceDataLine.class, fmt); - javax.sound.sampled.SourceDataLine line = (javax.sound.sampled.SourceDataLine) javax.sound.sampled.AudioSystem.getLine(info); - line.open(fmt); - line.start(); - line.write(buf, 0, buf.length); - line.drain(); - line.close(); - } catch (Exception ignored) {} - }, "screenshot-click"); - t.setDaemon(true); - t.start(); - } - - private void archiveOriginal(File source, String assetType) { - java.nio.file.Path dest = de.blight.editor.ProjectRoot.PATH.resolve("assets").resolve("imported").resolve(assetType); - try { - java.nio.file.Files.createDirectories(dest); - java.nio.file.Files.copy(source.toPath(), dest.resolve(source.getName()), - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - log.info("[Import] Archiviert: {}/{}", assetType, source.getName()); - } catch (IOException ex) { - log.warn("[Import] Archivierung fehlgeschlagen ({}/{}): {}", - assetType, source.getName(), ex.getMessage()); - } - } - - private void handleTextureImport(javafx.stage.Window owner) { - FileChooser fc = new FileChooser(); - fc.setTitle("Texturen importieren (nicht-PNG wird automatisch konvertiert)"); - fc.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("Bildateien", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"), - new FileChooser.ExtensionFilter("PNG", "*.png"), - new FileChooser.ExtensionFilter("JPEG", "*.jpg","*.jpeg"), - new FileChooser.ExtensionFilter("Weitere", "*.bmp","*.tga","*.dds")); - List files = fc.showOpenMultipleDialog(owner); - if (files == null) return; - Path destDir = ASSET_ROOT.resolve("Textures"); - int ok = 0; - for (File file : files) { - try { - Files.createDirectories(destDir); - Path dest = importTextureAsPng(file, destDir); - TreeItem item = new TreeItem<>(dest.getFileName().toString()); - itemPaths.put(item, dest); - texturesNode.getChildren().add(item); - ok++; - } catch (IOException ex) { - setStatus("Fehler beim Import von " + file.getName() + ": " + ex.getMessage()); - } - } - if (ok > 0) { - texturesNode.setExpanded(true); - setStatus(ok + " Textur(en) importiert"); - } - } - - /** - * Kopiert oder konvertiert eine Bilddatei als PNG in destDir. - * PNG + DDS werden unverändert kopiert; JPG/BMP/TGA werden nach PNG konvertiert. - * Gibt den Pfad der resultierenden Datei zurück. - */ - private static Path importTextureAsPng(File src, Path destDir) throws IOException { - String nameL = src.getName().toLowerCase(); - String base = src.getName().replaceFirst("\\.[^.]+$", ""); - - // DDS bleibt unverändert (GPU-komprimiertes Format, JME3 lädt es nativ) - if (nameL.endsWith(".dds") || nameL.endsWith(".png")) { - Path dest = destDir.resolve(src.getName()); - Files.copy(src.toPath(), dest, StandardCopyOption.REPLACE_EXISTING); - return dest; - } - - Path destPng = destDir.resolve(base + ".png"); - BufferedImage img = null; - - if (nameL.endsWith(".tga")) { - img = loadTgaAsBufferedImage(src); - } else { - // JPG, JPEG, BMP, GIF – von Java ImageIO unterstützt - img = ImageIO.read(src); - } - - if (img == null) { - // Fallback: Datei unverändert kopieren - Path dest = destDir.resolve(src.getName()); - Files.copy(src.toPath(), dest, StandardCopyOption.REPLACE_EXISTING); - return dest; - } - - // Sicherstellen, dass das Bild ARGB hat (für verlustfreies PNG) - if (img.getType() != BufferedImage.TYPE_INT_ARGB) { - BufferedImage rgba = new BufferedImage(img.getWidth(), img.getHeight(), - BufferedImage.TYPE_INT_ARGB); - java.awt.Graphics2D g = rgba.createGraphics(); - g.drawImage(img, 0, 0, null); - g.dispose(); - img = rgba; - } - - ImageIO.write(img, "PNG", destPng.toFile()); - return destPng; - } - - /** Lädt eine TGA-Datei mit JME3's TGALoader und gibt ein BufferedImage zurück. */ - private static BufferedImage loadTgaAsBufferedImage(File tgaFile) throws IOException { - com.jme3.texture.plugins.TGALoader loader = new com.jme3.texture.plugins.TGALoader(); - com.jme3.asset.TextureKey key = new com.jme3.asset.TextureKey(tgaFile.getName(), true); - com.jme3.asset.AssetInfo info = new com.jme3.asset.AssetInfo(null, key) { - @Override public java.io.InputStream openStream() { - try { return new java.io.FileInputStream(tgaFile); } - catch (java.io.FileNotFoundException e) { throw new RuntimeException(e); } - } - }; - - com.jme3.texture.Image jmeImg = (com.jme3.texture.Image) loader.load(info); - int w = jmeImg.getWidth(), h = jmeImg.getHeight(); - ByteBuffer data = jmeImg.getData(0); - data.rewind(); - - BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); - int[] pixels = new int[w * h]; - com.jme3.texture.Image.Format fmt = jmeImg.getFormat(); - for (int i = 0; i < w * h; i++) { - int r, g, b, a; - switch (fmt) { - case RGBA8 -> { r = data.get()&0xFF; g = data.get()&0xFF; b = data.get()&0xFF; a = data.get()&0xFF; } - case RGB8 -> { r = data.get()&0xFF; g = data.get()&0xFF; b = data.get()&0xFF; a = 255; } - case BGR8 -> { b = data.get()&0xFF; g = data.get()&0xFF; r = data.get()&0xFF; a = 255; } - case BGRA8 -> { b = data.get()&0xFF; g = data.get()&0xFF; r = data.get()&0xFF; a = data.get()&0xFF; } - default -> { r = g = b = a = 128; } - } - pixels[i] = (a<<24)|(r<<16)|(g<<8)|b; - } - bi.setRGB(0, 0, w, h, pixels, 0, w); - return bi; - } - - // ── Textur-Set-Import ───────────────────────────────────────────────────── - - private void openTextureSetImport(javafx.stage.Window owner) { - de.blight.editor.ui.TextureImportDialog dlg = new de.blight.editor.ui.TextureImportDialog(ASSET_ROOT); - if (owner != null) dlg.initOwner(owner); - dlg.showAndWait(); - Path imported = dlg.getLastImportedDir(); - if (imported == null) return; - refreshCategoryNode(texturesNode, true); - // importierten Knoten im Baum vorselektieren - for (Map.Entry, Path> e : itemPaths.entrySet()) { - if (imported.equals(e.getValue())) { - TreeItem target = e.getKey(); - if (assetTree != null) { - assetTree.getSelectionModel().select(target); - int idx = assetTree.getRow(target); - if (idx >= 0) assetTree.scrollTo(idx); - } - break; - } - } - } - - // ── Texturen-ZIP-Import ─────────────────────────────────────────────────── - - /** Fragt nach einer oder mehreren ZIP-Dateien, entpackt sie und verknüpft Maps. */ - private void handleZipTextureImport(javafx.stage.Window owner) { - FileChooser fc = new FileChooser(); - fc.setTitle("Texturen-ZIPs importieren"); - fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("ZIP-Archiv", "*.zip")); - List zipFiles = fc.showOpenMultipleDialog(owner); - if (zipFiles == null || zipFiles.isEmpty()) return; - - int totalExtracted = 0; - int totalLinked = 0; - List errors = new java.util.ArrayList<>(); - - for (File zipFile : zipFiles) { - try { - int[] result = importSingleZip(zipFile); - totalExtracted += result[0]; - totalLinked += result[1]; - } catch (IOException ex) { - errors.add(zipFile.getName() + ": " + ex.getMessage()); - } - } - - refreshCategoryNode(texturesNode, true); - if (texturesNode != null) texturesNode.setExpanded(true); - - if (!errors.isEmpty()) { - setStatus("Fehler: " + String.join(", ", errors)); - } else { - String s = totalExtracted + " Dateien aus " + zipFiles.size() + " ZIP(s) importiert"; - if (totalLinked > 0) s += " – " + totalLinked + " Map(s) verknüpft"; - setStatus(s); - } - } - - /** - * Importiert eine einzelne ZIP-Datei nach Textures/{zipBaseName}/. - * @return int[2]: [0] = extrahierte Dateien, [1] = verknüpfte Maps (0 oder 1) - */ - private int[] importSingleZip(File zipFile) throws IOException { - String zipBase = zipFile.getName().replaceFirst("\\.zip$", ""); - Path destDir = ASSET_ROOT.resolve("Textures").resolve(zipBase); - Files.createDirectories(destDir); - - Map typeToPath = new HashMap<>(); - int extracted = 0; - - try (ZipInputStream zis = new ZipInputStream( - new java.io.BufferedInputStream(new java.io.FileInputStream(zipFile)))) { - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - if (entry.isDirectory()) { zis.closeEntry(); continue; } - String entryName = java.nio.file.Paths.get(entry.getName()).getFileName().toString(); - if (!isSupportedAsset(entryName)) { zis.closeEntry(); continue; } - Path tmp = destDir.resolve(entryName); - Files.copy(zis, tmp, StandardCopyOption.REPLACE_EXISTING); - Path png = importTextureAsPng(tmp.toFile(), destDir); - if (!png.equals(tmp)) Files.deleteIfExists(tmp); - String type = detectTexMapType(entryName); - if (type != null) typeToPath.merge(type, png, (a, b) -> a); // GL bevorzugt - extracted++; - zis.closeEntry(); - } - } - - // NormalGL bevorzugen, NormalDX nur als Fallback - if (!typeToPath.containsKey("normal")) { - if (typeToPath.containsKey("normalGL")) typeToPath.put("normal", typeToPath.get("normalGL")); - else if (typeToPath.containsKey("normalDX")) typeToPath.put("normal", typeToPath.get("normalDX")); - } - - Path colorPath = typeToPath.get("color"); - int linked = 0; - if (colorPath != null) { - TextureMapData meta = new TextureMapData(); - meta.colorMap = ASSET_ROOT.relativize(colorPath).toString().replace('\\', '/'); - Path normPath = typeToPath.get("normal"); - Path dispPath = typeToPath.get("displacement"); - if (normPath != null) meta.normalMap = ASSET_ROOT.relativize(normPath).toString().replace('\\', '/'); - if (dispPath != null) meta.displacementMap = ASSET_ROOT.relativize(dispPath).toString().replace('\\', '/'); - if (typeToPath.containsKey("roughness")) meta.roughnessMap = ASSET_ROOT.relativize(typeToPath.get("roughness")).toString().replace('\\', '/'); - if (typeToPath.containsKey("ao")) meta.aoMap = ASSET_ROOT.relativize(typeToPath.get("ao")).toString().replace('\\', '/'); - saveTexMeta(colorPath, meta); - autoAssignMapsForTexture(meta.colorMap, meta); - linked = 1; - } - - return new int[]{ extracted, linked }; - } - - /** Gibt true zurück wenn die Datei ein unterstütztes Asset ist (Bild, Audio, Modell …). */ - private static boolean isSupportedAsset(String filename) { - String lo = filename.toLowerCase(); - return lo.endsWith(".png") || lo.endsWith(".jpg") || lo.endsWith(".jpeg") - || lo.endsWith(".tga") || lo.endsWith(".bmp") || lo.endsWith(".dds") - || lo.endsWith(".exr") || lo.endsWith(".hdr") - || lo.endsWith(".ogg") || lo.endsWith(".wav") - || lo.endsWith(".gltf") || lo.endsWith(".glb") || lo.endsWith(".fbx") - || lo.endsWith(".obj") || lo.endsWith(".j3o"); - } - - /** Erkennt den Map-Typ anhand des Dateinamens (Suffix vor der Extension). */ - private static String detectTexMapType(String filename) { - String base = filename.toLowerCase().replaceFirst("\\.[^.]+$", ""); - if (base.endsWith("_color") || base.endsWith("_colour") || base.endsWith("_diffuse") || base.endsWith("_albedo")) - return "color"; - if (base.endsWith("_normalgl") || base.endsWith("_normal_gl") || base.endsWith("_normalgl_directx")) - return "normalGL"; - if (base.endsWith("_normaldx") || base.endsWith("_normal_dx") || base.endsWith("_normaldirectx")) - return "normalDX"; - if (base.endsWith("_normal")) - return "normalGL"; // generic normal = GL convention assumed - if (base.endsWith("_displacement") || base.endsWith("_height") || base.endsWith("_disp")) - return "displacement"; - if (base.endsWith("_roughness") || base.endsWith("_rough")) - return "roughness"; - if (base.endsWith("_ambientocclusion") || base.endsWith("_ao") || base.endsWith("_occlusion")) - return "ao"; - return null; - } - - // ── Texture-Map-Kontextmenü-Aktion ──────────────────────────────────────── - - /** - * Standalone-Textur: erstellt einen Set-Ordner, verschiebt die Textur hinein, - * lässt den User eine Normal- oder Displacement-Map wählen und schreibt textureset.json. - */ - private void assignMapToTexture(Path colorTexPath, boolean isDisplacement, javafx.stage.Window owner) { - TextureChooser chooser = new TextureChooser(ASSET_ROOT, true); - if (primaryStage != null) chooser.initOwner(primaryStage); - chooser.showAndWait().ifPresent(picked -> { - if (picked == null || picked.isEmpty()) return; - - // Set-Ordner anlegen (gleicher Name wie Textur ohne Extension) - String baseName = colorTexPath.getFileName().toString().replaceFirst("\\.[^.]+$", ""); - Path setDir = colorTexPath.getParent().resolve(baseName); - Path newColor; - try { - Files.createDirectories(setDir); - newColor = setDir.resolve(colorTexPath.getFileName()); - if (!Files.exists(newColor)) Files.move(colorTexPath, newColor); - } catch (IOException ex) { - setStatus("Fehler beim Erstellen des Set-Ordners: " + ex.getMessage()); - return; - } - - String oldColorRel = ASSET_ROOT.relativize(colorTexPath).toString().replace('\\', '/'); - String newColorRel = ASSET_ROOT.relativize(newColor).toString().replace('\\', '/'); - - TextureMapData meta = new TextureMapData(); - meta.colorMap = newColorRel; - if (isDisplacement) meta.displacementMap = picked; - else meta.normalMap = picked; - saveTexMeta(newColor, meta); - - // Slots aktualisieren: alten Pfad → neuen Pfad - updateSlotsColorPath(oldColorRel, newColorRel); - autoAssignMapsForTexture(newColorRel, meta); - - // Baum aktualisieren: Datei-Item → Set-Item - TreeItem fileItem = itemPaths.entrySet().stream() - .filter(en -> colorTexPath.equals(en.getValue())) - .map(Map.Entry::getKey).findFirst().orElse(null); - if (fileItem != null) { - TreeItem parentItem = fileItem.getParent(); - if (parentItem != null) parentItem.getChildren().remove(fileItem); - itemPaths.remove(fileItem); - } - TreeItem parentByPath = itemPaths.entrySet().stream() - .filter(en -> setDir.getParent().equals(en.getValue())) - .map(Map.Entry::getKey).findFirst().orElse(null); - if (parentByPath != null) { - TreeItem setItem = new TreeItem<>(baseName); - itemPaths.put(setItem, setDir); - textureSetNodes.add(setItem); - parentByPath.getChildren().add(setItem); - } - - String type = isDisplacement ? "Displacement" : "Normal"; - setStatus(type + "-Map für " + baseName + " gesetzt – Textur-Set erstellt"); - }); - } - - /** - * Textur-Set-Knoten: lädt textureset.json, lässt den User eine Map wählen, speichert. - */ - private void assignMapToSetNode(TreeItem setItem, Path setDir, boolean isDisplacement, - javafx.stage.Window owner) { - TextureChooser chooser = new TextureChooser(ASSET_ROOT, true); - if (primaryStage != null) chooser.initOwner(primaryStage); - chooser.showAndWait().ifPresent(picked -> { - if (picked == null || picked.isEmpty()) return; - TextureMapData meta = loadTexMeta(setDir); - if (isDisplacement) meta.displacementMap = picked; - else meta.normalMap = picked; - saveTexMeta(setDir, meta); - if (!meta.colorMap.isEmpty()) autoAssignMapsForTexture(meta.colorMap, meta); - String type = isDisplacement ? "Displacement" : "Normal"; - setStatus(type + "-Map für " + setDir.getFileName() + " gesetzt"); - }); - } - - /** Ersetzt in allen Slots einen alten Texturpfad durch den neuen (nach Datei-Verschiebung). */ - private void updateSlotsColorPath(String oldRel, String newRel) { - for (int i = 0; i < 4; i++) { - if (oldRel.equals(input.terrainTexturePaths[i])) input.terrainTexturePaths[i] = newRel; - if (oldRel.equals(input.upperTexturePaths[i])) input.upperTexturePaths[i] = newRel; - if (oldRel.equals(input.thirdTexturePaths[i])) input.thirdTexturePaths[i] = newRel; - } - input.terrainTexturesChanged = true; - input.upperTexturesChanged = true; - input.voxelTexturesChanged = true; - } - - /** - * Wenn colorTexRelPath in irgendeinem Terrain/Upper/Third-Slot steckt, - * wird die Normal-Map aus meta dort automatisch eingetragen. - */ - private void autoAssignMapsForTexture(String colorTexRelPath, TextureMapData meta) { - boolean anyChanged = false; - for (int i = 0; i < 4; i++) { - if (colorTexRelPath.equals(input.terrainTexturePaths[i])) { - if (!meta.normalMap.isEmpty()) { input.terrainNormalMapPaths[i] = meta.normalMap; anyChanged = true; } - if (!meta.displacementMap.isEmpty()) { input.terrainDisplacementMapPaths[i] = meta.displacementMap; anyChanged = true; } - } - if (colorTexRelPath.equals(input.upperTexturePaths[i])) { - if (!meta.normalMap.isEmpty()) { input.upperNormalMapPaths[i] = meta.normalMap; anyChanged = true; } - if (!meta.displacementMap.isEmpty()) { input.upperDisplacementMapPaths[i] = meta.displacementMap; anyChanged = true; } - } - if (colorTexRelPath.equals(input.thirdTexturePaths[i])) { - if (!meta.normalMap.isEmpty()) { input.thirdNormalMapPaths[i] = meta.normalMap; anyChanged = true; } - if (!meta.displacementMap.isEmpty()) { input.thirdDisplacementMapPaths[i] = meta.displacementMap; anyChanged = true; } - } - } - if (anyChanged) { - input.terrainNormalMapsChanged = true; - input.upperNormalMapsChanged = true; - input.thirdNormalMapsChanged = true; - input.voxelTexturesChanged = true; - } - } - - // ── Texture-Metadaten (textureset.json) ─────────────────────────────────── - - private static final Gson TEX_GSON = new Gson(); - - /** Löscht ein Verzeichnis mitsamt aller Inhalte. */ - private static void deleteDirectoryRecursive(Path dir) throws IOException { - try (var walk = Files.walk(dir)) { - walk.sorted(Comparator.reverseOrder()) - .forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException ignored) {} }); - } - } - - private Path texMetaFile(Path colorTexPath) { - Path dir = Files.isDirectory(colorTexPath) ? colorTexPath : colorTexPath.getParent(); - return dir.resolve("textureset.json"); - } - - private TextureMapData loadTexMeta(Path colorTexPath) { - Path mf = texMetaFile(colorTexPath); - if (!Files.exists(mf)) return new TextureMapData(); - try (Reader r = Files.newBufferedReader(mf)) { - TextureMapData d = TEX_GSON.fromJson(r, TextureMapData.class); - return d != null ? d : new TextureMapData(); - } catch (IOException e) { - return new TextureMapData(); - } - } - - private void saveTexMeta(Path colorTexPath, TextureMapData meta) { - Path mf = texMetaFile(colorTexPath); - try { - Files.createDirectories(mf.getParent()); - try (Writer w = Files.newBufferedWriter(mf)) { - TEX_GSON.toJson(meta, w); - } - } catch (IOException ex) { - setStatus("Fehler beim Speichern der Textur-Metadaten: " + ex.getMessage()); - } - } - - // ── Textur-Slot Details-Popup ───────────────────────────────────────────── - - private void showTextureSlotDetails(String texRelPath, int slot, int group, - javafx.scene.Node owner, double screenX, double screenY) { - Path texAbs = ASSET_ROOT.resolve(texRelPath.replace('/', java.io.File.separatorChar)); - TextureMapData meta = loadTexMeta(texAbs); - - String normPath = switch (group) { - case 0 -> input.terrainNormalMapPaths[slot]; - case 1 -> input.upperNormalMapPaths[slot]; - default -> input.thirdNormalMapPaths[slot]; - }; - - javafx.scene.layout.VBox box = new javafx.scene.layout.VBox(5); - box.setPadding(new Insets(10)); - box.setStyle("-fx-background-color:#fafafa; -fx-border-color:#888; -fx-border-width:1;" - + "-fx-effect:dropshadow(gaussian,rgba(0,0,0,0.25),8,0,2,2);"); - box.setMinWidth(270); - - addDetailRow(box, "🖼 Textur", texAbs); - - if (normPath != null && !normPath.isEmpty()) { - box.getChildren().add(new javafx.scene.control.Separator()); - addDetailRow(box, "🔵 Normal", ASSET_ROOT.resolve(normPath.replace('/', java.io.File.separatorChar))); - } - if (!meta.roughnessMap.isEmpty()) { - box.getChildren().add(new javafx.scene.control.Separator()); - addDetailRow(box, "🟤 Roughness", ASSET_ROOT.resolve(meta.roughnessMap.replace('/', java.io.File.separatorChar))); - } - if (!meta.aoMap.isEmpty()) { - box.getChildren().add(new javafx.scene.control.Separator()); - addDetailRow(box, "⬛ AO", ASSET_ROOT.resolve(meta.aoMap.replace('/', java.io.File.separatorChar))); - } - - javafx.stage.Popup popup = new javafx.stage.Popup(); - popup.getContent().add(box); - popup.setAutoHide(true); - if (owner.getScene() != null) - popup.show(owner.getScene().getWindow(), screenX, screenY); - } - - private static void addDetailRow(javafx.scene.layout.VBox box, String label, Path file) { - Label nameLbl = new Label(label + ": " + file.getFileName()); - nameLbl.setStyle("-fx-font-weight:bold; -fx-font-size:11;"); - box.getChildren().add(nameLbl); - - if (Files.exists(file)) { - StringBuilder sb = new StringBuilder(" "); - int[] dims = readImageDimensions(file); - if (dims != null) sb.append(dims[0]).append(" × ").append(dims[1]).append(" · "); - String ext = file.getFileName().toString(); - int dot = ext.lastIndexOf('.'); - if (dot >= 0) sb.append(ext.substring(dot + 1).toUpperCase()).append(" · "); - try { sb.append(formatFileSize(Files.size(file))); } catch (IOException ignored) { sb.append("?"); } - Label info = new Label(sb.toString()); - info.setStyle("-fx-text-fill:#555; -fx-font-size:10;"); - box.getChildren().add(info); - } else { - Label missing = new Label(" (Datei nicht gefunden)"); - missing.setStyle("-fx-text-fill:#c00; -fx-font-size:10;"); - box.getChildren().add(missing); - } - } - - private static int[] readImageDimensions(Path p) { - try (var iis = javax.imageio.ImageIO.createImageInputStream(p.toFile())) { - java.util.Iterator readers = javax.imageio.ImageIO.getImageReaders(iis); - if (readers.hasNext()) { - javax.imageio.ImageReader reader = readers.next(); - try { - reader.setInput(iis); - return new int[]{ reader.getWidth(0), reader.getHeight(0) }; - } finally { - reader.dispose(); - } - } - } catch (Exception ignored) {} - return null; - } - - private static String formatFileSize(long bytes) { - if (bytes < 1024) return bytes + " B"; - if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); - return String.format("%.1f MB", bytes / (1024.0 * 1024)); - } - - private void handleAudioImport(javafx.stage.Window owner) { - FileChooser fc = new FileChooser(); - fc.setTitle("Audio importieren"); - fc.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("Audio-Dateien", "*.ogg","*.wav","*.mp3"), - new FileChooser.ExtensionFilter("OGG", "*.ogg"), - new FileChooser.ExtensionFilter("WAV / MP3", "*.wav","*.mp3")); - List files = fc.showOpenMultipleDialog(owner); - if (files == null) return; - Path destDir = ASSET_ROOT.resolve("audio"); - int ok = 0; - for (File file : files) { - try { - Files.createDirectories(destDir); - String name = file.getName().toLowerCase(); - String baseName = file.getName().replaceFirst("\\.[^.]+$", ""); - Path dest = destDir.resolve(baseName + ".ogg"); - setStatus("Normalisiere " + file.getName() + " → 48 kHz OGG …"); - convertToOgg(file.toPath(), dest); - TreeItem item = new TreeItem<>(dest.getFileName().toString()); - itemPaths.put(item, dest); - audioNode.getChildren().add(item); - ok++; - } catch (IOException ex) { - setStatus("Fehler: " + ex.getMessage()); - } - } - if (ok > 0) { - audioNode.setExpanded(true); - setStatus(ok + " Audio-Datei(en) importiert"); - } - } - - // ── Modell-Editor ──────────────────────────────────────────────────────── - - - // ── Zentraler Bereich: JME3-Viewport ──────────────────────────────────── - - private StackPane buildViewport() { - viewport = new ImageView(jfxImage); - viewport.setPreserveRatio(false); - viewport.setFocusTraversable(true); - - compassCanvas = new javafx.scene.canvas.Canvas(100, 100); - compassCanvas.setMouseTransparent(true); - StackPane.setAlignment(compassCanvas, javafx.geometry.Pos.BOTTOM_RIGHT); - StackPane.setMargin(compassCanvas, new Insets(0, 10, 10, 0)); - drawCompass(); - - StackPane pane = new StackPane(viewport, compassCanvas); - pane.setStyle("-fx-background-color: #1a1a2e;"); - - javafx.animation.PauseTransition resizeDebounce = - new javafx.animation.PauseTransition(javafx.util.Duration.millis(150)); - resizeDebounce.setOnFinished(ev -> { - int w = (int) pane.getWidth(); - int h = (int) pane.getHeight(); - if (w > 0 && h > 0) input.resizeRequest.set(new int[]{w, h}); - }); - - pane.widthProperty().addListener((o, oldW, newW) -> { - viewport.setFitWidth(newW.doubleValue()); - resizeDebounce.playFromStart(); - }); - pane.heightProperty().addListener((o, oldH, newH) -> { - viewport.setFitHeight(newH.doubleValue()); - resizeDebounce.playFromStart(); - }); - - // Camera-Rotation: Filter am StackPane (Capture-Phase) → feuert immer, - // auch wenn Overlay-Nodes (Tab-Button, Asset-Panel) das Event abfangen würden. - pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_PRESSED, e -> { - if (e.getButton() == MouseButton.MIDDLE - || (e.isPrimaryButtonDown() && e.isSecondaryButtonDown()) - || (e.getButton() == MouseButton.PRIMARY && e.isAltDown())) { - prevDragX = e.getX(); prevDragY = e.getY(); - } - }); - pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_DRAGGED, e -> { - boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); - boolean altLeft = e.isPrimaryButtonDown() && e.isAltDown(); - if (e.isMiddleButtonDown() || bothDown || altLeft) { - double dx = e.getX() - prevDragX; - double dy = e.getY() - prevDragY; - input.addMouseDelta((int) dx, (int) dy); - prevDragX = e.getX(); prevDragY = e.getY(); - if (bothDown) stopEditTimer(); - } - }); - - viewport.setOnMousePressed(e -> { - viewport.requestFocus(); - boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); - - if (isObjectMode()) { - if (e.getButton() == MouseButton.PRIMARY && !bothDown) { - input.objectClickQueue.offer( - new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, e.isShiftDown())); - objDragPrevX = e.getX(); objDragPrevY = e.getY(); - objDragging = true; - } else if (e.getButton() == MouseButton.SECONDARY && !bothDown) { - input.objectClickQueue.offer( - new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), true, false)); - objDragPrevX = e.getX(); objDragPrevY = e.getY(); - } - return; - } - - if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR) { - if (e.getButton() == MouseButton.PRIMARY) { - input.routinePointClickQueue.offer( - new SharedInput.RoutinePointClick((float) e.getX(), (float) e.getY())); - } else if (e.getButton() == MouseButton.SECONDARY) { - input.routineYawDragQueue.offer( - new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), false)); - } - return; - } - - if (input.activeLayer == SharedInput.LAYER_WATERFALL) { - if (e.getButton() == MouseButton.PRIMARY) { - input.waterfallClickQueue.offer( - new SharedInput.WaterfallClick((float)e.getX(), (float)e.getY(), false)); - } else if (e.getButton() == MouseButton.SECONDARY) { - input.waterfallClickQueue.offer( - new SharedInput.WaterfallClick((float)e.getX(), (float)e.getY(), true)); - } - return; - } - - if (input.activeLayer == SharedInput.LAYER_ITEMS) { - if (e.getButton() == MouseButton.PRIMARY && !bothDown) { - input.itemClickQueue.offer( - new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, false)); - } else if (e.getButton() == MouseButton.SECONDARY) { - input.itemClickQueue.offer( - new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), true, false)); - } - return; - } - - if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) { - if (e.getButton() == MouseButton.PRIMARY && !bothDown) { - input.pathNetworkClickQueue.offer( - new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), false)); - } else if (e.getButton() == MouseButton.SECONDARY && !bothDown) { - input.pathNetworkClickQueue.offer( - new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), true)); - } - return; - } - - if (input.activeLayer == SharedInput.LAYER_STONE - && input.stoneTool.brushRadius.getValue() == 1.0) { - // Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer - if (e.getButton() == MouseButton.SECONDARY && !bothDown) { - input.stoneEditQueue.offer( - new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), -1)); - } - } else if (bothDown) { - stopEditTimer(); - } else if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) { - boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_PLAY_TOOL - || input.activeLayer == SharedInput.LAYER_ZONEN - || input.activeLayer == SharedInput.LAYER_SOUND_AREAS - || input.activeLayer == SharedInput.LAYER_AREAS - || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES - || input.activeLayer == SharedInput.LAYER_WATER; - if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { - // Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam) - input.playToolClickQueue.offer( - new SharedInput.PlayToolClick((float) e.getX(), (float) e.getY())); - } else if (singleClickLayer) { - submitEdit(e.getX(), e.getY(), +1); - } else { - editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1; - submitEdit(editPressX, editPressY, editPressAction); - startEditTimer(); - } - } else if (e.getButton() == MouseButton.SECONDARY) { - boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_ZONEN - || input.activeLayer == SharedInput.LAYER_SOUND_AREAS - || input.activeLayer == SharedInput.LAYER_AREAS - || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES - || input.activeLayer == SharedInput.LAYER_WATER; - editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1; - submitEdit(editPressX, editPressY, editPressAction); - if (!singleClickLayer) startEditTimer(); - } - }); - - viewport.setOnMouseDragged(e -> { - if (isObjectMode()) { - if (objDragging && e.isPrimaryButtonDown() - && !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) { - float dx = (float)(e.getX() - objDragPrevX); - float dy = (float)(e.getY() - objDragPrevY); - input.objectDragQueue.offer(new SharedInput.ObjectDrag(dx, dy)); - objDragPrevX = e.getX(); objDragPrevY = e.getY(); - } else if (e.isSecondaryButtonDown() && !e.isPrimaryButtonDown() - && !e.isMiddleButtonDown() - && input.activeLayer == SharedInput.LAYER_OBJECTS) { - float dx = (float)(e.getX() - objDragPrevX); - input.objectRotateDragQueue.offer(new SharedInput.ObjectRotateDrag(dx)); - objDragPrevX = e.getX(); objDragPrevY = e.getY(); - } - return; - } - - if (input.activeLayer == SharedInput.LAYER_WATER && e.isPrimaryButtonDown()) { - input.waterDragQueue.offer(new SharedInput.WaterDrag((float) e.getX(), (float) e.getY())); - input.mouseScreenX = (float) e.getX(); - input.mouseScreenY = (float) e.getY(); - return; - } - - if (input.activeLayer == SharedInput.LAYER_WATERFALL && e.isPrimaryButtonDown()) { - input.waterfallDragQueue.offer(new SharedInput.WaterfallDrag((float) e.getX(), (float) e.getY())); - return; - } - - if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR && e.isSecondaryButtonDown()) { - input.routineYawDragQueue.offer( - new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), false)); - return; - } - - // Play-Tool EDIT: Drag-Events an JME3 weiterleiten - if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL - && input.playToolMode == SharedInput.PlayToolMode.EDIT - && e.isPrimaryButtonDown()) { - input.playToolDragQueue.offer( - new SharedInput.PlayToolDrag((float) e.getX(), (float) e.getY())); - return; - } - - if (e.isPrimaryButtonDown() || e.isSecondaryButtonDown()) { - editPressX = e.getX(); - editPressY = e.getY(); - input.mouseScreenX = (float) e.getX(); - input.mouseScreenY = (float) e.getY(); - } - }); - - viewport.setOnMouseReleased(e -> { - objDragging = false; - if (!isObjectMode()) stopEditTimer(); - if (input.activeLayer == SharedInput.LAYER_WATER) { - input.waterDragEnd = true; - } - if (input.activeLayer == SharedInput.LAYER_WATERFALL) { - input.waterfallDragEnd = true; - } - if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR) { - input.routineYawDragQueue.offer( - new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), true)); - } - if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { - input.playToolMouseUp = true; - } - if (input.vertexSnapEnabled - && input.objectSelectionMode == SharedInput.SEL_MODE_VERTEX) { - input.vertexSnapTrigger = true; - } - }); - - pane.setOnMouseMoved(e -> { - input.mouseScreenX = (float) e.getX(); - input.mouseScreenY = (float) e.getY(); - }); - pane.setOnMouseExited(e -> input.mouseScreenX = -1f); - - viewport.setOnScroll(e -> { - int steps = (int) Math.signum(e.getDeltaY()); - if (e.isControlDown()) steps *= 10; - input.scrollAccum.addAndGet(steps); - }); - - return pane; - } - - private static void applyModeButtonStyle(ToggleButton btn, boolean active) { - if (active) { - btn.setStyle("-fx-border-color: #2277cc; -fx-border-width: 3; -fx-border-radius: 5;" + - "-fx-background-color: #d4e8ff; -fx-padding: 2;"); - } else { - btn.setStyle("-fx-border-color: #bbbbbb; -fx-border-width: 1; -fx-border-radius: 5;" + - "-fx-background-color: #f4f4f4; -fx-padding: 4;"); - } - } - - private void startEditTimer() { - stopEditTimer(); - editTimer = new javafx.animation.Timeline( - new javafx.animation.KeyFrame(javafx.util.Duration.millis(50), - ev -> submitEdit(editPressX, editPressY, editPressAction)) - ); - editTimer.setCycleCount(javafx.animation.Timeline.INDEFINITE); - editTimer.play(); - } - - private void stopEditTimer() { - if (editTimer != null) { editTimer.stop(); editTimer = null; } - } - - private void scheduleAutoSave() { worldAutoSave.playFromStart(); } - - private void submitEdit(double x, double y, int action) { - switch (input.activeLayer) { - case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action)); - case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action)); - case SharedInput.LAYER_GRASS_VERTEX -> { - input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); - if (action < 0) input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); - } - case SharedInput.LAYER_TERRAIN_GRASS -> { - input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); - if (action < 0) input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); - } - case 4 -> { - SharedInput.TextureEdit te = new SharedInput.TextureEdit((float) x, (float) y, action); - input.textureEditQueue.offer(te); - input.voxelTextureEditQueue.offer(te); - } - case SharedInput.LAYER_LIGHTS -> - input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0)); - case SharedInput.LAYER_EMITTERS -> - input.emitterClickQueue.offer(new SharedInput.EmitterClick((float) x, (float) y, action < 0)); - case SharedInput.LAYER_WATER -> - input.waterClickQueue.offer(new SharedInput.WaterClick((float) x, (float) y, action < 0)); - case SharedInput.LAYER_SOUND_AREAS -> - input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0)); - case SharedInput.LAYER_AREAS -> - input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0)); - case SharedInput.LAYER_LOCATION_ZONES -> - input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0)); - case SharedInput.LAYER_ZONEN -> { - input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0)); - input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0)); - input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0)); - } - case SharedInput.LAYER_VOXEL_CLIFF -> - input.voxelCliffClickQueue.offer(new SharedInput.VoxelCliffClick((float) x, (float) y, action < 0)); - case SharedInput.LAYER_PLAY_TOOL -> { - if (action > 0) // only left-click sets spawn - input.playToolClickQueue.offer(new SharedInput.PlayToolClick((float) x, (float) y)); - } - case SharedInput.LAYER_VOXEL -> - input.voxelEditQueue.offer(new SharedInput.VoxelEdit((float) x, (float) y, action)); - case SharedInput.LAYER_STONE -> - input.stoneEditQueue.offer(new SharedInput.StoneEdit((float) x, (float) y, action)); - case SharedInput.LAYER_MODEL_INTERACTABLE -> { - if (action > 0) - input.modelInteractableClickQueue.offer( - new SharedInput.ModelInteractableClick((float) x, (float) y)); - } - } - scheduleAutoSave(); - } // ── Statusleiste ───────────────────────────────────────────────────────── @@ -4931,272 +691,6 @@ public class EditorApp extends Application { return statusBar; } - - private void launchGame() { - if (launchGameAfterSave) return; - launchGameAfterSave = true; - pendingNewGame = false; - pendingAutostart = true; - if (gamePlayBtn != null) { gamePlayBtn.setDisable(true); gamePlayBtn.setText("⏳ Startet…"); } - if (gameNewBtn != null) gameNewBtn.setDisable(true); - if (gameFullBtn != null) gameFullBtn.setDisable(true); - input.saveRequested = true; - setStatus("Karte wird gespeichert, Spiel startet…"); - } - - private void launchNewGame() { - if (launchGameAfterSave) return; - launchGameAfterSave = true; - pendingNewGame = true; - pendingAutostart = true; - if (gameNewBtn != null) { gameNewBtn.setDisable(true); gameNewBtn.setText("⏳ Startet…"); } - if (gamePlayBtn != null) gamePlayBtn.setDisable(true); - if (gameFullBtn != null) gameFullBtn.setDisable(true); - input.saveRequested = true; - setStatus("Karte wird gespeichert, Neues Spiel startet…"); - } - - private void launchFullGame() { - if (launchGameAfterSave) return; - launchGameAfterSave = true; - pendingNewGame = false; - pendingAutostart = false; - if (gameFullBtn != null) { gameFullBtn.setDisable(true); gameFullBtn.setText("⏳ Startet…"); } - if (gamePlayBtn != null) gamePlayBtn.setDisable(true); - if (gameNewBtn != null) gameNewBtn.setDisable(true); - input.saveRequested = true; - setStatus("Karte wird gespeichert, vollständiger Spielstart…"); - } - - private void startGameProcess() { - final boolean isNewGame = pendingNewGame; - final boolean isAutostart = pendingAutostart; - pendingNewGame = false; - pendingAutostart = true; - // Editor minimieren, damit das GLFW-Vollbild-Fenster keinen Focus-Konkurrenten hat. - Platform.runLater(() -> { if (primaryStage != null) primaryStage.setIconified(true); }); - new Thread(() -> { - try { - String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString(); - String classpath = System.getProperty("java.class.path"); - String libPath = System.getProperty("java.library.path", ""); - String projRoot = ProjectRoot.PATH.toString(); - - // Karte in run/session-/ kopieren – dort gitignored, aber im Projekt sichtbar. - // Spielaktionen (Item-Aufheben etc.) verändern so nie die Editor-Karte. - Path srcMapDir = ProjectRoot.PATH.resolve( - Paths.get("blight-map", "src", "main", "map")); - Path runDir = ProjectRoot.PATH.resolve("run"); - Path sessionDir = runDir.resolve("session-" + System.currentTimeMillis()); - copyDirectory(srcMapDir, sessionDir); - - // Alte Sessions aufräumen – nur die letzten 3 behalten. - try { - java.util.List sessions; - try (java.util.stream.Stream s = Files.list(runDir)) { - sessions = s.filter(p -> p.getFileName().toString().startsWith("session-")) - .sorted(java.util.Comparator.comparing(p -> p.getFileName().toString())) - .collect(java.util.stream.Collectors.toList()); - } - int keep = 3; - for (int i = 0; i < sessions.size() - keep; i++) { - deleteDirectoryRecursive(sessions.get(i)); - } - } catch (Exception e) { - log.warn("run/-Cleanup fehlgeschlagen: {}", e.getMessage()); - } - String sessionMapPath = sessionDir.resolve("blight_map.blm").toString(); - - java.util.List cmd = new java.util.ArrayList<>(List.of( - javaExe, - "--add-opens", "java.base/java.lang=ALL-UNNAMED", - "--add-opens", "java.desktop/sun.awt=ALL-UNNAMED", - "-Djava.library.path=" + libPath, - "-Dblight.project.root=" + projRoot, - "-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath, - // Vom Editor gestartet: ggf. Hauptmenü überspringen, letzten Stand fortsetzen - "-Dblight.autostart=" + isAutostart)); - - 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")); - - Process proc = new ProcessBuilder(cmd) - .directory(ProjectRoot.PATH.toFile()) - .redirectErrorStream(true) - .start(); - - Platform.runLater(() -> { - setStatus(isNewGame ? "Neues Spiel gestartet" : "Spiel gestartet"); - if (gamePlayBtn != null) gamePlayBtn.setText("🎮 Läuft…"); - if (gameNewBtn != null) gameNewBtn.setText("🎮 Läuft…"); - if (gameFullBtn != null) gameFullBtn.setText("🎮 Läuft…"); - if (debugConsoleCB != null && debugConsoleCB.isSelected()) openGameConsole(); - }); - - // Stdout des Spiels in Puffer schreiben — Flush erfolgt gebündelt im UI-Timer - try (java.io.BufferedReader br = new java.io.BufferedReader( - new java.io.InputStreamReader(proc.getInputStream(), - java.nio.charset.StandardCharsets.UTF_8))) { - String line; - while ((line = br.readLine()) != null) { - consoleBuffer.offer(line); - } - } - // Spiel beendet → Buttons freigeben und Editor wiederherstellen - consoleBuffer.offer("--- Spiel beendet ---"); - Platform.runLater(() -> { - if (primaryStage != null) primaryStage.setIconified(false); - if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); } - if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); } - if (gameFullBtn != null) { gameFullBtn.setText("▶ Vollst. Start"); gameFullBtn.setDisable(false); } - }); - - } catch (IOException ex) { - Platform.runLater(() -> { - setStatus("Spielstart fehlgeschlagen: " + ex.getMessage()); - if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); } - if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); } - if (gameFullBtn != null) { gameFullBtn.setText("▶ Vollst. Start"); gameFullBtn.setDisable(false); } - }); - } - }, "game-launcher").start(); - } - - private static void copyDirectory(Path src, Path dst) throws IOException { - try (java.util.stream.Stream walk = java.nio.file.Files.walk(src)) { - for (Path s : (Iterable) walk::iterator) { - Path d = dst.resolve(src.relativize(s)); - if (java.nio.file.Files.isDirectory(s)) { - java.nio.file.Files.createDirectories(d); - } else { - java.nio.file.Files.copy(s, d, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - } - } - } - } - - private void openGameConsole() { - if (gameConsoleStage == null) { - gameConsoleArea = new TextArea(); - gameConsoleArea.setEditable(false); - gameConsoleArea.setStyle( - "-fx-control-inner-background: #1e1e1e; -fx-text-fill: #d4d4d4;" + - " -fx-font-family: monospace; -fx-font-size: 11;"); - - Button clearBtn = new Button("Löschen"); - clearBtn.setOnAction(e -> gameConsoleArea.clear()); - - HBox toolbar = new HBox(5, clearBtn); - toolbar.setPadding(new Insets(4)); - - VBox layout = new VBox(toolbar, gameConsoleArea); - VBox.setVgrow(gameConsoleArea, javafx.scene.layout.Priority.ALWAYS); - - Scene scene = new Scene(layout, 800, 350); - gameConsoleStage = new Stage(); - gameConsoleStage.setTitle("Spiel-Konsole"); - gameConsoleStage.setScene(scene); - gameConsoleStage.initOwner(primaryStage); - // Auf zweitem Monitor platzieren und maximieren, falls vorhanden - var screens = javafx.stage.Screen.getScreens(); - if (screens.size() > 1) { - javafx.geometry.Rectangle2D sb = screens.get(1).getVisualBounds(); - gameConsoleStage.setX(sb.getMinX()); - gameConsoleStage.setY(sb.getMinY()); - gameConsoleStage.setWidth(sb.getWidth()); - gameConsoleStage.setHeight(sb.getHeight()); - } else { - gameConsoleStage.setX(primaryStage.getX()); - gameConsoleStage.setY(primaryStage.getY() + primaryStage.getHeight()); - } - } else { - gameConsoleArea.clear(); - } - gameConsoleStage.show(); - gameConsoleStage.toFront(); - } - - private void appendToConsole(String line) { - if (gameConsoleArea == null) return; - gameConsoleArea.appendText(line + "\n"); - gameConsoleArea.setScrollTop(Double.MAX_VALUE); - } - - // ── Kompass ─────────────────────────────────────────────────────────────── - - private void drawCompass() { - if (compassCanvas == null) return; - final double S = compassCanvas.getWidth(); // 100 - final double cx = S / 2.0; - final double cy = S / 2.0; - final double R = cx - 3; // Radius bis zum Rand - - javafx.scene.canvas.GraphicsContext gc = compassCanvas.getGraphicsContext2D(); - gc.clearRect(0, 0, S, S); - - // Hintergrund: dunkler Kreis - gc.setFill(javafx.scene.paint.Color.rgb(12, 12, 24, 0.82)); - gc.fillOval(2, 2, S - 4, S - 4); - gc.setStroke(javafx.scene.paint.Color.rgb(100, 100, 160, 0.85)); - gc.setLineWidth(1.5); - gc.strokeOval(2, 2, S - 4, S - 4); - - // Rotierende Rose: -camYaw so dass die aktuelle Blickrichtung oben erscheint - gc.save(); - gc.translate(cx, cy); - gc.rotate(-input.camYaw); - - // Tick-Striche an 8 Positionen (45°-Schritte) - for (int i = 0; i < 8; i++) { - double a = Math.toRadians(i * 45.0); - double len = (i % 2 == 0) ? 9 : 5; - gc.setStroke(javafx.scene.paint.Color.rgb(160, 160, 200, 0.65)); - gc.setLineWidth(i % 2 == 0 ? 1.5 : 1.0); - gc.strokeLine( - Math.sin(a) * (R - len), -Math.cos(a) * (R - len), - Math.sin(a) * R, -Math.cos(a) * R); - } - - // Himmelsrichtungen (N/E/S/W), Text durch Gegenrotation immer lesbar - String[] dirs = {"N", "E", "S", "W"}; - double[] angles = {0.0, 90.0, 180.0, 270.0}; - for (int i = 0; i < 4; i++) { - double a = Math.toRadians(angles[i]); - double tx = Math.sin(a) * (R - 16); - double ty = -Math.cos(a) * (R - 16); - gc.save(); - gc.translate(tx, ty); - gc.rotate(input.camYaw); // Gegenrotation zur Rose → Buchstabe immer aufrecht - gc.setFill(i == 0 - ? javafx.scene.paint.Color.rgb(255, 80, 80) - : javafx.scene.paint.Color.rgb(210, 210, 230)); - gc.setFont(javafx.scene.text.Font.font( - "System", javafx.scene.text.FontWeight.BOLD, i == 0 ? 13 : 11)); - gc.fillText(dirs[i], -4.5, 5.0); - gc.restore(); - } - - gc.restore(); // Ende rotierende Rose - - // Fixer Richtungszeiger oben (gelbes Dreieck, zeigt immer die Blickrichtung) - gc.setFill(javafx.scene.paint.Color.rgb(255, 220, 60, 0.95)); - gc.fillPolygon( - new double[]{cx, cx - 5, cx + 5}, - new double[]{cy - R + 5, cy - R + 16, cy - R + 16}, - 3); - - // Mittelpunkt - gc.setFill(javafx.scene.paint.Color.rgb(200, 200, 240, 0.85)); - gc.fillOval(cx - 2.5, cy - 2.5, 5, 5); - } - private void openSettings() { EditorPrefs ep = EditorPrefs.load(); @@ -5265,7 +759,7 @@ public class EditorApp extends Application { javafx.scene.control.ToggleGroup wg = new javafx.scene.control.ToggleGroup(); oneWinBtn.setToggleGroup(wg); twoWinBtn.setToggleGroup(wg); - (dualWindowMode ? twoWinBtn : oneWinBtn).setSelected(true); + (contentController.isDualWindowMode() ? twoWinBtn : oneWinBtn).setSelected(true); // ── Layout ──────────────────────────────────────────────────────────── GridPane grid = new GridPane(); @@ -5318,9 +812,9 @@ public class EditorApp extends Application { ep.camPitch = input.camPitch; } ep.save(); - if (wantDual && !dualWindowMode) enableDualWindowMode(); - else if (!wantDual && dualWindowMode) disableDualWindowMode(); - if (dualWindowMenuItem != null) dualWindowMenuItem.setSelected(dualWindowMode); + if (wantDual && !contentController.isDualWindowMode()) contentController.enableDualWindowMode(); + else if (!wantDual && contentController.isDualWindowMode()) contentController.disableDualWindowMode(); + topBarController.getDualWindowMenuItem().setSelected(contentController.isDualWindowMode()); }); } @@ -5341,13 +835,14 @@ public class EditorApp extends Application { ep.tempSpawnYaw = input.tempSpawnYaw; } ep.showGrid = input.showGrid; - ep.dualWindow = dualWindowMode; - ep.contentSection = contentLastSection; - if (contentEditorStage != null && contentEditorStage.isShowing()) { - ep.contentX = contentEditorStage.getX(); - ep.contentY = contentEditorStage.getY(); - ep.contentW = contentEditorStage.getWidth(); - ep.contentH = contentEditorStage.getHeight(); + ep.dualWindow = contentController.isDualWindowMode(); + ep.contentSection = contentController.getContentLastSection(); + javafx.stage.Stage contentStage = contentController.getContentEditorStage(); + if (contentStage != null && contentStage.isShowing()) { + ep.contentX = contentStage.getX(); + ep.contentY = contentStage.getY(); + ep.contentW = contentStage.getWidth(); + ep.contentH = contentStage.getHeight(); } ep.save(); } @@ -5356,11 +851,6 @@ public class EditorApp extends Application { Platform.runLater(() -> statusLabel.setText(msg)); } - private String camHintText() { - String mode = (input.camMode == SharedInput.CAM_FREEFLY) ? "FreeFly" : "Orbit"; - return "WASD/QE: Kamera | Mitte-Drag: Drehen | Alt+C: Kamera wechseln | Kamera: " + mode; - } - // ── Tastatur-Handling ──────────────────────────────────────────────────── private void handleKeyPress(KeyCode code, boolean pressed) { @@ -5464,24 +954,23 @@ public class EditorApp extends Application { } case G -> { if (pressed && input.altHeld) { - wireframeActive = !wireframeActive; - input.wireframeRequest = wireframeActive ? 1 : 2; + input.wireframeRequest = topBarController.toggleWireframe() ? 1 : 2; } } case T -> { - if (pressed && input.altHeld && viewTopologyItem != null) { - boolean sel = !viewTopologyItem.isSelected(); - viewTopologyItem.setSelected(sel); + if (pressed && input.altHeld && topBarController.getViewTopologyItem() != null) { + boolean sel = !topBarController.getViewTopologyItem().isSelected(); + topBarController.getViewTopologyItem().setSelected(sel); input.topologyRequest = sel ? 1 : 2; } } - case F1 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (auswahlBtn != null) auswahlBtn.fire(); }); } - case F2 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (baseBtn != null) baseBtn.fire(); }); } - case F3 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (voxelMergedBtn != null) voxelMergedBtn.fire(); }); } - case F4 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (textureBtn != null) textureBtn.fire(); }); } + case F1 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getAuswahlBtn().fire()); } + case F2 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getBaseBtn().fire()); } + case F3 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getVoxelMergedBtn().fire()); } + case F4 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getTextureBtn().fire()); } case F5 -> { if (pressed) { - if ("world".equals(currentTool)) Platform.runLater(() -> { if (grassBtn != null) grassBtn.fire(); }); + if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getGrassBtn().fire()); } else { ToolController active = registry.getActive(); if (active != null) { active.onPreview(); } else if (onF5 != null) { onF5.run(); } @@ -5490,7 +979,7 @@ public class EditorApp extends Application { } case F6 -> { if (pressed) { - if ("world".equals(currentTool)) Platform.runLater(() -> { if (stoneBtn != null) stoneBtn.fire(); }); + if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getStoneBtn().fire()); } else { ToolController active = registry.getActive(); if (active != null) { active.onRandomSeed(); } else if (onF6 != null) { onF6.run(); } @@ -5498,834 +987,26 @@ public class EditorApp extends Application { } } case F7 -> { - if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (objPlaceBtn != null) objPlaceBtn.fire(); }); - else if (pressed && "objecteditor".equals(registry.activeToolId())) Platform.runLater(objectController::activatePlaceMode); + if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getObjPlaceBtn().fire()); } + else if (pressed && "objecteditor".equals(registry.activeToolId())) { Platform.runLater(objectController::activatePlaceMode); } } case F8 -> { if (pressed) { - if ("world".equals(currentTool)) Platform.runLater(() -> { if (atmosphaereBtn != null) atmosphaereBtn.fire(); }); - else if (input.ctrlHeld) input.debugNoLightToggle = true; - } else if (input.ctrlHeld) input.debugNoLightToggle = true; + if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getAtmosphaereBtn().fire()); } + else if (input.ctrlHeld) { input.debugNoLightToggle = true; } + } else if (input.ctrlHeld) { input.debugNoLightToggle = true; } } case F9 -> { - if (pressed && input.ctrlHeld) Platform.runLater(() -> { - if (!dualWindowMode && !"content".equals(currentTool)) switchToContentEditor(); - if (contentLangBtn != null) contentLangBtn.fire(); - }); - else if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wasserMergedBtn != null) wasserMergedBtn.fire(); }); + if (pressed && input.ctrlHeld) { Platform.runLater(() -> + contentController.activateContentSection(8, currentTool)); } + else if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getWasserMergedBtn().fire()); } } - case F10 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wegeBtn != null) wegeBtn.fire(); }); } - case F11 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (zonenBtn != null) zonenBtn.fire(); }); } - case F12 -> { if (pressed && !input.ctrlHeld && "world".equals(currentTool)) Platform.runLater(() -> { if (playToolBtn != null) playToolBtn.fire(); }); } + case F10 -> { if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getWegeBtn().fire()); } } + case F11 -> { if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getZonenBtn().fire()); } } + case F12 -> { if (pressed && !input.ctrlHeld && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getPlayToolBtn().fire()); } } } } - // ── Sound-Bereich-Panel ─────────────────────────────────────────────────── - - // ── Voxel-Klippen-Zone-Panel ────────────────────────────────────────────── - - // ── Zonen-Panel (vereint Sound, Area, Location) ─────────────────────────── - - private VBox buildZonenPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - final String STYLE_AREA_ON = "-fx-background-color: #e05050; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;"; - final String STYLE_AREA_OFF = "-fx-background-color: #b03030; -fx-text-fill: #ddd; -fx-font-weight: bold;"; - final String STYLE_SOUND_ON = "-fx-background-color: #e07818; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;"; - final String STYLE_SOUND_OFF = "-fx-background-color: #a05010; -fx-text-fill: #ddd; -fx-font-weight: bold;"; - final String STYLE_LOC_ON = "-fx-background-color: #c8a800; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;"; - final String STYLE_LOC_OFF = "-fx-background-color: #8a7200; -fx-text-fill: #ddd; -fx-font-weight: bold;"; - - Button areaKindBtn = new Button("+ Neue Area zeichnen"); - Button soundKindBtn = new Button("+ Neuen Sound zeichnen"); - Button locationKindBtn = new Button("+ Neue Location zeichnen"); - areaKindBtn.setMaxWidth(Double.MAX_VALUE); - soundKindBtn.setMaxWidth(Double.MAX_VALUE); - locationKindBtn.setMaxWidth(Double.MAX_VALUE); - - Runnable updateKindStyles = () -> { - String k = input.zonenNewKind; // null = kein Zeichenmodus aktiv - areaKindBtn.setStyle("area".equals(k) ? STYLE_AREA_ON : STYLE_AREA_OFF); - soundKindBtn.setStyle("sound".equals(k) ? STYLE_SOUND_ON : STYLE_SOUND_OFF); - locationKindBtn.setStyle("location".equals(k) ? STYLE_LOC_ON : STYLE_LOC_OFF); - }; - updateKindStyles.run(); - zonenKindStyleUpdater = updateKindStyles; - - areaKindBtn.setOnAction(e -> { - input.zonenNewKind = "area"; - updateKindStyles.run(); - }); - soundKindBtn.setOnAction(e -> { - input.zonenNewKind = "sound"; - updateKindStyles.run(); - }); - locationKindBtn.setOnAction(e -> { - input.zonenNewKind = "location"; - updateKindStyles.run(); - }); - - areaKindBtn.setTooltip(new javafx.scene.control.Tooltip( - "Area: Musik-Zone mit Tag-, Nacht- und Kampf-Track.\nAreas dürfen sich nicht überschneiden.")); - soundKindBtn.setTooltip(new javafx.scene.control.Tooltip( - "Sound: Ambient-Soundbereich mit wählbarer Audio-Datei,\nLautstärke und Loop-Crossfade.")); - locationKindBtn.setTooltip(new javafx.scene.control.Tooltip( - "Location: Gameplay-Trigger-Zone, z.B. zum Starten\nvon Quests oder Dialogen beim Betreten.")); - - inner.getChildren().addAll( - sectionTitle("Neue Zone"), - areaKindBtn, soundKindBtn, locationKindBtn, - new Separator(), - styledHint("L-Klick → Polygon-Punkte setzen"), - styledHint("R-Klick (beim Zeichnen) → letzten Punkt rückgängig"), - styledHint("R-Klick (sonst) → Auswahl aufheben"), - styledHint("ESC → Zeichnen abbrechen"), - styledHint("Entf → Gewählte Zone löschen"), - new Separator(), - sectionTitle("Gewählte Zone"), - new Separator()); - - zonenDynamicContent = new VBox(6); - Label noSel = new Label("Keine Zone ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - zonenDynamicContent.getChildren().add(noSel); - inner.getChildren().add(zonenDynamicContent); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - private void updateZonenPanel(String kind) { - if (zonenDynamicContent == null) return; - zonenDynamicContent.getChildren().clear(); - switch (kind != null ? kind : "") { - case "area" -> { - VBox saved = areaDynamicContent; - areaDynamicContent = zonenDynamicContent; - updateAreaPanel(input.selectedAreaInfo); - areaDynamicContent = saved; - } - case "sound" -> { - VBox saved = soundAreaDynamicContent; - soundAreaDynamicContent = zonenDynamicContent; - updateSoundAreaPanel(input.selectedSoundAreaInfo); - soundAreaDynamicContent = saved; - } - case "location" -> { - VBox saved = locationZoneDynamicContent; - locationZoneDynamicContent = zonenDynamicContent; - updateLocationZonePanel(input.selectedLocationZoneInfo); - locationZoneDynamicContent = saved; - } - default -> { - Label noSel = new Label("Keine Zone ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - zonenDynamicContent.getChildren().add(noSel); - } - } - } - - private VBox buildSoundAreaPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - inner.getChildren().addAll( - sectionTitle("Sound-Bereiche"), - styledHint("L-Klick → Polygon-Punkte setzen"), - styledHint("R-Klick → Polygon schließen / Auswahl aufheben"), - styledHint("Entf → Bereich löschen"), - new Separator(), - sectionTitle("Gewählter Bereich"), - new Separator()); - - soundAreaDynamicContent = new VBox(6); - Label noSel = new Label("Kein Bereich ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - soundAreaDynamicContent.getChildren().add(noSel); - inner.getChildren().add(soundAreaDynamicContent); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - private void updateSoundAreaPanel(String info) { - if (soundAreaDynamicContent == null) return; - soundAreaDynamicContent.getChildren().clear(); - - if (info == null) { - Label noSel = new Label("Kein Bereich ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - soundAreaDynamicContent.getChildren().add(noSel); - return; - } - // Format: "idx|soundPath|volume|crossfade" - String[] p = info.split("\\|", -1); - if (p.length < 4) return; - try { - int idx = Integer.parseInt(p[0]); - float volume = Float.parseFloat(p[2]); - boolean xfade = Boolean.parseBoolean(p[3]); - final String[] soundPath = {p[1]}; - final float[] vol = {volume}; - final boolean[] cf = {xfade}; - - Runnable publish = () -> { - if (input.selectedSoundAreaInfo == null) return; - String[] cur = input.selectedSoundAreaInfo.split("\\|", -1); - if (cur.length < 1) return; - // Polygon unchanged; only update sound properties via pendingSoundArea - // We need the polygon from the state – use a zero-size placeholder, - // the state will merge only the properties fields. - // Instead we pass through a special sentinel: same polygon, new props. - // Simplification: publish via selectedSoundAreaInfo update + let state re-read - input.selectedSoundAreaInfo = idx + "|" + soundPath[0] + "|" + vol[0] + "|" + cf[0]; - }; - - Label soundLabel = new Label("Sound: " + (soundPath[0].isEmpty() ? "(keine)" : soundPath[0])); - soundLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); - soundLabel.setWrapText(true); - - Button browseBtn = new Button("🔊 Sound wählen…"); - browseBtn.setMaxWidth(Double.MAX_VALUE); - browseBtn.setOnAction(e -> { - Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources"); - new de.blight.editor.ui.SoundChooser(assetRoot, de.blight.editor.ui.SoundChooser.Mode.ALL) - .showAndWait() - .ifPresent(rel -> { - soundPath[0] = rel; - soundLabel.setText("Sound: " + rel); - sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]); - }); - }); - - Label volLbl = new Label("Lautstärke"); - volLbl.setStyle("-fx-font-size: 11;"); - Slider volSlider = new Slider(0, 1, vol[0]); - volSlider.setShowTickLabels(false); - volSlider.valueProperty().addListener((o, ov, nv) -> { - vol[0] = nv.floatValue(); - sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]); - }); - - CheckBox xfadeCB = new CheckBox("Loop-Crossfade"); - xfadeCB.setSelected(cf[0]); - xfadeCB.setOnAction(e -> { - cf[0] = xfadeCB.isSelected(); - sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]); - }); - - Button delBtn = new Button("🗑 Löschen"); - delBtn.setMaxWidth(Double.MAX_VALUE); - delBtn.setStyle("-fx-text-fill: #c0392b;"); - delBtn.setOnAction(e -> input.deleteSoundAreaRequested = true); - - soundAreaDynamicContent.getChildren().addAll( - soundLabel, browseBtn, new Separator(), volLbl, withField(volSlider, "%.2f"), xfadeCB, - new Separator(), delBtn); - - } catch (NumberFormatException ignored) {} - } - - private void sendSoundAreaUpdate(int idx, String soundPath, float volume, boolean crossfade) { - if (input.selectedSoundAreaInfo == null) return; - // We only update metadata (sound path, volume, crossfade). - // The polygon stays as-is in the state. - // Signal the state by setting pendingSoundArea with the current polygon. - // EditorApp doesn't have the polygon points – so we send the info string update - // and let the state handle the property merge via a dedicated flag. - // Simpler: encode the delta into selectedSoundAreaInfo and let state read it. - input.selectedSoundAreaInfo = idx + "|" + soundPath + "|" + volume + "|" + crossfade; - // Build a PlacedSoundArea with zero-length polygon as sentinel – state must check - // pendingSoundArea != null and idx matches to update only the props. - input.pendingSoundArea.set(new de.blight.common.PlacedSoundArea( - new float[0], new float[0], soundPath, volume, crossfade)); - scheduleAutoSave(); - } - - // ── Musik-Bereich-Panel ─────────────────────────────────────────────────── - - private VBox buildAreaPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - inner.getChildren().addAll( - sectionTitle("Areas"), - styledHint("L-Klick → Polygon-Punkte setzen"), - styledHint("R-Klick → Polygon schließen / Auswahl aufheben"), - styledHint("ESC → Zeichnen abbrechen"), - styledHint("Entf → Area löschen"), - styledHint("Areas dürfen sich nicht überschneiden"), - new Separator(), - sectionTitle("Gewählte Area"), - new Separator()); - - areaDynamicContent = new VBox(6); - Label noSel = new Label("Keine Area ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - areaDynamicContent.getChildren().add(noSel); - inner.getChildren().add(areaDynamicContent); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - private void updateAreaPanel(String info) { - if (areaDynamicContent == null) return; - areaDynamicContent.getChildren().clear(); - - if (info == null) { - Label noSel = new Label("Keine Area ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - areaDynamicContent.getChildren().add(noSel); - return; - } - // Format: "idx|areaId|triggersJson" - String[] p = info.split("\\|", 3); - if (p.length < 1) return; - try { - int idx = Integer.parseInt(p[0]); - String currentAreaId = p.length > 1 ? p[1] : ""; - java.util.List existingTriggers = - p.length > 2 - ? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2]) - : new java.util.ArrayList<>(); - - // Verfügbare Area-Definitionen laden - java.util.List areaIds = new java.util.ArrayList<>(); - areaIds.add(""); - try { - de.blight.common.AreaDefinitionIO.load().stream() - .map(de.blight.common.AreaDefinition::id) - .filter(s -> !s.isBlank()) - .forEach(areaIds::add); - } catch (Exception ignored) {} - - Label areaLbl = new Label("Verknüpfte Area:"); - areaLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); - - Label nameKeyLbl = new Label(currentAreaId.isBlank() ? "" : currentAreaId + ".name"); - nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;"); - - // editorHolder muss vor areaCombo deklariert sein (Lambda-Forward-Ref) - de.blight.editor.ui.TriggerListEditor[] editorHolder = {null}; - - ComboBox areaCombo = new ComboBox<>(); - areaCombo.getItems().setAll(areaIds); - areaCombo.setValue(areaIds.contains(currentAreaId) ? currentAreaId : ""); - areaCombo.setMaxWidth(Double.MAX_VALUE); - areaCombo.setPromptText("Area wählen…"); - areaCombo.valueProperty().addListener((obs, o, n) -> { - String aid = n != null ? n : ""; - nameKeyLbl.setText(aid.isBlank() ? "" : aid + ".name"); - java.util.List curTriggers = - editorHolder[0] != null - ? new java.util.ArrayList<>(editorHolder[0].getTriggers()) - : existingTriggers; - input.pendingArea.set(new de.blight.common.PlacedArea( - new float[0], new float[0], aid, curTriggers)); - scheduleAutoSave(); - }); - - Label triggerLbl = new Label("Trigger:"); - triggerLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); - - editorHolder[0] = new de.blight.editor.ui.TriggerListEditor(existingTriggers, () -> { - String aid = areaCombo.getValue() != null ? areaCombo.getValue() : currentAreaId; - input.pendingArea.set(new de.blight.common.PlacedArea( - new float[0], new float[0], aid, - new java.util.ArrayList<>(editorHolder[0].getTriggers()))); - scheduleAutoSave(); - }); - - Button delBtn = new Button("🗑 Löschen"); - delBtn.setMaxWidth(Double.MAX_VALUE); - delBtn.setStyle("-fx-text-fill: #c0392b;"); - delBtn.setOnAction(e -> { input.deleteAreaRequested = true; scheduleAutoSave(); }); - - areaDynamicContent.getChildren().addAll( - areaLbl, areaCombo, nameKeyLbl, - new Separator(), - triggerLbl, editorHolder[0], - new Separator(), - delBtn); - - } catch (NumberFormatException ignored) {} - } - - private VBox buildLocationZonePanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - inner.getChildren().addAll( - sectionTitle("Locations"), - styledHint("L-Klick → Polygon-Punkte setzen"), - styledHint("R-Klick → Polygon schließen / Auswahl aufheben"), - styledHint("ESC → Zeichnen abbrechen"), - styledHint("Entf → Location löschen"), - styledHint("Locations dürfen sich überschneiden"), - new Separator(), - sectionTitle("Gewählte Location"), - new Separator()); - - locationZoneDynamicContent = new VBox(6); - Label noSel = new Label("Keine Location ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - locationZoneDynamicContent.getChildren().add(noSel); - inner.getChildren().add(locationZoneDynamicContent); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - private void updateLocationZonePanel(String info) { - if (locationZoneDynamicContent == null) return; - locationZoneDynamicContent.getChildren().clear(); - - if (info == null) { - Label noSel = new Label("Keine Location ausgewählt"); - noSel.setStyle("-fx-text-fill: #888;"); - locationZoneDynamicContent.getChildren().add(noSel); - return; - } - // Format: "idx|nameId|triggersJson" - String[] p = info.split("\\|", 3); - if (p.length < 1) return; - try { - int idx = Integer.parseInt(p[0]); - String nameId = p.length > 1 ? p[1] : ""; - java.util.List existingTriggers = - p.length > 2 - ? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2]) - : new java.util.ArrayList<>(); - - // Verfügbare Locations aus LocationIO laden - java.util.List locationIds = new java.util.ArrayList<>(); - locationIds.add(""); - try { - de.blight.common.LocationIO.load().stream() - .map(de.blight.common.model.Location::getId) - .filter(s -> !s.isBlank()) - .forEach(locationIds::add); - } catch (Exception ignored) {} - - Label nameLbl = new Label("Verknüpfte Location:"); - nameLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); - - Label nameKeyLbl = new Label(nameId.isBlank() ? "" : nameId + ".name"); - nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;"); - - ComboBox locationCombo = new ComboBox<>(); - locationCombo.getItems().setAll(locationIds); - locationCombo.setValue(locationIds.contains(nameId) ? nameId : ""); - locationCombo.setMaxWidth(Double.MAX_VALUE); - locationCombo.setPromptText("Location wählen…"); - locationCombo.valueProperty().addListener((obs, o, n) -> { - String lid = n != null ? n : ""; - nameKeyLbl.setText(lid.isBlank() ? "" : lid + ".name"); - input.pendingLocationZone.set(new de.blight.common.PlacedLocationZone( - new float[0], new float[0], lid, existingTriggers)); - scheduleAutoSave(); - }); - - Button delBtn = new Button("🗑 Löschen"); - delBtn.setMaxWidth(Double.MAX_VALUE); - delBtn.setStyle("-fx-text-fill: #c0392b;"); - delBtn.setOnAction(e -> { input.deleteLocationZoneRequested = true; scheduleAutoSave(); }); - - locationZoneDynamicContent.getChildren().addAll( - nameLbl, locationCombo, nameKeyLbl, - new Separator(), - delBtn); - - } catch (NumberFormatException ignored) {} - } - - // ── Spiel-Starten-Panel ─────────────────────────────────────────────────── - - private VBox buildPlayToolPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - // ── Abschnitt: Spawnpunkte setzen ──────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Spawnpunkte"), new Separator()); - - Button setTempBtn = new Button("📍 Temp. Spawn hier setzen"); - setTempBtn.setMaxWidth(Double.MAX_VALUE); - setTempBtn.setTooltip(new javafx.scene.control.Tooltip( - "Aktiviert Klick-Modus im Viewport zum Setzen des temporären Spawnpunkts")); - setTempBtn.setOnAction(e -> { - input.playToolMode = SharedInput.PlayToolMode.SET_TEMP; - setStatus("L-Klick im Viewport → temporären Spawnpunkt setzen"); - }); - - Button setPermBtn = new Button("🏁 Perm. Spawn hier setzen"); - setPermBtn.setMaxWidth(Double.MAX_VALUE); - setPermBtn.setTooltip(new javafx.scene.control.Tooltip( - "Aktiviert Klick-Modus im Viewport zum Setzen des permanenten Spawnpunkts")); - setPermBtn.setOnAction(e -> { - input.playToolMode = SharedInput.PlayToolMode.SET_PERM; - setStatus("L-Klick im Viewport → permanenten Spawnpunkt setzen"); - }); - - inner.getChildren().addAll(setTempBtn, setPermBtn, 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; - playBtn.setMaxWidth(Double.MAX_VALUE); - playBtn.setStyle( - "-fx-background-color: #2d8a3e; -fx-text-fill: white; " + - "-fx-font-weight: bold; -fx-padding: 6 12 6 12;"); - playBtn.setTooltip(new javafx.scene.control.Tooltip( - "Startet das Spiel mit dem temp. Spawnpunkt (bzw. gespeicherter Position)")); - playBtn.setOnAction(e -> launchGame()); - - Button newGameBtn = new Button("🆕 Neues Spiel"); - gameNewBtn = newGameBtn; - newGameBtn.setMaxWidth(Double.MAX_VALUE); - newGameBtn.setStyle( - "-fx-background-color: #7b3ea4; -fx-text-fill: white; " + - "-fx-font-weight: bold; -fx-padding: 6 12 6 12;"); - newGameBtn.setTooltip(new javafx.scene.control.Tooltip( - "Startet ein neues Spiel am perm. Spawnpunkt mit Intro-Sequenz")); - newGameBtn.setOnAction(e -> launchNewGame()); - - Button fullStartBtn = new Button("▶ Vollst. Start"); - gameFullBtn = fullStartBtn; - fullStartBtn.setMaxWidth(Double.MAX_VALUE); - fullStartBtn.setStyle( - "-fx-background-color: #1a6080; -fx-text-fill: white; " + - "-fx-font-weight: bold; -fx-padding: 6 12 6 12;"); - fullStartBtn.setTooltip(new javafx.scene.control.Tooltip( - "Startet das Spiel wie von außen: mit OAA-Logo, JME-Logo und Hauptmenü")); - fullStartBtn.setOnAction(e -> launchFullGame()); - - debugConsoleCB = new CheckBox("Debug-Konsole anzeigen"); - debugConsoleCB.setTooltip(new javafx.scene.control.Tooltip( - "Öffnet bei Spielstart die Ausgabekonsole mit der Spielausgabe")); - - inner.getChildren().addAll(playBtn, newGameBtn, fullStartBtn, debugConsoleCB); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(270); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - 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 (info == null) return; - String[] p = info.split("\\|", -1); - if (p.length < 2) return; - if (spawnXField != null) spawnXField.setText(p[0]); - if (spawnZField != null) spawnZField.setText(p[1]); - if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText(tempSpawnCoordsText()); - } - - // ── Tripo3D-Generator ──────────────────────────────────────────────────── - - private ToolBar buildTripoToolBar() { - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("AI Modell-Generator · Tripo3D"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - ToolBar tb = new ToolBar(); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - return tb; - } - - private VBox buildTripoPanel() { - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - // ── API-Schlüssel ───────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Tripo3D API"), new Separator()); - Label apiHint = styledHint("Schlüssel unter platform.tripo3d.ai → API Keys"); - apiHint.setWrapText(true); - TextField apiKeyField = new TextField(loadTripoApiKey()); - apiKeyField.setPromptText("tsk_xxxx…"); - apiKeyField.setStyle("-fx-font-family: monospace; -fx-font-size: 11;"); - inner.getChildren().addAll(new Label("API-Schlüssel:"), apiKeyField, apiHint); - - // ── Prompt ──────────────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Modell beschreiben"), new Separator()); - TextArea promptArea = new TextArea(); - promptArea.setPromptText("z. B. \"Ein mittelalterlicher Ritter in Ruestung\"\n" - + "Tipp: Englische Prompts liefern meist bessere Ergebnisse."); - promptArea.setPrefRowCount(5); - promptArea.setWrapText(true); - inner.getChildren().addAll(new Label("Prompt:"), promptArea); - - TextField nameField = new TextField(); - nameField.setPromptText("dateiname (optional)"); - inner.getChildren().addAll(new Label("Dateiname:"), nameField); - - // ── Generieren ──────────────────────────────────────────────────────── - inner.getChildren().add(new Separator()); - Button generateBtn = new Button("Modell generieren"); - generateBtn.setMaxWidth(Double.MAX_VALUE); - generateBtn.setStyle("-fx-font-weight: bold;"); - generateBtn.setDisable(tripoGenerating); - - tripoProgressBar = new ProgressBar(tripoGenerating ? ProgressBar.INDETERMINATE_PROGRESS : 0); - tripoProgressBar.setMaxWidth(Double.MAX_VALUE); - tripoStatusLabel = new Label(tripoGenerating ? "Generierung läuft…" : "Bereit"); - tripoStatusLabel.setWrapText(true); - tripoStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #444;"); - inner.getChildren().addAll(generateBtn, tripoProgressBar, tripoStatusLabel); - - // ── Import ──────────────────────────────────────────────────────────── - inner.getChildren().addAll(sectionTitle("Import"), new Separator()); - tripoImportBtn = new Button("Importieren (GLB → j3o)"); - tripoImportRigBtn = new Button("Importieren + Skelett (Humanoid)"); - tripoImportBtn.setMaxWidth(Double.MAX_VALUE); - tripoImportRigBtn.setMaxWidth(Double.MAX_VALUE); - boolean hasPrev = tripoLastModelUrl != null && !tripoGenerating; - tripoImportBtn.setDisable(!hasPrev); - tripoImportRigBtn.setDisable(!hasPrev || tripoLastTaskId == null); - inner.getChildren().addAll(tripoImportBtn, tripoImportRigBtn); - inner.getChildren().add(styledHint("Das Skelett-Modell eignet sich für Humanoid-Animationen.")); - - // ── Verknüpfungen ───────────────────────────────────────────────────── - generateBtn.setOnAction(e -> { - if (tripoGenerating) return; - String apiKey = apiKeyField.getText().trim(); - String prompt = promptArea.getText().trim(); - if (apiKey.isEmpty()) { setStatus("Bitte API-Schlüssel eingeben"); return; } - if (prompt.isEmpty()) { setStatus("Bitte Prompt eingeben"); return; } - - saveTripoApiKey(apiKey); - tripoGenerating = true; - generateBtn.setDisable(true); - tripoImportBtn.setDisable(true); - tripoImportRigBtn.setDisable(true); - tripoProgressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); - tripoStatusLabel.setText("Erstelle Aufgabe…"); - if (tripoPreviewView != null) tripoPreviewView.setImage(null); - - new Thread(() -> { - try { - String taskId = TripoGenerator.createTextToModelTask(apiKey, prompt); - tripoLastTaskId = taskId; - Platform.runLater(() -> tripoStatusLabel.setText("Generiere… (ID: " + taskId + ")")); - - pollUntilDone(apiKey, taskId, result -> { - tripoLastModelUrl = result.modelUrl(); - if (result.previewUrl() != null && tripoPreviewView != null) - tripoPreviewView.setImage( - new javafx.scene.image.Image(result.previewUrl(), true)); - tripoProgressBar.setProgress(1.0); - tripoStatusLabel.setText("Fertig! Modell bereit zum Import."); - generateBtn.setDisable(false); - tripoImportBtn.setDisable(false); - tripoImportRigBtn.setDisable(false); - tripoGenerating = false; - }, err -> { - tripoProgressBar.setProgress(0); - tripoStatusLabel.setText("Fehler: " + err); - generateBtn.setDisable(false); - tripoGenerating = false; - }); - } catch (Exception ex) { - Platform.runLater(() -> { - tripoProgressBar.setProgress(0); - tripoStatusLabel.setText("Fehler: " + ex.getMessage()); - generateBtn.setDisable(false); - tripoGenerating = false; - }); - } - }).start(); - }); - - tripoImportBtn.setOnAction(e -> { - if (tripoLastModelUrl == null) return; - String name = nameField.getText().trim(); - if (name.isEmpty()) name = "tripo_" + System.currentTimeMillis(); - importTripoModel(tripoLastModelUrl, name); - }); - - tripoImportRigBtn.setOnAction(e -> { - if (tripoLastTaskId == null || tripoLastModelUrl == null) return; - String apiKey = apiKeyField.getText().trim(); - String baseName = nameField.getText().trim(); - if (baseName.isEmpty()) baseName = "tripo_" + System.currentTimeMillis(); - final String rigName = baseName + "_rigged"; - - tripoImportRigBtn.setDisable(true); - tripoProgressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); - tripoStatusLabel.setText("Erstelle Skelett…"); - - final String taskId = tripoLastTaskId; - new Thread(() -> { - try { - String rigTaskId = TripoGenerator.createRigTask(apiKey, taskId); - Platform.runLater(() -> tripoStatusLabel.setText( - "Rigging… (ID: " + rigTaskId + ")")); - - pollUntilDone(apiKey, rigTaskId, result -> { - tripoProgressBar.setProgress(1.0); - tripoStatusLabel.setText("Skelett fertig! Importiere…"); - tripoImportRigBtn.setDisable(false); - importTripoModel(result.modelUrl(), rigName); - }, err -> { - tripoProgressBar.setProgress(0); - tripoStatusLabel.setText("Skelett-Fehler: " + err); - tripoImportRigBtn.setDisable(false); - }); - } catch (Exception ex) { - Platform.runLater(() -> { - tripoProgressBar.setProgress(0); - tripoStatusLabel.setText("Fehler: " + ex.getMessage()); - tripoImportRigBtn.setDisable(false); - }); - } - }).start(); - }); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); - VBox panel = new VBox(scroll); - VBox.setVgrow(scroll, Priority.ALWAYS); - panel.setPrefWidth(280); - panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); - return panel; - } - - /** - * Blocks (in a background thread) until the task reaches a terminal state, - * then calls onSuccess or onError on the JavaFX thread. - */ - private void pollUntilDone(String apiKey, String taskId, - java.util.function.Consumer onSuccess, - java.util.function.Consumer onError) { - while (true) { - try { Thread.sleep(3_000); } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - Platform.runLater(() -> onError.accept("Unterbrochen")); - return; - } - try { - TripoGenerator.TaskResult r = TripoGenerator.pollTask(apiKey, taskId); - switch (r.status()) { - case "success" -> { Platform.runLater(() -> onSuccess.accept(r)); return; } - case "failed", "cancelled" -> { - Platform.runLater(() -> onError.accept(r.status())); return; - } - default -> { - int p = r.progress(); - Platform.runLater(() -> { - if (tripoProgressBar != null) tripoProgressBar.setProgress(p / 100.0); - if (tripoStatusLabel != null) tripoStatusLabel.setText("Generiere… " + p + "%"); - }); - } - } - } catch (java.io.IOException ex) { - // transient network error – keep retrying - Platform.runLater(() -> { - if (tripoStatusLabel != null) - tripoStatusLabel.setText("Prüfe… (" + ex.getMessage() + ")"); - }); - } - } - } - - /** Downloads the GLB from Tripo, copies to Models/, queues j3o conversion. */ - private void importTripoModel(String url, String name) { - final String safeName = name.replaceAll("[^a-zA-Z0-9_\\-]", "_"); - new Thread(() -> { - try { - Platform.runLater(() -> setStatus("Lade Modell herunter: " + safeName + "…")); - Path destDir = ASSET_ROOT.resolve("Models"); - Files.createDirectories(destDir); - Path destGlb = destDir.resolve(safeName + ".glb"); - TripoGenerator.downloadFile(url, destGlb); - - String assetPath = "Models/" + safeName + ".glb"; - Path destJ3o = destDir.resolve(safeName + ".j3o"); - input.modelConvertQueue.offer( - new SharedInput.ModelConvertRequest(assetPath, destJ3o, destGlb, true)); - - Platform.runLater(() -> setStatus("Konvertiere " + safeName + ".glb → .j3o…")); - } catch (java.io.IOException ex) { - Platform.runLater(() -> setStatus("Import-Fehler: " + ex.getMessage())); - } - }).start(); - } - - private static String loadTripoApiKey() { - return java.util.prefs.Preferences - .userNodeForPackage(EditorApp.class) - .get("tripo_api_key", ""); - } - - private static void saveTripoApiKey(String key) { - java.util.prefs.Preferences - .userNodeForPackage(EditorApp.class) - .put("tripo_api_key", key); - } // ── Animations-Vorschau-Werkzeug ───────────────────────────────────────── @@ -6347,1761 +1028,65 @@ public class EditorApp extends Application { } - private void autoSaveCharacter() { - if (charIdField == null || charIdField.getText().trim().isBlank()) return; - saveCharacter(ASSET_ROOT.resolve("Characters")); - } - - // ── Character-Editor ────────────────────────────────────────────────────── - - private void switchToCharacterEditor() { - if (dualWindowMode) { - showContentEditor(); - contentLastSection = "character"; - if (contentCharBtn != null) contentCharBtn.setSelected(true); - buildCharacterViewInContent(); - return; - } - onF5 = null; - currentTool = "charEditor"; - topBar.getChildren().set(1, buildCharacterEditorToolBar()); - dialogEditorView = new de.blight.editor.ui.DialogEditorView(); - dialogEditorView.setDisable(true); - routineEditorView = new de.blight.editor.ui.RoutineEditorView(input, ASSET_ROOT.resolve("Characters")); - routineEditorView.setDisable(true); - monologueEditorView = new de.blight.editor.ui.MonologueEditorView(); - - // worldViewport in den Tagesabläufe-Tab verschieben; centerStack bekommt Platzhalter - routineJmeSlot = new StackPane(); - routineJmeSlot.setMinWidth(0); - routineJmeSlot.getChildren().add(worldViewport); // JavaFX entfernt worldViewport aus centerStack - centerStack.getChildren().add(new StackPane()); // Platzhalter, damit setCenterView(set(0,...)) später klappt - - javafx.scene.control.SplitPane routineSplit = - new javafx.scene.control.SplitPane(routineEditorView, routineJmeSlot); - routineSplit.setDividerPositions(0.2); - routineSplit.widthProperty().addListener((obs, ov, nv) -> { - if (nv.doubleValue() > 0) routineSplit.setDividerPositions(0.2); - }); - - javafx.scene.control.TabPane centerTabs = new javafx.scene.control.TabPane(); - centerTabs.setTabClosingPolicy(javafx.scene.control.TabPane.TabClosingPolicy.UNAVAILABLE); - centerTabs.getTabs().addAll( - new javafx.scene.control.Tab("Dialog", dialogEditorView), - new javafx.scene.control.Tab("Tagesabläufe", routineSplit), - new javafx.scene.control.Tab("Monologe", monologueEditorView) - ); - root.setCenter(centerTabs); - root.setRight(null); - } - - private void switchToRecipeEditor() { - if (dualWindowMode) { - showContentEditor(); - contentLastSection = "recipe"; - if (contentRezeptBtn != null) contentRezeptBtn.setSelected(true); - buildRecipeViewInContent(); - return; - } - onF5 = null; - currentTool = "recipeEditor"; - ToolBar tb = new ToolBar(); - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Rezept-Manager"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - topBar.getChildren().set(1, tb); - root.setCenter(new de.blight.editor.ui.RecipeEditorView(ASSET_ROOT.resolve("recipes"))); - root.setRight(null); - } - - private void switchToCraftingTableEditor() { - if (dualWindowMode) { - showContentEditor(); - contentLastSection = "crafting"; - if (contentCraftingBtn != null) contentCraftingBtn.setSelected(true); - buildCraftingViewInContent(); - return; - } - onF5 = null; - currentTool = "craftingTableEditor"; - ToolBar tb = new ToolBar(); - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Crafting-Table-Manager"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - topBar.getChildren().set(1, tb); - root.setCenter(new de.blight.editor.ui.CraftingTableEditorView(ASSET_ROOT.resolve("craftingtables"))); - root.setRight(null); - } - - private void switchToFractionEditor() { - if (dualWindowMode) { - showContentEditor(); - contentLastSection = "fraction"; - if (contentFraktionBtn != null) contentFraktionBtn.setSelected(true); - buildFractionViewInContent(); - return; - } - onF5 = null; - currentTool = "fractionEditor"; - ToolBar tb = new ToolBar(); - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Fraktionen-Manager"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - topBar.getChildren().set(1, tb); - root.setCenter(new de.blight.editor.ui.FractionEditorView(ASSET_ROOT.resolve("fractions"))); - root.setRight(null); - } - - private void refreshFractionCombo() { - loadedFractions.clear(); - loadedFractions.addAll( - de.blight.common.model.FractionIO.loadAll(ASSET_ROOT.resolve("fractions"))); - if (charFractionCombo == null) return; - de.blight.common.model.Fraction current = charFractionCombo.getValue(); - charFractionCombo.getItems().clear(); - charFractionCombo.getItems().add(null); // "— keine Fraktion —" - charFractionCombo.getItems().addAll(loadedFractions); - // Selektion wiederherstellen per fractionId-Vergleich - if (current != null && current.getFractionId() != null) { - loadedFractions.stream() - .filter(f -> current.getFractionId().equals(f.getFractionId())) - .findFirst() - .ifPresentOrElse(charFractionCombo::setValue, () -> charFractionCombo.setValue(null)); - } - } - - private String fractionDisplay(de.blight.common.model.Fraction f) { - if (f == null) return "— keine Fraktion —"; - return f.getName() != null ? f.getName().id() : (f.getFractionId() != null ? f.getFractionId().toString().substring(0, 8) + "…" : "?"); - } - - private void switchToItemEditor() { - switchToItemEditor(null); - } - - private void switchToItemEditor(String selectItemId) { - if (dualWindowMode) { - showContentEditor(); - contentLastSection = "item"; - if (contentItemBtn != null) contentItemBtn.setSelected(true); - de.blight.editor.ui.ItemEditorView view = - new de.blight.editor.ui.ItemEditorView(ASSET_ROOT.resolve("items")); - contentTarget().setCenter(view); - contentTarget().setRight(null); - if (selectItemId != null) view.selectItem(selectItemId); - return; - } - onF5 = null; - currentTool = "itemEditor"; - ToolBar tb = new ToolBar(); - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Item-Manager"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - topBar.getChildren().set(1, tb); - de.blight.editor.ui.ItemEditorView view = - new de.blight.editor.ui.ItemEditorView(ASSET_ROOT.resolve("items")); - root.setCenter(view); - root.setRight(null); - if (selectItemId != null) view.selectItem(selectItemId); - } - - private void switchToQuestEditor() { - if (dualWindowMode) { - showContentEditor(); - contentLastSection = "quest"; - if (contentQuestBtn != null) contentQuestBtn.setSelected(true); - buildQuestViewInContent(); - return; - } - onF5 = null; - currentTool = "questEditor"; - ToolBar tb = new ToolBar(); - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Quest-Manager"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - topBar.getChildren().set(1, tb); - Path questDir = ASSET_ROOT.resolve("quests"); - root.setCenter(new de.blight.editor.ui.QuestEditorView(questDir)); - root.setRight(null); - } - - private ToolBar buildCharacterEditorToolBar() { - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Animationset Editor"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - ToolBar tb = new ToolBar(); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - return tb; - } - - private ScrollPane buildCharacterSettingsPane() { - Path charDir = ASSET_ROOT.resolve("Characters"); - - VBox inner = new VBox(8); - inner.setPadding(new Insets(10)); - - - charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen"); - charEditorStatusLabel.setWrapText(true); - charEditorStatusLabel.setStyle("-fx-font-size:11;-fx-text-fill:#777;"); - inner.getChildren().add(charEditorStatusLabel); - - // Inline-Helfer für dunkles Theme - final String LBL = "-fx-text-fill:#ccc;"; - final String SEC = "-fx-font-weight:bold;-fx-font-size:11;-fx-text-fill:#8ab4d4;"; - java.util.function.Function dl = t -> { Label l = new Label(t); l.setStyle(LBL); return l; }; - java.util.function.Function dsl = t -> { Label l = new Label(t); l.setStyle(SEC); VBox.setMargin(l, new Insets(8,0,2,0)); return l; }; - - inner.setStyle("-fx-background-color:#1a1a2a;"); - - // ── Editierbarer Bereich (deaktiviert bis Character geladen) ────────── - charEditContainer = new VBox(6); - charEditContainer.setDisable(true); - charEditContainer.setStyle("-fx-background-color:#1a1a2a;"); - - charEditContainer.getChildren().addAll(new Separator(), dsl.apply("Character-Daten"), new Separator()); - - charIdField = new javafx.scene.control.TextField(); - charIdField.setPromptText("eindeutige ID (Dateiname)"); - charNameField = new javafx.scene.control.TextField(); - charNameField.setPromptText("Anzeigename"); - charNameField.setEditable(false); - - charTypeCombo = new ComboBox<>(); - charTypeCombo.getItems().addAll("MainCharacter", "NPC"); - charTypeCombo.setValue("NPC"); - charTypeCombo.setMaxWidth(Double.MAX_VALUE); - - charModelCombo = new ComboBox<>(); - charModelCombo.setMaxWidth(Double.MAX_VALUE); - charModelCombo.setPromptText("Modell wählen…"); - Path charModelBase = ASSET_ROOT.resolve("Characters").resolve("Models"); - if (java.nio.file.Files.isDirectory(charModelBase)) { - try (var walk = java.nio.file.Files.walk(charModelBase)) { - walk.filter(p -> p.toString().endsWith(".j3o")) - .map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/')) - .sorted().forEach(charModelCombo.getItems()::add); - } catch (IOException ignored) {} - } - - charAnimSetCombo = new ComboBox<>(); - charAnimSetCombo.setMaxWidth(Double.MAX_VALUE); - charAnimSetCombo.setPromptText("Animations-Set wählen…"); - refreshCharAnimSetCombo(); - charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet()); - - - charEditContainer.getChildren().addAll( - dl.apply("ID:"), charIdField, - dl.apply("Name:"), charNameField, - dl.apply("Typ:"), charTypeCombo - ); - - // ── Fraktion (NPC-only) ─────────────────────────────────────────────── - charFractionCombo = new javafx.scene.control.ComboBox<>(); - charFractionCombo.setMaxWidth(Double.MAX_VALUE); - charFractionCombo.setPromptText("— keine Fraktion —"); - charFractionCombo.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { - @Override protected void updateItem(de.blight.common.model.Fraction f, boolean empty) { - super.updateItem(f, empty); - setText(empty ? null : fractionDisplay(f)); - } - }); - charFractionCombo.setButtonCell(new javafx.scene.control.ListCell<>() { - @Override protected void updateItem(de.blight.common.model.Fraction f, boolean empty) { - super.updateItem(f, empty); - setText(fractionDisplay(f)); - } - }); - refreshFractionCombo(); - charFractionSection = new VBox(2, dl.apply("Fraktion:"), charFractionCombo); - boolean initiallyNpc = "NPC".equals(charTypeCombo.getValue()); - charFractionSection.setVisible(initiallyNpc); - charFractionSection.setManaged(initiallyNpc); - charTypeCombo.valueProperty().addListener((obs, ov, nv) -> { - boolean isNpc = "NPC".equals(nv); - charFractionSection.setVisible(isNpc); - charFractionSection.setManaged(isNpc); - }); - charEditContainer.getChildren().add(charFractionSection); - - // ── NPC-spezifische Felder ──────────────────────────────────────────── - charStatusCombo = new ComboBox<>(); - charStatusCombo.getItems().addAll("FRIENDLY", "NEUTRAL", "ENRAGED", "ENEMY"); - charStatusCombo.setValue("NEUTRAL"); - charStatusCombo.setMaxWidth(Double.MAX_VALUE); - - charTraderCheck = new CheckBox("Ist Händler"); - charTraderCheck.setStyle(LBL); - - charTraderItemsView = new javafx.scene.control.ListView<>(); - charTraderItemsView.setPrefHeight(80); - charTraderItemsView.setStyle("-fx-background-color:#2a2a3a;-fx-control-inner-background:#2a2a3a;-fx-text-fill:#ddd;"); - Button addTraderItemBtn = new Button("+"); - Button delTraderItemBtn = new Button("−"); - addTraderItemBtn.setOnAction(e -> { - javafx.scene.control.TextInputDialog d = new javafx.scene.control.TextInputDialog(); - d.initOwner(primaryStage); - d.setTitle("Item-ID"); - d.setHeaderText("Item-ID der Handelsware:"); - d.showAndWait().ifPresent(id -> { if (!id.isBlank()) charTraderItemsView.getItems().add(id.trim()); }); - }); - delTraderItemBtn.setOnAction(e -> { - String sel = charTraderItemsView.getSelectionModel().getSelectedItem(); - if (sel != null) charTraderItemsView.getItems().remove(sel); - }); - HBox traderBtns = new HBox(4, addTraderItemBtn, delTraderItemBtn); - - 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, - dl.apply("Status:"), charStatusCombo, - dl.apply("Handel:"), charTraderCheck, - dl.apply("Waren:"), charTraderItemsView, traderBtns, - dsl.apply("Standard-Nachrichten je Status"), - dl.apply("Friendly:"), charMsgFriendly, - dl.apply("Neutral:"), charMsgNeutral, - dl.apply("Enraged:"), charMsgEnraged, - dl.apply("Enemy:"), charMsgEnemy); - boolean npcInitial = "NPC".equals(charTypeCombo.getValue()); - charNpcSection.setVisible(npcInitial); - charNpcSection.setManaged(npcInitial); - - charTypeCombo.valueProperty().addListener((obs, ov, nv) -> { - boolean isNpc = "NPC".equals(nv); - charNpcSection.setVisible(isNpc); - charNpcSection.setManaged(isNpc); - }); - - charEditContainer.getChildren().add(charNpcSection); - charEditContainer.getChildren().addAll( - dl.apply("Modell:"), charModelCombo, - dl.apply("Anim-Set:"), charAnimSetCombo - ); - - // ── Aktions-Zuweisung ───────────────────────────────────────────────── - charEditContainer.getChildren().addAll(new Separator(), dsl.apply("Aktions-Zuweisung"), new Separator()); - charActionLabelsBox = new VBox(4); - Label actionHint = new Label("(wird aus Animations-Set geladen)"); - actionHint.setStyle("-fx-font-size:10;-fx-text-fill:#666;"); - charActionLabelsBox.getChildren().add(actionHint); - charEditContainer.getChildren().add(charActionLabelsBox); - - // ── Auto-Save-Listener ──────────────────────────────────────────────── - charIdField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); }); - charTypeCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter()); - charModelCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter()); - charAnimSetCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter()); - charFractionCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter()); - charStatusCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter()); - charTraderCheck.selectedProperty().addListener((obs, ov, nv) -> autoSaveCharacter()); - charTraderItemsView.getItems().addListener( - (javafx.collections.ListChangeListener) c -> autoSaveCharacter()); - charMsgFriendly.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); }); - charMsgNeutral.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); }); - charMsgEnraged.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); }); - charMsgEnemy.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(); }); - - inner.getChildren().add(charEditContainer); - - ScrollPane scroll = new ScrollPane(inner); - scroll.setFitToWidth(true); - scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); - scroll.setStyle("-fx-background-color:#1a1a2a;-fx-background:#1a1a2a;"); - return scroll; - } - - private void refreshCharacterList() { - refreshContentCharacterTree(); - } - - private void refreshCharAnimSetCombo() { - if (charAnimSetCombo == null) return; - String cur = charAnimSetCombo.getValue(); - charAnimSetCombo.getItems().clear(); - Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); - if (java.nio.file.Files.isDirectory(setDir)) { - try (var walk = java.nio.file.Files.walk(setDir)) { - walk.filter(p -> p.toString().endsWith(".animset.json")) - .map(p -> p.getFileName().toString().replace(".animset.json", "")) - .sorted().forEach(charAnimSetCombo.getItems()::add); - } catch (IOException ignored) {} - } - if (cur != null && charAnimSetCombo.getItems().contains(cur)) charAnimSetCombo.setValue(cur); - } - - private void updateCharActionCombosFromSet() { - if (charActionLabelsBox == null) return; - charActionLabelsBox.getChildren().clear(); - String setName = charAnimSetCombo != null ? charAnimSetCombo.getValue() : null; - if (setName == null || setName.isBlank()) { - Label hint = new Label("(kein Animations-Set gewählt)"); - hint.setStyle("-fx-font-size:10;-fx-text-fill:#555;"); - charActionLabelsBox.getChildren().add(hint); - return; - } - Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets"); - de.blight.game.animation.AnimSet animSet; - try { - animSet = de.blight.game.animation.AnimSet.load(setDir, setName); - } catch (IOException e) { - animSet = new de.blight.game.animation.AnimSet(); - } - java.util.Map actionMap = animSet.getActionMap(); - if (actionMap == null || actionMap.isEmpty()) { - Label hint = new Label("(keine Aktions-Zuweisung im Set)"); - hint.setStyle("-fx-font-size:10;-fx-text-fill:#555;"); - charActionLabelsBox.getChildren().add(hint); - return; - } - for (de.blight.game.animation.AnimationAction action - : de.blight.game.animation.AnimationAction.values()) { - String clip = actionMap.get(action.name()); - HBox row = new HBox(6); - row.setAlignment(Pos.CENTER_LEFT); - Label lbl = new Label(action.displayName() + ":"); - lbl.setMinWidth(50); - lbl.setStyle("-fx-font-weight:bold;-fx-text-fill:#ccc;"); - Label val = new Label(clip != null && !clip.isBlank() ? clip : "—"); - val.setStyle("-fx-text-fill:" + (clip != null && !clip.isBlank() ? "#aad4aa" : "#555") + ";"); - row.getChildren().addAll(lbl, val); - charActionLabelsBox.getChildren().add(row); - } - } - - private void clearCharacterForm() { - if (charIdField != null) charIdField.clear(); - if (charNameField != null) charNameField.clear(); - if (charTypeCombo != null) charTypeCombo.setValue("NPC"); - if (charModelCombo != null) charModelCombo.setValue(null); - if (charAnimSetCombo != null) charAnimSetCombo.setValue(null); - if (charFractionCombo != null) charFractionCombo.setValue(null); - 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); - if (dialogEditorView != null) { dialogEditorView.setDisable(false); dialogEditorView.clear(); } - if (routineEditorView != null) { routineEditorView.setDisable(false); routineEditorView.loadNpc(new de.blight.common.model.NPC()); } - if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Neuer Charakter"); - } - - private void loadSelectedCharacter(Path charDir) { - String id = charListView != null ? charListView.getSelectionModel().getSelectedItem() : null; - if (id == null) return; - try { - de.blight.common.model.GameCharacter c = - de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character")); - if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : ""); - if (charNameField != null) charNameField.setText(c.getName() != null ? c.getName().id() : ""); - if (charTypeCombo != null) - charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC"); - if (charModelCombo != null) charModelCombo.setValue(c.getModelPath()); - if (charAnimSetCombo != null) { - charAnimSetCombo.setValue(c.getAnimSetPath()); - updateCharActionCombosFromSet(); - } - if (charFractionCombo != null) { - de.blight.common.model.Fraction npcFraction = - (c instanceof de.blight.common.model.NPC npc) ? npc.getFraction() : null; - if (npcFraction != null && npcFraction.getFractionId() != null) { - final java.util.UUID fid = npcFraction.getFractionId(); - charFractionCombo.getItems().stream() - .filter(f -> f != null && fid.equals(f.getFractionId())) - .findFirst() - .ifPresentOrElse(charFractionCombo::setValue, () -> charFractionCombo.setValue(null)); - } else { - charFractionCombo.setValue(null); - } - } - if (c instanceof de.blight.common.model.NPC npc) { - if (charStatusCombo != null && npc.getStatus() != null) - charStatusCombo.setValue(npc.getStatus().name()); - if (charTraderCheck != null) charTraderCheck.setSelected(npc.isTrader()); - if (charTraderItemsView != null) { - charTraderItemsView.getItems().clear(); - if (npc.getItems() != null) - npc.getItems().forEach(it -> charTraderItemsView.getItems().add(it.getItemId())); - } - java.util.Map 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()); - } - if (charEditContainer != null) charEditContainer.setDisable(false); - if (dialogEditorView != null) { - dialogEditorView.setDisable(false); - if (c instanceof de.blight.common.model.NPC npc) dialogEditorView.loadNpc(npc); - else dialogEditorView.clear(); - } - if (routineEditorView != null) { - routineEditorView.setDisable(false); - if (c instanceof de.blight.common.model.NPC npc) routineEditorView.loadNpc(npc); - else routineEditorView.loadNpc(new de.blight.common.model.NPC()); - } - if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id); - } catch (Exception e) { - if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage()); - } - } - - private void saveCharacter(Path charDir) { - String id = charIdField != null ? charIdField.getText().trim() : ""; - if (id.isBlank()) { - if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: ID darf nicht leer sein"); - return; - } - String type = charTypeCombo != null ? charTypeCombo.getValue() : "NPC"; - boolean isMain = "MainCharacter".equals(type); - - if (isMain && de.blight.common.model.CharacterIO.mainCharacterExists(charDir, id)) { - if (charEditorStatusLabel != null) - charEditorStatusLabel.setText("Fehler: Es gibt bereits einen MainCharacter!"); - return; - } - - de.blight.common.model.GameCharacter c = isMain - ? new de.blight.common.model.MainCharacter() - : new de.blight.common.model.NPC(); - c.setCharacterId(id); - String nameId = charNameField != null ? charNameField.getText().trim() : ""; - c.setName(nameId.isBlank() ? null : new de.blight.common.model.TextReference(nameId)); - c.setModelPath(charModelCombo != null ? charModelCombo.getValue() : null); - c.setAnimSetPath(charAnimSetCombo != null ? charAnimSetCombo.getValue() : null); - if (c instanceof de.blight.common.model.NPC npc) { - if (dialogEditorView != null) dialogEditorView.exportToNpc(npc); - if (routineEditorView != null) routineEditorView.exportToNpc(npc); - npc.setFraction(charFractionCombo != null ? charFractionCombo.getValue() : null); - if (charStatusCombo != null && charStatusCombo.getValue() != null) { - try { npc.setStatus(de.blight.common.model.Status.valueOf(charStatusCombo.getValue())); } - catch (IllegalArgumentException ignored) {} - } - if (charTraderCheck != null) npc.setTrader(charTraderCheck.isSelected()); - // Default-Nachrichten je Status - java.util.Map 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 items = new java.util.ArrayList<>(); - charTraderItemsView.getItems().forEach(id2 -> { - de.blight.common.model.Item it = new de.blight.common.model.Item(); - it.setItemId(id2); - items.add(it); - }); - npc.setItems(items); - } else { - npc.setItems(null); - } - } - if (c instanceof de.blight.common.model.MainCharacter mc) { - mc.setAbilities(buildAbilities()); - } - - try { - de.blight.common.model.CharacterIO.save(c, charDir); - refreshCharacterList(); - if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id); - } catch (Exception e) { - if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage()); - } - } - - /** - * Zeigt einen Dialog für Import-Korrekturen. - * @return float[2] { yRotationDeg, centerOrigin(0f/1f) } oder null bei Abbruch - */ - private float[] showImportCorrectionDialog(javafx.stage.Window owner) { - javafx.scene.control.Dialog dlg = new javafx.scene.control.Dialog<>(); - dlg.initOwner(owner); - dlg.setTitle("Import-Korrektur"); - dlg.setHeaderText("Orientierungs-Korrektur beim Import"); - - javafx.scene.control.ButtonType okBtn = - new javafx.scene.control.ButtonType("Importieren", - javafx.scene.control.ButtonBar.ButtonData.OK_DONE); - dlg.getDialogPane().getButtonTypes().addAll(okBtn, - javafx.scene.control.ButtonType.CANCEL); - - // Y-Rotation - Label rotLabel = new Label("Y-Rotation:"); - ComboBox rotBox = new ComboBox<>(); - rotBox.getItems().addAll("0°", "90°", "180°", "-90°", "-180°"); - rotBox.setValue("180°"); // Tripo3D-Standard: Modell steht oft verkehrt herum - rotBox.setMaxWidth(Double.MAX_VALUE); - - // Translation - CheckBox centerCB = new CheckBox("Position auf Ursprung setzen (0 / 0 / 0)"); - centerCB.setSelected(true); - - // Hint - Label hint = new Label( - "Die Korrektur wird einmalig beim Speichern der .j3o-Datei eingebacken.\n" + - "Ändert sich die Ausrichtung, Modell einfach erneut importieren."); - hint.setWrapText(true); - hint.setStyle("-fx-font-size: 11; -fx-text-fill: #666;"); - - VBox content = new VBox(8, rotLabel, rotBox, centerCB, new Separator(), hint); - content.setPadding(new Insets(10)); - content.setPrefWidth(340); - dlg.getDialogPane().setContent(content); - - dlg.setResultConverter(bt -> { - if (bt != okBtn) return null; - String sel = rotBox.getValue(); - float deg = switch (sel) { - case "90°" -> 90f; - case "180°" -> 180f; - case "-90°" -> -90f; - case "-180°" -> -180f; - default -> 0f; - }; - return new float[]{ deg, centerCB.isSelected() ? 1f : 0f }; - }); - - return dlg.showAndWait().orElse(null); - } - - private static javafx.scene.layout.HBox abRow(String label, javafx.scene.control.Spinner s) { - javafx.scene.control.Label l = new javafx.scene.control.Label(label); - l.setMinWidth(160); - l.setStyle("-fx-text-fill:#ccc;"); - javafx.scene.layout.HBox.setHgrow(s, javafx.scene.layout.Priority.ALWAYS); - javafx.scene.layout.HBox row = new javafx.scene.layout.HBox(6, l, s); - row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - return row; - } - - private static javafx.scene.control.Spinner abSpinner() { - var s = new javafx.scene.control.Spinner(1, 10, 1); - s.setEditable(true); return s; - } - - private static javafx.scene.control.Spinner ab3Spinner() { - var s = new javafx.scene.control.Spinner(0, 3, 0); - s.setEditable(true); return s; - } - - private void resetAbilities() { - if (abMagicSpin == null) return; - abMagicSpin.getValueFactory().setValue(1); abStaffSpin.getValueFactory().setValue(1); - abSwordSpin.getValueFactory().setValue(1); abArcherySpin.getValueFactory().setValue(1); - abHeavySpin.getValueFactory().setValue(1); abCrossbowSpin.getValueFactory().setValue(1); - abThieverySpin.getValueFactory().setValue(0); abAlchemySpin.getValueFactory().setValue(0); - abEngineeringSpin.getValueFactory().setValue(0); abSmitherySpin.getValueFactory().setValue(0); - abEnchantingSpin.getValueFactory().setValue(0); - } - - private void loadAbilities(de.blight.common.model.abilities.Abilities ab) { - if (ab == null) { resetAbilities(); return; } - if (abMagicSpin == null) return; - abMagicSpin.getValueFactory().setValue(Math.max(1, ab.getLvlMagic())); - abStaffSpin.getValueFactory().setValue(Math.max(1, ab.getLvlStaffCombat())); - abSwordSpin.getValueFactory().setValue(Math.max(1, ab.getLvlSwordsmanship())); - abArcherySpin.getValueFactory().setValue(Math.max(1, ab.getLvlArchery())); - abHeavySpin.getValueFactory().setValue(Math.max(1, ab.getLvlHeavyWeapons())); - abCrossbowSpin.getValueFactory().setValue(Math.max(1, ab.getLvlCrossbow())); - abThieverySpin.getValueFactory().setValue(ab.getLvlThievery()); - abAlchemySpin.getValueFactory().setValue(ab.getLvlAlchemy()); - abEngineeringSpin.getValueFactory().setValue(ab.getLvlEngineering()); - abSmitherySpin.getValueFactory().setValue(ab.getLvlSmithery()); - abEnchantingSpin.getValueFactory().setValue(ab.getLvlEnchanting()); - } - - private de.blight.common.model.abilities.Abilities buildAbilities() { - if (abMagicSpin == null) return null; - de.blight.common.model.abilities.Abilities ab = new de.blight.common.model.abilities.Abilities(); - ab.setLvlMagic(abMagicSpin.getValue()); - ab.setLvlStaffCombat(abStaffSpin.getValue()); - ab.setLvlSwordsmanship(abSwordSpin.getValue()); - ab.setLvlArchery(abArcherySpin.getValue()); - ab.setLvlHeavyWeapons(abHeavySpin.getValue()); - ab.setLvlCrossbow(abCrossbowSpin.getValue()); - ab.setLvlThievery(abThieverySpin.getValue()); - ab.setLvlAlchemy(abAlchemySpin.getValue()); - ab.setLvlEngineering(abEngineeringSpin.getValue()); - ab.setLvlSmithery(abSmitherySpin.getValue()); - ab.setLvlEnchanting(abEnchantingSpin.getValue()); - return ab; - } - - private static String msgKey( - java.util.Map 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 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() { - if (dualWindowMode) { - showContentEditor(); - contentLastSection = "location"; - if (contentLocationBtn != null) contentLocationBtn.setSelected(true); - buildLocationViewInContent(); - return; - } - onF5 = null; - currentTool = "locationEditor"; - ToolBar tb = new ToolBar(); - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Locations-Manager"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - topBar.getChildren().set(1, tb); - root.setCenter(new de.blight.editor.ui.LocationEditorView()); - root.setRight(null); - } - - private void switchToAreaEditor() { - if (dualWindowMode) { - showContentEditor(); - contentLastSection = "area"; - if (contentAreaBtn != null) contentAreaBtn.setSelected(true); - buildAreaViewInContent(); - return; - } - onF5 = null; - currentTool = "areaEditor"; - ToolBar tb = new ToolBar(); - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Areas-Manager"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - topBar.getChildren().set(1, tb); - root.setCenter(new de.blight.editor.ui.AreaEditorView()); - root.setRight(null); - } - - private void switchToLocalizationEditor() { - onF5 = null; - currentTool = "localizationEditor"; - ToolBar tb = new ToolBar(); - Button backBtn = new Button("← Welteneditor"); - backBtn.setOnAction(e -> switchToWorldEditor()); - Label label = new Label("Lokalisierungs-Editor"); - label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); - tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); - topBar.getChildren().set(1, tb); - root.setCenter(new de.blight.editor.ui.LocalizationEditorView( - de.blight.editor.ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang"))); - root.setRight(null); - } - - // ── Content Editor ──────────────────────────────────────────────────────── - - /** Gibt den Ziel-BorderPane für Content-Editor-Operationen zurück. */ - private BorderPane contentTarget() { - return (dualWindowMode && contentEditorRoot != null) ? contentEditorRoot : root; - } - - private javafx.scene.layout.Region createToolBarSpacer() { - javafx.scene.layout.Region spacer = new javafx.scene.layout.Region(); - javafx.scene.layout.HBox.setHgrow(spacer, javafx.scene.layout.Priority.ALWAYS); - return spacer; - } - - /** "> Content"-Button für den rechten Rand der Welt-Toolbar (nur 1-Fenster-Modus). */ - private Button buildContentSwitchBtn() { - contentSwitchBtn = new Button("> Content"); - contentSwitchBtn.setStyle("-fx-font-weight:bold;"); - contentSwitchBtn.setOnAction(e -> switchToContentEditor()); - return contentSwitchBtn; - } - - /** 2-Fenster-Modus aktivieren: Titelleisten-Buttons ausblenden, Content-Stage öffnen. */ - private void enableDualWindowMode() { - dualWindowMode = true; - if (contentSwitchBtn != null) { - contentSwitchBtn.setVisible(false); - contentSwitchBtn.setManaged(false); - } - showContentEditor(); - restoreContentSection(contentLastSection); - } - - /** Content-Editor-Stage erstellen und anzeigen. - * Schließen per X oder Taskbar ist geblockt; Min/Max bleiben erhalten. - * initOwner gruppiert beide Fenster zu einer Taskbar-Instanz. */ - private void showContentEditor() { - if (contentEditorStage == null) { - contentEditorStage = new Stage(); - contentEditorStage.setTitle("Blight Content Editor"); - contentEditorStage.setOnCloseRequest(e -> e.consume()); - try (var is = getClass().getResourceAsStream("/icon_editor.png")) { - if (is != null) contentEditorStage.getIcons().add(new Image(is)); - } catch (Exception ignored) {} - - contentEditorRoot = new BorderPane(); - contentEditorRoot.setTop(buildContentTopBar()); - contentEditorRoot.setLeft(buildContentTreePanel()); - contentEditorRoot.setCenter(new Label("Wähle einen Bereich in der Toolbar.")); - - Scene scene = new Scene(contentEditorRoot, 1200, 750); - java.net.URL cssUrl = getClass().getResource("/editor.css"); - if (cssUrl != null) scene.getStylesheets().add(cssUrl.toExternalForm()); - scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> { - if (!e.isControlDown()) return; - int idx = switch (e.getCode()) { - case F1 -> 0; case F2 -> 1; case F3 -> 2; case F4 -> 3; - case F5 -> 4; case F6 -> 5; case F7 -> 6; case F8 -> 7; - default -> -1; - }; - if (idx >= 0) { e.consume(); fireContentBtn(idx); } - }); - - contentEditorStage.setScene(scene); - contentEditorStage.setMinWidth(800); - contentEditorStage.setMinHeight(500); - } - contentEditorStage.show(); - contentEditorStage.toFront(); - } - - /** 2-Fenster-Modus deaktivieren: Stage verstecken, Wechsel-Button wieder zeigen. */ - private void disableDualWindowMode() { - dualWindowMode = false; - if (dualWindowMenuItem != null) dualWindowMenuItem.setSelected(false); - if (contentEditorStage != null) contentEditorStage.hide(); - if (contentSwitchBtn != null) { - contentSwitchBtn.setVisible(true); - contentSwitchBtn.setManaged(true); - } - } - - /** Wechselt im 1-Fenster-Modus zur Content-Editor-Ansicht in der Welt-Stage. */ - private void switchToContentEditor() { - currentTool = "content"; - if (jmeApp != null) jmeApp.setRenderThrottled(true); - root.setTop(topBar); - topBar.getChildren().set(1, buildSingleWindowContentToolBar()); - root.setLeft(buildContentTreePanel()); - root.setBottom(null); - restoreContentSection(contentLastSection); - } - - /** Content-Toolbar für 1-Fenster-Modus: mit "< Welt"-Button. */ - private ToolBar buildSingleWindowContentToolBar() { - return buildContentEditorToolBar(true); - } - - /** Obere Leiste des Content-Editors im 2-Fenster-Modus: nur Toolbar (kein "< Welt"). */ - private VBox buildContentTopBar() { - ToolBar tb = buildContentEditorToolBar(false); - return new VBox(tb); - } - - private ToolBar buildContentEditorToolBar(boolean includeWeltBtn) { - contentCharBtn = new ToggleButton("@ Character"); - contentQuestBtn = new ToggleButton("! Quest"); - contentFraktionBtn = new ToggleButton("* Fraktion"); - contentItemBtn = new ToggleButton("+ Item"); - contentRezeptBtn = new ToggleButton("= Rezept"); - contentCraftingBtn = new ToggleButton("# Crafting"); - contentAreaBtn = new ToggleButton("□ Area"); - contentLocationBtn = new ToggleButton("^ Location"); - contentLangBtn = new ToggleButton("≈ Lang"); - - String bold = "-fx-font-weight:bold;"; - for (ToggleButton b : new ToggleButton[]{contentCharBtn, contentQuestBtn, contentFraktionBtn, - contentItemBtn, contentRezeptBtn, contentCraftingBtn, contentAreaBtn, contentLocationBtn, - contentLangBtn}) { - b.setStyle(bold); - } - - ToggleGroup cg = new ToggleGroup(); - contentCharBtn.setToggleGroup(cg); - contentQuestBtn.setToggleGroup(cg); - contentFraktionBtn.setToggleGroup(cg); - contentItemBtn.setToggleGroup(cg); - contentRezeptBtn.setToggleGroup(cg); - contentCraftingBtn.setToggleGroup(cg); - contentAreaBtn.setToggleGroup(cg); - contentLocationBtn.setToggleGroup(cg); - contentLangBtn.setToggleGroup(cg); - - contentCharBtn.setOnAction(e -> { - contentLastSection = "character"; buildCharacterViewInContent(); - }); - contentQuestBtn.setOnAction(e -> { - contentLastSection = "quest"; buildQuestViewInContent(); - }); - contentFraktionBtn.setOnAction(e -> { - contentLastSection = "fraction"; buildFractionViewInContent(); - }); - contentItemBtn.setOnAction(e -> { - contentLastSection = "item"; buildItemViewInContent(); - }); - contentRezeptBtn.setOnAction(e -> { - contentLastSection = "recipe"; buildRecipeViewInContent(); - }); - contentCraftingBtn.setOnAction(e -> { - contentLastSection = "crafting"; buildCraftingViewInContent(); - }); - contentAreaBtn.setOnAction(e -> { - contentLastSection = "area"; buildAreaViewInContent(); - }); - contentLocationBtn.setOnAction(e -> { - contentLastSection = "location"; buildLocationViewInContent(); - }); - contentLangBtn.setOnAction(e -> { - contentLastSection = "lang"; buildLocalizationViewInContent(); - }); - - ToolBar tb = new ToolBar(); - tb.getItems().addAll( - contentCharBtn, - new Separator(Orientation.VERTICAL), contentQuestBtn, - new Separator(Orientation.VERTICAL), contentFraktionBtn, - new Separator(Orientation.VERTICAL), contentItemBtn, - new Separator(Orientation.VERTICAL), contentRezeptBtn, contentCraftingBtn, - new Separator(Orientation.VERTICAL), contentAreaBtn, contentLocationBtn, - new Separator(Orientation.VERTICAL), contentLangBtn); - if (includeWeltBtn) { - Button weltBtn = new Button("< Welt"); - weltBtn.setStyle("-fx-font-weight:bold;"); - weltBtn.setOnAction(e -> switchToWorldEditor()); - tb.getItems().addAll(createToolBarSpacer(), weltBtn); - } - return tb; - } - - private javafx.scene.control.TreeView buildContentTree() { - javafx.scene.control.TreeItem treeRoot = - new javafx.scene.control.TreeItem<>("Content"); - treeRoot.setExpanded(true); - - contentCharNode = new javafx.scene.control.TreeItem<>("Characters"); - contentQuestNode = new javafx.scene.control.TreeItem<>("Quests"); - contentFractionNode = new javafx.scene.control.TreeItem<>("Fraktionen"); - contentItemNode = new javafx.scene.control.TreeItem<>("Items"); - contentRecipeNode = new javafx.scene.control.TreeItem<>("Rezepte"); - contentCraftingNode = new javafx.scene.control.TreeItem<>("Crafting Tables"); - contentAreaNode = new javafx.scene.control.TreeItem<>("Areas"); - contentLocationNode = new javafx.scene.control.TreeItem<>("Locations"); - - for (var node : new javafx.scene.control.TreeItem[]{ - contentCharNode, contentQuestNode, contentFractionNode, contentItemNode, - contentRecipeNode, contentCraftingNode, contentAreaNode, contentLocationNode}) { - node.setExpanded(false); - treeRoot.getChildren().add(node); - } - - refreshContentCharacterTree(); - refreshContentCraftingTree(); - refreshContentItemTree(); - refreshContentRecipeTree(); - refreshContentQuestTree(); - refreshContentFractionTree(); - refreshContentAreaTree(); - refreshContentLocationTree(); - - contentTreeView = new javafx.scene.control.TreeView<>(treeRoot); - contentTreeView.setShowRoot(false); - contentTreeView.setPrefWidth(420); - contentTreeView.setMinWidth(200); - contentTreeView.setCellFactory(tv -> new javafx.scene.control.TreeCell<>() { - @Override protected void updateItem(String item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { setText(null); setGraphic(null); setStyle(""); return; } - javafx.scene.control.TreeItem ti = getTreeItem(); - javafx.scene.control.TreeItem par = ti != null ? ti.getParent() : null; - boolean isColored = item.contains(":") && - (par == contentQuestNode || par == contentItemNode || par == contentRecipeNode); - if (isColored) { - String prefix = item.substring(0, item.indexOf(':')); - String id = item.substring(item.indexOf(':') + 1); - String color; - if (par == contentQuestNode) color = questTypeColor(prefix); - else if (par == contentItemNode) color = de.blight.editor.ui.ItemEditorView.categoryColor(safeItemCategory(prefix)); - else color = contentRecipeColor(prefix); - javafx.scene.shape.Rectangle box = new javafx.scene.shape.Rectangle(10, 10); - box.setFill(javafx.scene.paint.Color.web(color)); - box.setArcWidth(3); box.setArcHeight(3); - HBox graphic = new HBox(5, box); - graphic.setAlignment(Pos.CENTER_LEFT); - setText(id); setGraphic(graphic); setStyle(""); - } else { - setText(item); setGraphic(null); - boolean isSection = par == null || par.getParent() == null; - setStyle(isSection ? "-fx-font-weight:bold;" : ""); - } - } - }); - - contentTreeView.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> { - if (contentTreeUpdating) return; - if (nv == null) { - if (contentDeleteBtn != null) contentDeleteBtn.setDisable(true); - return; - } - boolean isLeaf = nv.isLeaf(); - if (contentDeleteBtn != null) contentDeleteBtn.setDisable(!isLeaf); - - javafx.scene.control.TreeItem parent = nv.getParent(); - if (parent == contentCharNode) { - if (contentCharBtn != null && !contentCharBtn.isSelected()) contentCharBtn.fire(); - loadCharacterById(nv.getValue()); return; - } - if (parent == contentQuestNode) { - if (contentQuestBtn != null && !contentQuestBtn.isSelected()) contentQuestBtn.fire(); - String qVal = nv.getValue(); - String qId = qVal.contains(":") ? qVal.substring(qVal.indexOf(':') + 1) : qVal; - if (activeQuestView != null) activeQuestView.selectById(qId); return; - } - if (parent == contentFractionNode) { - if (contentFraktionBtn != null && !contentFraktionBtn.isSelected()) contentFraktionBtn.fire(); - if (activeFractionView != null) activeFractionView.selectByNameId(nv.getValue()); return; - } - if (parent == contentItemNode) { - if (contentItemBtn != null && !contentItemBtn.isSelected()) contentItemBtn.fire(); - String iVal = nv.getValue(); - String iId = iVal.contains(":") ? iVal.substring(iVal.indexOf(':') + 1) : iVal; - if (activeItemView != null) activeItemView.selectItem(iId); return; - } - if (parent == contentRecipeNode) { - if (contentRezeptBtn != null && !contentRezeptBtn.isSelected()) contentRezeptBtn.fire(); - String rVal = nv.getValue(); - String rId = rVal.contains(":") ? rVal.substring(rVal.indexOf(':') + 1) : rVal; - if (activeRecipeView != null) activeRecipeView.selectById(rId); return; - } - if (parent == contentCraftingNode) { - if (contentCraftingBtn != null && !contentCraftingBtn.isSelected()) contentCraftingBtn.fire(); - if (activeCraftingView != null) activeCraftingView.selectByTypeName(nv.getValue()); return; - } - if (parent == contentAreaNode) { - if (contentAreaBtn != null && !contentAreaBtn.isSelected()) contentAreaBtn.fire(); - if (activeAreaView != null) activeAreaView.selectById(nv.getValue()); return; - } - if (parent == contentLocationNode) { - if (contentLocationBtn != null && !contentLocationBtn.isSelected()) contentLocationBtn.fire(); - if (activeLocationView != null) activeLocationView.selectById(nv.getValue()); return; - } - switch (nv.getValue()) { - case "Characters" -> { if (contentCharBtn != null && !contentCharBtn.isSelected()) contentCharBtn.fire(); } - case "Quests" -> { if (contentQuestBtn != null && !contentQuestBtn.isSelected()) contentQuestBtn.fire(); } - case "Fraktionen" -> { if (contentFraktionBtn != null && !contentFraktionBtn.isSelected()) contentFraktionBtn.fire(); } - case "Items" -> { if (contentItemBtn != null && !contentItemBtn.isSelected()) contentItemBtn.fire(); } - case "Rezepte" -> { if (contentRezeptBtn != null && !contentRezeptBtn.isSelected()) contentRezeptBtn.fire(); } - case "Crafting Tables" -> { if (contentCraftingBtn != null && !contentCraftingBtn.isSelected()) contentCraftingBtn.fire(); } - case "Areas" -> { if (contentAreaBtn != null && !contentAreaBtn.isSelected()) contentAreaBtn.fire(); } - case "Locations" -> { if (contentLocationBtn != null && !contentLocationBtn.isSelected()) contentLocationBtn.fire(); } - } - }); - - // Kontextmenü - javafx.scene.control.ContextMenu ctxMenu = new javafx.scene.control.ContextMenu(); - contentTreeView.setOnContextMenuRequested(e -> { - ctxMenu.getItems().clear(); - javafx.scene.control.TreeItem sel = - contentTreeView.getSelectionModel().getSelectedItem(); - boolean isLeaf = sel != null && sel.isLeaf(); - - javafx.scene.control.MenuItem addMi = new javafx.scene.control.MenuItem("Hinzufügen"); - addMi.setOnAction(ev -> contentNew()); - ctxMenu.getItems().add(addMi); - - if (isLeaf) { - javafx.scene.control.MenuItem delMi = new javafx.scene.control.MenuItem("Löschen"); - delMi.setOnAction(ev -> contentDelete()); - javafx.scene.control.MenuItem dupMi = new javafx.scene.control.MenuItem("Duplizieren"); - dupMi.setOnAction(ev -> contentDuplicate()); - ctxMenu.getItems().addAll(delMi, dupMi); - } - - ctxMenu.getItems().add(new javafx.scene.control.SeparatorMenuItem()); - javafx.scene.control.MenuItem refreshMi = new javafx.scene.control.MenuItem("Aktualisieren"); - refreshMi.setOnAction(ev -> contentRefresh()); - ctxMenu.getItems().add(refreshMi); - - ctxMenu.show(contentTreeView, e.getScreenX(), e.getScreenY()); - e.consume(); - }); - - // F5 = Refresh, Delete = Löschen - contentTreeView.setOnKeyPressed(e -> { - if (e.getCode() == javafx.scene.input.KeyCode.F5) { - contentRefresh(); e.consume(); - } else if (e.getCode() == javafx.scene.input.KeyCode.DELETE) { - contentDelete(); e.consume(); - } - }); - - return contentTreeView; - } - - private javafx.scene.layout.VBox buildContentTreePanel() { - javafx.scene.control.TreeView tree = buildContentTree(); - - contentNewBtn = new Button("+ Neu"); - contentDeleteBtn = new Button("- Löschen"); - Button refreshBtn = new Button("..."); - - contentNewBtn.setStyle("-fx-background-color:#2a7a2a;-fx-text-fill:white;"); - contentDeleteBtn.setStyle("-fx-background-color:#7a2a2a;-fx-text-fill:white;"); - contentDeleteBtn.setDisable(true); - refreshBtn.setText("⟳"); - refreshBtn.setTooltip(new Tooltip("Aktualisieren (F5)")); - refreshBtn.setMinWidth(javafx.scene.control.Control.USE_PREF_SIZE); - - Button collapseBtn = new Button("◀"); - collapseBtn.setTooltip(new Tooltip("Alle zuklappen")); - collapseBtn.setMinWidth(javafx.scene.control.Control.USE_PREF_SIZE); - - contentNewBtn.setMaxWidth(Double.MAX_VALUE); - contentDeleteBtn.setMaxWidth(Double.MAX_VALUE); - HBox.setHgrow(contentNewBtn, Priority.ALWAYS); - HBox.setHgrow(contentDeleteBtn, Priority.ALWAYS); - - contentNewBtn.setOnAction(e -> contentNew()); - contentDeleteBtn.setOnAction(e -> contentDelete()); - refreshBtn.setOnAction(e -> contentRefresh()); - collapseBtn.setOnAction(e -> collapseAllContentSections()); - - HBox btnBar = new HBox(4, contentNewBtn, contentDeleteBtn, collapseBtn, refreshBtn); - btnBar.setPadding(new Insets(4)); - btnBar.setAlignment(Pos.CENTER_LEFT); - - javafx.scene.layout.VBox panel = new javafx.scene.layout.VBox(btnBar, tree); - javafx.scene.layout.VBox.setVgrow(tree, Priority.ALWAYS); - return panel; - } - - private void contentNew() { - switch (contentLastSection != null ? contentLastSection : "character") { - case "character" -> { clearCharacterForm(); if (contentCharBtn != null && !contentCharBtn.isSelected()) contentCharBtn.fire(); } - case "quest" -> { if (activeQuestView != null) activeQuestView.createNew(); } - case "fraction" -> { if (activeFractionView != null) activeFractionView.createNew(); } - case "item" -> { if (activeItemView != null) activeItemView.createNew(); } - case "recipe" -> { if (activeRecipeView != null) activeRecipeView.createNew(); } - case "area" -> { if (activeAreaView != null) activeAreaView.createNew(); } - case "location" -> { if (activeLocationView != null) activeLocationView.createNew(); } - } - } - - private void contentDelete() { - if (contentTreeView == null) return; - javafx.scene.control.TreeItem sel = - contentTreeView.getSelectionModel().getSelectedItem(); - if (sel == null || !sel.isLeaf()) return; - - javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( - javafx.scene.control.Alert.AlertType.CONFIRMATION); - confirm.initOwner(primaryStage); - confirm.setTitle("Löschen bestätigen"); - confirm.setHeaderText("\"" + sel.getValue() + "\" wirklich löschen?"); - confirm.setContentText("Diese Aktion kann nicht rückgängig gemacht werden."); - confirm.showAndWait().ifPresent(bt -> { - if (bt != javafx.scene.control.ButtonType.OK) return; - javafx.scene.control.TreeItem parent = sel.getParent(); - if (parent == contentCharNode) deleteCharacterById(sel.getValue()); - else if (parent == contentQuestNode && activeQuestView != null) { activeQuestView.deleteCurrentItem(); refreshContentQuestTree(); } - else if (parent == contentFractionNode && activeFractionView != null) { activeFractionView.deleteCurrentItem(); refreshContentFractionTree(); } - else if (parent == contentItemNode && activeItemView != null) { activeItemView.deleteCurrentItem(); refreshContentItemTree(); } - else if (parent == contentRecipeNode && activeRecipeView != null) { activeRecipeView.deleteCurrentItem(); refreshContentRecipeTree(); } - else if (parent == contentAreaNode && activeAreaView != null) { activeAreaView.deleteCurrentItem(); refreshContentAreaTree(); } - else if (parent == contentLocationNode && activeLocationView != null) { activeLocationView.deleteCurrentItem(); refreshContentLocationTree(); } - else if (parent == contentCraftingNode && activeCraftingView != null) { activeCraftingView.deleteCurrentItem(); } - }); - } - - private void deleteCharacterById(String id) { - Path charFile = ASSET_ROOT.resolve("Characters").resolve(id + ".character"); - try { - java.nio.file.Files.deleteIfExists(charFile); - } catch (IOException ex) { - new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.ERROR, - "Fehler beim Löschen: " + ex.getMessage()).showAndWait(); - return; - } - refreshContentCharacterTree(); - clearCharacterForm(); - if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Character gelöscht."); - } - - private void contentDuplicate() { - if (contentTreeView == null) return; - javafx.scene.control.TreeItem sel = - contentTreeView.getSelectionModel().getSelectedItem(); - if (sel == null || !sel.isLeaf()) return; - javafx.scene.control.TreeItem parent = sel.getParent(); - if (parent == contentCharNode) { - duplicateCharacterById(sel.getValue()); - } - } - - private void duplicateCharacterById(String sourceId) { - javafx.scene.control.TextInputDialog dlg = - new javafx.scene.control.TextInputDialog(sourceId + "_copy"); - dlg.initOwner(primaryStage); - dlg.setTitle("Duplizieren"); - dlg.setHeaderText("Neue ID für die Kopie von \"" + sourceId + "\":"); - dlg.setContentText("ID:"); - dlg.showAndWait().ifPresent(newId -> { - if (newId == null || newId.isBlank()) return; - Path charDir = ASSET_ROOT.resolve("Characters"); - Path src = charDir.resolve(sourceId + ".character"); - Path dest = charDir.resolve(newId.trim() + ".character"); - try { - java.nio.file.Files.copy(src, dest, - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - refreshContentCharacterTree(); - loadCharacterById(newId.trim()); - // neu erstellten Knoten selektieren - contentCharNode.getChildren().stream() - .filter(ti -> ti.getValue().equals(newId.trim())) - .findFirst() - .ifPresent(ti -> contentTreeView.getSelectionModel().select(ti)); - } catch (IOException ex) { - new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.ERROR, - "Fehler beim Duplizieren: " + ex.getMessage()).showAndWait(); - } - }); - } - - private void contentRefresh() { - refreshContentCharacterTree(); - refreshContentQuestTree(); - refreshContentItemTree(); - refreshContentFractionTree(); - refreshContentRecipeTree(); - refreshContentAreaTree(); - refreshContentLocationTree(); - } - - private void refreshContentCharacterTree() { - if (contentCharNode == null) return; - contentTreeUpdating = true; - try { - contentCharNode.getChildren().clear(); - Path charDir = ASSET_ROOT.resolve("Characters"); - if (!java.nio.file.Files.isDirectory(charDir)) return; - java.util.List> items = new java.util.ArrayList<>(); - try (var walk = java.nio.file.Files.list(charDir)) { - walk.filter(p -> p.toString().endsWith(".character")) - .map(p -> p.getFileName().toString().replace(".character", "")) - .sorted() - .forEach(id -> items.add(new javafx.scene.control.TreeItem<>(id))); - } catch (IOException ignored) {} - contentCharNode.getChildren().addAll(items); - } finally { - contentTreeUpdating = false; - } - } - - private void loadCharacterById(String id) { - if (id == null || id.isBlank()) return; - Path charDir = ASSET_ROOT.resolve("Characters"); - try { - de.blight.common.model.GameCharacter c = - de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character")); - if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : ""); - if (charNameField != null) charNameField.setText(c.getName() != null ? c.getName().id() : ""); - if (charTypeCombo != null) - charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC"); - if (charModelCombo != null) charModelCombo.setValue(c.getModelPath()); - if (charAnimSetCombo != null) { - charAnimSetCombo.setValue(c.getAnimSetPath()); - updateCharActionCombosFromSet(); - } - if (charFractionCombo != null) { - de.blight.common.model.Fraction npcFraction = - (c instanceof de.blight.common.model.NPC npc) ? npc.getFraction() : null; - if (npcFraction != null && npcFraction.getFractionId() != null) { - final java.util.UUID fid = npcFraction.getFractionId(); - charFractionCombo.getItems().stream() - .filter(f -> f != null && fid.equals(f.getFractionId())) - .findFirst() - .ifPresentOrElse(charFractionCombo::setValue, () -> charFractionCombo.setValue(null)); - } else { - charFractionCombo.setValue(null); - } - } - if (c instanceof de.blight.common.model.NPC npc) { - if (charStatusCombo != null && npc.getStatus() != null) - charStatusCombo.setValue(npc.getStatus().name()); - if (charTraderCheck != null) charTraderCheck.setSelected(npc.isTrader()); - if (charTraderItemsView != null) { - charTraderItemsView.getItems().clear(); - if (npc.getItems() != null) - npc.getItems().forEach(it -> charTraderItemsView.getItems().add(it.getItemId())); - } - java.util.Map 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 (dialogEditorView != null) { - dialogEditorView.setDisable(false); - dialogEditorView.loadNpc(npc); - } - } else { - if (dialogEditorView != null) { - dialogEditorView.setDisable(false); - dialogEditorView.clear(); - } - } - if (c instanceof de.blight.common.model.MainCharacter mc) { - loadAbilities(mc.getAbilities()); - } - if (charEditContainer != null) charEditContainer.setDisable(false); - if (routineEditorView != null) { - routineEditorView.setDisable(false); - if (c instanceof de.blight.common.model.NPC npc) routineEditorView.loadNpc(npc); - else routineEditorView.loadNpc(new de.blight.common.model.NPC()); - } - if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id); - } catch (Exception e) { - if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage()); - } - } - - /** Ctrl+F1–F8 im Hauptfenster: ggf. in Content-Modus wechseln, dann Werkzeug aktivieren. */ - private void activateContentSection(int idx) { - if (dualWindowMode) { - if (contentEditorStage != null && !contentEditorStage.isShowing()) { - contentEditorStage.show(); - } - fireContentBtn(idx); - } else { - if (!"content".equals(currentTool)) { - switchToContentEditor(); - } - fireContentBtn(idx); - } - } - - private void fireContentBtn(int idx) { - if (contentCharBtn == null) return; - ToggleButton[] btns = { contentCharBtn, contentQuestBtn, contentFraktionBtn, - contentItemBtn, contentRezeptBtn, contentCraftingBtn, - contentAreaBtn, contentLocationBtn }; - if (idx >= 0 && idx < btns.length) btns[idx].fire(); - } - - private void restoreContentSection(String section) { - if (contentCharBtn == null) return; - switch (section) { - case "character" -> contentCharBtn.fire(); - case "quest" -> contentQuestBtn.fire(); - case "fraction" -> contentFraktionBtn.fire(); - case "item" -> contentItemBtn.fire(); - case "recipe" -> contentRezeptBtn.fire(); - case "crafting" -> contentCraftingBtn.fire(); - case "area" -> contentAreaBtn.fire(); - case "location" -> contentLocationBtn.fire(); - case "lang" -> contentLangBtn.fire(); - } - } - private void restoreEditorPrefs() { EditorPrefs ep = EditorPrefs.load(); input.showGrid = ep.showGrid; - if (showGridItem != null) showGridItem.setSelected(ep.showGrid); - if (ep.dualWindow) { - if (dualWindowMenuItem != null) dualWindowMenuItem.setSelected(true); - enableDualWindowMode(); - if (ep.contentX >= 0 && contentEditorStage != null) { - boolean onScreen = !javafx.stage.Screen - .getScreensForRectangle(ep.contentX, ep.contentY, ep.contentW, ep.contentH).isEmpty(); - if (onScreen) { - contentEditorStage.setX(ep.contentX); - contentEditorStage.setY(ep.contentY); - contentEditorStage.setWidth(ep.contentW); - contentEditorStage.setHeight(ep.contentH); + topBarController.applyShowGridPref(ep.showGrid); + contentController.restoreFromPrefs(ep.dualWindow, + ep.contentX, ep.contentY, ep.contentW, ep.contentH, ep.contentSection); + } + + // ── View-Wechsel ────────────────────────────────────────────────────────── + + private void setCenterView(javafx.scene.Node view) { + centerStack.getChildren().set(0, view); + if (root.getCenter() != centerStack) root.setCenter(centerStack); + } + + // ── Screenshot ─────────────────────────────────────────────────────────── + + private void takeEditorScreenshot() { + playScreenshotClick(); + WritableImage image = primaryStage.getScene().snapshot(null); + String timestamp = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")); + Path dir = BlightHome.resolve("screenshots"); + try { + Files.createDirectories(dir); + File out = dir.resolve("editor_" + timestamp + ".png").toFile(); + ImageIO.write(SwingFXUtils.fromFXImage(image, null), "PNG", out); + setStatus("Screenshot gespeichert: " + out.getPath()); + } catch (IOException ex) { + setStatus("Screenshot fehlgeschlagen: " + ex.getMessage()); + } + } + + private void playScreenshotClick() { + Thread t = new Thread(() -> { + try { + int sr = 44100; + int frames = (int) (sr * 0.08); + byte[] buf = new byte[frames * 2]; + java.util.Random rng = new java.util.Random(); + for (int i = 0; i < frames; i++) { + double env = Math.exp(-i / (sr * 0.012)); + double s = (rng.nextGaussian() * 0.55 + Math.sin(2 * Math.PI * 3200.0 * i / sr) * 0.45) * env * 0.85; + short val = (short) Math.max(-32767, Math.min(32767, s * 32767)); + buf[i * 2] = (byte) (val & 0xff); + buf[i * 2 + 1] = (byte) ((val >> 8) & 0xff); } - } - contentLastSection = ep.contentSection; - restoreContentSection(contentLastSection); - } - } - - private void collapseAllContentSections() { - for (javafx.scene.control.TreeItem node : new javafx.scene.control.TreeItem[]{ - contentCharNode, contentQuestNode, contentFractionNode, contentItemNode, - contentRecipeNode, contentCraftingNode, contentAreaNode, contentLocationNode}) { - if (node != null) node.setExpanded(false); - } - } - - private void buildCharacterViewInContent() { - collapseAllContentSections(); - onF5 = null; - dialogEditorView = new de.blight.editor.ui.DialogEditorView(); - dialogEditorView.setDisable(true); - monologueEditorView = new de.blight.editor.ui.MonologueEditorView(); - - javafx.scene.control.TabPane tabs = new javafx.scene.control.TabPane(); - tabs.setTabClosingPolicy(javafx.scene.control.TabPane.TabClosingPolicy.UNAVAILABLE); - javafx.scene.control.Tab charTab = new javafx.scene.control.Tab("Charakter", buildCharacterSettingsPane()); - javafx.scene.control.Tab dialogTab = new javafx.scene.control.Tab("Dialog", dialogEditorView); - javafx.scene.control.Tab monologTab = new javafx.scene.control.Tab("Monologe", monologueEditorView); - tabs.getTabs().addAll(charTab, dialogTab, monologTab); - tabs.getSelectionModel().select(charTab); - - contentTarget().setCenter(tabs); - contentTarget().setRight(null); - contentTarget().setBottom(null); - - if (contentCharNode != null) contentCharNode.setExpanded(true); - } - - private void buildQuestViewInContent() { - collapseAllContentSections(); - onF5 = null; - activeQuestView = new de.blight.editor.ui.QuestEditorView(ASSET_ROOT.resolve("quests")); - contentTarget().setCenter(activeQuestView); - contentTarget().setRight(null); - contentTarget().setBottom(buildQuestLegend()); - refreshContentQuestTree(); - if (contentQuestNode != null) contentQuestNode.setExpanded(true); - } - - private void buildFractionViewInContent() { - collapseAllContentSections(); - onF5 = null; - activeFractionView = new de.blight.editor.ui.FractionEditorView(ASSET_ROOT.resolve("fractions")); - contentTarget().setCenter(activeFractionView); - contentTarget().setRight(null); - contentTarget().setBottom(null); - refreshContentFractionTree(); - if (contentFractionNode != null) contentFractionNode.setExpanded(true); - } - - private void buildItemViewInContent() { - collapseAllContentSections(); - onF5 = null; - activeItemView = new de.blight.editor.ui.ItemEditorView(ASSET_ROOT.resolve("items")); - contentTarget().setCenter(activeItemView); - contentTarget().setRight(null); - contentTarget().setBottom(buildItemLegend()); - refreshContentItemTree(); - if (contentItemNode != null) contentItemNode.setExpanded(true); - } - - private void buildRecipeViewInContent() { - collapseAllContentSections(); - onF5 = null; - activeRecipeView = new de.blight.editor.ui.RecipeEditorView(ASSET_ROOT.resolve("recipes")); - contentTarget().setCenter(activeRecipeView); - contentTarget().setRight(null); - contentTarget().setBottom(buildRecipeLegend()); - refreshContentRecipeTree(); - if (contentRecipeNode != null) contentRecipeNode.setExpanded(true); - } - - private void buildCraftingViewInContent() { - collapseAllContentSections(); - onF5 = null; - activeCraftingView = new de.blight.editor.ui.CraftingTableEditorView(ASSET_ROOT.resolve("craftingtables")); - contentTarget().setCenter(activeCraftingView); - contentTarget().setRight(null); - contentTarget().setBottom(null); - if (contentCraftingNode != null) contentCraftingNode.setExpanded(true); - } - - private void buildAreaViewInContent() { - collapseAllContentSections(); - onF5 = null; - activeAreaView = new de.blight.editor.ui.AreaEditorView(); - contentTarget().setCenter(activeAreaView); - contentTarget().setRight(null); - contentTarget().setBottom(null); - refreshContentAreaTree(); - if (contentAreaNode != null) contentAreaNode.setExpanded(true); - } - - private void buildLocationViewInContent() { - collapseAllContentSections(); - onF5 = null; - activeLocationView = new de.blight.editor.ui.LocationEditorView(); - contentTarget().setCenter(activeLocationView); - contentTarget().setRight(null); - contentTarget().setBottom(null); - refreshContentLocationTree(); - if (contentLocationNode != null) contentLocationNode.setExpanded(true); - } - - private void buildLocalizationViewInContent() { - collapseAllContentSections(); - onF5 = null; - contentTarget().setCenter(new de.blight.editor.ui.LocalizationEditorView( - de.blight.editor.ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang"))); - contentTarget().setRight(null); - contentTarget().setBottom(null); - } - - // ── Baum-Refresh-Methoden ───────────────────────────────────────────────── - - private void refreshContentQuestTree() { - if (contentQuestNode == null) return; - contentTreeUpdating = true; - try { - contentQuestNode.getChildren().clear(); - Path dir = ASSET_ROOT.resolve("quests"); - if (!java.nio.file.Files.isDirectory(dir)) return; - java.util.List allQuests = - de.blight.common.model.quests.QuestIO.loadAll(dir); - java.util.List> items = allQuests.stream() - .sorted(java.util.Comparator - .comparing( - q -> de.blight.common.model.quests.QuestIO.typeOf(q)) - .thenComparing(q -> q.getQuestId() != null ? q.getQuestId() : "")) - .map(q -> { - String type = de.blight.common.model.quests.QuestIO.typeOf(q); - String id = q.getQuestId() != null ? q.getQuestId() : ""; - return new javafx.scene.control.TreeItem<>(type + ":" + id); - }) - .collect(java.util.stream.Collectors.toList()); - contentQuestNode.getChildren().addAll(items); - } finally { - contentTreeUpdating = false; - } - } - - private static String questTypeColor(String type) { - return switch (type) { - case "BRING" -> "#cc5555"; - case "FOLLOW" -> "#55aacc"; - case "INTERACT" -> "#aa55cc"; - case "ITEM" -> "#55cc88"; - case "TALK" -> "#ccaa33"; - default -> "#888888"; - }; - } - - private javafx.scene.layout.HBox buildQuestLegend() { - javafx.scene.layout.HBox bar = new javafx.scene.layout.HBox(14); - bar.setPadding(new javafx.geometry.Insets(5, 10, 5, 10)); - bar.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - bar.setStyle("-fx-background-color: #1a1a2a;"); - for (String[] entry : new String[][]{ - {"BRING", "Bring"}, {"FOLLOW", "Follow"}, {"INTERACT", "Interact"}, - {"ITEM", "Item"}, {"TALK", "Talk"}}) { - javafx.scene.shape.Rectangle box = new javafx.scene.shape.Rectangle(10, 10); - box.setFill(javafx.scene.paint.Color.web(questTypeColor(entry[0]))); - box.setArcWidth(3); box.setArcHeight(3); - javafx.scene.control.Label lbl = new javafx.scene.control.Label(entry[1]); - lbl.setStyle("-fx-text-fill: #aaa; -fx-font-size: 11;"); - javafx.scene.layout.HBox item = new javafx.scene.layout.HBox(4, box, lbl); - item.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - bar.getChildren().add(item); - } - return bar; - } - - private static de.blight.common.model.ItemCategory safeItemCategory(String name) { - try { return de.blight.common.model.ItemCategory.valueOf(name); } - catch (IllegalArgumentException e) { return null; } - } - - private static String contentRecipeColor(String tableTypeName) { - try { - de.blight.common.model.CraftingTable.CraftingTableType t = - de.blight.common.model.CraftingTable.CraftingTableType.valueOf(tableTypeName); - return de.blight.editor.ui.CraftingTableEditorView.typeColor(t); - } catch (IllegalArgumentException e) { - return "#666666"; - } - } - - private javafx.scene.layout.HBox buildLegendBar(String[][] entries, - java.util.function.Function colorFn) { - javafx.scene.layout.HBox bar = new javafx.scene.layout.HBox(14); - bar.setPadding(new javafx.geometry.Insets(5, 10, 5, 10)); - bar.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - bar.setStyle("-fx-background-color: #1a1a2a;"); - for (String[] entry : entries) { - javafx.scene.shape.Rectangle box = new javafx.scene.shape.Rectangle(10, 10); - box.setFill(javafx.scene.paint.Color.web(colorFn.apply(entry[0]))); - box.setArcWidth(3); box.setArcHeight(3); - javafx.scene.control.Label lbl = new javafx.scene.control.Label(entry[1]); - lbl.setStyle("-fx-text-fill: #aaa; -fx-font-size: 11;"); - javafx.scene.layout.HBox item = new javafx.scene.layout.HBox(4, box, lbl); - item.setAlignment(javafx.geometry.Pos.CENTER_LEFT); - bar.getChildren().add(item); - } - return bar; - } - - private javafx.scene.layout.HBox buildItemLegend() { - return buildLegendBar(new String[][]{ - {"WEAPON", "Waffe"}, - {"GEAR", "Ausrüstung"}, - {"CONSUMABLES", "Verbrauchbar"}, - {"QUEST_ITEMS", "Quest"}, - {"CRAFTING_ITEMS","Crafting"}, - {"MISC_ITEMS", "Sonstiges"} - }, k -> de.blight.editor.ui.ItemEditorView.categoryColor(safeItemCategory(k))); - } - - private javafx.scene.layout.HBox buildRecipeLegend() { - return buildLegendBar(new String[][]{ - {"AlchemyTable", "Alchemie"}, - {"EnchantmentTable", "Verzauberung"}, - {"Smithy", "Schmiede"}, - {"Goldsmiths", "Goldschmied"}, - {"Workshop", "Werkstatt"}, - {"Fireplace", "Feuerstelle"}, - {"Kitchen", "Küche"}, - {"NONE", "Kein Tisch"} - }, EditorApp::contentRecipeColor); - } - - private void refreshContentItemTree() { - if (contentItemNode == null) return; - contentTreeUpdating = true; - try { - contentItemNode.getChildren().clear(); - Path dir = ASSET_ROOT.resolve("items"); - if (!java.nio.file.Files.isDirectory(dir)) return; - java.util.List> treeItems = - de.blight.common.model.ItemIO.loadAll(dir).stream() - .filter(i -> i.getItemId() != null && !i.getItemId().isBlank()) - .map(i -> { - String cat = i.getCategory() != null ? i.getCategory().name() : "MISC_ITEMS"; - return new javafx.scene.control.TreeItem<>(cat + ":" + i.getItemId()); - }) - .collect(java.util.stream.Collectors.toList()); - contentItemNode.getChildren().addAll(treeItems); - } finally { - contentTreeUpdating = false; - } - } - - private void refreshContentFractionTree() { - if (contentFractionNode == null) return; - contentTreeUpdating = true; - try { - contentFractionNode.getChildren().clear(); - Path dir = ASSET_ROOT.resolve("fractions"); - if (!java.nio.file.Files.isDirectory(dir)) return; - java.util.List> items = - de.blight.common.model.FractionIO.loadAll(dir).stream() - .filter(f -> f.getName() != null && !f.getName().id().isBlank()) - .map(f -> new javafx.scene.control.TreeItem<>(f.getName().id())) - .collect(java.util.stream.Collectors.toList()); - contentFractionNode.getChildren().addAll(items); - } finally { - contentTreeUpdating = false; - } - } - - private void refreshContentAreaTree() { - if (contentAreaNode == null) return; - contentTreeUpdating = true; - try { - contentAreaNode.getChildren().clear(); - java.util.List> items; - try { - items = de.blight.common.AreaDefinitionIO.load().stream() - .filter(a -> a.id() != null && !a.id().isBlank()) - .map(a -> new javafx.scene.control.TreeItem<>(a.id())) - .collect(java.util.stream.Collectors.toList()); - } catch (IOException ignored) { return; } - contentAreaNode.getChildren().addAll(items); - } finally { - contentTreeUpdating = false; - } - } - - private void refreshContentLocationTree() { - if (contentLocationNode == null) return; - contentTreeUpdating = true; - try { - contentLocationNode.getChildren().clear(); - java.util.List> items; - try { - items = de.blight.common.LocationIO.load().stream() - .filter(l -> l.getId() != null && !l.getId().isBlank()) - .map(l -> new javafx.scene.control.TreeItem<>(l.getId())) - .collect(java.util.stream.Collectors.toList()); - } catch (IOException ignored) { return; } - contentLocationNode.getChildren().addAll(items); - } finally { - contentTreeUpdating = false; - } - } - - private void refreshContentRecipeTree() { - if (contentRecipeNode == null) return; - contentTreeUpdating = true; - try { - contentRecipeNode.getChildren().clear(); - Path dir = ASSET_ROOT.resolve("recipes"); - if (!java.nio.file.Files.isDirectory(dir)) return; - java.util.List> treeItems = - de.blight.common.model.RecipeIO.loadAll(dir).stream() - .filter(r -> r.getCreates() != null && r.getCreates().getItemId() != null) - .map(r -> { - String table = (r.getTable() != null && r.getTable().getType() != null) - ? r.getTable().getType().name() : "NONE"; - return new javafx.scene.control.TreeItem<>(table + ":" + r.getCreates().getItemId()); - }) - .collect(java.util.stream.Collectors.toList()); - contentRecipeNode.getChildren().addAll(treeItems); - } finally { - contentTreeUpdating = false; - } - } - - private void refreshContentCraftingTree() { - if (contentCraftingNode == null) return; - contentTreeUpdating = true; - try { - contentCraftingNode.getChildren().clear(); - java.util.List> items = new java.util.ArrayList<>(); - for (de.blight.common.model.CraftingTable.CraftingTableType t : - de.blight.common.model.CraftingTable.CraftingTableType.values()) { - items.add(new javafx.scene.control.TreeItem<>(t.name())); - } - contentCraftingNode.getChildren().addAll(items); - } finally { - contentTreeUpdating = false; - } + javax.sound.sampled.AudioFormat fmt = new javax.sound.sampled.AudioFormat(sr, 16, 1, true, false); + javax.sound.sampled.DataLine.Info info = new javax.sound.sampled.DataLine.Info(javax.sound.sampled.SourceDataLine.class, fmt); + javax.sound.sampled.SourceDataLine line = (javax.sound.sampled.SourceDataLine) javax.sound.sampled.AudioSystem.getLine(info); + line.open(fmt); + line.start(); + line.write(buf, 0, buf.length); + line.drain(); + line.close(); + } catch (Exception ignored) {} + }, "screenshot-click"); + t.setDaemon(true); + t.start(); } } diff --git a/blight-editor/src/main/java/de/blight/editor/ToolContext.java b/blight-editor/src/main/java/de/blight/editor/ToolContext.java index c5ebd81..af256d6 100644 --- a/blight-editor/src/main/java/de/blight/editor/ToolContext.java +++ b/blight-editor/src/main/java/de/blight/editor/ToolContext.java @@ -10,22 +10,23 @@ import java.util.function.Supplier; /** * Shared-Dependencies-Container: wird einmalig in EditorApp erzeugt und - * an alle {@link ToolController}-Implementierungen weitergegeben. + * an alle Controller-Implementierungen weitergegeben. * - * Ersetzt das implizite "EditorApp kennt alles" — Controller greifen nur - * über diesen Record auf Infrastruktur zu, nicht auf EditorApp direkt. - * - * @param input Gemeinsame JavaFX ↔ JME3 Datenbasis - * @param jmeApp JME3-Anwendung (für StateManager-Zugriff) - * @param stageSupplier Liefert die primäre Stage (für Dialoge) - * @param setStatus Setzt den Text im Statusbalken - * @param setCenterView Setzt den Center-Node des Haupt-BorderPane - * @param setRightPanel Setzt den Right-Node des Haupt-BorderPane - * @param setMainToolbar Ersetzt die aktive Werkzeug-Toolbar in der TopBar - * @param viewportSupplier Liefert den JME3-Viewport-StackPane - * @param assetRoot Wurzelpfad der blight-assets (für Dateidialoge / IO) - * @param switchToWorld Wechselt zurück zum Welteneditor (für "← Welteneditor"-Buttons) - * @param registry ToolRegistry (damit Controller sich selbst aktivieren können) + * @param input Gemeinsame JavaFX ↔ JME3 Datenbasis + * @param jmeApp JME3-Anwendung (für StateManager-Zugriff) + * @param stageSupplier Liefert die primäre Stage (für Dialoge) + * @param setStatus Setzt den Text im Statusbalken + * @param setCenterView Setzt den Center-Node des Haupt-BorderPane + * @param setRightPanel Setzt den Right-Node des Haupt-BorderPane + * @param setMainToolbar Ersetzt die aktive Werkzeug-Toolbar in der TopBar + * @param viewportSupplier Liefert den JME3-Viewport-StackPane + * @param assetRoot Wurzelpfad der blight-assets (für Dateidialoge / IO) + * @param switchToWorld Wechselt zurück zum Welteneditor + * @param registry ToolRegistry (damit Controller sich selbst aktivieren können) + * @param setLeftPanel Setzt den Left-Node des Haupt-BorderPane + * @param setBottomPanel Setzt den Bottom-Node des Haupt-BorderPane + * @param centerStackSupplier Liefert den persistenten 3D-Viewport-Wrapper + * @param setCurrentTool Setzt das aktive Tool-Label in EditorApp (für Keyboard-Routing) */ public record ToolContext( SharedInput input, @@ -38,5 +39,9 @@ public record ToolContext( Supplier viewportSupplier, Path assetRoot, Runnable switchToWorld, - ToolRegistry registry + ToolRegistry registry, + Consumer setLeftPanel, + Consumer setBottomPanel, + Supplier centerStackSupplier, + Consumer setCurrentTool ) {} diff --git a/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java b/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java index ee0b3cc..0a8fdda 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/WaterBodyState.java @@ -40,8 +40,8 @@ public class WaterBodyState extends BaseAppState { private static final float LINE_OFFSET = 0.1f; private static final float POLE_HEIGHT = 5.0f; - private static final float CAP_SIZE = 1.2f; - private static final float HANDLE_PICK_PX = 24f; + private static final float CAP_SIZE = 2.0f; + private static final float HANDLE_PICK_PX = 32f; private static final float EDGE_SPLIT_THRESHOLD = 1.5f; private static final ColorRGBA COLOR_WATER = new ColorRGBA(0.05f, 0.25f, 0.70f, 0.50f); @@ -53,8 +53,9 @@ public class WaterBodyState extends BaseAppState { private static final ColorRGBA COLOR_ARROW = new ColorRGBA(1f, 0.85f, 0f, 1f); private static final ColorRGBA COLOR_POLE_CORNER = new ColorRGBA(0f, 0.9f, 1.0f, 1f); private static final ColorRGBA COLOR_POLE_EDGE = new ColorRGBA(0f, 0.55f, 0.75f, 1f); - private static final ColorRGBA COLOR_POLE_HOVER = new ColorRGBA(1f, 0.9f, 0f, 1f); - private static final ColorRGBA COLOR_EDGE_SPLIT = new ColorRGBA(0.1f, 1.0f, 0.4f, 1f); + private static final ColorRGBA COLOR_POLE_HOVER = new ColorRGBA(1f, 0.9f, 0f, 1f); + private static final ColorRGBA COLOR_POLE_SELECTED = new ColorRGBA(1f, 0.35f, 0.05f, 1f); + private static final ColorRGBA COLOR_EDGE_SPLIT = new ColorRGBA(0.1f, 1.0f, 0.4f, 1f); private static final ColorRGBA PILLAR_SUB = new ColorRGBA(0.2f, 0.85f, 1.0f, 1f); private static final ColorRGBA PILLAR_DRY = new ColorRGBA(1.0f, 0.50f, 0.1f, 1f); @@ -90,8 +91,9 @@ public class WaterBodyState extends BaseAppState { private final List handleGeos = new ArrayList<>(); private final List handleMats = new ArrayList<>(); private final List handleCapY = new ArrayList<>(); - private int hoveredHandle = -1; - private int dragHandle = -1; + private int hoveredHandle = -1; + private int dragHandle = -1; + private int selectedHandle = -1; private boolean isDragging = false; private float[] liveXs = new float[0]; private float[] liveYs = new float[0]; @@ -187,7 +189,11 @@ public class WaterBodyState extends BaseAppState { if (input.deleteWaterRequested) { input.deleteWaterRequested = false; - if (selectedIdx >= 0) removeBody(selectedIdx); + if (selectedHandle >= 0 && selectedIdx >= 0 && selectedHandle < liveXs.length) { + deleteCornerPoint(selectedIdx, selectedHandle); + } else if (selectedIdx >= 0) { + removeBody(selectedIdx); + } } SharedInput.WaterClick click; @@ -235,6 +241,7 @@ public class WaterBodyState extends BaseAppState { PlacedWater b = bodies.get(selectedIdx); int h = findHandleAt(jmeX, jmeY, b.pointsX(), b.pointsZ()); if (h >= 0) { + setSelectedHandle(h); startDrag(h, b.pointsX(), b.pointsY(), b.pointsZ()); return; } @@ -569,10 +576,50 @@ public class WaterBodyState extends BaseAppState { handleMats.clear(); handleCapY.clear(); hideSplitIndicator(); - hoveredHandle = -1; + hoveredHandle = -1; + dragHandle = -1; + selectedHandle = -1; splitHoverEdge = -1; } + private void setSelectedHandle(int h) { + if (selectedHandle >= 0 && selectedHandle < handleMats.size() && selectedHandle != hoveredHandle) { + int n = liveXs.length; + handleMats.get(selectedHandle).setColor("Color", selectedHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE); + } + selectedHandle = h; + if (h >= 0 && h < handleMats.size() && h != hoveredHandle) { + handleMats.get(h).setColor("Color", COLOR_POLE_SELECTED); + } + } + + private void deleteCornerPoint(int bodyIdx, int cornerIdx) { + int n = liveXs.length; + if (cornerIdx >= n) return; // Kantenmittelpunkte nicht löschbar + if (n <= 3) { + removeBody(bodyIdx); + return; + } + float[] newXs = new float[n - 1]; + float[] newYs = new float[n - 1]; + float[] newZs = new float[n - 1]; + for (int i = 0, j = 0; i < n; i++) { + if (i == cornerIdx) continue; + newXs[j] = liveXs[i]; newYs[j] = liveYs[i]; newZs[j] = liveZs[i]; + j++; + } + PlacedWater updated = bodies.get(bodyIdx).withPoints(newXs, newYs, newZs); + bodies.set(bodyIdx, updated); + rebuildQuadVisual(bodyIdx, newXs, newYs, newZs); + rebuildArrow(bodyIdx, updated); + liveXs = newXs.clone(); + liveYs = newYs.clone(); + liveZs = newZs.clone(); + removeHandles(); + buildHandles(newXs, newYs, newZs); + publishSelection(bodyIdx); + } + private void updateHandleHover() { if (selectedIdx < 0) return; PlacedWater b = bodies.get(selectedIdx); @@ -585,7 +632,8 @@ public class WaterBodyState extends BaseAppState { if (h != hoveredHandle) { if (hoveredHandle >= 0 && hoveredHandle < handleMats.size()) { - ColorRGBA c = hoveredHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE; + ColorRGBA c = (hoveredHandle == selectedHandle) ? COLOR_POLE_SELECTED + : (hoveredHandle < n ? COLOR_POLE_CORNER : COLOR_POLE_EDGE); handleMats.get(hoveredHandle).setColor("Color", c); } if (h >= 0 && h < handleMats.size()) { @@ -846,7 +894,7 @@ public class WaterBodyState extends BaseAppState { rebuildPoleMesh(mesh, x, z, waterH, terrH); Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", color.clone()); - mat.getAdditionalRenderState().setLineWidth(2.5f); + mat.getAdditionalRenderState().setLineWidth(4f); Geometry geo = new Geometry(name, mesh); geo.setMaterial(mat); return geo; diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/EditorUiHelper.java b/blight-editor/src/main/java/de/blight/editor/toolset/EditorUiHelper.java new file mode 100644 index 0000000..e6aeeea --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/EditorUiHelper.java @@ -0,0 +1,91 @@ +package de.blight.editor.toolset; + +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.*; + +/** Shared static UI helpers used by multiple tool controllers. */ +public final class EditorUiHelper { + + private EditorUiHelper() {} + + public static Label bold(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + return l; + } + + public static Label styledHint(String text) { + Label l = new Label(text); + l.setStyle("-fx-text-fill: #333333; -fx-font-size: 11;"); + return l; + } + + public static Label sectionTitle(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #1a3a6a;"); + VBox.setMargin(l, new Insets(8, 0, 0, 0)); + return l; + } + + public static HBox withField(Slider slider, String fmt) { + slider.setMaxWidth(Double.MAX_VALUE); + TextField tf = new TextField(String.format(fmt, slider.getValue())); + tf.setPrefWidth(62); + tf.setMaxWidth(62); + tf.setStyle("-fx-font-size: 11;"); + slider.valueProperty().addListener((o, ov, nv) -> + tf.setText(String.format(fmt, nv.doubleValue()))); + Runnable commit = () -> { + try { + double v = Math.max(slider.getMin(), Math.min(slider.getMax(), + Double.parseDouble(tf.getText().replace(',', '.')))); + slider.setValue(v); + } catch (NumberFormatException ignored) { + tf.setText(String.format(fmt, slider.getValue())); + } + }; + tf.setOnAction(e -> commit.run()); + tf.focusedProperty().addListener((o, ov, focused) -> { if (!focused) commit.run(); }); + HBox row = new HBox(5, slider, tf); + HBox.setHgrow(slider, Priority.ALWAYS); + return row; + } + + public static VBox paramSlider(String label, double min, double max, double init, + java.util.function.Consumer setter) { + Label name = new Label(label); + name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + Slider slider = new Slider(min, max, init); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setMajorTickUnit((max - min) / 2.0); + slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue())); + return new VBox(3, name, withField(slider, "%.3f")); + } + + public static VBox ezFloat(String label, double min, double max, double init, + java.util.function.Consumer setter) { + Label name = new Label(label); + name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + Slider slider = new Slider(min, max, Math.max(min, Math.min(max, init))); + slider.setShowTickMarks(false); + slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue())); + return new VBox(2, name, withField(slider, "%.3f")); + } + + public static Spinner intSpinner(int min, int max, int init) { + Spinner sp = new Spinner<>(min, max, Math.max(min, Math.min(max, init))); + sp.setEditable(true); + sp.setMaxWidth(Double.MAX_VALUE); + return sp; + } + + public static String pathToName(String path, String[] paths, String[] names) { + if (path == null) return names[0]; + for (int i = 0; i < paths.length; i++) { + if (path.equals(paths[i])) return names[i]; + } + return names[0]; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/asset/AssetTreeController.java b/blight-editor/src/main/java/de/blight/editor/toolset/asset/AssetTreeController.java new file mode 100644 index 0000000..5aa5b6e --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/asset/AssetTreeController.java @@ -0,0 +1,2228 @@ +package de.blight.editor.toolset.asset; + +import de.blight.editor.SharedInput; +import de.blight.editor.TextureMapData; +import de.blight.editor.ToolContext; +import de.blight.editor.ui.MapObjectsView; +import de.blight.editor.ui.TextureChooser; +import de.blight.editor.ui.TextureDetailDialog; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.input.*; +import javafx.scene.layout.*; +import javafx.stage.FileChooser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.file.*; +import java.util.*; +import java.util.zip.*; +import javax.imageio.ImageIO; + +public class AssetTreeController { + + private static final Logger log = LoggerFactory.getLogger(AssetTreeController.class); + + // ── Deps ───────────────────────────────────────────────────────────────── + + public record Deps( + de.blight.editor.toolset.object.ObjectController objectController, + de.blight.editor.toolset.anim.AnimPreviewController animPreviewController, + de.blight.editor.toolset.anim.AnimSetController animSetController, + de.blight.editor.toolset.model.ModelEditorController modelEditorController, + de.blight.editor.toolset.content.ContentController contentController, + de.blight.editor.toolset.atmosphere.AtmosphereController atmosphereController, + de.blight.editor.toolset.water.WaterController waterController, + de.blight.editor.toolset.world.WorldPanelController worldPanelController, + javafx.scene.control.ToggleButton atmosphaereBtn, + javafx.scene.control.ToggleButton wasserMergedBtn, + javafx.scene.control.ToggleButton zonenBtn, + Runnable switchToAnimPreview, + java.util.function.Consumer activateItemPlacement, + java.util.function.Supplier buildItemPlacePanel, + Runnable scheduleAutoSave, + java.util.function.Function loadTexMeta, + java.util.function.BiConsumer saveTexMeta, + java.util.function.Supplier currentTool, + java.util.function.Consumer setPendingAfterSave + ) {} + + // ── Fields ─────────────────────────────────────────────────────────────── + + private final ToolContext ctx; + private final Deps deps; + private final SharedInput input; + private final Path assetRoot; + private final Runnable scheduleAutoSave; + + private VBox assetPanel; + private Button assetTabBtn; + private boolean assetOverlayOpen = false; + private StackPane centerStack; + + private final Map, Path> itemPaths = new HashMap<>(); + private final Map, String> jmePaths = new HashMap<>(); + private final java.util.Set> jmeFolderNodes = new java.util.HashSet<>(); + private final java.util.Set> textureSetNodes = new java.util.HashSet<>(); + private TreeItem draggedItem = null; + + private java.util.Set skeletalModelPaths = null; + + private TreeItem assetTreeRoot; + private TreeView assetTree; + private TreeItem modelsNode; + private TreeItem texturesNode; + private TreeItem audioNode; + private TreeItem jmeModelsNode; + private TreeItem jmeTexturesNode; + private TreeItem animationsNode; + private TreeItem charactersNode; + private TreeItem itemsNode; + + private MapObjectsView mapObjectsView; + private de.blight.editor.ui.WorldMapView worldMapView; + private de.blight.editor.ui.CaveMapView caveMapView; + private Tab weltkartTab; + private Tab caveMapTab; + + // ── Constructor ────────────────────────────────────────────────────────── + + public AssetTreeController(ToolContext ctx, Deps deps) { + this.ctx = ctx; + this.deps = deps; + this.input = ctx.input(); + this.assetRoot = ctx.assetRoot(); + this.scheduleAutoSave = deps.scheduleAutoSave(); + } + + // ── Public API ─────────────────────────────────────────────────────────── + + public boolean isOverlayOpen() { return assetOverlayOpen; } + + public void showTabBtn() { if (assetTabBtn != null) assetTabBtn.setVisible(true); } + public void hideTabBtn() { if (assetTabBtn != null) assetTabBtn.setVisible(false); } + + public void reattachToCenterStack(StackPane cs, javafx.scene.Node worldViewport) { + cs.getChildren().setAll(worldViewport, assetPanel, assetTabBtn); + assetTabBtn.setTranslateX(assetPanel.getTranslateX() + 420); + } + + public boolean isMapReady() { + return worldMapView != null && worldMapView.isMapReady(); + } + + public String getWorldMapLoadingStatus() { + return worldMapView != null ? worldMapView.getLoadingStatus() : "…"; + } + + public void refreshMapObjects() { + if (mapObjectsView != null) mapObjectsView.refresh(); + } + + public void setSkeletalModelPaths(java.util.Set paths) { + this.skeletalModelPaths = paths; + } + + public void expandModelsNode() { + if (modelsNode != null) modelsNode.setExpanded(true); + } + + public void refreshModels(boolean preserveExpansion) { + if (modelsNode != null) refreshCategoryNode(modelsNode, preserveExpansion); + } + + public void refreshAnimations(boolean preserveExpansion) { + if (animationsNode != null) refreshCategoryNode(animationsNode, preserveExpansion); + } + + public void refreshCharacters(boolean preserveExpansion) { + if (charactersNode != null) refreshCategoryNode(charactersNode, preserveExpansion); + } + + // ── Private stage helper ───────────────────────────────────────────────── + + private javafx.stage.Stage stage() { + return (javafx.stage.Stage) ctx.stageSupplier().get(); + } + + // ── Static utility ─────────────────────────────────────────────────────── + + private static void deleteDirectoryRecursive(Path dir) throws IOException { + try (var walk = Files.walk(dir)) { + walk.sorted(Comparator.reverseOrder()) + .forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException ignored) {} }); + } + } + + public void handleAudioImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Audio importieren"); + fc.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Audio-Dateien", "*.ogg","*.wav","*.mp3"), + new FileChooser.ExtensionFilter("OGG", "*.ogg"), + new FileChooser.ExtensionFilter("WAV / MP3", "*.wav","*.mp3")); + List files = fc.showOpenMultipleDialog(owner); + if (files == null) return; + Path destDir = assetRoot.resolve("audio"); + int ok = 0; + for (File file : files) { + try { + Files.createDirectories(destDir); + String baseName = file.getName().replaceFirst("\\.[^.]+$", ""); + Path dest = destDir.resolve(baseName + ".ogg"); + ctx.setStatus().accept("Normalisiere " + file.getName() + " → 48 kHz OGG …"); + convertToOgg(file.toPath(), dest); + TreeItem item = new TreeItem<>(dest.getFileName().toString()); + itemPaths.put(item, dest); + audioNode.getChildren().add(item); + ok++; + } catch (IOException ex) { + ctx.setStatus().accept("Fehler: " + ex.getMessage()); + } + } + if (ok > 0) { + audioNode.setExpanded(true); + ctx.setStatus().accept(ok + " Audio-Datei(en) importiert"); + } + } + + public VBox buildPanel() { + // ── Tab 1: Asset-Baum ───────────────────────────────────────────────── + VBox assetContent = new VBox(6); + assetContent.setPadding(new Insets(8)); + assetContent.setStyle("-fx-background-color: #f0f0f0;"); + + Label title = new Label("Assets"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + + HBox titleBar = new HBox(4, title); + titleBar.setAlignment(Pos.CENTER_LEFT); + + assetTreeRoot = new TreeItem<>("Projekt"); + assetTreeRoot.setExpanded(true); + populateAssetTree(assetTreeRoot); + + TreeView tree = new TreeView<>(assetTreeRoot); + assetTree = tree; + tree.setShowRoot(false); + VBox.setVgrow(tree, Priority.ALWAYS); + tree.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + tree.setCellFactory(tv -> buildAssetCell()); + + // F2 → Umbenennen + tree.setOnKeyPressed(e -> { + if (e.getCode() != KeyCode.F2) return; + TreeItem sel = tree.getSelectionModel().getSelectedItem(); + if (sel == null || sel == modelsNode || sel == texturesNode + || sel == audioNode || sel == animationsNode || sel == itemsNode + || sel == charactersNode) return; + renameAsset(sel); + e.consume(); + }); + + // Doppelklick auf Modell / Textur / Animation + tree.setOnMouseClicked(e -> { + if (e.getClickCount() != 2) return; + TreeItem sel = tree.getSelectionModel().getSelectedItem(); + if (sel == null) return; + + // Ordner-Doppelklick im Models-Baum → Zufalls-Ordner-Platzierung aktivieren + if (isAssetFolder(sel)) { + TreeItem folderCat = getCategoryRoot(sel); + if (folderCat == modelsNode) { + Path folderPath = itemPaths.get(sel); + if (folderPath != null && java.nio.file.Files.isDirectory(folderPath)) { + String relFolder = assetRoot.relativize(folderPath).toString().replace('\\', '/'); + input.treeFolderPath = relFolder; + input.pendingModelPath = null; + deps.objectController().activatePlaceMode(); + deps.objectController().enableMultiPlace(); + ctx.setStatus().accept("Ordner: " + relFolder + " | Linksklick → zufälliges Modell | Rechtsklick → neu würfeln"); + } + } + return; + } + + TreeItem cat = getCategoryRoot(sel); + Path p = itemPaths.get(sel); + String relPath = (p != null) + ? assetRoot.relativize(p).toString().replace('\\', '/') + : jmePaths.get(sel); + if (relPath == null) return; + + boolean isSkeletal = skeletalModelPaths == null /* noch nicht gescannt – zulassen */ + || skeletalModelPaths.contains(relPath); + + // Im Animations Viewer: j3o direkt laden + if ("animpreview".equals(deps.currentTool().get()) + && (cat == modelsNode || cat == animationsNode || cat == charactersNode) + && relPath.endsWith(".j3o")) { + if (!isSkeletal) { ctx.setStatus().accept("Kein Skelett: " + relPath); return; } + input.animPreviewLoadPath = relPath; + deps.animPreviewController().setStatusText("Lade…"); + deps.animPreviewController().clearClipList(); + return; + } + + if (cat == itemsNode && relPath.endsWith(".item")) { + deps.activateItemPlacement().accept(p); + return; + } + + if (cat == modelsNode) { + if ("modeleditor".equals(ctx.registry().activeToolId()) && relPath.endsWith(".j3o")) { + deps.modelEditorController().openModelEditor(relPath, p); + return; + } + input.pendingModelPath = relPath; + deps.objectController().activatePlaceMode(); + ctx.setStatus().accept("Modell: " + relPath + " | Linksklick ins Terrain zum Platzieren"); + } else if (cat == texturesNode) { + input.pendingTexturePath = relPath; + deps.objectController().activatePlaceMode(); + ctx.setStatus().accept("Textur: " + relPath + " | Wird auf platzierte Primitive angewendet"); + } else if ((cat == animationsNode || cat == charactersNode) && relPath.endsWith(".j3o") && isSkeletal) { + deps.switchToAnimPreview().run(); + input.animPreviewLoadPath = relPath; + deps.animPreviewController().setStatusText("Lade…"); + } else if (cat == audioNode && relPath.endsWith(".ogg")) { + try { + new ProcessBuilder("xdg-open", p.toAbsolutePath().toString()) + .start(); + } catch (Exception ex) { + log.warn("[Audio] xdg-open fehlgeschlagen: {}", ex.getMessage()); + } + ctx.setStatus().accept("▶ " + relPath); + } else if (relPath.endsWith(".animset.json")) { + deps.animSetController().openWithSet(relPath, p); ctx.registry().activate(deps.animSetController()); + } else if (relPath.endsWith(".character")) { + String charName = p.getFileName().toString().replace(".character", ""); + deps.contentController().switchToCharacterEditor(); + deps.contentController().loadCharacterById(charName); + } + }); + + Button importBtn = new Button("⊕ Import…"); + importBtn.setMaxWidth(Double.MAX_VALUE); + importBtn.setOnAction(e -> { + ContextMenu popup = new ContextMenu(); + MenuItem miModel = new MenuItem("Modell mit LOD-Editor…"); + MenuItem miTex = new MenuItem("Texturen…"); + MenuItem miTexZip = new MenuItem("Texturen-ZIP…"); + MenuItem miAudio = new MenuItem("Audio…"); + MenuItem miAnim = new MenuItem("Animationen…"); + miModel.setOnAction(ev -> deps.modelEditorController().openModelImport(importBtn.getScene().getWindow())); + miTex.setOnAction(ev -> handleTextureImport(importBtn.getScene().getWindow())); + miTexZip.setOnAction(ev -> handleZipTextureImport(importBtn.getScene().getWindow())); + miAudio.setOnAction(ev -> handleAudioImport(importBtn.getScene().getWindow())); + miAnim.setOnAction(ev -> deps.animSetController().handleAnimationImport(importBtn.getScene().getWindow())); + MenuItem miCharModel = new MenuItem("Character-Modell (FBX)…"); + miCharModel.setOnAction(ev -> deps.animSetController().handleCharacterModelImport(importBtn.getScene().getWindow())); + popup.getItems().addAll(miModel, miTex, miTexZip, miAudio, miAnim, + new javafx.scene.control.SeparatorMenuItem(), miCharModel); + popup.show(importBtn, javafx.geometry.Side.TOP, 0, 0); + }); + + Button refreshBtn = new Button("↻"); + refreshBtn.setTooltip(new javafx.scene.control.Tooltip("Asset-Baum aktualisieren")); + refreshBtn.setOnAction(e -> refreshAllAssets(shouldPreserveExpansion())); + + HBox bottomBar = new HBox(4, importBtn, refreshBtn); + HBox.setHgrow(importBtn, Priority.ALWAYS); + bottomBar.setMaxWidth(Double.MAX_VALUE); + + assetContent.getChildren().addAll(titleBar, tree, bottomBar); + + // ── Tab 2: Karte (platzierte Objekte) ───────────────────────────────── + mapObjectsView = new MapObjectsView( + (x, y, z, toolHint) -> { + input.pendingGotoX = x; + input.pendingGotoY = y; + input.pendingGotoZ = z; + input.pendingGotoPitch = (float) (-Math.PI / 3); + switch (toolHint) { + case "model" -> { deps.objectController().activateEditMode(); } + case "item" -> { input.activeLayer = SharedInput.LAYER_ITEMS; ctx.setRightPanel().accept(deps.buildItemPlacePanel().get()); } + case "light" -> { input.activeLayer = SharedInput.LAYER_LIGHTS; if (deps.atmosphaereBtn() != null) deps.atmosphaereBtn().setSelected(true); ctx.setRightPanel().accept(deps.atmosphereController().buildPanel()); } + case "emitter" -> { input.activeLayer = SharedInput.LAYER_EMITTERS; if (deps.atmosphaereBtn() != null) deps.atmosphaereBtn().setSelected(true); ctx.setRightPanel().accept(deps.atmosphereController().buildPanel()); } + case "water" -> { input.activeLayer = SharedInput.LAYER_WATER; if (deps.wasserMergedBtn() != null) deps.wasserMergedBtn().setSelected(true); ctx.setRightPanel().accept(deps.waterController().buildPanel()); } + case "soundarea" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "sound"; ctx.setRightPanel().accept(deps.worldPanelController().buildZonenPanel()); deps.zonenBtn().setSelected(true); } + case "area" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "area"; ctx.setRightPanel().accept(deps.worldPanelController().buildZonenPanel()); deps.zonenBtn().setSelected(true); } + case "locationzone" -> { input.activeLayer = SharedInput.LAYER_ZONEN; input.zonenNewKind = "location"; ctx.setRightPanel().accept(deps.worldPanelController().buildZonenPanel()); deps.zonenBtn().setSelected(true); } + case "waterfall" -> { input.activeLayer = SharedInput.LAYER_WATERFALL; if (deps.wasserMergedBtn() != null) deps.wasserMergedBtn().setSelected(true); ctx.setRightPanel().accept(deps.waterController().buildPanel()); } + } + }, + (obj, index, toolHint) -> { + switch (toolHint) { + case "model" -> { + java.util.List list = new java.util.ArrayList<>(de.blight.common.PlacedModelIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.PlacedModelIO.save(list); + input.reloadPlacedModels = true; + } + case "item" -> { + de.blight.common.PlacedItem pi = (de.blight.common.PlacedItem) obj; + java.util.List list = new java.util.ArrayList<>(de.blight.common.PlacedItemIO.load()); + list.removeIf(it -> it.uuid().equals(pi.uuid())); + de.blight.common.PlacedItemIO.save(list); + input.reloadPlacedItems = true; + } + case "light" -> { + java.util.List list = new java.util.ArrayList<>(de.blight.common.LightIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.LightIO.save(list); + input.reloadPlacedOther = true; + } + case "emitter" -> { + java.util.List list = new java.util.ArrayList<>(de.blight.common.EmitterIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.EmitterIO.save(list); + input.reloadPlacedOther = true; + } + case "water" -> { + java.util.List list = new java.util.ArrayList<>(de.blight.common.WaterBodyIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.WaterBodyIO.save(list); + input.reloadPlacedOther = true; + } + case "soundarea" -> { + java.util.List list = new java.util.ArrayList<>(de.blight.common.SoundAreaIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.SoundAreaIO.save(list); + input.reloadPlacedOther = true; + } + case "area" -> { + java.util.List list = new java.util.ArrayList<>(de.blight.common.AreaIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.AreaIO.save(list); + input.reloadPlacedOther = true; + } + case "locationzone" -> { + java.util.List list = new java.util.ArrayList<>(de.blight.common.LocationZoneIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.LocationZoneIO.save(list); + input.reloadPlacedOther = true; + } + case "waterfall" -> { + java.util.List list = new java.util.ArrayList<>(de.blight.common.WaterfallIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.WaterfallIO.save(list); + input.reloadPlacedOther = true; + } + case "river" -> { + java.util.List> list = new java.util.ArrayList<>(de.blight.common.RiverIO.load()); + if (index >= 0 && index < list.size()) list.remove(index); + de.blight.common.RiverIO.save(list); + input.reloadPlacedOther = true; + } + } + scheduleAutoSave.run(); + } + ); + + // ── TabPane ─────────────────────────────────────────────────────────── + Tab assetsTab = new Tab("Assets", assetContent); + assetsTab.setClosable(false); + + Tab karteTab = new Tab("Karte", mapObjectsView); + karteTab.setClosable(false); + karteTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { + if (isSelected) mapObjectsView.refresh(); + }); + + if (worldMapView == null) { + worldMapView = new de.blight.editor.ui.WorldMapView(() -> stage()); + worldMapView.setVoxelChunkSupplier(() -> { + de.blight.editor.state.VoxelEditorState ves = ctx.jmeApp() == null ? null + : ctx.jmeApp().getStateManager().getState(de.blight.editor.state.VoxelEditorState.class); + return ves != null ? ves.getChunksSnapshot() : de.blight.common.VoxelChunkIO.loadAll(); + }); + worldMapView.setCameraInfoSupplier(() -> + new de.blight.editor.ui.WorldMapView.CameraInfo( + input.camX, input.camY, input.camZ, input.camYaw)); + worldMapView.setTeleportCallback(pos -> { + input.pendingGotoX = pos[0]; + input.pendingGotoY = pos[1]; + input.pendingGotoZ = pos[2]; + }); + worldMapView.setRefreshAction(() -> { + input.saveRequested = true; + deps.setPendingAfterSave().accept(worldMapView::loadAndRender); + ctx.setStatus().accept("Speichere vor Weltkarten-Refresh…"); + }); + worldMapView.loadAndRender(); // Vorladen beim Start, nicht erst beim Tab-Öffnen + } + + weltkartTab = new Tab("Weltkarte", worldMapView); + weltkartTab.setClosable(false); + weltkartTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { + if (!isSelected) return; + if (!worldMapView.isLoaded()) { + worldMapView.loadAndRender(); + } else if (caveMapView != null && caveMapView.isLoaded() && caveMapView.isViewInitialized()) { + final double s = caveMapView.getScale(), px = caveMapView.getPanX(), py = caveMapView.getPanY(); + javafx.application.Platform.runLater(() -> worldMapView.setView(s, px, py)); + } else { + worldMapView.ensureFitToView(); + } + }); + + caveMapView = new de.blight.editor.ui.CaveMapView(() -> stage()); + caveMapTab = new Tab("Höhlenkarte", caveMapView); + caveMapTab.setClosable(false); + caveMapTab.selectedProperty().addListener((obs, wasSelected, isSelected) -> { + if (!isSelected) return; + if (!caveMapView.isLoaded()) { + caveMapView.loadAndRender(); + } else if (worldMapView.isMapReady() && worldMapView.isViewInitialized()) { + final double s = worldMapView.getScale(), px = worldMapView.getPanX(), py = worldMapView.getPanY(); + javafx.application.Platform.runLater(() -> caveMapView.setView(s, px, py)); + } else { + caveMapView.ensureFitToView(); + } + }); + caveMapView.setFullscreenCallbacks( + () -> { + final javafx.scene.control.Label placeholder = new javafx.scene.control.Label(); + caveMapTab.setContent(placeholder); + assetTabBtn.setVisible(false); + javafx.application.Platform.runLater(() -> { + caveMapView.setMaxWidth(Double.MAX_VALUE); + javafx.scene.layout.StackPane.setAlignment(caveMapView, null); + caveMapView.setTranslateX(0); + try { + centerStack.getChildren().add(caveMapView); + } catch (Exception ex) { + log.error("[FS-cave-enter] add failed", ex); + return; + } + javafx.application.Platform.runLater(() -> { + double w = caveMapView.getWidth(); + caveMapView.fitCanvas(); + caveMapView.setTranslateX(-w); + javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( + javafx.util.Duration.millis(300), caveMapView); + tt.setToX(0); + tt.setInterpolator(javafx.animation.Interpolator.EASE_OUT); + tt.play(); + }); + }); + }, + () -> { + double w = caveMapView.getWidth(); + javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( + javafx.util.Duration.millis(220), caveMapView); + tt.setToX(-w); + tt.setInterpolator(javafx.animation.Interpolator.EASE_IN); + tt.setOnFinished(ev -> { + centerStack.getChildren().remove(caveMapView); + caveMapView.setTranslateX(0); + caveMapTab.setContent(caveMapView); + assetTabBtn.setVisible(true); + }); + tt.play(); + }); + caveMapView.loadAndRender(); // Vorladen beim Start, nicht erst beim Tab-Öffnen + worldMapView.setFullscreenCallbacks( + () -> { + // Placeholder ersetzt worldMapView im Tab → sauberes Detach + final javafx.scene.control.Label placeholder = new javafx.scene.control.Label(); + weltkartTab.setContent(placeholder); + assetTabBtn.setVisible(false); + // Nach einem Pulse hat der TabPane-Skin den Swap verarbeitet + javafx.application.Platform.runLater(() -> { + log.debug("[FS-enter] parent after detach: {}", worldMapView.getParent()); + worldMapView.setMaxWidth(Double.MAX_VALUE); + // KEIN explizites Alignment → null = CENTER → fillWidth=true → View füllt volle Breite + StackPane.setAlignment(worldMapView, null); + worldMapView.setTranslateX(0); + try { + centerStack.getChildren().add(worldMapView); + } catch (Exception ex) { + log.error("[FS-enter] add failed", ex); + return; + } + // Nach nächstem Layout-Pass hat centerStack die View korrekt dimensioniert + javafx.application.Platform.runLater(() -> { + double w = worldMapView.getWidth(); + log.debug("[FS-enter] post-layout: w={} h={}", w, worldMapView.getHeight()); + // Karte auf volle Canvas-Größe einpassen (scale/pan), dann animieren + worldMapView.fitCanvas(); + worldMapView.setTranslateX(-w); + javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( + javafx.util.Duration.millis(300), worldMapView); + tt.setToX(0); + tt.setInterpolator(javafx.animation.Interpolator.EASE_OUT); + tt.play(); + }); + }); + }, + () -> { + // Zurück nach links schieben, dann in Tab zurücklegen + double w = worldMapView.getWidth(); + javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( + javafx.util.Duration.millis(220), worldMapView); + tt.setToX(-w); + tt.setInterpolator(javafx.animation.Interpolator.EASE_IN); + tt.setOnFinished(ev -> { + centerStack.getChildren().remove(worldMapView); + worldMapView.setTranslateX(0); + weltkartTab.setContent(worldMapView); + assetTabBtn.setVisible(true); + }); + tt.play(); + }); + + TabPane tabPane = new TabPane(assetsTab, karteTab, weltkartTab, caveMapTab); + tabPane.setStyle("-fx-background-color: #e8e8e8;"); + VBox.setVgrow(tabPane, Priority.ALWAYS); + + assetPanel = new VBox(tabPane); + assetPanel.setPrefWidth(420); + assetPanel.setStyle("-fx-background-color: #e8e8e8;"); + VBox.setVgrow(tabPane, Priority.ALWAYS); + return assetPanel; + } + + // ── Zentralansicht wechseln (Asset-Overlay bleibt sichtbar) ────────────── + + // ── Asset-Overlay ───────────────────────────────────────────────────────── + + public void setupOverlay(StackPane centerStackParam) { + this.centerStack = centerStackParam; + assetPanel.setMaxWidth(420); + assetPanel.setMaxHeight(Double.MAX_VALUE); + assetPanel.setTranslateX(0); + StackPane.setAlignment(assetPanel, Pos.TOP_LEFT); + assetPanel.setEffect(new javafx.scene.effect.DropShadow( + 14, 5, 0, javafx.scene.paint.Color.rgb(0, 0, 0, 0.55))); + + assetTabBtn = new Button("›"); + assetTabBtn.setPrefSize(26, 80); + assetTabBtn.setStyle( + "-fx-background-color: #4a4a5a; -fx-text-fill: #ddd; -fx-font-size: 18; " + + "-fx-background-radius: 0 6 6 0; -fx-border-width: 0; -fx-cursor: hand; -fx-padding: 0;"); + StackPane.setAlignment(assetTabBtn, Pos.CENTER_LEFT); + + assetPanel.translateXProperty().addListener((obs, old, nv) -> + assetTabBtn.setTranslateX(nv.doubleValue() + 420)); + + assetTabBtn.setOnMouseClicked(e -> { + if (assetOverlayOpen) closeOverlay(); + else openOverlay(); + }); + + // Panel startet ausgeklappt + assetOverlayOpen = true; + assetTabBtn.setText("‹"); + assetTabBtn.setTranslateX(420); + + centerStack.getChildren().addAll(assetPanel, assetTabBtn); + } + + public void openOverlay() { + if (assetOverlayOpen) return; + assetOverlayOpen = true; + assetTabBtn.setText("‹"); + javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( + javafx.util.Duration.millis(200), assetPanel); + tt.setToX(0); + tt.play(); + } + + public void closeOverlay() { + if (!assetOverlayOpen) return; + assetOverlayOpen = false; + assetTabBtn.setText("›"); + javafx.animation.TranslateTransition tt = new javafx.animation.TranslateTransition( + javafx.util.Duration.millis(150), assetPanel); + tt.setToX(-420); + tt.play(); + } + + // ── Asset-Tree-Cell (DnD + Kontextmenü) ────────────────────────────────── + + private TreeCell buildAssetCell() { + TreeCell cell = new TreeCell<>() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { setText(null); setGraphic(null); setStyle(""); return; } + javafx.scene.shape.Rectangle swatch = null; + if (textureSetNodes.contains(getTreeItem())) { + swatch = new javafx.scene.shape.Rectangle(10, 10); + swatch.setFill(javafx.scene.paint.Color.web("#8b5cf6")); + } else { + Path tp = itemPaths.get(getTreeItem()); + if (tp != null && getCategoryRoot(getTreeItem()) == texturesNode) { + String lo = tp.getFileName().toString().toLowerCase(); + if (lo.endsWith(".png") || lo.endsWith(".jpg") + || lo.endsWith(".jpeg") || lo.endsWith(".dds")) { + swatch = new javafx.scene.shape.Rectangle(10, 10); + swatch.setFill(javafx.scene.paint.Color.web("#22c55e")); + } + } + } + if (swatch != null) { + swatch.setArcWidth(3); swatch.setArcHeight(3); + setGraphic(swatch); + } else { + setGraphic(null); + } + setText(item); + setCellStyle(this, false); + } + }; + + // ── Drag-Quelle: Dateien + Unterordner (keine Kategorie-Wurzeln, keine JME-Einträge) ─ + cell.setOnDragDetected(e -> { + TreeItem item = cell.getTreeItem(); + if (item == null) return; + if (jmePaths.containsKey(item) || jmeFolderNodes.contains(item)) return; + // Kategorie-Wurzeln (Models, Textures, …) nicht verschiebbar + if (item == modelsNode || item == texturesNode || item == audioNode + || item == animationsNode || item == itemsNode || item == charactersNode) return; + draggedItem = item; + Dragboard db = cell.startDragAndDrop(TransferMode.MOVE); + ClipboardContent cc = new ClipboardContent(); + cc.putString(item.getValue()); + db.setContent(cc); + e.consume(); + }); + + // ── Drag-Ziel: nur Ordner der gleichen Kategorie ───────────────────── + cell.setOnDragOver(e -> { + if (e.getGestureSource() != cell && isValidDropTarget(cell.getTreeItem())) + e.acceptTransferModes(TransferMode.MOVE); + e.consume(); + }); + + cell.setOnDragEntered(e -> { + if (e.getGestureSource() != cell && isValidDropTarget(cell.getTreeItem())) + setCellStyle(cell, true); + e.consume(); + }); + + cell.setOnDragExited(e -> { setCellStyle(cell, false); e.consume(); }); + + cell.setOnDragDropped(e -> { + boolean ok = false; + TreeItem target = cell.getTreeItem(); + if (draggedItem != null && target != null && isValidDropTarget(target)) { + Path src = itemPaths.get(draggedItem); + Path destDir = itemPaths.get(target); + Path dest = destDir.resolve(src.getFileName()); + if (!src.equals(dest)) { + if (Files.exists(dest)) { + ctx.setStatus().accept("Fehler: " + src.getFileName() + " existiert bereits im Zielordner."); + } else { + // isDirectory VOR dem Verschieben prüfen (danach ist src weg) + boolean srcIsFolder = Files.isDirectory(src); + try { + Files.move(src, dest); + draggedItem.getParent().getChildren().remove(draggedItem); + if (srcIsFolder) { + // Ordner: TreeItem mit gesamtem Subtree reparentieren, + // alle itemPaths im Subtree auf neuen Pfad umbiegen + updateItemPathsUnder(draggedItem, src, dest); + itemPaths.put(draggedItem, dest); + } else { + // Datei: TreeItem neu anlegen; Set-Markierung übertragen + boolean wasSet = textureSetNodes.remove(draggedItem); + itemPaths.remove(draggedItem); + draggedItem = new TreeItem<>(src.getFileName().toString()); + itemPaths.put(draggedItem, dest); + if (wasSet) textureSetNodes.add(draggedItem); + } + target.getChildren().add(draggedItem); + target.setExpanded(true); + int updated = srcIsFolder + ? renameAssetPathsForFolder(src, dest) + : renameAssetPaths(src, dest); + String suffix = updated > 0 ? " (" + updated + " Referenz" + (updated == 1 ? "" : "en") + " aktualisiert)" : ""; + ctx.setStatus().accept("Verschoben: " + src.getFileName() + suffix); + ok = true; + } catch (IOException ex) { + ctx.setStatus().accept("Verschieben fehlgeschlagen: " + ex.getMessage()); + } + } + } + } + draggedItem = null; + e.setDropCompleted(ok); + e.consume(); + }); + + cell.setOnDragDone(e -> { draggedItem = null; e.consume(); }); + + // ── Kontextmenü ────────────────────────────────────────────────────── + cell.setOnContextMenuRequested(e -> { + TreeItem item = cell.getTreeItem(); + if (item == null) return; + if (jmeFolderNodes.contains(item) || jmePaths.containsKey(item)) return; + ContextMenu ctxMenu = new ContextMenu(); + + // ── Multi-Select: Auswahl löschen ──────────────────────────────── + var selectedItems = assetTree.getSelectionModel().getSelectedItems(); + java.util.List> deletableSelection = selectedItems.stream() + .filter(si -> si != null && !isAssetFolder(si) + && itemPaths.containsKey(si) + && !jmePaths.containsKey(si) + && !jmeFolderNodes.contains(si)) + .collect(java.util.stream.Collectors.toList()); + if (deletableSelection.size() > 1) { + MenuItem multiDel = new MenuItem("🗑 Auswahl löschen (" + deletableSelection.size() + " Dateien)"); + multiDel.setStyle("-fx-text-fill: #c0392b; -fx-font-weight: bold;"); + multiDel.setOnAction(ev -> { + StringBuilder names = new StringBuilder(); + for (int i = 0; i < Math.min(5, deletableSelection.size()); i++) { + names.append(" ").append(deletableSelection.get(i).getValue()).append('\n'); + } + if (deletableSelection.size() > 5) + names.append(" … und ").append(deletableSelection.size() - 5).append(" weitere"); + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, + deletableSelection.size() + " Dateien löschen?\n\n" + names, + ButtonType.OK, ButtonType.CANCEL); + confirm.initOwner(stage()); + confirm.setHeaderText(null); + confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { + int deleted = 0; + for (TreeItem si : new java.util.ArrayList<>(deletableSelection)) { + Path p = itemPaths.get(si); + if (p == null) continue; + try { + Files.deleteIfExists(p); + if (p.toString().endsWith(".j3o")) { + deleteJ3oSideFiles(p); + String rel = assetRoot.relativize(p).toString().replace('\\', '/'); + if (rel.startsWith("Characters/clips/")) { + String clipName = p.getFileName().toString().replaceFirst("\\.j3o$", ""); + removeClipFromAllAnimSets(clipName); + } + } + si.getParent().getChildren().remove(si); + itemPaths.remove(si); + deleted++; + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Löschen: " + p.getFileName()); + } + } + ctx.setStatus().accept(deleted + " Datei(en) gelöscht"); + }); + }); + ctxMenu.getItems().addAll(multiDel, new SeparatorMenuItem()); + ctxMenu.show(cell, e.getScreenX(), e.getScreenY()); + e.consume(); + return; + } + + // Texture-Set-Knoten: eigenes Menü + if (textureSetNodes.contains(item)) { + Path setDir = itemPaths.get(item); + boolean setReadOnly = isInternalTexture(setDir); + MenuItem details = new MenuItem("Details…"); + details.setOnAction(ev -> { + TextureDetailDialog dlg = TextureDetailDialog.forSet(assetRoot, setDir, setReadOnly); + dlg.initOwner(ctxMenu.getOwnerWindow()); + dlg.showAndWait().filter(Boolean.TRUE::equals).ifPresent(ok -> + refreshCategoryNode(texturesNode, true)); + }); + MenuItem reveal = new MenuItem("Im Dateisystem anzeigen"); + reveal.setOnAction(ev -> { + try { Runtime.getRuntime().exec(new String[]{"xdg-open", setDir.toString()}); } + catch (IOException ignored) {} + }); + MenuItem del = new MenuItem("🗑 Löschen"); + del.setStyle("-fx-text-fill: #c0392b;"); + del.setDisable(setReadOnly); + del.setOnAction(ev -> { + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, + "Textur-Set wirklich löschen?\n" + setDir.getFileName() + + "\n(Alle enthaltenen Dateien werden gelöscht!)", + ButtonType.OK, ButtonType.CANCEL); + confirm.initOwner(stage()); + confirm.setHeaderText(null); + confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { + try { + deleteDirectoryRecursive(setDir); + item.getParent().getChildren().remove(item); + itemPaths.remove(item); + textureSetNodes.remove(item); + ctx.setStatus().accept("Textur-Set gelöscht: " + setDir.getFileName()); + } catch (IOException ex) { + ctx.setStatus().accept("Fehler: " + ex.getMessage()); + } + }); + }); + ctxMenu.getItems().addAll(details, new SeparatorMenuItem(), reveal, new SeparatorMenuItem(), del); + ctxMenu.show(cell, e.getScreenX(), e.getScreenY()); + e.consume(); + return; + } + + if (isAssetFolder(item)) { + MenuItem newSub = new MenuItem("📁 Neuer Unterordner…"); + newSub.setOnAction(ev -> createSubfolder(item)); + ctxMenu.getItems().add(newSub); + + boolean isCatRoot = (item == modelsNode || item == texturesNode + || item == audioNode || item == animationsNode || item == itemsNode + || item == charactersNode); + Path dir = itemPaths.get(item); + + // Ordner unter Models: Zufällig-Platzieren-Eintrag + if (getCategoryRoot(item) == modelsNode && dir != null + && java.nio.file.Files.isDirectory(dir)) { + String relFolder = assetRoot.relativize(dir).toString().replace('\\', '/'); + MenuItem placeRandom = new MenuItem("🎲 Zufällig platzieren"); + placeRandom.setOnAction(ev -> { + input.treeFolderPath = relFolder; + input.pendingModelPath = null; + deps.objectController().activatePlaceMode(); + deps.objectController().enableMultiPlace(); + ctx.setStatus().accept("Ordner: " + relFolder + " | Linksklick → zufälliges Modell | Rechtsklick → neu würfeln"); + }); + ctxMenu.getItems().addAll(new SeparatorMenuItem(), placeRandom); + } + + if (item == animationsNode) { + MenuItem importAnim = new MenuItem("⊕ Animation importieren…"); + importAnim.setOnAction(ev -> deps.animSetController().handleAnimationImport(ctxMenu.getOwnerWindow())); + ctxMenu.getItems().add(importAnim); + } + + if (item == charactersNode) { + MenuItem importChar = new MenuItem("⊕ Character-Modell importieren (FBX)…"); + importChar.setOnAction(ev -> deps.animSetController().handleCharacterModelImport(ctxMenu.getOwnerWindow())); + MenuItem importAnim = new MenuItem("⊕ Animation hinzufügen…"); + importAnim.setOnAction(ev -> deps.animSetController().handleAnimationImport(ctxMenu.getOwnerWindow())); + ctxMenu.getItems().addAll(importChar, importAnim); + } + + Path setsDir = assetRoot.resolve("Characters").resolve("sets"); + if (dir != null && setsDir.equals(dir)) { + MenuItem newSet = new MenuItem("➕ Neues Set…"); + newSet.setOnAction(ev -> deps.animSetController().createNewAnimSet()); + ctxMenu.getItems().add(newSet); + } + + if (!isCatRoot && dir != null) { + MenuItem rename = new MenuItem("✏ Umbenennen…"); + rename.setOnAction(ev -> renameAsset(item)); + ctxMenu.getItems().add(rename); + + // Ordner löschen nur wenn leer + boolean empty; + try (var s = Files.list(dir)) { empty = s.findFirst().isEmpty(); } + catch (IOException ex) { empty = false; } + MenuItem del = new MenuItem("🗑 Ordner löschen"); + del.setDisable(!empty); + if (!empty) del.setStyle("-fx-text-fill: grey;"); + del.setOnAction(ev -> { + try { + Files.deleteIfExists(dir); + item.getParent().getChildren().remove(item); + itemPaths.remove(item); + ctx.setStatus().accept("Ordner gelöscht: " + dir.getFileName()); + } catch (IOException ex) { + ctx.setStatus().accept("Fehler: " + ex.getMessage()); + } + }); + ctxMenu.getItems().addAll(new SeparatorMenuItem(), del); + } + } else { + // Datei: Umbenennen + Im Animationseditor öffnen (j3o) + Im Dateisystem anzeigen + Löschen + Path p = itemPaths.get(item); + if (p != null) { + MenuItem rename = new MenuItem("✏ Umbenennen…"); + rename.setOnAction(ev -> renameAsset(item)); + + String pStr = p.toString(); + if (pStr.endsWith(".j3o")) { + String relPath = assetRoot.relativize(p).toString().replace('\\', '/'); + + MenuItem placeModel = new MenuItem("📦 Platzieren"); + placeModel.setOnAction(ev -> { + input.pendingModelPath = relPath; + deps.objectController().activatePlaceMode(); + ctx.setStatus().accept("Modell: " + relPath + " | Linksklick ins Terrain zum Platzieren"); + }); + MenuItem openEditor = new MenuItem("🔧 Im Model Editor öffnen"); + openEditor.setOnAction(ev -> deps.modelEditorController().openModelEditor(relPath, p)); + ctxMenu.getItems().addAll(placeModel, openEditor, new SeparatorMenuItem()); + + boolean skeletal = skeletalModelPaths == null + || skeletalModelPaths.contains(relPath); + if (skeletal) { + MenuItem openAnim = new MenuItem("▶ Im Animationseditor öffnen"); + openAnim.setOnAction(ev -> { + deps.switchToAnimPreview().run(); + input.animPreviewLoadPath = relPath; + deps.animPreviewController().setStatusText("Lade…"); + deps.animPreviewController().clearClipList(); + }); + ctxMenu.getItems().addAll(openAnim, new SeparatorMenuItem()); + } + } + + if (pStr.endsWith(".animset.json")) { + String animSetRel = assetRoot.relativize(p).toString().replace('\\', '/'); + MenuItem editSet = new MenuItem("✏ AnimSet bearbeiten"); + editSet.setOnAction(ev -> { deps.animSetController().openWithSet(animSetRel, p); ctx.registry().activate(deps.animSetController()); }); + ctxMenu.getItems().addAll(editSet, new SeparatorMenuItem()); + } + + if (pStr.endsWith(".item")) { + MenuItem placeItem = new MenuItem("📍 Platzieren"); + placeItem.setOnAction(ev -> deps.activateItemPlacement().accept(p)); + MenuItem editItem = new MenuItem("✏ Im Item-Editor öffnen"); + editItem.setOnAction(ev -> { + String itemId = p.getFileName().toString().replace(".item", ""); + deps.contentController().switchToItemEditor(itemId); + }); + ctxMenu.getItems().addAll(placeItem, editItem, new SeparatorMenuItem()); + } + + // Textur-Map-Zuordnung (nur für Bilddateien im Textures-Zweig) + String loName = pStr.toLowerCase(); + boolean isImage = loName.endsWith(".png") || loName.endsWith(".jpg") + || loName.endsWith(".jpeg") || loName.endsWith(".dds"); + if (isImage && getCategoryRoot(item) == texturesNode && !jmePaths.containsKey(item)) { + boolean imgReadOnly = isInternalTexture(p); + MenuItem imgDetails = new MenuItem("Details…"); + imgDetails.setOnAction(ev -> { + TextureDetailDialog dlg = TextureDetailDialog.forFile(assetRoot, p, imgReadOnly); + dlg.initOwner(ctxMenu.getOwnerWindow()); + dlg.setOnConvertedToSet(newSetDir -> + Platform.runLater(() -> refreshCategoryNode(texturesNode, true))); + dlg.showAndWait().filter(Boolean.TRUE::equals).ifPresent(ok -> + refreshCategoryNode(texturesNode, true)); + }); + ctxMenu.getItems().addAll(imgDetails, new SeparatorMenuItem()); + } + + MenuItem reveal = new MenuItem("Im Dateisystem anzeigen"); + reveal.setOnAction(ev -> { + try { Runtime.getRuntime().exec( + new String[]{"xdg-open", p.getParent().toString()}); + } catch (IOException ignored) {} + }); + MenuItem del = new MenuItem("🗑 Löschen"); + del.setStyle("-fx-text-fill: #c0392b;"); + del.setOnAction(ev -> { + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, + "Datei wirklich löschen?\n" + p.getFileName(), + ButtonType.OK, ButtonType.CANCEL); + confirm.initOwner(stage()); + confirm.setHeaderText(null); + confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { + try { + Files.deleteIfExists(p); + item.getParent().getChildren().remove(item); + itemPaths.remove(item); + if (pStr.endsWith(".j3o")) { + deleteJ3oSideFiles(p); + String rel = assetRoot.relativize(p).toString().replace('\\', '/'); + if (rel.startsWith("Characters/clips/")) { + String clipName = p.getFileName().toString().replaceFirst("\\.j3o$", ""); + removeClipFromAllAnimSets(clipName); + } + } + ctx.setStatus().accept("Gelöscht: " + p.getFileName()); + } catch (IOException ex) { + ctx.setStatus().accept("Fehler: " + ex.getMessage()); + } + }); + }); + ctxMenu.getItems().addAll(rename, reveal, new SeparatorMenuItem(), del); + } + } + + if (!ctxMenu.getItems().isEmpty()) + ctxMenu.show(cell, e.getScreenX(), e.getScreenY()); + e.consume(); + }); + + return cell; + } + + private void setCellStyle(TreeCell cell, boolean highlighted) { + TreeItem ti = cell.getTreeItem(); + boolean isBold = isAssetFolder(ti) && !textureSetNodes.contains(ti); + String base = isBold ? "-fx-font-weight:bold;" : ""; + cell.setStyle(highlighted ? base + "-fx-background-color:#cce5ff;" : base); + } + + // ── Asset-Baum Hilfsmethoden ────────────────────────────────────────────── + + /** Gibt true zurück wenn das Item ein Verzeichnis ist (inkl. Kategoriewurzeln). */ + private boolean isAssetFolder(TreeItem item) { + if (item == null) return false; + if (jmeFolderNodes.contains(item)) return true; + Path p = itemPaths.get(item); + return p != null && Files.isDirectory(p); + } + + /** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode / animationsNode / itemsNode / charactersNode). */ + private TreeItem getCategoryRoot(TreeItem item) { + for (TreeItem cur = item; cur != null; cur = cur.getParent()) + if (cur == modelsNode || cur == texturesNode || cur == audioNode + || cur == animationsNode || cur == itemsNode || cur == charactersNode) return cur; + return null; + } + + /** Gibt true zurück wenn der Pfad unter Textures/internal/ liegt (schreibgeschützt). */ + private static boolean isInternalTexture(Path p) { + if (p == null) return false; + for (Path part : p) { + if (part.toString().equals("internal")) return true; + } + return false; + } + + /** Gibt true zurück wenn draggedItem auf dieses target verschoben werden darf. */ + private boolean isValidDropTarget(TreeItem target) { + if (draggedItem == null || target == null) return false; + if (!isAssetFolder(target)) return false; + if (jmeFolderNodes.contains(target)) return false; + if (textureSetNodes.contains(target)) return false; // Set-Ordner sind keine Drop-Ziele + if (target == draggedItem.getParent()) return false; + if (target == draggedItem) return false; + // Ordner darf nicht in einen eigenen Nachfolger gezogen werden + if (isAssetFolder(draggedItem) && isDescendantOf(draggedItem, target)) return false; + TreeItem dragCat = getCategoryRoot(draggedItem); + TreeItem dropCat = getCategoryRoot(target); + return dragCat != null && dragCat == dropCat; + } + + private static boolean isDescendantOf(TreeItem ancestor, TreeItem item) { + TreeItem cur = item.getParent(); + while (cur != null) { + if (cur == ancestor) return true; + cur = cur.getParent(); + } + return false; + } + + /** Aktualisiert rekursiv alle itemPaths-Einträge unter einem verschobenen Ordner. */ + private void updateItemPathsUnder(TreeItem node, Path oldBase, Path newBase) { + for (TreeItem child : node.getChildren()) { + Path old = itemPaths.get(child); + if (old != null) { + Path relative = oldBase.relativize(old); + itemPaths.put(child, newBase.resolve(relative)); + } + if (!child.getChildren().isEmpty()) { + updateItemPathsUnder(child, oldBase, newBase); + } + } + } + + /** Erstellt einen neuen Unterordner unter parentItem (Dialog + Filesystem). */ + private void createSubfolder(TreeItem parentItem) { + Path parentPath = itemPaths.get(parentItem); + if (parentPath == null) return; + TextInputDialog dlg = new TextInputDialog("Neuer Ordner"); + dlg.initOwner(stage()); + dlg.setTitle("Ordner erstellen"); + dlg.setHeaderText("Neuen Unterordner in " + parentPath.getFileName() + " erstellen:"); + dlg.setContentText("Name:"); + dlg.showAndWait().ifPresent(raw -> { + String name = raw.trim(); + if (name.isEmpty()) return; + Path newDir = parentPath.resolve(name); + try { + Files.createDirectories(newDir); + TreeItem newItem = new TreeItem<>(name); + itemPaths.put(newItem, newDir); + parentItem.getChildren().add(0, newItem); // Ordner an erster Stelle + parentItem.setExpanded(true); + ctx.setStatus().accept("Ordner erstellt: " + name); + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Erstellen: " + ex.getMessage()); + } + }); + } + + /** Benennt eine Datei oder einen Ordner um (Filesystem + TreeItem + itemPaths). */ + private void renameAsset(TreeItem item) { + Path oldPath = itemPaths.get(item); + if (oldPath == null) return; + + boolean isFile = !Files.isDirectory(oldPath); + String oldFileName = oldPath.getFileName().toString(); + + // For files: extract extension and pre-fill dialog with base name only + String ext = ""; + String baseName = oldFileName; + if (isFile) { + int dot = oldFileName.lastIndexOf('.'); + if (dot > 0) { + ext = oldFileName.substring(dot); // e.g. ".j3o" + baseName = oldFileName.substring(0, dot); + } + } + + String headerExt = ext.isEmpty() ? "" : " (Endung " + ext + " wird beibehalten)"; + TextInputDialog dlg = new TextInputDialog(baseName); + dlg.initOwner(stage()); + dlg.setTitle("Umbenennen"); + dlg.setHeaderText(oldFileName + " umbenennen:" + headerExt); + dlg.setContentText("Neuer Name:"); + + final String fixedExt = ext; + dlg.showAndWait().ifPresent(raw -> { + String newBase = raw.trim(); + if (newBase.isEmpty()) return; + // Append original extension for files + String newName = isFile ? newBase + fixedExt : newBase; + if (newName.equals(oldFileName)) return; + if (newName.contains("/") || newName.contains("\\")) { + ctx.setStatus().accept("Ungültiger Name: keine Pfadtrennzeichen erlaubt."); + return; + } + Path newPath = oldPath.getParent().resolve(newName); + if (Files.exists(newPath)) { + ctx.setStatus().accept("Fehler: " + newName + " existiert bereits."); + return; + } + try { + Files.move(oldPath, newPath); + itemPaths.put(item, newPath); + // Bei Ordnern alle Kind-Pfade anpassen + if (Files.isDirectory(newPath)) { + updateDescendantPaths(item, oldPath, newPath); + } + item.setValue(newName); + int updated = Files.isDirectory(newPath) + ? renameAssetPathsForFolder(oldPath, newPath) + : renameAssetPaths(oldPath, newPath); + String suffix = updated > 0 ? " (" + updated + " Referenz" + (updated == 1 ? "" : "en") + " aktualisiert)" : ""; + ctx.setStatus().accept("Umbenannt: " + oldFileName + " → " + newName + suffix); + } catch (IOException ex) { + ctx.setStatus().accept("Umbenennen fehlgeschlagen: " + ex.getMessage()); + } + }); + } + + /** Aktualisiert itemPaths aller Kinder nach einer Ordner-Umbenennung. */ + private void updateDescendantPaths(TreeItem parent, Path oldBase, Path newBase) { + for (TreeItem child : parent.getChildren()) { + Path p = itemPaths.get(child); + if (p != null) itemPaths.put(child, newBase.resolve(oldBase.relativize(p))); + updateDescendantPaths(child, oldBase, newBase); + } + } + + // ── Asset-Pfad-Umbenennung bei Move/Rename ──────────────────────────────── + + /** + * Aktualisiert alle in SharedInput gespeicherten Asset-Pfade wenn eine einzelne Datei + * von oldAbs nach newAbs verschoben/umbenannt wurde. + * Gibt die Anzahl aktualisierter Referenzen zurück. + */ + private int renameAssetPaths(Path oldAbs, Path newAbs) { + if (!oldAbs.startsWith(assetRoot) || !newAbs.startsWith(assetRoot)) return 0; + String oldRel = assetRoot.relativize(oldAbs).toString().replace('\\', '/'); + String newRel = assetRoot.relativize(newAbs).toString().replace('\\', '/'); + return applyPathRenameToSharedInput(oldRel, newRel); + } + + /** + * Aktualisiert alle Pfade wenn ein Ordner verschoben/umbenannt wurde: + * Walk durch den neuen Ordner, für jede Datei alte/neue rel. Pfade berechnen. + */ + private int renameAssetPathsForFolder(Path oldFolderAbs, Path newFolderAbs) { + int total = 0; + try (java.util.stream.Stream stream = Files.walk(newFolderAbs)) { + for (Path newFile : (Iterable) stream.filter(Files::isRegularFile)::iterator) { + Path relToNew = newFolderAbs.relativize(newFile); + Path oldFile = oldFolderAbs.resolve(relToNew); + total += renameAssetPaths(oldFile, newFile); + } + } catch (IOException ex) { + log.warn("[Asset] Ordner-Pfad-Update fehlgeschlagen: {}", ex.getMessage()); + } + return total; + } + + /** Wendet ein (oldRel→newRel)-Rename auf alle SharedInput-Pfad-Arrays an. */ + private int applyPathRenameToSharedInput(String oldRel, String newRel) { + int count = 0; + + count += replaceInPaths(input.terrainTexturePaths, oldRel, newRel, + () -> input.terrainTexturesChanged = true); + count += replaceInPaths(input.terrainNormalMapPaths, oldRel, newRel, + () -> input.terrainNormalMapsChanged = true); + count += replaceInPaths(input.terrainDisplacementMapPaths, oldRel, newRel, null); + + count += replaceInPaths(input.upperTexturePaths, oldRel, newRel, + () -> input.upperTexturesChanged = true); + count += replaceInPaths(input.upperNormalMapPaths, oldRel, newRel, + () -> input.upperNormalMapsChanged = true); + count += replaceInPaths(input.upperDisplacementMapPaths, oldRel, newRel, null); + + count += replaceInPaths(input.thirdTexturePaths, oldRel, newRel, + () -> input.thirdTexturesChanged = true); + count += replaceInPaths(input.thirdNormalMapPaths, oldRel, newRel, + () -> input.thirdNormalMapsChanged = true); + count += replaceInPaths(input.thirdDisplacementMapPaths, oldRel, newRel, null); + + count += replaceInPaths(input.voxelSplatTexturePaths, oldRel, newRel, + () -> input.voxelSplatTexturesChanged = true); + + if (oldRel.equals(input.grassTexturePath)) { + input.grassTexturePath = newRel; + input.grassSlotsChanged = true; + count++; + } + count += replaceInPaths(input.grassTextureSlots, oldRel, newRel, + () -> input.grassSlotsChanged = true); + count += replaceInPaths(input.grassNormalMapPaths, oldRel, newRel, + () -> input.grassSlotsChanged = true); + + // JME3-States (SceneObjects, Emitter) über Queue informieren + SharedInput.PathRename rename = new SharedInput.PathRename(oldRel, newRel); + input.sceneObjectPathRenames.offer(rename); + input.emitterPathRenames.offer(rename); + + return count; + } + + /** Ersetzt oldRel durch newRel in einem String-Array; ruft onChange einmalig wenn geändert. */ + private static int replaceInPaths(String[] arr, String oldRel, String newRel, Runnable onChange) { + int count = 0; + for (int i = 0; i < arr.length; i++) { + if (oldRel.equals(arr[i])) { + arr[i] = newRel; + count++; + } + } + if (count > 0 && onChange != null) onChange.run(); + return count; + } + + /** Scannt alle JME-JARs im Classpath und baut den Textur-Teilbaum unter root auf. */ + private void loadJmeTexturesInto(TreeItem root) { + String[] texExts = {".png", ".jpg", ".jpeg", ".bmp", ".tga", ".dds"}; + java.util.Set seen = new java.util.HashSet<>(); + java.util.List collected = new java.util.ArrayList<>(); + + String cp = System.getProperty("java.class.path", ""); + for (String entry : cp.split(File.pathSeparator)) { + if (!entry.toLowerCase().contains("jme")) continue; + File f = new File(entry); + if (!f.exists() || !f.getName().endsWith(".jar")) continue; + try (java.util.jar.JarFile jar = new java.util.jar.JarFile(f)) { + jar.stream() + .filter(je -> !je.isDirectory()) + .map(java.util.jar.JarEntry::getName) + .filter(name -> name.startsWith("Textures/")) + .filter(name -> { + String lo = name.toLowerCase(); + for (String ext : texExts) if (lo.endsWith(ext)) return true; + return false; + }) + .filter(seen::add) + .forEach(collected::add); + } catch (IOException ignored) {} + } + + java.util.Collections.sort(collected); + for (String path : collected) addJmeTreeEntry(root, path); + } + + private void addJmeTreeEntry(TreeItem root, String fullPath) { + String[] parts = fullPath.split("/"); + TreeItem cur = root; + for (int i = 0; i < parts.length - 1; i++) { + final String dirName = parts[i]; + TreeItem found = null; + for (TreeItem child : cur.getChildren()) { + if (child.getValue().equals(dirName) && jmeFolderNodes.contains(child)) { + found = child; + break; + } + } + if (found == null) { + found = new TreeItem<>(dirName); + jmeFolderNodes.add(found); + cur.getChildren().add(found); + } + cur = found; + } + TreeItem file = new TreeItem<>(parts[parts.length - 1]); + jmePaths.put(file, fullPath); + cur.getChildren().add(file); + } + + /** + * Scannt alle JME-JARs im Classpath und baut den Modell-Teilbaum unter root auf. + * Nur Einträge unter "Models/" werden berücksichtigt; der Präfix wird für die + * Anzeige entfernt, der vollständige Classpath-Pfad in jmePaths hinterlegt. + */ + private void loadJmeModelsInto(TreeItem root) { + // .mesh.xml (OGRE-Format) braucht jme3-plugins, das nicht auf dem Classpath ist + String[] modelExts = {".j3o", ".obj", ".gltf", ".glb"}; + java.util.Set seen = new java.util.HashSet<>(); + java.util.List collected = new java.util.ArrayList<>(); + + String cp = System.getProperty("java.class.path", ""); + for (String entry : cp.split(File.pathSeparator)) { + if (!entry.toLowerCase().contains("jme")) continue; + File f = new File(entry); + if (!f.exists() || !f.getName().endsWith(".jar")) continue; + try (java.util.jar.JarFile jar = new java.util.jar.JarFile(f)) { + jar.stream() + .filter(je -> !je.isDirectory()) + .map(java.util.jar.JarEntry::getName) + .filter(name -> name.startsWith("Models/")) + .filter(name -> { + String lo = name.toLowerCase(); + for (String ext : modelExts) if (lo.endsWith(ext)) return true; + return false; + }) + .filter(seen::add) + .forEach(collected::add); + } catch (IOException ignored) {} + } + + java.util.Collections.sort(collected); + for (String fullPath : collected) { + // Präfix "Models/" für Anzeige entfernen; jmePaths speichert den vollen Pfad + String displayPath = fullPath.substring("Models/".length()); + addJmeModelEntry(root, displayPath, fullPath); + } + } + + /** Fügt einen JME-Modell-Eintrag ein (displayPath für Baum, fullPath für AssetManager). */ + private void addJmeModelEntry(TreeItem root, String displayPath, String fullPath) { + String[] parts = displayPath.split("/"); + TreeItem cur = root; + for (int i = 0; i < parts.length - 1; i++) { + final String dirName = parts[i]; + TreeItem found = null; + for (TreeItem child : cur.getChildren()) { + if (child.getValue().equals(dirName) && jmeFolderNodes.contains(child)) { + found = child; break; + } + } + if (found == null) { + found = new TreeItem<>(dirName); + jmeFolderNodes.add(found); + cur.getChildren().add(found); + } + cur = found; + } + TreeItem file = new TreeItem<>(parts[parts.length - 1]); + jmePaths.put(file, fullPath); + cur.getChildren().add(file); + } + + /** Lädt Assets rekursiv: Ordner zuerst (alphabetisch), dann Dateien. Keine exts = alles. */ + private void loadAssetsRecursive(TreeItem parent, Path dir, String... exts) { + if (!Files.exists(dir)) return; + try (var stream = Files.list(dir)) { + List paths = stream + .sorted(Comparator.comparing((Path p) -> Files.isDirectory(p) ? 0 : 1) + .thenComparing(p -> p.getFileName().toString().toLowerCase())) + .toList(); + for (Path p : paths) { + String name = p.getFileName().toString(); + if (Files.isDirectory(p)) { + // Texture-Set-Ordner: enthält textureset.json → als einzelner Blattknoten + if (Files.exists(p.resolve("textureset.json"))) { + TreeItem sub = new TreeItem<>(name); + itemPaths.put(sub, p); + textureSetNodes.add(sub); + parent.getChildren().add(sub); + } else { + TreeItem sub = new TreeItem<>(name); + itemPaths.put(sub, p); + parent.getChildren().add(sub); + loadAssetsRecursive(sub, p, exts); + } + } else { + String lo = name.toLowerCase(); + if (lo.endsWith(".meta")) continue; + boolean include = exts.length == 0 + || java.util.Arrays.stream(exts).anyMatch(lo::endsWith); + if (include) { + TreeItem file = new TreeItem<>(name); + itemPaths.put(file, p); + parent.getChildren().add(file); + } + } + } + } catch (IOException ignored) {} + } + + /** Liest die AnimClip-Namen aus einer J3O-Datei, ohne den JME3-Thread zu benötigen. */ + private List 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); + if (ac == null) return List.of(); + List names = new ArrayList<>(ac.getAnimClipsNames()); + names.sort(String.CASE_INSENSITIVE_ORDER); + return names; + } catch (Exception e) { + return List.of(); + } + } + + /** Hängt Clip-Namen als Unterknoten unter jeden J3O-Eintrag im Animations-Teilbaum. */ + private void addAnimClipSubNodes(TreeItem node) { + for (TreeItem child : node.getChildren()) { + Path p = itemPaths.get(child); + if (p != null && !Files.isDirectory(p) + && p.getFileName().toString().toLowerCase().endsWith(".j3o")) { + child.getChildren().clear(); + for (String clip : readAnimClipNames(p)) { + child.getChildren().add(new TreeItem<>(clip)); + } + } else if (p != null && Files.isDirectory(p)) { + addAnimClipSubNodes(child); + } + } + } + + /** Löscht Thumbnail, das zu einer .j3o-Datei gehört. */ + private void deleteJ3oSideFiles(Path j3oPath) { + try { + Files.deleteIfExists( + de.blight.editor.state.ThumbnailRenderer.sidecarPath(j3oPath, assetRoot)); + } catch (IOException ignored) {} + } + + /** + * Entfernt einen Animations-Clip aus allen AnimSet-Dateien in Characters/sets/. + * Wird aufgerufen wenn eine .j3o-Datei aus Characters/clips/ gelöscht wird. + */ + private void removeClipFromAllAnimSets(String clipName) { + Path setsDir = assetRoot.resolve("Characters").resolve("sets"); + if (!Files.isDirectory(setsDir)) return; + try (var walk = Files.walk(setsDir, 1)) { + walk.filter(p -> p.toString().endsWith(".animset.json")).forEach(setFile -> { + try { + String json = Files.readString(setFile); + com.google.gson.JsonObject root = com.google.gson.JsonParser.parseString(json).getAsJsonObject(); + com.google.gson.JsonArray clips = root.has("clips") ? root.getAsJsonArray("clips") : null; + if (clips == null) return; + boolean changed = false; + com.google.gson.JsonArray updated = new com.google.gson.JsonArray(); + for (com.google.gson.JsonElement el : clips) { + if (el.isJsonPrimitive() && el.getAsString().equals(clipName)) { + changed = true; + } else { + updated.add(el); + } + } + if (!changed) return; + root.add("clips", updated); + String pretty = new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(root); + Files.writeString(setFile, pretty); + log.info("[AssetTree] Clip '{}' aus AnimSet '{}' entfernt", clipName, setFile.getFileName()); + } catch (Exception ex) { + log.warn("[AssetTree] Fehler beim Bereinigen von {}: {}", setFile.getFileName(), ex.getMessage()); + } + }); + } catch (IOException ignored) {} + } + + /** + * Befüllt den Asset-Baum vollständig aus dem Dateisystem. + * Bekannte Verzeichnisse werden den Kategorie-Feldern zugewiesen; + * alle anderen erscheinen als generische Knoten. + */ + private void populateAssetTree(TreeItem root) { + modelsNode = null; texturesNode = null; + audioNode = null; animationsNode = null; itemsNode = null; + charactersNode = null; + jmeModelsNode = null; jmeTexturesNode = null; + jmeFolderNodes.clear(); + textureSetNodes.clear(); + + List topDirs; + try (var s = Files.list(assetRoot)) { + topDirs = s.filter(Files::isDirectory) + .filter(p -> { + String n = p.getFileName().toString(); + return !n.equals(de.blight.editor.state.ThumbnailRenderer.THUMB_DIR); + }) + .sorted(Comparator.comparing(p -> p.getFileName().toString().toLowerCase())) + .collect(java.util.stream.Collectors.toList()); + } catch (IOException e) { topDirs = List.of(); } + + for (Path dir : topDirs) { + String name = dir.getFileName().toString(); + TreeItem node = new TreeItem<>(name); + itemPaths.put(node, dir); + root.getChildren().add(node); + loadAssetsRecursive(node, dir); + + switch (name.toLowerCase()) { + case "models" -> { + modelsNode = node; + jmeModelsNode = new TreeItem<>("JME"); + jmeFolderNodes.add(jmeModelsNode); + node.getChildren().add(jmeModelsNode); + loadJmeModelsInto(jmeModelsNode); + } + case "textures" -> { + texturesNode = node; + jmeTexturesNode = new TreeItem<>("JME"); + jmeFolderNodes.add(jmeTexturesNode); + node.getChildren().add(jmeTexturesNode); + loadJmeTexturesInto(jmeTexturesNode); + } + case "audio" -> audioNode = node; + case "animations" -> { + animationsNode = node; + addAnimClipSubNodes(node); + } + case "characters" -> charactersNode = node; + case "items" -> itemsNode = node; + } + } + } + + /** Baut den Teilbaum einer Kategorie komplett neu auf. + * @param preserveExpansion true → aufgeklappte Knoten bleiben nach dem Rebuild erhalten */ + private void refreshCategoryNode(TreeItem catNode, boolean preserveExpansion) { + java.util.Set expanded = preserveExpansion ? collectExpandedPaths() : java.util.Set.of(); + clearItemPathsFor(catNode); + catNode.getChildren().clear(); + Path dir = itemPaths.get(catNode); + if (dir != null) loadAssetsRecursive(catNode, dir); + if (catNode == animationsNode) addAnimClipSubNodes(catNode); + if (catNode == modelsNode && jmeModelsNode != null) + catNode.getChildren().add(jmeModelsNode); + if (catNode == texturesNode && jmeTexturesNode != null) + catNode.getChildren().add(jmeTexturesNode); + if (preserveExpansion) { + restoreExpansion(expanded); + catNode.setExpanded(true); + } + } + + /** Baut den gesamten Asset-Baum neu aus dem Dateisystem auf. + * @param preserveExpansion true → aufgeklappte Knoten bleiben nach dem Rebuild erhalten */ + private void refreshAllAssets(boolean preserveExpansion) { + if (assetTreeRoot == null) return; + java.util.Set expanded = preserveExpansion ? collectExpandedPaths() : java.util.Set.of(); + itemPaths.clear(); + jmePaths.clear(); + assetTreeRoot.getChildren().clear(); + populateAssetTree(assetTreeRoot); + if (preserveExpansion) restoreExpansion(expanded); + } + + /** Gibt zurück, ob das aktive Tool den aufgeklappten Zustand des Asset-Baums behalten soll. */ + public boolean shouldPreserveExpansion() { + return "vegetation".equals(deps.currentTool().get()) || "eztree".equals(deps.currentTool().get()) + || "objecteditor".equals(ctx.registry().activeToolId()) || "modeleditor".equals(ctx.registry().activeToolId()) + || ("world".equals(deps.currentTool().get()) + && (input.activeLayer == SharedInput.LAYER_OBJECTS + || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)); + } + + /** Sammelt die Pfade (Name-Kette mit '/') aller aufgeklappten TreeItems. */ + private java.util.Set collectExpandedPaths() { + java.util.Set result = new java.util.HashSet<>(); + collectExpandedPaths(assetTreeRoot, "", result); + return result; + } + + private void collectExpandedPaths(TreeItem node, String prefix, java.util.Set result) { + for (TreeItem child : node.getChildren()) { + String path = prefix.isEmpty() ? child.getValue() : prefix + "/" + child.getValue(); + if (child.isExpanded()) result.add(path); + collectExpandedPaths(child, path, result); + } + } + + /** Klappt alle TreeItems wieder auf, deren Pfad in {@code expanded} enthalten ist. */ + private void restoreExpansion(java.util.Set expanded) { + if (expanded.isEmpty()) return; + restoreExpansion(assetTreeRoot, "", expanded); + } + + private void restoreExpansion(TreeItem node, String prefix, java.util.Set expanded) { + for (TreeItem child : node.getChildren()) { + String path = prefix.isEmpty() ? child.getValue() : prefix + "/" + child.getValue(); + if (expanded.contains(path)) child.setExpanded(true); + restoreExpansion(child, path, expanded); + } + } + + private void clearItemPathsFor(TreeItem item) { + for (TreeItem child : item.getChildren()) clearItemPathsFor(child); + if (item != modelsNode && item != texturesNode && item != audioNode && item != animationsNode && item != charactersNode) + itemPaths.remove(item); + textureSetNodes.remove(item); + } + + /** + * Kopiert MTL-Dateien und darin referenzierte Texturen aus dem Quell-Ordner + * der OBJ-Datei in das Ziel-Verzeichnis (blight-assets/Models/). + */ + private static void copyObjCompanions(File objFile, Path destDir) { + Path srcDir = objFile.toPath().getParent(); + + // MTL-Dateinamen aus dem OBJ selbst lesen (mtllib-Direktiven) + java.util.Set mtlNames = new java.util.LinkedHashSet<>(); + String baseName = objFile.getName().replaceFirst("\\.[^.]+$", ""); + mtlNames.add(baseName + ".mtl"); // Standardname + mtlNames.add(baseName + ".MTL"); + try (java.io.BufferedReader br = java.nio.file.Files.newBufferedReader(objFile.toPath())) { + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith("mtllib ")) mtlNames.add(line.substring(7).trim()); + } + } catch (IOException ignored) {} + + for (String mtlName : mtlNames) { + Path mtlSrc = srcDir.resolve(mtlName); + if (!java.nio.file.Files.exists(mtlSrc)) continue; + copyQuiet(mtlSrc, destDir.resolve(mtlSrc.getFileName())); + copyMtlTextures(mtlSrc, srcDir, destDir); + } + } + + /** Kopiert Texturdateien, die in einer MTL-Datei referenziert werden. */ + private static void copyMtlTextures(Path mtlFile, Path srcDir, Path destDir) { + try (java.io.BufferedReader br = java.nio.file.Files.newBufferedReader(mtlFile)) { + String line; + while ((line = br.readLine()) != null) { + String lo = line.strip().toLowerCase(); + if (!lo.startsWith("map_")) continue; + int sp = line.indexOf(' '); + if (sp < 0) continue; + String ref = line.substring(sp + 1).strip(); + // Optionale -Flags überspringen (z.B. "map_Kd -s 1 1 1 texture.png") + String[] parts = ref.split("\\s+"); + String texName = parts[parts.length - 1]; + if (texName.isEmpty()) continue; + Path texSrc = srcDir.resolve(texName); + if (java.nio.file.Files.exists(texSrc)) { + copyQuiet(texSrc, destDir.resolve(texSrc.getFileName())); + } + } + } catch (IOException ignored) {} + } + + private static void copyQuiet(Path src, Path dst) { + try { Files.copy(src, dst, java.nio.file.StandardCopyOption.REPLACE_EXISTING); } + catch (IOException ignored) {} + } + + /** + * Konvertiert / normalisiert eine Audiodatei zu OGG Vorbis bei 48000 Hz. + * Das Ziel-Format stimmt mit PipeWire/PulseAudio überein, sodass OpenALSofts + * Spatial-Mixer kein On-the-fly-Resampling durchführen muss (kein Knistern). + */ + private static Path convertToOgg(Path src, Path destOgg) throws IOException { + try { + Process proc = new ProcessBuilder( + "ffmpeg", "-i", src.toString(), + "-ar", "48000", + "-q:a", "4", + destOgg.toString(), "-y") + .redirectErrorStream(true) + .start(); + proc.getInputStream().transferTo(java.io.OutputStream.nullOutputStream()); + int exit = proc.waitFor(); + if (exit != 0) throw new IOException("ffmpeg fehlgeschlagen (exit " + exit + ")"); + return destOgg; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Konvertierung unterbrochen", e); + } + } + + /** Stellt sicher, dass eine Audiodatei als OGG vorliegt. + * Ist sie bereits OGG, wird sie einfach kopiert. Andernfalls konvertiert. + * Gibt den relativen Asset-Pfad zurück. */ + private static String ensureOgg(File chosen, Path assetRoot) throws IOException { + Path src = chosen.toPath(); + String baseName = chosen.getName().replaceFirst("\\.[^.]+$", ""); + boolean alreadyOgg = chosen.getName().toLowerCase().endsWith(".ogg"); + Path destOgg = src.resolveSibling(baseName + ".ogg"); + if (!alreadyOgg) convertToOgg(src, destOgg); + else if (!src.equals(destOgg)) Files.copy(src, destOgg, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + return assetRoot.relativize(destOgg).toString().replace('\\', '/'); + } + + private void handleImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Assets importieren"); + fc.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Alle unterstützten Dateien", + "*.j3o","*.obj","*.gltf","*.glb", + "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds", + "*.ogg","*.wav","*.mp3"), + new FileChooser.ExtensionFilter("Modelle", "*.j3o","*.obj","*.gltf","*.glb"), + new FileChooser.ExtensionFilter("Texturen", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"), + new FileChooser.ExtensionFilter("Audio (OGG, WAV, MP3)", "*.ogg","*.wav","*.mp3") + ); + var files = fc.showOpenMultipleDialog(owner); + if (files == null) return; + + for (File file : files) { + String name = file.getName().toLowerCase(); + boolean isNativeModel = name.matches(".*\\.(obj|fbx|gltf|glb)"); + boolean isJ3o = name.endsWith(".j3o"); + boolean isAudio = name.matches(".*\\.(ogg|wav|mp3)"); + boolean isModel = isNativeModel || isJ3o; + + String subDir = isModel ? "Models" : isAudio ? "audio" : "Textures"; + TreeItem parent = isModel ? modelsNode : isAudio ? audioNode : texturesNode; + archiveOriginal(file, isModel ? "models" : isAudio ? "audio" : "textures"); + + try { + Path destDir = assetRoot.resolve(subDir); + Files.createDirectories(destDir); + + if (isAudio) { + String baseName = file.getName().replaceFirst("\\.[^.]+$", ""); + Path destOgg = destDir.resolve(baseName + ".ogg"); + ctx.setStatus().accept("Normalisiere " + file.getName() + " → 48 kHz OGG …"); + convertToOgg(file.toPath(), destOgg); + String finalName = baseName + ".ogg"; + TreeItem newItem = new TreeItem<>(finalName); + itemPaths.put(newItem, destOgg); + parent.getChildren().add(newItem); + parent.setExpanded(true); + ctx.setStatus().accept("Importiert: " + finalName); + } else { + Path destFile = destDir.resolve(file.getName()); + Files.copy(file.toPath(), destFile, StandardCopyOption.REPLACE_EXISTING); + if (isNativeModel) { + if (name.endsWith(".obj")) copyObjCompanions(file, destDir); + String assetPath = subDir + "/" + file.getName(); + String baseName = file.getName().replaceFirst("\\.[^.]+$", ""); + Path destJ3o = destDir.resolve(baseName + ".j3o"); + boolean keepCtrls = name.matches(".*\\.(gltf|glb)"); + input.modelConvertQueue.offer( + new SharedInput.ModelConvertRequest(assetPath, destJ3o, destFile, keepCtrls)); + ctx.setStatus().accept("Konvertiere " + file.getName() + " → .j3o …"); + } else { + TreeItem newItem = new TreeItem<>(file.getName()); + itemPaths.put(newItem, destFile); + parent.getChildren().add(newItem); + parent.setExpanded(true); + ctx.setStatus().accept("Importiert: " + file.getName()); + } + } + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Import: " + ex.getMessage()); + } + } + } + + + private void archiveOriginal(File source, String assetType) { + java.nio.file.Path dest = de.blight.editor.ProjectRoot.PATH.resolve("assets").resolve("imported").resolve(assetType); + try { + java.nio.file.Files.createDirectories(dest); + java.nio.file.Files.copy(source.toPath(), dest.resolve(source.getName()), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + log.info("[Import] Archiviert: {}/{}", assetType, source.getName()); + } catch (IOException ex) { + log.warn("[Import] Archivierung fehlgeschlagen ({}/{}): {}", + assetType, source.getName(), ex.getMessage()); + } + } + + public void handleTextureImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Texturen importieren (nicht-PNG wird automatisch konvertiert)"); + fc.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Bildateien", "*.png","*.jpg","*.jpeg","*.bmp","*.tga","*.dds"), + new FileChooser.ExtensionFilter("PNG", "*.png"), + new FileChooser.ExtensionFilter("JPEG", "*.jpg","*.jpeg"), + new FileChooser.ExtensionFilter("Weitere", "*.bmp","*.tga","*.dds")); + List files = fc.showOpenMultipleDialog(owner); + if (files == null) return; + Path destDir = assetRoot.resolve("Textures"); + int ok = 0; + for (File file : files) { + try { + Files.createDirectories(destDir); + Path dest = importTextureAsPng(file, destDir); + TreeItem item = new TreeItem<>(dest.getFileName().toString()); + itemPaths.put(item, dest); + texturesNode.getChildren().add(item); + ok++; + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Import von " + file.getName() + ": " + ex.getMessage()); + } + } + if (ok > 0) { + texturesNode.setExpanded(true); + ctx.setStatus().accept(ok + " Textur(en) importiert"); + } + } + + /** + * Kopiert oder konvertiert eine Bilddatei als PNG in destDir. + * PNG + DDS werden unverändert kopiert; JPG/BMP/TGA werden nach PNG konvertiert. + * Gibt den Pfad der resultierenden Datei zurück. + */ + private static Path importTextureAsPng(File src, Path destDir) throws IOException { + String nameL = src.getName().toLowerCase(); + String base = src.getName().replaceFirst("\\.[^.]+$", ""); + + // DDS bleibt unverändert (GPU-komprimiertes Format, JME3 lädt es nativ) + if (nameL.endsWith(".dds") || nameL.endsWith(".png")) { + Path dest = destDir.resolve(src.getName()); + Files.copy(src.toPath(), dest, StandardCopyOption.REPLACE_EXISTING); + return dest; + } + + Path destPng = destDir.resolve(base + ".png"); + BufferedImage img = null; + + if (nameL.endsWith(".tga")) { + img = loadTgaAsBufferedImage(src); + } else { + // JPG, JPEG, BMP, GIF – von Java ImageIO unterstützt + img = ImageIO.read(src); + } + + if (img == null) { + // Fallback: Datei unverändert kopieren + Path dest = destDir.resolve(src.getName()); + Files.copy(src.toPath(), dest, StandardCopyOption.REPLACE_EXISTING); + return dest; + } + + // Sicherstellen, dass das Bild ARGB hat (für verlustfreies PNG) + if (img.getType() != BufferedImage.TYPE_INT_ARGB) { + BufferedImage rgba = new BufferedImage(img.getWidth(), img.getHeight(), + BufferedImage.TYPE_INT_ARGB); + java.awt.Graphics2D g = rgba.createGraphics(); + g.drawImage(img, 0, 0, null); + g.dispose(); + img = rgba; + } + + ImageIO.write(img, "PNG", destPng.toFile()); + return destPng; + } + + /** Lädt eine TGA-Datei mit JME3's TGALoader und gibt ein BufferedImage zurück. */ + private static BufferedImage loadTgaAsBufferedImage(File tgaFile) throws IOException { + com.jme3.texture.plugins.TGALoader loader = new com.jme3.texture.plugins.TGALoader(); + com.jme3.asset.TextureKey key = new com.jme3.asset.TextureKey(tgaFile.getName(), true); + com.jme3.asset.AssetInfo info = new com.jme3.asset.AssetInfo(null, key) { + @Override public java.io.InputStream openStream() { + try { return new java.io.FileInputStream(tgaFile); } + catch (java.io.FileNotFoundException e) { throw new RuntimeException(e); } + } + }; + + com.jme3.texture.Image jmeImg = (com.jme3.texture.Image) loader.load(info); + int w = jmeImg.getWidth(), h = jmeImg.getHeight(); + ByteBuffer data = jmeImg.getData(0); + data.rewind(); + + BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + int[] pixels = new int[w * h]; + com.jme3.texture.Image.Format fmt = jmeImg.getFormat(); + for (int i = 0; i < w * h; i++) { + int r, g, b, a; + switch (fmt) { + case RGBA8 -> { r = data.get()&0xFF; g = data.get()&0xFF; b = data.get()&0xFF; a = data.get()&0xFF; } + case RGB8 -> { r = data.get()&0xFF; g = data.get()&0xFF; b = data.get()&0xFF; a = 255; } + case BGR8 -> { b = data.get()&0xFF; g = data.get()&0xFF; r = data.get()&0xFF; a = 255; } + case BGRA8 -> { b = data.get()&0xFF; g = data.get()&0xFF; r = data.get()&0xFF; a = data.get()&0xFF; } + default -> { r = g = b = a = 128; } + } + pixels[i] = (a<<24)|(r<<16)|(g<<8)|b; + } + bi.setRGB(0, 0, w, h, pixels, 0, w); + return bi; + } + + // ── Textur-Set-Import ───────────────────────────────────────────────────── + + public void openTextureSetImport(javafx.stage.Window owner) { + de.blight.editor.ui.TextureImportDialog dlg = new de.blight.editor.ui.TextureImportDialog(assetRoot); + if (owner != null) dlg.initOwner(owner); + dlg.showAndWait(); + Path imported = dlg.getLastImportedDir(); + if (imported == null) return; + refreshCategoryNode(texturesNode, true); + // importierten Knoten im Baum vorselektieren + for (Map.Entry, Path> e : itemPaths.entrySet()) { + if (imported.equals(e.getValue())) { + TreeItem target = e.getKey(); + if (assetTree != null) { + assetTree.getSelectionModel().select(target); + int idx = assetTree.getRow(target); + if (idx >= 0) assetTree.scrollTo(idx); + } + break; + } + } + } + + // ── Texturen-ZIP-Import ─────────────────────────────────────────────────── + + /** Fragt nach einer oder mehreren ZIP-Dateien, entpackt sie und verknüpft Maps. */ + public void handleZipTextureImport(javafx.stage.Window owner) { + FileChooser fc = new FileChooser(); + fc.setTitle("Texturen-ZIPs importieren"); + fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("ZIP-Archiv", "*.zip")); + List zipFiles = fc.showOpenMultipleDialog(owner); + if (zipFiles == null || zipFiles.isEmpty()) return; + + int totalExtracted = 0; + int totalLinked = 0; + List errors = new java.util.ArrayList<>(); + + for (File zipFile : zipFiles) { + try { + int[] result = importSingleZip(zipFile); + totalExtracted += result[0]; + totalLinked += result[1]; + } catch (IOException ex) { + errors.add(zipFile.getName() + ": " + ex.getMessage()); + } + } + + refreshCategoryNode(texturesNode, true); + if (texturesNode != null) texturesNode.setExpanded(true); + + if (!errors.isEmpty()) { + ctx.setStatus().accept("Fehler: " + String.join(", ", errors)); + } else { + String s = totalExtracted + " Dateien aus " + zipFiles.size() + " ZIP(s) importiert"; + if (totalLinked > 0) s += " – " + totalLinked + " Map(s) verknüpft"; + ctx.setStatus().accept(s); + } + } + + /** + * Importiert eine einzelne ZIP-Datei nach Textures/{zipBaseName}/. + * @return int[2]: [0] = extrahierte Dateien, [1] = verknüpfte Maps (0 oder 1) + */ + private int[] importSingleZip(File zipFile) throws IOException { + String zipBase = zipFile.getName().replaceFirst("\\.zip$", ""); + Path destDir = assetRoot.resolve("Textures").resolve(zipBase); + Files.createDirectories(destDir); + + Map typeToPath = new HashMap<>(); + int extracted = 0; + + try (ZipInputStream zis = new ZipInputStream( + new java.io.BufferedInputStream(new java.io.FileInputStream(zipFile)))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) { zis.closeEntry(); continue; } + String entryName = java.nio.file.Paths.get(entry.getName()).getFileName().toString(); + if (!isSupportedAsset(entryName)) { zis.closeEntry(); continue; } + Path tmp = destDir.resolve(entryName); + Files.copy(zis, tmp, StandardCopyOption.REPLACE_EXISTING); + Path png = importTextureAsPng(tmp.toFile(), destDir); + if (!png.equals(tmp)) Files.deleteIfExists(tmp); + String type = detectTexMapType(entryName); + if (type != null) typeToPath.merge(type, png, (a, b) -> a); // GL bevorzugt + extracted++; + zis.closeEntry(); + } + } + + // NormalGL bevorzugen, NormalDX nur als Fallback + if (!typeToPath.containsKey("normal")) { + if (typeToPath.containsKey("normalGL")) typeToPath.put("normal", typeToPath.get("normalGL")); + else if (typeToPath.containsKey("normalDX")) typeToPath.put("normal", typeToPath.get("normalDX")); + } + + Path colorPath = typeToPath.get("color"); + int linked = 0; + if (colorPath != null) { + TextureMapData meta = new TextureMapData(); + meta.colorMap = assetRoot.relativize(colorPath).toString().replace('\\', '/'); + Path normPath = typeToPath.get("normal"); + Path dispPath = typeToPath.get("displacement"); + if (normPath != null) meta.normalMap = assetRoot.relativize(normPath).toString().replace('\\', '/'); + if (dispPath != null) meta.displacementMap = assetRoot.relativize(dispPath).toString().replace('\\', '/'); + if (typeToPath.containsKey("roughness")) meta.roughnessMap = assetRoot.relativize(typeToPath.get("roughness")).toString().replace('\\', '/'); + if (typeToPath.containsKey("ao")) meta.aoMap = assetRoot.relativize(typeToPath.get("ao")).toString().replace('\\', '/'); + deps.saveTexMeta().accept(colorPath, meta); + autoAssignMapsForTexture(meta.colorMap, meta); + linked = 1; + } + + return new int[]{ extracted, linked }; + } + + /** Gibt true zurück wenn die Datei ein unterstütztes Asset ist (Bild, Audio, Modell …). */ + private static boolean isSupportedAsset(String filename) { + String lo = filename.toLowerCase(); + return lo.endsWith(".png") || lo.endsWith(".jpg") || lo.endsWith(".jpeg") + || lo.endsWith(".tga") || lo.endsWith(".bmp") || lo.endsWith(".dds") + || lo.endsWith(".exr") || lo.endsWith(".hdr") + || lo.endsWith(".ogg") || lo.endsWith(".wav") + || lo.endsWith(".gltf") || lo.endsWith(".glb") || lo.endsWith(".fbx") + || lo.endsWith(".obj") || lo.endsWith(".j3o"); + } + + /** Erkennt den Map-Typ anhand des Dateinamens (Suffix vor der Extension). */ + private static String detectTexMapType(String filename) { + String base = filename.toLowerCase().replaceFirst("\\.[^.]+$", ""); + if (base.endsWith("_color") || base.endsWith("_colour") || base.endsWith("_diffuse") || base.endsWith("_albedo")) + return "color"; + if (base.endsWith("_normalgl") || base.endsWith("_normal_gl") || base.endsWith("_normalgl_directx")) + return "normalGL"; + if (base.endsWith("_normaldx") || base.endsWith("_normal_dx") || base.endsWith("_normaldirectx")) + return "normalDX"; + if (base.endsWith("_normal")) + return "normalGL"; // generic normal = GL convention assumed + if (base.endsWith("_displacement") || base.endsWith("_height") || base.endsWith("_disp")) + return "displacement"; + if (base.endsWith("_roughness") || base.endsWith("_rough")) + return "roughness"; + if (base.endsWith("_ambientocclusion") || base.endsWith("_ao") || base.endsWith("_occlusion")) + return "ao"; + return null; + } + + // ── Texture-Map-Kontextmenü-Aktion ──────────────────────────────────────── + + /** + * Standalone-Textur: erstellt einen Set-Ordner, verschiebt die Textur hinein, + * lässt den User eine Normal- oder Displacement-Map wählen und schreibt textureset.json. + */ + private void assignMapToTexture(Path colorTexPath, boolean isDisplacement, javafx.stage.Window owner) { + TextureChooser chooser = new TextureChooser(assetRoot, true); + if (stage() != null) chooser.initOwner(stage()); + chooser.showAndWait().ifPresent(picked -> { + if (picked == null || picked.isEmpty()) return; + + // Set-Ordner anlegen (gleicher Name wie Textur ohne Extension) + String baseName = colorTexPath.getFileName().toString().replaceFirst("\\.[^.]+$", ""); + Path setDir = colorTexPath.getParent().resolve(baseName); + Path newColor; + try { + Files.createDirectories(setDir); + newColor = setDir.resolve(colorTexPath.getFileName()); + if (!Files.exists(newColor)) Files.move(colorTexPath, newColor); + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Erstellen des Set-Ordners: " + ex.getMessage()); + return; + } + + String oldColorRel = assetRoot.relativize(colorTexPath).toString().replace('\\', '/'); + String newColorRel = assetRoot.relativize(newColor).toString().replace('\\', '/'); + + TextureMapData meta = new TextureMapData(); + meta.colorMap = newColorRel; + if (isDisplacement) meta.displacementMap = picked; + else meta.normalMap = picked; + deps.saveTexMeta().accept(newColor, meta); + + // Slots aktualisieren: alten Pfad → neuen Pfad + updateSlotsColorPath(oldColorRel, newColorRel); + autoAssignMapsForTexture(newColorRel, meta); + + // Baum aktualisieren: Datei-Item → Set-Item + TreeItem fileItem = itemPaths.entrySet().stream() + .filter(en -> colorTexPath.equals(en.getValue())) + .map(Map.Entry::getKey).findFirst().orElse(null); + if (fileItem != null) { + TreeItem parentItem = fileItem.getParent(); + if (parentItem != null) parentItem.getChildren().remove(fileItem); + itemPaths.remove(fileItem); + } + TreeItem parentByPath = itemPaths.entrySet().stream() + .filter(en -> setDir.getParent().equals(en.getValue())) + .map(Map.Entry::getKey).findFirst().orElse(null); + if (parentByPath != null) { + TreeItem setItem = new TreeItem<>(baseName); + itemPaths.put(setItem, setDir); + textureSetNodes.add(setItem); + parentByPath.getChildren().add(setItem); + } + + String type = isDisplacement ? "Displacement" : "Normal"; + ctx.setStatus().accept(type + "-Map für " + baseName + " gesetzt – Textur-Set erstellt"); + }); + } + + /** + * Textur-Set-Knoten: lädt textureset.json, lässt den User eine Map wählen, speichert. + */ + private void assignMapToSetNode(TreeItem setItem, Path setDir, boolean isDisplacement, + javafx.stage.Window owner) { + TextureChooser chooser = new TextureChooser(assetRoot, true); + if (stage() != null) chooser.initOwner(stage()); + chooser.showAndWait().ifPresent(picked -> { + if (picked == null || picked.isEmpty()) return; + TextureMapData meta = deps.loadTexMeta().apply(setDir); + if (isDisplacement) meta.displacementMap = picked; + else meta.normalMap = picked; + deps.saveTexMeta().accept(setDir, meta); + if (!meta.colorMap.isEmpty()) autoAssignMapsForTexture(meta.colorMap, meta); + String type = isDisplacement ? "Displacement" : "Normal"; + ctx.setStatus().accept(type + "-Map für " + setDir.getFileName() + " gesetzt"); + }); + } + + /** Ersetzt in allen Slots einen alten Texturpfad durch den neuen (nach Datei-Verschiebung). */ + private void updateSlotsColorPath(String oldRel, String newRel) { + for (int i = 0; i < 4; i++) { + if (oldRel.equals(input.terrainTexturePaths[i])) input.terrainTexturePaths[i] = newRel; + if (oldRel.equals(input.upperTexturePaths[i])) input.upperTexturePaths[i] = newRel; + if (oldRel.equals(input.thirdTexturePaths[i])) input.thirdTexturePaths[i] = newRel; + } + input.terrainTexturesChanged = true; + input.upperTexturesChanged = true; + input.voxelTexturesChanged = true; + } + + /** + * Wenn colorTexRelPath in irgendeinem Terrain/Upper/Third-Slot steckt, + * wird die Normal-Map aus meta dort automatisch eingetragen. + */ + private void autoAssignMapsForTexture(String colorTexRelPath, TextureMapData meta) { + boolean anyChanged = false; + for (int i = 0; i < 4; i++) { + if (colorTexRelPath.equals(input.terrainTexturePaths[i])) { + if (!meta.normalMap.isEmpty()) { input.terrainNormalMapPaths[i] = meta.normalMap; anyChanged = true; } + if (!meta.displacementMap.isEmpty()) { input.terrainDisplacementMapPaths[i] = meta.displacementMap; anyChanged = true; } + } + if (colorTexRelPath.equals(input.upperTexturePaths[i])) { + if (!meta.normalMap.isEmpty()) { input.upperNormalMapPaths[i] = meta.normalMap; anyChanged = true; } + if (!meta.displacementMap.isEmpty()) { input.upperDisplacementMapPaths[i] = meta.displacementMap; anyChanged = true; } + } + if (colorTexRelPath.equals(input.thirdTexturePaths[i])) { + if (!meta.normalMap.isEmpty()) { input.thirdNormalMapPaths[i] = meta.normalMap; anyChanged = true; } + if (!meta.displacementMap.isEmpty()) { input.thirdDisplacementMapPaths[i] = meta.displacementMap; anyChanged = true; } + } + } + if (anyChanged) { + input.terrainNormalMapsChanged = true; + input.upperNormalMapsChanged = true; + input.thirdNormalMapsChanged = true; + input.voxelTexturesChanged = true; + } + } + +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java b/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java new file mode 100644 index 0000000..925e575 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java @@ -0,0 +1,1736 @@ +package de.blight.editor.toolset.content; + +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.layout.*; +import javafx.geometry.Orientation; +import javafx.stage.Stage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Verwaltet den Content-Editor (Dual-Window- und Einzelfenster-Modus) sowie den Character-Editor. + * Enthält die gesamte Content-Tree-Logik, Werkzeug-Toolbar und alle Sektion-Views + * (Quest, Item, Rezept, Crafting, Area, Location, Lokalisierung, Charakter). + */ +public class ContentController { + + private final ToolContext ctx; + + // ── Dual-Window-Zustand ──────────────────────────────────────────────────── + private Stage contentEditorStage; + private BorderPane contentEditorRoot; + private boolean dualWindowMode = false; + private String contentLastSection = "character"; + private CheckMenuItem dualWindowMenuItem; + + // ── Content-Toolbar-Buttons ──────────────────────────────────────────────── + private ToggleButton contentCharBtn, contentQuestBtn, contentFraktionBtn, + contentItemBtn, contentRezeptBtn, contentCraftingBtn, + contentAreaBtn, contentLocationBtn, contentLangBtn; + private Button contentSwitchBtn; + + // ── Content-Tree ─────────────────────────────────────────────────────────── + private TreeView contentTreeView; + private TreeItem contentCharNode, contentQuestNode, contentItemNode, + contentFractionNode, contentAreaNode, contentLocationNode, + contentCraftingNode, contentRecipeNode; + private Button contentNewBtn, contentDeleteBtn; + private boolean contentTreeUpdating = false; + + // ── Aktive Content-Views ─────────────────────────────────────────────────── + private de.blight.editor.ui.QuestEditorView activeQuestView; + private de.blight.editor.ui.ItemEditorView activeItemView; + private de.blight.editor.ui.FractionEditorView activeFractionView; + private de.blight.editor.ui.AreaEditorView activeAreaView; + private de.blight.editor.ui.LocationEditorView activeLocationView; + private de.blight.editor.ui.CraftingTableEditorView activeCraftingView; + private de.blight.editor.ui.RecipeEditorView activeRecipeView; + + // ── Character-Editor-Views ───────────────────────────────────────────────── + private de.blight.editor.ui.DialogEditorView dialogEditorView; + private de.blight.editor.ui.RoutineEditorView routineEditorView; + private de.blight.editor.ui.MonologueEditorView monologueEditorView; + private StackPane routineJmeSlot; + + // ── Character-Formular-Felder ────────────────────────────────────────────── + private VBox charEditContainer; + private VBox charFractionSection; + private VBox charNpcSection; + private VBox charActionLabelsBox; + private ComboBox charStatusCombo; + private CheckBox charTraderCheck; + private ListView charTraderItemsView; + private TextField charMsgFriendly, charMsgNeutral, charMsgEnraged, charMsgEnemy; + private TextField charNameField, charIdField; + private ComboBox charTypeCombo, charModelCombo, charAnimSetCombo; + private ComboBox charFractionCombo; + private Label charEditorStatusLabel; + + // ── Fähigkeiten-Spinner ──────────────────────────────────────────────────── + private Spinner abMagicSpin, abStaffSpin, abSwordSpin, abArcherySpin, + abHeavySpin, abCrossbowSpin, abThieverySpin, abAlchemySpin, + abEngineeringSpin, abSmitherySpin, abEnchantingSpin; + + // ── Fraktions-Cache ──────────────────────────────────────────────────────── + private final List loadedFractions = new ArrayList<>(); + + // ────────────────────────────────────────────────────────────────────────── + + public ContentController(ToolContext ctx) { + this.ctx = ctx; + } + + // ── Öffentliche API ──────────────────────────────────────────────────────── + + public void setDualWindowMenuItem(CheckMenuItem item) { + this.dualWindowMenuItem = item; + } + + /** Erstellt den "> Content"-Button für die Welt-Toolbar (nur 1-Fenster). */ + public Button buildContentSwitchBtn() { + contentSwitchBtn = new Button("> Content"); + contentSwitchBtn.setStyle("-fx-font-weight:bold;"); + contentSwitchBtn.setOnAction(e -> switchToContentEditor()); + return contentSwitchBtn; + } + + /** Per-Frame-Polling: animEmbedStatus + animStripStatus → charEditorStatusLabel. */ + public void onFrame() { + String embedStatus = ctx.input().animEmbedStatus; + if (embedStatus != null) { + ctx.input().animEmbedStatus = null; + if (charEditorStatusLabel != null) charEditorStatusLabel.setText(embedStatus); + } + String stripStatus = ctx.input().animStripStatus; + if (stripStatus != null) { + ctx.input().animStripStatus = null; + if (charEditorStatusLabel != null) charEditorStatusLabel.setText(stripStatus); + } + } + + /** Wird aus EditorApp gerufen, wenn ein Character-Modell-Import abgeschlossen ist. */ + public void onCharImportResult(SharedInput.CharModelImportResult result) { + if (result.error() != null) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.initOwner(ctx.stageSupplier().get()); + alert.setTitle("Character-Import fehlgeschlagen"); + alert.setHeaderText("FBX konnte nicht als Character-Modell importiert werden"); + alert.setContentText(result.error()); + alert.showAndWait(); + if (charEditorStatusLabel != null) + charEditorStatusLabel.setText("Import fehlgeschlagen"); + } else { + String relPath = result.relPath(); + if (charModelCombo != null && !charModelCombo.getItems().contains(relPath)) { + charModelCombo.getItems().add(relPath); + } + if (charModelCombo != null) charModelCombo.setValue(relPath); + if (charEditorStatusLabel != null) + charEditorStatusLabel.setText("Modell importiert: " + relPath); + } + } + + public boolean isDualWindowMode() { + return dualWindowMode; + } + + public String getContentLastSection() { + return contentLastSection; + } + + public Stage getContentEditorStage() { + return contentEditorStage; + } + + public boolean hasRoutineJmeSlot() { + return routineJmeSlot != null; + } + + public void detachRoutineJmeSlot() { + if (routineJmeSlot == null) return; + routineJmeSlot.getChildren().remove(ctx.viewportSupplier().get()); + routineJmeSlot = null; + } + + /** Im 1-Fenster-Modus: ist der Content-Editor aktuell das aktive Tool? */ + public boolean isSingleWindowContentActive(String currentTool) { + return !dualWindowMode && "content".equals(currentTool); + } + + /** Ctrl+F1–F8 Shortcut: ggf. Content-Modus aktivieren, dann Section laden. */ + public void activateContentSection(int idx, String currentTool) { + if (dualWindowMode) { + if (contentEditorStage != null && !contentEditorStage.isShowing()) { + contentEditorStage.show(); + } + fireContentBtn(idx); + } else { + if (!"content".equals(currentTool)) { + switchToContentEditor(); + } + fireContentBtn(idx); + } + } + + // ── Modus-Wechsel ────────────────────────────────────────────────────────── + + /** 2-Fenster-Modus aktivieren. */ + public void enableDualWindowMode() { + dualWindowMode = true; + if (contentSwitchBtn != null) { + contentSwitchBtn.setVisible(false); + contentSwitchBtn.setManaged(false); + } + showContentEditor(); + restoreContentSection(contentLastSection); + } + + /** 2-Fenster-Modus deaktivieren. */ + public void disableDualWindowMode() { + dualWindowMode = false; + if (dualWindowMenuItem != null) dualWindowMenuItem.setSelected(false); + if (contentEditorStage != null) contentEditorStage.hide(); + if (contentSwitchBtn != null) { + contentSwitchBtn.setVisible(true); + contentSwitchBtn.setManaged(true); + } + } + + /** 1-Fenster-Content-Modus aktivieren. */ + public void switchToContentEditor() { + ctx.setCurrentTool().accept("content"); + if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(true); + ctx.setMainToolbar().accept(buildSingleWindowContentToolBar()); + ctx.setLeftPanel().accept(buildContentTreePanel()); + ctx.setBottomPanel().accept(null); + restoreContentSection(contentLastSection); + } + + public void switchToCharacterEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "character"; + if (contentCharBtn != null) contentCharBtn.setSelected(true); + buildCharacterViewInContent(); + return; + } + ctx.setCurrentTool().accept("charEditor"); + ctx.setMainToolbar().accept(buildCharacterEditorToolBar()); + + dialogEditorView = new de.blight.editor.ui.DialogEditorView(); + dialogEditorView.setDisable(true); + routineEditorView = new de.blight.editor.ui.RoutineEditorView(ctx.input(), ctx.assetRoot().resolve("Characters")); + routineEditorView.setDisable(true); + monologueEditorView = new de.blight.editor.ui.MonologueEditorView(); + + routineJmeSlot = new StackPane(); + routineJmeSlot.setMinWidth(0); + routineJmeSlot.getChildren().add(ctx.viewportSupplier().get()); + ctx.centerStackSupplier().get().getChildren().add(new StackPane()); + + SplitPane routineSplit = new SplitPane(routineEditorView, routineJmeSlot); + routineSplit.setDividerPositions(0.2); + routineSplit.widthProperty().addListener((obs, ov, nv) -> { + if (nv.doubleValue() > 0) routineSplit.setDividerPositions(0.2); + }); + + TabPane centerTabs = new TabPane(); + centerTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + centerTabs.getTabs().addAll( + new Tab("Dialog", dialogEditorView), + new Tab("Tagesabläufe", routineSplit), + new Tab("Monologe", monologueEditorView) + ); + ctx.setCenterView().accept(centerTabs); + ctx.setRightPanel().accept(null); + } + + public void loadCharacterById(String id) { + if (id == null || id.isBlank()) return; + Path charDir = ctx.assetRoot().resolve("Characters"); + try { + de.blight.common.model.GameCharacter c = + de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character")); + if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : ""); + if (charNameField != null) charNameField.setText(c.getName() != null ? c.getName().id() : ""); + if (charTypeCombo != null) + charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC"); + if (charModelCombo != null) charModelCombo.setValue(c.getModelPath()); + if (charAnimSetCombo != null) { + charAnimSetCombo.setValue(c.getAnimSetPath()); + updateCharActionCombosFromSet(); + } + if (charFractionCombo != null) { + de.blight.common.model.Fraction npcFraction = + (c instanceof de.blight.common.model.NPC npc) ? npc.getFraction() : null; + if (npcFraction != null && npcFraction.getFractionId() != null) { + final java.util.UUID fid = npcFraction.getFractionId(); + charFractionCombo.getItems().stream() + .filter(f -> f != null && fid.equals(f.getFractionId())) + .findFirst() + .ifPresentOrElse(charFractionCombo::setValue, () -> charFractionCombo.setValue(null)); + } else { + charFractionCombo.setValue(null); + } + } + if (c instanceof de.blight.common.model.NPC npc) { + if (charStatusCombo != null && npc.getStatus() != null) + charStatusCombo.setValue(npc.getStatus().name()); + if (charTraderCheck != null) charTraderCheck.setSelected(npc.isTrader()); + if (charTraderItemsView != null) { + charTraderItemsView.getItems().clear(); + if (npc.getItems() != null) + npc.getItems().forEach(it -> charTraderItemsView.getItems().add(it.getItemId())); + } + java.util.Map 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 (dialogEditorView != null) { + dialogEditorView.setDisable(false); + dialogEditorView.loadNpc(npc); + } + } else { + if (dialogEditorView != null) { + dialogEditorView.setDisable(false); + dialogEditorView.clear(); + } + } + if (c instanceof de.blight.common.model.MainCharacter mc) { + loadAbilities(mc.getAbilities()); + } + if (charEditContainer != null) charEditContainer.setDisable(false); + if (routineEditorView != null) { + routineEditorView.setDisable(false); + if (c instanceof de.blight.common.model.NPC npc) routineEditorView.loadNpc(npc); + else routineEditorView.loadNpc(new de.blight.common.model.NPC()); + } + if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id); + } catch (Exception e) { + if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage()); + } + } + + public void switchToItemEditor(String selectItemId) { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "item"; + if (contentItemBtn != null) contentItemBtn.setSelected(true); + de.blight.editor.ui.ItemEditorView view = + new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); + setContentCenter(view); + setContentRight(null); + if (selectItemId != null) view.selectItem(selectItemId); + return; + } + ctx.setCurrentTool().accept("itemEditor"); + ToolBar tb = makeSingleWindowToolBar("Item-Manager"); + ctx.setMainToolbar().accept(tb); + de.blight.editor.ui.ItemEditorView view = + new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); + ctx.setCenterView().accept(view); + ctx.setRightPanel().accept(null); + if (selectItemId != null) view.selectItem(selectItemId); + } + + /** Prefs-Restore: stellt Dual-Window-Position und Content-Section wieder her. */ + public void restoreFromPrefs(boolean dualWindow, double contentX, double contentY, + double contentW, double contentH, String section) { + if (dualWindow) { + enableDualWindowMode(); + if (contentX >= 0 && contentEditorStage != null) { + boolean onScreen = !javafx.stage.Screen + .getScreensForRectangle(contentX, contentY, contentW, contentH).isEmpty(); + if (onScreen) { + contentEditorStage.setX(contentX); + contentEditorStage.setY(contentY); + contentEditorStage.setWidth(contentW); + contentEditorStage.setHeight(contentH); + } + } + contentLastSection = section; + restoreContentSection(contentLastSection); + } + } + + // ── Interne Einzel-Fenster-Switcher ──────────────────────────────────────── + + private void switchToRecipeEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "recipe"; + if (contentRezeptBtn != null) contentRezeptBtn.setSelected(true); + buildRecipeViewInContent(); + return; + } + ctx.setCurrentTool().accept("recipeEditor"); + ctx.setMainToolbar().accept(makeSingleWindowToolBar("Rezept-Manager")); + ctx.setCenterView().accept(new de.blight.editor.ui.RecipeEditorView(ctx.assetRoot().resolve("recipes"))); + ctx.setRightPanel().accept(null); + } + + private void switchToCraftingTableEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "crafting"; + if (contentCraftingBtn != null) contentCraftingBtn.setSelected(true); + buildCraftingViewInContent(); + return; + } + ctx.setCurrentTool().accept("craftingTableEditor"); + ctx.setMainToolbar().accept(makeSingleWindowToolBar("Crafting-Table-Manager")); + ctx.setCenterView().accept(new de.blight.editor.ui.CraftingTableEditorView(ctx.assetRoot().resolve("craftingtables"))); + ctx.setRightPanel().accept(null); + } + + private void switchToFractionEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "fraction"; + if (contentFraktionBtn != null) contentFraktionBtn.setSelected(true); + buildFractionViewInContent(); + return; + } + ctx.setCurrentTool().accept("fractionEditor"); + ctx.setMainToolbar().accept(makeSingleWindowToolBar("Fraktionen-Manager")); + ctx.setCenterView().accept(new de.blight.editor.ui.FractionEditorView(ctx.assetRoot().resolve("fractions"))); + ctx.setRightPanel().accept(null); + } + + private void switchToQuestEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "quest"; + if (contentQuestBtn != null) contentQuestBtn.setSelected(true); + buildQuestViewInContent(); + return; + } + ctx.setCurrentTool().accept("questEditor"); + ctx.setMainToolbar().accept(makeSingleWindowToolBar("Quest-Manager")); + ctx.setCenterView().accept(new de.blight.editor.ui.QuestEditorView(ctx.assetRoot().resolve("quests"))); + ctx.setRightPanel().accept(null); + } + + private void switchToLocationEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "location"; + if (contentLocationBtn != null) contentLocationBtn.setSelected(true); + buildLocationViewInContent(); + return; + } + ctx.setCurrentTool().accept("locationEditor"); + ctx.setMainToolbar().accept(makeSingleWindowToolBar("Locations-Manager")); + ctx.setCenterView().accept(new de.blight.editor.ui.LocationEditorView()); + ctx.setRightPanel().accept(null); + } + + private void switchToAreaEditor() { + if (dualWindowMode) { + showContentEditor(); + contentLastSection = "area"; + if (contentAreaBtn != null) contentAreaBtn.setSelected(true); + buildAreaViewInContent(); + return; + } + ctx.setCurrentTool().accept("areaEditor"); + ctx.setMainToolbar().accept(makeSingleWindowToolBar("Areas-Manager")); + ctx.setCenterView().accept(new de.blight.editor.ui.AreaEditorView()); + ctx.setRightPanel().accept(null); + } + + private void switchToLocalizationEditor() { + ctx.setCurrentTool().accept("localizationEditor"); + ctx.setMainToolbar().accept(makeSingleWindowToolBar("Lokalisierungs-Editor")); + ctx.setCenterView().accept(new de.blight.editor.ui.LocalizationEditorView( + de.blight.editor.ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang"))); + ctx.setRightPanel().accept(null); + } + + // ── Toolbar-Builder ──────────────────────────────────────────────────────── + + private ToolBar makeSingleWindowToolBar(String title) { + Button backBtn = new Button("← Welteneditor"); + backBtn.setOnAction(e -> ctx.switchToWorld().run()); + Label label = new Label(title); + label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + ToolBar tb = new ToolBar(); + tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); + return tb; + } + + private ToolBar buildCharacterEditorToolBar() { + Button backBtn = new Button("← Welteneditor"); + backBtn.setOnAction(e -> ctx.switchToWorld().run()); + Label label = new Label("Animationset Editor"); + label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + ToolBar tb = new ToolBar(); + tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); + return tb; + } + + private ToolBar buildSingleWindowContentToolBar() { + return buildContentEditorToolBar(true); + } + + private VBox buildContentTopBar() { + return new VBox(buildContentEditorToolBar(false)); + } + + private ToolBar buildContentEditorToolBar(boolean includeWeltBtn) { + contentCharBtn = new ToggleButton("@ Character"); + contentQuestBtn = new ToggleButton("! Quest"); + contentFraktionBtn = new ToggleButton("* Fraktion"); + contentItemBtn = new ToggleButton("+ Item"); + contentRezeptBtn = new ToggleButton("= Rezept"); + contentCraftingBtn = new ToggleButton("# Crafting"); + contentAreaBtn = new ToggleButton("□ Area"); + contentLocationBtn = new ToggleButton("^ Location"); + contentLangBtn = new ToggleButton("≈ Lang"); + + String bold = "-fx-font-weight:bold;"; + for (ToggleButton b : new ToggleButton[]{contentCharBtn, contentQuestBtn, contentFraktionBtn, + contentItemBtn, contentRezeptBtn, contentCraftingBtn, contentAreaBtn, contentLocationBtn, + contentLangBtn}) { + b.setStyle(bold); + } + + ToggleGroup cg = new ToggleGroup(); + contentCharBtn.setToggleGroup(cg); + contentQuestBtn.setToggleGroup(cg); + contentFraktionBtn.setToggleGroup(cg); + contentItemBtn.setToggleGroup(cg); + contentRezeptBtn.setToggleGroup(cg); + contentCraftingBtn.setToggleGroup(cg); + contentAreaBtn.setToggleGroup(cg); + contentLocationBtn.setToggleGroup(cg); + contentLangBtn.setToggleGroup(cg); + + contentCharBtn.setOnAction(e -> { contentLastSection = "character"; buildCharacterViewInContent(); }); + contentQuestBtn.setOnAction(e -> { contentLastSection = "quest"; buildQuestViewInContent(); }); + contentFraktionBtn.setOnAction(e -> { contentLastSection = "fraction"; buildFractionViewInContent(); }); + contentItemBtn.setOnAction(e -> { contentLastSection = "item"; buildItemViewInContent(); }); + contentRezeptBtn.setOnAction(e -> { contentLastSection = "recipe"; buildRecipeViewInContent(); }); + contentCraftingBtn.setOnAction(e -> { contentLastSection = "crafting"; buildCraftingViewInContent(); }); + contentAreaBtn.setOnAction(e -> { contentLastSection = "area"; buildAreaViewInContent(); }); + contentLocationBtn.setOnAction(e -> { contentLastSection = "location"; buildLocationViewInContent(); }); + contentLangBtn.setOnAction(e -> { contentLastSection = "lang"; buildLocalizationViewInContent(); }); + + ToolBar tb = new ToolBar(); + tb.getItems().addAll( + contentCharBtn, + new Separator(Orientation.VERTICAL), contentQuestBtn, + new Separator(Orientation.VERTICAL), contentFraktionBtn, + new Separator(Orientation.VERTICAL), contentItemBtn, + new Separator(Orientation.VERTICAL), contentRezeptBtn, contentCraftingBtn, + new Separator(Orientation.VERTICAL), contentAreaBtn, contentLocationBtn, + new Separator(Orientation.VERTICAL), contentLangBtn); + if (includeWeltBtn) { + Button weltBtn = new Button("< Welt"); + weltBtn.setStyle("-fx-font-weight:bold;"); + weltBtn.setOnAction(e -> ctx.switchToWorld().run()); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + tb.getItems().addAll(spacer, weltBtn); + } + return tb; + } + + // ── Content-Editor-Stage ─────────────────────────────────────────────────── + + private void showContentEditor() { + if (contentEditorStage == null) { + contentEditorStage = new Stage(); + contentEditorStage.setTitle("Blight Content Editor"); + contentEditorStage.setOnCloseRequest(e -> e.consume()); + try (var is = getClass().getResourceAsStream("/icon_editor.png")) { + if (is != null) contentEditorStage.getIcons().add(new Image(is)); + } catch (Exception ignored) {} + + contentEditorRoot = new BorderPane(); + contentEditorRoot.setTop(buildContentTopBar()); + contentEditorRoot.setLeft(buildContentTreePanel()); + contentEditorRoot.setCenter(new Label("Wähle einen Bereich in der Toolbar.")); + + javafx.scene.Scene scene = new javafx.scene.Scene(contentEditorRoot, 1200, 750); + java.net.URL cssUrl = getClass().getResource("/editor.css"); + if (cssUrl != null) scene.getStylesheets().add(cssUrl.toExternalForm()); + scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> { + if (!e.isControlDown()) return; + int idx = switch (e.getCode()) { + case F1 -> 0; case F2 -> 1; case F3 -> 2; case F4 -> 3; + case F5 -> 4; case F6 -> 5; case F7 -> 6; case F8 -> 7; + default -> -1; + }; + if (idx >= 0) { e.consume(); fireContentBtn(idx); } + }); + + contentEditorStage.setScene(scene); + contentEditorStage.setMinWidth(800); + contentEditorStage.setMinHeight(500); + } + contentEditorStage.show(); + contentEditorStage.toFront(); + } + + // ── Content-Tree ─────────────────────────────────────────────────────────── + + private TreeView buildContentTree() { + TreeItem treeRoot = new TreeItem<>("Content"); + treeRoot.setExpanded(true); + + contentCharNode = new TreeItem<>("Characters"); + contentQuestNode = new TreeItem<>("Quests"); + contentFractionNode = new TreeItem<>("Fraktionen"); + contentItemNode = new TreeItem<>("Items"); + contentRecipeNode = new TreeItem<>("Rezepte"); + contentCraftingNode = new TreeItem<>("Crafting Tables"); + contentAreaNode = new TreeItem<>("Areas"); + contentLocationNode = new TreeItem<>("Locations"); + + for (var node : new TreeItem[]{ + contentCharNode, contentQuestNode, contentFractionNode, contentItemNode, + contentRecipeNode, contentCraftingNode, contentAreaNode, contentLocationNode}) { + node.setExpanded(false); + treeRoot.getChildren().add(node); + } + + refreshContentCharacterTree(); + refreshContentCraftingTree(); + refreshContentItemTree(); + refreshContentRecipeTree(); + refreshContentQuestTree(); + refreshContentFractionTree(); + refreshContentAreaTree(); + refreshContentLocationTree(); + + contentTreeView = new TreeView<>(treeRoot); + contentTreeView.setShowRoot(false); + contentTreeView.setPrefWidth(420); + contentTreeView.setMinWidth(200); + contentTreeView.setCellFactory(tv -> new TreeCell<>() { + @Override protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { setText(null); setGraphic(null); setStyle(""); return; } + TreeItem ti = getTreeItem(); + TreeItem par = ti != null ? ti.getParent() : null; + boolean isColored = item.contains(":") && + (par == contentQuestNode || par == contentItemNode || par == contentRecipeNode); + if (isColored) { + String prefix = item.substring(0, item.indexOf(':')); + String id = item.substring(item.indexOf(':') + 1); + String color; + if (par == contentQuestNode) color = questTypeColor(prefix); + else if (par == contentItemNode) color = de.blight.editor.ui.ItemEditorView.categoryColor(safeItemCategory(prefix)); + else color = contentRecipeColor(prefix); + javafx.scene.shape.Rectangle box = new javafx.scene.shape.Rectangle(10, 10); + box.setFill(javafx.scene.paint.Color.web(color)); + box.setArcWidth(3); box.setArcHeight(3); + HBox graphic = new HBox(5, box); + graphic.setAlignment(Pos.CENTER_LEFT); + setText(id); setGraphic(graphic); setStyle(""); + } else { + setText(item); setGraphic(null); + boolean isSection = par == null || par.getParent() == null; + setStyle(isSection ? "-fx-font-weight:bold;" : ""); + } + } + }); + + contentTreeView.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> { + if (contentTreeUpdating) return; + if (nv == null) { + if (contentDeleteBtn != null) contentDeleteBtn.setDisable(true); + return; + } + boolean isLeaf = nv.isLeaf(); + if (contentDeleteBtn != null) contentDeleteBtn.setDisable(!isLeaf); + + TreeItem parent = nv.getParent(); + if (parent == contentCharNode) { + if (contentCharBtn != null && !contentCharBtn.isSelected()) contentCharBtn.fire(); + loadCharacterById(nv.getValue()); return; + } + if (parent == contentQuestNode) { + if (contentQuestBtn != null && !contentQuestBtn.isSelected()) contentQuestBtn.fire(); + String qVal = nv.getValue(); + String qId = qVal.contains(":") ? qVal.substring(qVal.indexOf(':') + 1) : qVal; + if (activeQuestView != null) activeQuestView.selectById(qId); return; + } + if (parent == contentFractionNode) { + if (contentFraktionBtn != null && !contentFraktionBtn.isSelected()) contentFraktionBtn.fire(); + if (activeFractionView != null) activeFractionView.selectByNameId(nv.getValue()); return; + } + if (parent == contentItemNode) { + if (contentItemBtn != null && !contentItemBtn.isSelected()) contentItemBtn.fire(); + String iVal = nv.getValue(); + String iId = iVal.contains(":") ? iVal.substring(iVal.indexOf(':') + 1) : iVal; + if (activeItemView != null) activeItemView.selectItem(iId); return; + } + if (parent == contentRecipeNode) { + if (contentRezeptBtn != null && !contentRezeptBtn.isSelected()) contentRezeptBtn.fire(); + String rVal = nv.getValue(); + String rId = rVal.contains(":") ? rVal.substring(rVal.indexOf(':') + 1) : rVal; + if (activeRecipeView != null) activeRecipeView.selectById(rId); return; + } + if (parent == contentCraftingNode) { + if (contentCraftingBtn != null && !contentCraftingBtn.isSelected()) contentCraftingBtn.fire(); + if (activeCraftingView != null) activeCraftingView.selectByTypeName(nv.getValue()); return; + } + if (parent == contentAreaNode) { + if (contentAreaBtn != null && !contentAreaBtn.isSelected()) contentAreaBtn.fire(); + if (activeAreaView != null) activeAreaView.selectById(nv.getValue()); return; + } + if (parent == contentLocationNode) { + if (contentLocationBtn != null && !contentLocationBtn.isSelected()) contentLocationBtn.fire(); + if (activeLocationView != null) activeLocationView.selectById(nv.getValue()); return; + } + switch (nv.getValue()) { + case "Characters" -> { if (contentCharBtn != null && !contentCharBtn.isSelected()) contentCharBtn.fire(); } + case "Quests" -> { if (contentQuestBtn != null && !contentQuestBtn.isSelected()) contentQuestBtn.fire(); } + case "Fraktionen" -> { if (contentFraktionBtn != null && !contentFraktionBtn.isSelected()) contentFraktionBtn.fire(); } + case "Items" -> { if (contentItemBtn != null && !contentItemBtn.isSelected()) contentItemBtn.fire(); } + case "Rezepte" -> { if (contentRezeptBtn != null && !contentRezeptBtn.isSelected()) contentRezeptBtn.fire(); } + case "Crafting Tables" -> { if (contentCraftingBtn != null && !contentCraftingBtn.isSelected()) contentCraftingBtn.fire(); } + case "Areas" -> { if (contentAreaBtn != null && !contentAreaBtn.isSelected()) contentAreaBtn.fire(); } + case "Locations" -> { if (contentLocationBtn != null && !contentLocationBtn.isSelected()) contentLocationBtn.fire(); } + } + }); + + ContextMenu ctxMenu = new ContextMenu(); + contentTreeView.setOnContextMenuRequested(e -> { + ctxMenu.getItems().clear(); + TreeItem sel = contentTreeView.getSelectionModel().getSelectedItem(); + boolean isLeaf = sel != null && sel.isLeaf(); + + MenuItem addMi = new MenuItem("Hinzufügen"); + addMi.setOnAction(ev -> contentNew()); + ctxMenu.getItems().add(addMi); + + if (isLeaf) { + MenuItem delMi = new MenuItem("Löschen"); + delMi.setOnAction(ev -> contentDelete()); + MenuItem dupMi = new MenuItem("Duplizieren"); + dupMi.setOnAction(ev -> contentDuplicate()); + ctxMenu.getItems().addAll(delMi, dupMi); + } + + ctxMenu.getItems().add(new SeparatorMenuItem()); + MenuItem refreshMi = new MenuItem("Aktualisieren"); + refreshMi.setOnAction(ev -> contentRefresh()); + ctxMenu.getItems().add(refreshMi); + + ctxMenu.show(contentTreeView, e.getScreenX(), e.getScreenY()); + e.consume(); + }); + + contentTreeView.setOnKeyPressed(e -> { + if (e.getCode() == javafx.scene.input.KeyCode.F5) { + contentRefresh(); e.consume(); + } else if (e.getCode() == javafx.scene.input.KeyCode.DELETE) { + contentDelete(); e.consume(); + } + }); + + return contentTreeView; + } + + private VBox buildContentTreePanel() { + TreeView tree = buildContentTree(); + + contentNewBtn = new Button("+ Neu"); + contentDeleteBtn = new Button("- Löschen"); + Button refreshBtn = new Button("⟳"); + + contentNewBtn.setStyle("-fx-background-color:#2a7a2a;-fx-text-fill:white;"); + contentDeleteBtn.setStyle("-fx-background-color:#7a2a2a;-fx-text-fill:white;"); + contentDeleteBtn.setDisable(true); + refreshBtn.setTooltip(new Tooltip("Aktualisieren (F5)")); + refreshBtn.setMinWidth(Control.USE_PREF_SIZE); + + Button collapseBtn = new Button("◀"); + collapseBtn.setTooltip(new Tooltip("Alle zuklappen")); + collapseBtn.setMinWidth(Control.USE_PREF_SIZE); + + contentNewBtn.setMaxWidth(Double.MAX_VALUE); + contentDeleteBtn.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(contentNewBtn, Priority.ALWAYS); + HBox.setHgrow(contentDeleteBtn, Priority.ALWAYS); + + contentNewBtn.setOnAction(e -> contentNew()); + contentDeleteBtn.setOnAction(e -> contentDelete()); + refreshBtn.setOnAction(e -> contentRefresh()); + collapseBtn.setOnAction(e -> collapseAllContentSections()); + + HBox btnBar = new HBox(4, contentNewBtn, contentDeleteBtn, collapseBtn, refreshBtn); + btnBar.setPadding(new Insets(4)); + btnBar.setAlignment(Pos.CENTER_LEFT); + + VBox panel = new VBox(btnBar, tree); + VBox.setVgrow(tree, Priority.ALWAYS); + return panel; + } + + // ── Content-Aktionen ─────────────────────────────────────────────────────── + + private void contentNew() { + switch (contentLastSection != null ? contentLastSection : "character") { + case "character" -> { clearCharacterForm(); if (contentCharBtn != null && !contentCharBtn.isSelected()) contentCharBtn.fire(); } + case "quest" -> { if (activeQuestView != null) activeQuestView.createNew(); } + case "fraction" -> { if (activeFractionView != null) activeFractionView.createNew(); } + case "item" -> { if (activeItemView != null) activeItemView.createNew(); } + case "recipe" -> { if (activeRecipeView != null) activeRecipeView.createNew(); } + case "area" -> { if (activeAreaView != null) activeAreaView.createNew(); } + case "location" -> { if (activeLocationView != null) activeLocationView.createNew(); } + } + } + + private void contentDelete() { + if (contentTreeView == null) return; + TreeItem sel = contentTreeView.getSelectionModel().getSelectedItem(); + if (sel == null || !sel.isLeaf()) return; + + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.initOwner(ctx.stageSupplier().get()); + confirm.setTitle("Löschen bestätigen"); + confirm.setHeaderText("\"" + sel.getValue() + "\" wirklich löschen?"); + confirm.setContentText("Diese Aktion kann nicht rückgängig gemacht werden."); + confirm.showAndWait().ifPresent(bt -> { + if (bt != ButtonType.OK) return; + TreeItem parent = sel.getParent(); + if (parent == contentCharNode) deleteCharacterById(sel.getValue()); + else if (parent == contentQuestNode && activeQuestView != null) { activeQuestView.deleteCurrentItem(); refreshContentQuestTree(); } + else if (parent == contentFractionNode && activeFractionView != null) { activeFractionView.deleteCurrentItem(); refreshContentFractionTree(); } + else if (parent == contentItemNode && activeItemView != null) { activeItemView.deleteCurrentItem(); refreshContentItemTree(); } + else if (parent == contentRecipeNode && activeRecipeView != null) { activeRecipeView.deleteCurrentItem(); refreshContentRecipeTree(); } + else if (parent == contentAreaNode && activeAreaView != null) { activeAreaView.deleteCurrentItem(); refreshContentAreaTree(); } + else if (parent == contentLocationNode && activeLocationView != null) { activeLocationView.deleteCurrentItem(); refreshContentLocationTree(); } + else if (parent == contentCraftingNode && activeCraftingView != null) { activeCraftingView.deleteCurrentItem(); } + }); + } + + private void deleteCharacterById(String id) { + Path charFile = ctx.assetRoot().resolve("Characters").resolve(id + ".character"); + try { + java.nio.file.Files.deleteIfExists(charFile); + } catch (IOException ex) { + new Alert(Alert.AlertType.ERROR, "Fehler beim Löschen: " + ex.getMessage()).showAndWait(); + return; + } + refreshContentCharacterTree(); + clearCharacterForm(); + if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Character gelöscht."); + } + + private void contentDuplicate() { + if (contentTreeView == null) return; + TreeItem sel = contentTreeView.getSelectionModel().getSelectedItem(); + if (sel == null || !sel.isLeaf()) return; + TreeItem parent = sel.getParent(); + if (parent == contentCharNode) { + duplicateCharacterById(sel.getValue()); + } + } + + private void duplicateCharacterById(String sourceId) { + TextInputDialog dlg = new TextInputDialog(sourceId + "_copy"); + dlg.initOwner(ctx.stageSupplier().get()); + dlg.setTitle("Duplizieren"); + dlg.setHeaderText("Neue ID für die Kopie von \"" + sourceId + "\":"); + dlg.setContentText("ID:"); + dlg.showAndWait().ifPresent(newId -> { + if (newId == null || newId.isBlank()) return; + Path charDir = ctx.assetRoot().resolve("Characters"); + Path src = charDir.resolve(sourceId + ".character"); + Path dest = charDir.resolve(newId.trim() + ".character"); + try { + java.nio.file.Files.copy(src, dest, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + refreshContentCharacterTree(); + loadCharacterById(newId.trim()); + } catch (IOException ex) { + new Alert(Alert.AlertType.ERROR, "Fehler beim Duplizieren: " + ex.getMessage()).showAndWait(); + } + }); + } + + private void contentRefresh() { + refreshContentCharacterTree(); + refreshContentQuestTree(); + refreshContentItemTree(); + refreshContentFractionTree(); + refreshContentRecipeTree(); + refreshContentAreaTree(); + refreshContentLocationTree(); + } + + // ── Section-Builder ──────────────────────────────────────────────────────── + + private void collapseAllContentSections() { + for (TreeItem node : new TreeItem[]{ + contentCharNode, contentQuestNode, contentFractionNode, contentItemNode, + contentRecipeNode, contentCraftingNode, contentAreaNode, contentLocationNode}) { + if (node != null) node.setExpanded(false); + } + } + + void restoreContentSection(String section) { + if (contentCharBtn == null) return; + switch (section != null ? section : "character") { + case "character" -> contentCharBtn.fire(); + case "quest" -> contentQuestBtn.fire(); + case "fraction" -> contentFraktionBtn.fire(); + case "item" -> contentItemBtn.fire(); + case "recipe" -> contentRezeptBtn.fire(); + case "crafting" -> contentCraftingBtn.fire(); + case "area" -> contentAreaBtn.fire(); + case "location" -> contentLocationBtn.fire(); + case "lang" -> contentLangBtn.fire(); + } + } + + private void fireContentBtn(int idx) { + if (contentCharBtn == null) return; + ToggleButton[] btns = { contentCharBtn, contentQuestBtn, contentFraktionBtn, + contentItemBtn, contentRezeptBtn, contentCraftingBtn, + contentAreaBtn, contentLocationBtn, contentLangBtn }; + if (idx >= 0 && idx < btns.length) btns[idx].fire(); + } + + // ── Content-Abschnitt-Views ──────────────────────────────────────────────── + + private void buildCharacterViewInContent() { + collapseAllContentSections(); + dialogEditorView = new de.blight.editor.ui.DialogEditorView(); + dialogEditorView.setDisable(true); + monologueEditorView = new de.blight.editor.ui.MonologueEditorView(); + + TabPane tabs = new TabPane(); + tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); + Tab charTab = new Tab("Charakter", buildCharacterSettingsPane()); + Tab dialogTab = new Tab("Dialog", dialogEditorView); + Tab monologTab = new Tab("Monologe", monologueEditorView); + tabs.getTabs().addAll(charTab, dialogTab, monologTab); + tabs.getSelectionModel().select(charTab); + + setContentCenter(tabs); + setContentRight(null); + setContentBottom(null); + + if (contentCharNode != null) contentCharNode.setExpanded(true); + } + + private void buildQuestViewInContent() { + collapseAllContentSections(); + activeQuestView = new de.blight.editor.ui.QuestEditorView(ctx.assetRoot().resolve("quests")); + setContentCenter(activeQuestView); + setContentRight(null); + setContentBottom(buildQuestLegend()); + refreshContentQuestTree(); + if (contentQuestNode != null) contentQuestNode.setExpanded(true); + } + + private void buildFractionViewInContent() { + collapseAllContentSections(); + activeFractionView = new de.blight.editor.ui.FractionEditorView(ctx.assetRoot().resolve("fractions")); + setContentCenter(activeFractionView); + setContentRight(null); + setContentBottom(null); + refreshContentFractionTree(); + if (contentFractionNode != null) contentFractionNode.setExpanded(true); + } + + private void buildItemViewInContent() { + collapseAllContentSections(); + activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); + setContentCenter(activeItemView); + setContentRight(null); + setContentBottom(buildItemLegend()); + refreshContentItemTree(); + if (contentItemNode != null) contentItemNode.setExpanded(true); + } + + private void buildRecipeViewInContent() { + collapseAllContentSections(); + activeRecipeView = new de.blight.editor.ui.RecipeEditorView(ctx.assetRoot().resolve("recipes")); + setContentCenter(activeRecipeView); + setContentRight(null); + setContentBottom(buildRecipeLegend()); + refreshContentRecipeTree(); + if (contentRecipeNode != null) contentRecipeNode.setExpanded(true); + } + + private void buildCraftingViewInContent() { + collapseAllContentSections(); + activeCraftingView = new de.blight.editor.ui.CraftingTableEditorView(ctx.assetRoot().resolve("craftingtables")); + setContentCenter(activeCraftingView); + setContentRight(null); + setContentBottom(null); + if (contentCraftingNode != null) contentCraftingNode.setExpanded(true); + } + + private void buildAreaViewInContent() { + collapseAllContentSections(); + activeAreaView = new de.blight.editor.ui.AreaEditorView(); + setContentCenter(activeAreaView); + setContentRight(null); + setContentBottom(null); + refreshContentAreaTree(); + if (contentAreaNode != null) contentAreaNode.setExpanded(true); + } + + private void buildLocationViewInContent() { + collapseAllContentSections(); + activeLocationView = new de.blight.editor.ui.LocationEditorView(); + setContentCenter(activeLocationView); + setContentRight(null); + setContentBottom(null); + refreshContentLocationTree(); + if (contentLocationNode != null) contentLocationNode.setExpanded(true); + } + + private void buildLocalizationViewInContent() { + collapseAllContentSections(); + setContentCenter(new de.blight.editor.ui.LocalizationEditorView( + de.blight.editor.ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang"))); + setContentRight(null); + setContentBottom(null); + } + + /** Setzt den Center-Bereich: dual = contentEditorRoot.setCenter, single = setCenterView. */ + private void setContentCenter(javafx.scene.Node node) { + if (dualWindowMode && contentEditorRoot != null) { + contentEditorRoot.setCenter(node); + } else { + ctx.setCenterView().accept(node); + } + } + + private void setContentRight(javafx.scene.Node node) { + if (dualWindowMode && contentEditorRoot != null) { + contentEditorRoot.setRight(node); + } else { + ctx.setRightPanel().accept(node); + } + } + + private void setContentBottom(javafx.scene.Node node) { + if (dualWindowMode && contentEditorRoot != null) { + contentEditorRoot.setBottom(node); + } else { + ctx.setBottomPanel().accept(node); + } + } + + // ── Character-Editor ─────────────────────────────────────────────────────── + + private ScrollPane buildCharacterSettingsPane() { + Path charDir = ctx.assetRoot().resolve("Characters"); + + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + inner.setStyle("-fx-background-color:#1a1a2a;"); + + final String LBL = "-fx-text-fill:#ccc;"; + final String SEC = "-fx-font-weight:bold;-fx-font-size:11;-fx-text-fill:#8ab4d4;"; + java.util.function.Function dl = t -> { Label l = new Label(t); l.setStyle(LBL); return l; }; + java.util.function.Function dsl = t -> { + Label l = new Label(t); l.setStyle(SEC); VBox.setMargin(l, new Insets(8, 0, 2, 0)); return l; }; + + charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen"); + charEditorStatusLabel.setWrapText(true); + charEditorStatusLabel.setStyle("-fx-font-size:11;-fx-text-fill:#777;"); + inner.getChildren().add(charEditorStatusLabel); + + charEditContainer = new VBox(6); + charEditContainer.setDisable(true); + charEditContainer.setStyle("-fx-background-color:#1a1a2a;"); + charEditContainer.getChildren().addAll(new Separator(), dsl.apply("Character-Daten"), new Separator()); + + charIdField = new TextField(); + charIdField.setPromptText("eindeutige ID (Dateiname)"); + charNameField = new TextField(); + charNameField.setPromptText("Anzeigename"); + charNameField.setEditable(false); + + charTypeCombo = new ComboBox<>(); + charTypeCombo.getItems().addAll("MainCharacter", "NPC"); + charTypeCombo.setValue("NPC"); + charTypeCombo.setMaxWidth(Double.MAX_VALUE); + + charModelCombo = new ComboBox<>(); + charModelCombo.setMaxWidth(Double.MAX_VALUE); + charModelCombo.setPromptText("Modell wählen…"); + Path charModelBase = ctx.assetRoot().resolve("Characters").resolve("Models"); + if (java.nio.file.Files.isDirectory(charModelBase)) { + try (var walk = java.nio.file.Files.walk(charModelBase)) { + walk.filter(p -> p.toString().endsWith(".j3o")) + .map(p -> ctx.assetRoot().relativize(p).toString().replace('\\', '/')) + .sorted().forEach(charModelCombo.getItems()::add); + } catch (IOException ignored) {} + } + + charAnimSetCombo = new ComboBox<>(); + charAnimSetCombo.setMaxWidth(Double.MAX_VALUE); + charAnimSetCombo.setPromptText("Animations-Set wählen…"); + refreshCharAnimSetCombo(); + charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet()); + + charEditContainer.getChildren().addAll( + dl.apply("ID:"), charIdField, + dl.apply("Name:"), charNameField, + dl.apply("Typ:"), charTypeCombo + ); + + charFractionCombo = new ComboBox<>(); + charFractionCombo.setMaxWidth(Double.MAX_VALUE); + charFractionCombo.setPromptText("— keine Fraktion —"); + charFractionCombo.setCellFactory(lv -> new ListCell<>() { + @Override protected void updateItem(de.blight.common.model.Fraction f, boolean empty) { + super.updateItem(f, empty); + setText(empty ? null : fractionDisplay(f)); + } + }); + charFractionCombo.setButtonCell(new ListCell<>() { + @Override protected void updateItem(de.blight.common.model.Fraction f, boolean empty) { + super.updateItem(f, empty); + setText(fractionDisplay(f)); + } + }); + refreshFractionCombo(); + charFractionSection = new VBox(2, dl.apply("Fraktion:"), charFractionCombo); + boolean initiallyNpc = "NPC".equals(charTypeCombo.getValue()); + charFractionSection.setVisible(initiallyNpc); + charFractionSection.setManaged(initiallyNpc); + charTypeCombo.valueProperty().addListener((obs, ov, nv) -> { + boolean isNpc = "NPC".equals(nv); + charFractionSection.setVisible(isNpc); + charFractionSection.setManaged(isNpc); + }); + charEditContainer.getChildren().add(charFractionSection); + + charStatusCombo = new ComboBox<>(); + charStatusCombo.getItems().addAll("FRIENDLY", "NEUTRAL", "ENRAGED", "ENEMY"); + charStatusCombo.setValue("NEUTRAL"); + charStatusCombo.setMaxWidth(Double.MAX_VALUE); + + charTraderCheck = new CheckBox("Ist Händler"); + charTraderCheck.setStyle(LBL); + + charTraderItemsView = new ListView<>(); + charTraderItemsView.setPrefHeight(80); + charTraderItemsView.setStyle("-fx-background-color:#2a2a3a;-fx-control-inner-background:#2a2a3a;-fx-text-fill:#ddd;"); + Button addTraderItemBtn = new Button("+"); + Button delTraderItemBtn = new Button("−"); + addTraderItemBtn.setOnAction(e -> { + TextInputDialog d = new TextInputDialog(); + d.initOwner(ctx.stageSupplier().get()); + d.setTitle("Item-ID"); + d.setHeaderText("Item-ID der Handelsware:"); + d.showAndWait().ifPresent(id -> { if (!id.isBlank()) charTraderItemsView.getItems().add(id.trim()); }); + }); + delTraderItemBtn.setOnAction(e -> { + String sel = charTraderItemsView.getSelectionModel().getSelectedItem(); + if (sel != null) charTraderItemsView.getItems().remove(sel); + }); + HBox traderBtns = new HBox(4, addTraderItemBtn, delTraderItemBtn); + + charMsgFriendly = new TextField(); + charMsgFriendly.setPromptText("TextReference-Schlüssel (z.B. silas.msg.friendly)"); + charMsgNeutral = new TextField(); + charMsgNeutral.setPromptText("TextReference-Schlüssel"); + charMsgEnraged = new TextField(); + charMsgEnraged.setPromptText("TextReference-Schlüssel"); + charMsgEnemy = new TextField(); + charMsgEnemy.setPromptText("TextReference-Schlüssel"); + + charNpcSection = new VBox(4, + dl.apply("Status:"), charStatusCombo, + dl.apply("Handel:"), charTraderCheck, + dl.apply("Waren:"), charTraderItemsView, traderBtns, + dsl.apply("Standard-Nachrichten je Status"), + dl.apply("Friendly:"), charMsgFriendly, + dl.apply("Neutral:"), charMsgNeutral, + dl.apply("Enraged:"), charMsgEnraged, + dl.apply("Enemy:"), charMsgEnemy); + boolean npcInitial = "NPC".equals(charTypeCombo.getValue()); + charNpcSection.setVisible(npcInitial); + charNpcSection.setManaged(npcInitial); + charTypeCombo.valueProperty().addListener((obs, ov, nv) -> { + boolean isNpc = "NPC".equals(nv); + charNpcSection.setVisible(isNpc); + charNpcSection.setManaged(isNpc); + }); + charEditContainer.getChildren().add(charNpcSection); + charEditContainer.getChildren().addAll( + dl.apply("Modell:"), charModelCombo, + dl.apply("Anim-Set:"), charAnimSetCombo + ); + + charEditContainer.getChildren().addAll(new Separator(), dsl.apply("Aktions-Zuweisung"), new Separator()); + charActionLabelsBox = new VBox(4); + Label actionHint = new Label("(wird aus Animations-Set geladen)"); + actionHint.setStyle("-fx-font-size:10;-fx-text-fill:#666;"); + charActionLabelsBox.getChildren().add(actionHint); + charEditContainer.getChildren().add(charActionLabelsBox); + + // Abilities-Sektion + charEditContainer.getChildren().addAll(new Separator(), dsl.apply("Fähigkeiten (nur MainCharacter)"), new Separator()); + abMagicSpin = abSpinner(); + abStaffSpin = abSpinner(); + abSwordSpin = abSpinner(); + abArcherySpin = abSpinner(); + abHeavySpin = abSpinner(); + abCrossbowSpin = abSpinner(); + abThieverySpin = ab3Spinner(); + abAlchemySpin = ab3Spinner(); + abEngineeringSpin = ab3Spinner(); + abSmitherySpin = ab3Spinner(); + abEnchantingSpin = ab3Spinner(); + charEditContainer.getChildren().addAll( + abRow("Magie:", abMagicSpin), + abRow("Stabkampf:", abStaffSpin), + abRow("Schwerthiebe:", abSwordSpin), + abRow("Bogenschießen:", abArcherySpin), + abRow("Schwere Waffen:", abHeavySpin), + abRow("Armbrust:", abCrossbowSpin), + abRow("Diebstahl:", abThieverySpin), + abRow("Alchemie:", abAlchemySpin), + abRow("Ingenieurwesen:", abEngineeringSpin), + abRow("Schmiedekunst:", abSmitherySpin), + abRow("Verzauberung:", abEnchantingSpin) + ); + + // Buttons + Button saveBtn = new Button("Speichern"); + Button clearBtn = new Button("Leeren"); + saveBtn.setMaxWidth(Double.MAX_VALUE); + clearBtn.setMaxWidth(Double.MAX_VALUE); + saveBtn.setStyle("-fx-background-color:#2a5a2a;-fx-text-fill:white;"); + saveBtn.setOnAction(e -> saveCharacter(charDir)); + clearBtn.setOnAction(e -> clearCharacterForm()); + HBox.setHgrow(saveBtn, Priority.ALWAYS); + HBox.setHgrow(clearBtn, Priority.ALWAYS); + HBox btnBar = new HBox(6, saveBtn, clearBtn); + charEditContainer.getChildren().addAll(new Separator(), btnBar); + + // Auto-Save-Listener + charIdField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); + charTypeCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir)); + charModelCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir)); + charAnimSetCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir)); + charFractionCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir)); + charStatusCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir)); + charTraderCheck.selectedProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir)); + charTraderItemsView.getItems().addListener( + (javafx.collections.ListChangeListener) c -> autoSaveCharacter(charDir)); + charMsgFriendly.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); + charMsgNeutral.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); + charMsgEnraged.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); + charMsgEnemy.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); + + inner.getChildren().add(charEditContainer); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color:#1a1a2a;-fx-background:#1a1a2a;"); + return scroll; + } + + private void autoSaveCharacter(Path charDir) { + if (charIdField == null || charIdField.getText().trim().isBlank()) return; + saveCharacter(charDir); + } + + private void refreshCharAnimSetCombo() { + if (charAnimSetCombo == null) return; + String cur = charAnimSetCombo.getValue(); + charAnimSetCombo.getItems().clear(); + Path setDir = ctx.assetRoot().resolve("Characters").resolve("sets"); + if (java.nio.file.Files.isDirectory(setDir)) { + try (var walk = java.nio.file.Files.walk(setDir)) { + walk.filter(p -> p.toString().endsWith(".animset.json")) + .map(p -> p.getFileName().toString().replace(".animset.json", "")) + .sorted().forEach(charAnimSetCombo.getItems()::add); + } catch (IOException ignored) {} + } + if (cur != null && charAnimSetCombo.getItems().contains(cur)) charAnimSetCombo.setValue(cur); + } + + private void updateCharActionCombosFromSet() { + if (charActionLabelsBox == null) return; + charActionLabelsBox.getChildren().clear(); + String setName = charAnimSetCombo != null ? charAnimSetCombo.getValue() : null; + if (setName == null || setName.isBlank()) { + Label hint = new Label("(kein Animations-Set gewählt)"); + hint.setStyle("-fx-font-size:10;-fx-text-fill:#555;"); + charActionLabelsBox.getChildren().add(hint); + return; + } + Path setDir = ctx.assetRoot().resolve("Characters").resolve("sets"); + de.blight.game.animation.AnimSet animSet; + try { + animSet = de.blight.game.animation.AnimSet.load(setDir, setName); + } catch (IOException e) { + animSet = new de.blight.game.animation.AnimSet(); + } + java.util.Map actionMap = animSet.getActionMap(); + if (actionMap == null || actionMap.isEmpty()) { + Label hint = new Label("(keine Aktions-Zuweisung im Set)"); + hint.setStyle("-fx-font-size:10;-fx-text-fill:#555;"); + charActionLabelsBox.getChildren().add(hint); + return; + } + for (de.blight.game.animation.AnimationAction action : de.blight.game.animation.AnimationAction.values()) { + String clip = actionMap.get(action.name()); + HBox row = new HBox(6); + row.setAlignment(Pos.CENTER_LEFT); + Label lbl = new Label(action.displayName() + ":"); + lbl.setMinWidth(50); + lbl.setStyle("-fx-font-weight:bold;-fx-text-fill:#ccc;"); + Label val = new Label(clip != null && !clip.isBlank() ? clip : "—"); + val.setStyle("-fx-text-fill:" + (clip != null && !clip.isBlank() ? "#aad4aa" : "#555") + ";"); + row.getChildren().addAll(lbl, val); + charActionLabelsBox.getChildren().add(row); + } + } + + private void clearCharacterForm() { + if (charIdField != null) charIdField.clear(); + if (charNameField != null) charNameField.clear(); + if (charTypeCombo != null) charTypeCombo.setValue("NPC"); + if (charModelCombo != null) charModelCombo.setValue(null); + if (charAnimSetCombo != null) charAnimSetCombo.setValue(null); + if (charFractionCombo != null) charFractionCombo.setValue(null); + 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); + if (dialogEditorView != null) { dialogEditorView.setDisable(false); dialogEditorView.clear(); } + if (routineEditorView != null) { routineEditorView.setDisable(false); routineEditorView.loadNpc(new de.blight.common.model.NPC()); } + if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Neuer Charakter"); + } + + private void saveCharacter(Path charDir) { + String id = charIdField != null ? charIdField.getText().trim() : ""; + if (id.isBlank()) { + if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: ID darf nicht leer sein"); + return; + } + String type = charTypeCombo != null ? charTypeCombo.getValue() : "NPC"; + boolean isMain = "MainCharacter".equals(type); + + if (isMain && de.blight.common.model.CharacterIO.mainCharacterExists(charDir, id)) { + if (charEditorStatusLabel != null) + charEditorStatusLabel.setText("Fehler: Es gibt bereits einen MainCharacter!"); + return; + } + + de.blight.common.model.GameCharacter c = isMain + ? new de.blight.common.model.MainCharacter() + : new de.blight.common.model.NPC(); + c.setCharacterId(id); + String nameId = charNameField != null ? charNameField.getText().trim() : ""; + c.setName(nameId.isBlank() ? null : new de.blight.common.model.TextReference(nameId)); + c.setModelPath(charModelCombo != null ? charModelCombo.getValue() : null); + c.setAnimSetPath(charAnimSetCombo != null ? charAnimSetCombo.getValue() : null); + if (c instanceof de.blight.common.model.NPC npc) { + if (dialogEditorView != null) dialogEditorView.exportToNpc(npc); + if (routineEditorView != null) routineEditorView.exportToNpc(npc); + npc.setFraction(charFractionCombo != null ? charFractionCombo.getValue() : null); + if (charStatusCombo != null && charStatusCombo.getValue() != null) { + try { npc.setStatus(de.blight.common.model.Status.valueOf(charStatusCombo.getValue())); } + catch (IllegalArgumentException ignored) {} + } + if (charTraderCheck != null) npc.setTrader(charTraderCheck.isSelected()); + java.util.Map 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 items = new java.util.ArrayList<>(); + charTraderItemsView.getItems().forEach(id2 -> { + de.blight.common.model.Item it = new de.blight.common.model.Item(); + it.setItemId(id2); + items.add(it); + }); + npc.setItems(items); + } else { + npc.setItems(null); + } + } + if (c instanceof de.blight.common.model.MainCharacter mc) { + mc.setAbilities(buildAbilities()); + } + try { + de.blight.common.model.CharacterIO.save(c, charDir); + refreshContentCharacterTree(); + if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id); + } catch (Exception e) { + if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage()); + } + } + + private void refreshFractionCombo() { + loadedFractions.clear(); + loadedFractions.addAll( + de.blight.common.model.FractionIO.loadAll(ctx.assetRoot().resolve("fractions"))); + if (charFractionCombo == null) return; + de.blight.common.model.Fraction current = charFractionCombo.getValue(); + charFractionCombo.getItems().clear(); + charFractionCombo.getItems().add(null); + charFractionCombo.getItems().addAll(loadedFractions); + if (current != null && current.getFractionId() != null) { + loadedFractions.stream() + .filter(f -> current.getFractionId().equals(f.getFractionId())) + .findFirst() + .ifPresentOrElse(charFractionCombo::setValue, () -> charFractionCombo.setValue(null)); + } + } + + private String fractionDisplay(de.blight.common.model.Fraction f) { + if (f == null) return "— keine Fraktion —"; + return f.getName() != null ? f.getName().id() + : (f.getFractionId() != null ? f.getFractionId().toString().substring(0, 8) + "…" : "?"); + } + + // ── Fähigkeiten-Helpers ──────────────────────────────────────────────────── + + private static HBox abRow(String label, Spinner s) { + Label l = new Label(label); + l.setMinWidth(160); + l.setStyle("-fx-text-fill:#ccc;"); + HBox.setHgrow(s, Priority.ALWAYS); + HBox row = new HBox(6, l, s); + row.setAlignment(Pos.CENTER_LEFT); + return row; + } + + private static Spinner abSpinner() { + var s = new Spinner(1, 10, 1); + s.setEditable(true); return s; + } + + private static Spinner ab3Spinner() { + var s = new Spinner(0, 3, 0); + s.setEditable(true); return s; + } + + private void resetAbilities() { + if (abMagicSpin == null) return; + abMagicSpin.getValueFactory().setValue(1); abStaffSpin.getValueFactory().setValue(1); + abSwordSpin.getValueFactory().setValue(1); abArcherySpin.getValueFactory().setValue(1); + abHeavySpin.getValueFactory().setValue(1); abCrossbowSpin.getValueFactory().setValue(1); + abThieverySpin.getValueFactory().setValue(0); abAlchemySpin.getValueFactory().setValue(0); + abEngineeringSpin.getValueFactory().setValue(0); abSmitherySpin.getValueFactory().setValue(0); + abEnchantingSpin.getValueFactory().setValue(0); + } + + private void loadAbilities(de.blight.common.model.abilities.Abilities ab) { + if (ab == null) { resetAbilities(); return; } + if (abMagicSpin == null) return; + abMagicSpin.getValueFactory().setValue(Math.max(1, ab.getLvlMagic())); + abStaffSpin.getValueFactory().setValue(Math.max(1, ab.getLvlStaffCombat())); + abSwordSpin.getValueFactory().setValue(Math.max(1, ab.getLvlSwordsmanship())); + abArcherySpin.getValueFactory().setValue(Math.max(1, ab.getLvlArchery())); + abHeavySpin.getValueFactory().setValue(Math.max(1, ab.getLvlHeavyWeapons())); + abCrossbowSpin.getValueFactory().setValue(Math.max(1, ab.getLvlCrossbow())); + abThieverySpin.getValueFactory().setValue(ab.getLvlThievery()); + abAlchemySpin.getValueFactory().setValue(ab.getLvlAlchemy()); + abEngineeringSpin.getValueFactory().setValue(ab.getLvlEngineering()); + abSmitherySpin.getValueFactory().setValue(ab.getLvlSmithery()); + abEnchantingSpin.getValueFactory().setValue(ab.getLvlEnchanting()); + } + + private de.blight.common.model.abilities.Abilities buildAbilities() { + if (abMagicSpin == null) return null; + de.blight.common.model.abilities.Abilities ab = new de.blight.common.model.abilities.Abilities(); + ab.setLvlMagic(abMagicSpin.getValue()); + ab.setLvlStaffCombat(abStaffSpin.getValue()); + ab.setLvlSwordsmanship(abSwordSpin.getValue()); + ab.setLvlArchery(abArcherySpin.getValue()); + ab.setLvlHeavyWeapons(abHeavySpin.getValue()); + ab.setLvlCrossbow(abCrossbowSpin.getValue()); + ab.setLvlThievery(abThieverySpin.getValue()); + ab.setLvlAlchemy(abAlchemySpin.getValue()); + ab.setLvlEngineering(abEngineeringSpin.getValue()); + ab.setLvlSmithery(abSmitherySpin.getValue()); + ab.setLvlEnchanting(abEnchantingSpin.getValue()); + return ab; + } + + private static String msgKey( + java.util.Map 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 msgs, + de.blight.common.model.Status status, + TextField field) { + if (field == null) return; + String s = field.getText().trim(); + if (!s.isBlank()) msgs.put(status, new de.blight.common.model.TextReference(s)); + } + + // ── Tree-Refresh-Methoden ────────────────────────────────────────────────── + + private void refreshContentCharacterTree() { + if (contentCharNode == null) return; + contentTreeUpdating = true; + try { + contentCharNode.getChildren().clear(); + Path charDir = ctx.assetRoot().resolve("Characters"); + if (!java.nio.file.Files.isDirectory(charDir)) return; + java.util.List> items = new java.util.ArrayList<>(); + try (var walk = java.nio.file.Files.list(charDir)) { + walk.filter(p -> p.toString().endsWith(".character")) + .map(p -> p.getFileName().toString().replace(".character", "")) + .sorted() + .forEach(id -> items.add(new TreeItem<>(id))); + } catch (IOException ignored) {} + contentCharNode.getChildren().addAll(items); + } finally { + contentTreeUpdating = false; + } + } + + private void refreshContentQuestTree() { + if (contentQuestNode == null) return; + contentTreeUpdating = true; + try { + contentQuestNode.getChildren().clear(); + Path dir = ctx.assetRoot().resolve("quests"); + if (!java.nio.file.Files.isDirectory(dir)) return; + java.util.List allQuests = + de.blight.common.model.quests.QuestIO.loadAll(dir); + java.util.List> items = allQuests.stream() + .sorted(java.util.Comparator + .comparing( + q -> de.blight.common.model.quests.QuestIO.typeOf(q)) + .thenComparing(q -> q.getQuestId() != null ? q.getQuestId() : "")) + .map(q -> { + String type = de.blight.common.model.quests.QuestIO.typeOf(q); + String id = q.getQuestId() != null ? q.getQuestId() : ""; + return new TreeItem<>(type + ":" + id); + }) + .collect(java.util.stream.Collectors.toList()); + contentQuestNode.getChildren().addAll(items); + } finally { + contentTreeUpdating = false; + } + } + + private void refreshContentItemTree() { + if (contentItemNode == null) return; + contentTreeUpdating = true; + try { + contentItemNode.getChildren().clear(); + Path dir = ctx.assetRoot().resolve("items"); + if (!java.nio.file.Files.isDirectory(dir)) return; + java.util.List> treeItems = + de.blight.common.model.ItemIO.loadAll(dir).stream() + .filter(i -> i.getItemId() != null && !i.getItemId().isBlank()) + .map(i -> { + String cat = i.getCategory() != null ? i.getCategory().name() : "MISC_ITEMS"; + return new TreeItem<>(cat + ":" + i.getItemId()); + }) + .collect(java.util.stream.Collectors.toList()); + contentItemNode.getChildren().addAll(treeItems); + } finally { + contentTreeUpdating = false; + } + } + + private void refreshContentFractionTree() { + if (contentFractionNode == null) return; + contentTreeUpdating = true; + try { + contentFractionNode.getChildren().clear(); + Path dir = ctx.assetRoot().resolve("fractions"); + if (!java.nio.file.Files.isDirectory(dir)) return; + java.util.List> items = + de.blight.common.model.FractionIO.loadAll(dir).stream() + .filter(f -> f.getName() != null && !f.getName().id().isBlank()) + .map(f -> new TreeItem<>(f.getName().id())) + .collect(java.util.stream.Collectors.toList()); + contentFractionNode.getChildren().addAll(items); + } finally { + contentTreeUpdating = false; + } + } + + private void refreshContentAreaTree() { + if (contentAreaNode == null) return; + contentTreeUpdating = true; + try { + contentAreaNode.getChildren().clear(); + java.util.List> items; + try { + items = de.blight.common.AreaDefinitionIO.load().stream() + .filter(a -> a.id() != null && !a.id().isBlank()) + .map(a -> new TreeItem<>(a.id())) + .collect(java.util.stream.Collectors.toList()); + } catch (IOException ignored) { return; } + contentAreaNode.getChildren().addAll(items); + } finally { + contentTreeUpdating = false; + } + } + + private void refreshContentLocationTree() { + if (contentLocationNode == null) return; + contentTreeUpdating = true; + try { + contentLocationNode.getChildren().clear(); + java.util.List> items; + try { + items = de.blight.common.LocationIO.load().stream() + .filter(l -> l.getId() != null && !l.getId().isBlank()) + .map(l -> new TreeItem<>(l.getId())) + .collect(java.util.stream.Collectors.toList()); + } catch (IOException ignored) { return; } + contentLocationNode.getChildren().addAll(items); + } finally { + contentTreeUpdating = false; + } + } + + private void refreshContentRecipeTree() { + if (contentRecipeNode == null) return; + contentTreeUpdating = true; + try { + contentRecipeNode.getChildren().clear(); + Path dir = ctx.assetRoot().resolve("recipes"); + if (!java.nio.file.Files.isDirectory(dir)) return; + java.util.List> treeItems = + de.blight.common.model.RecipeIO.loadAll(dir).stream() + .filter(r -> r.getCreates() != null && r.getCreates().getItemId() != null) + .map(r -> { + String table = (r.getTable() != null && r.getTable().getType() != null) + ? r.getTable().getType().name() : "NONE"; + return new TreeItem<>(table + ":" + r.getCreates().getItemId()); + }) + .collect(java.util.stream.Collectors.toList()); + contentRecipeNode.getChildren().addAll(treeItems); + } finally { + contentTreeUpdating = false; + } + } + + private void refreshContentCraftingTree() { + if (contentCraftingNode == null) return; + contentTreeUpdating = true; + try { + contentCraftingNode.getChildren().clear(); + java.util.List> items = new java.util.ArrayList<>(); + for (de.blight.common.model.CraftingTable.CraftingTableType t : + de.blight.common.model.CraftingTable.CraftingTableType.values()) { + items.add(new TreeItem<>(t.name())); + } + contentCraftingNode.getChildren().addAll(items); + } finally { + contentTreeUpdating = false; + } + } + + // ── Legende-Builder ──────────────────────────────────────────────────────── + + private static String questTypeColor(String type) { + return switch (type) { + case "BRING" -> "#cc5555"; + case "FOLLOW" -> "#55aacc"; + case "INTERACT" -> "#aa55cc"; + case "ITEM" -> "#55cc88"; + case "TALK" -> "#ccaa33"; + default -> "#888888"; + }; + } + + private static de.blight.common.model.ItemCategory safeItemCategory(String name) { + try { return de.blight.common.model.ItemCategory.valueOf(name); } + catch (IllegalArgumentException e) { return null; } + } + + private static String contentRecipeColor(String tableTypeName) { + try { + de.blight.common.model.CraftingTable.CraftingTableType t = + de.blight.common.model.CraftingTable.CraftingTableType.valueOf(tableTypeName); + return de.blight.editor.ui.CraftingTableEditorView.typeColor(t); + } catch (IllegalArgumentException e) { + return "#666666"; + } + } + + private static HBox buildLegendBar(String[][] entries, + java.util.function.Function colorFn) { + HBox bar = new HBox(14); + bar.setPadding(new Insets(5, 10, 5, 10)); + bar.setAlignment(Pos.CENTER_LEFT); + bar.setStyle("-fx-background-color: #1a1a2a;"); + for (String[] entry : entries) { + javafx.scene.shape.Rectangle box = new javafx.scene.shape.Rectangle(10, 10); + box.setFill(javafx.scene.paint.Color.web(colorFn.apply(entry[0]))); + box.setArcWidth(3); box.setArcHeight(3); + Label lbl = new Label(entry[1]); + lbl.setStyle("-fx-text-fill: #aaa; -fx-font-size: 11;"); + HBox item = new HBox(4, box, lbl); + item.setAlignment(Pos.CENTER_LEFT); + bar.getChildren().add(item); + } + return bar; + } + + private static HBox buildQuestLegend() { + return buildLegendBar(new String[][]{ + {"BRING", "Bring"}, {"FOLLOW", "Follow"}, {"INTERACT", "Interact"}, + {"ITEM", "Item"}, {"TALK", "Talk"} + }, ContentController::questTypeColor); + } + + private static HBox buildItemLegend() { + return buildLegendBar(new String[][]{ + {"WEAPON", "Waffe"}, + {"GEAR", "Ausrüstung"}, + {"CONSUMABLES", "Verbrauchbar"}, + {"QUEST_ITEMS", "Quest"}, + {"CRAFTING_ITEMS","Crafting"}, + {"MISC_ITEMS", "Sonstiges"} + }, k -> de.blight.editor.ui.ItemEditorView.categoryColor(safeItemCategory(k))); + } + + private static HBox buildRecipeLegend() { + return buildLegendBar(new String[][]{ + {"AlchemyTable", "Alchemie"}, + {"EnchantmentTable", "Verzauberung"}, + {"Smithy", "Schmiede"}, + {"Goldsmiths", "Goldschmied"}, + {"Workshop", "Werkstatt"}, + {"Fireplace", "Feuerstelle"}, + {"Kitchen", "Küche"}, + {"NONE", "Kein Tisch"} + }, ContentController::contentRecipeColor); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java b/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java new file mode 100644 index 0000000..e156ade --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java @@ -0,0 +1,387 @@ +package de.blight.editor.toolset.game; + +import de.blight.editor.ProjectRoot; +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import de.blight.editor.toolset.EditorUiHelper; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.stage.Stage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class GameController { + + private final ToolContext ctx; + + // Game launch state + private boolean launchGameAfterSave = false; + private boolean pendingNewGame = false; + private boolean pendingAutostart = true; + + // Play panel UI + private Button gamePlayBtn; + private Button gameNewBtn; + private Button gameFullBtn; + private CheckBox debugConsoleCB; + private Label tempSpawnCoordsLabel; + private Label permSpawnCoordsLabel; + + // Game console + private Stage gameConsoleStage; + private TextArea gameConsoleArea; + private final ConcurrentLinkedQueue consoleBuffer = new ConcurrentLinkedQueue<>(); + + public GameController(ToolContext ctx) { + this.ctx = ctx; + } + + // ── Play-Tool-Panel ─────────────────────────────────────────────────────── + + public VBox buildPlayToolPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + inner.getChildren().addAll(EditorUiHelper.sectionTitle("Spawnpunkte"), new Separator()); + + Button setTempBtn = new Button("Temp. Spawn hier setzen"); + setTempBtn.setMaxWidth(Double.MAX_VALUE); + setTempBtn.setTooltip(new Tooltip( + "Aktiviert Klick-Modus im Viewport zum Setzen des temporären Spawnpunkts")); + setTempBtn.setOnAction(e -> { + ctx.input().playToolMode = SharedInput.PlayToolMode.SET_TEMP; + ctx.setStatus().accept("L-Klick im Viewport → temporären Spawnpunkt setzen"); + }); + + Button setPermBtn = new Button("Perm. Spawn hier setzen"); + setPermBtn.setMaxWidth(Double.MAX_VALUE); + setPermBtn.setTooltip(new Tooltip( + "Aktiviert Klick-Modus im Viewport zum Setzen des permanenten Spawnpunkts")); + setPermBtn.setOnAction(e -> { + ctx.input().playToolMode = SharedInput.PlayToolMode.SET_PERM; + ctx.setStatus().accept("L-Klick im Viewport → permanenten Spawnpunkt setzen"); + }); + + inner.getChildren().addAll(setTempBtn, setPermBtn, new Separator()); + + tempSpawnCoordsLabel = new Label(tempSpawnCoordsText()); + tempSpawnCoordsLabel.setStyle("-fx-font-family: monospace; -fx-font-size: 11; -fx-text-fill: #333;"); + permSpawnCoordsLabel = new Label(permSpawnCoordsText()); + permSpawnCoordsLabel.setStyle("-fx-font-family: monospace; -fx-font-size: 11; -fx-text-fill: #333;"); + + Button clearTempBtn = new Button("X Temp. Spawn loeschen"); + clearTempBtn.setMaxWidth(Double.MAX_VALUE); + clearTempBtn.setOnAction(e -> { + ctx.input().tempSpawnX = Float.NaN; + ctx.input().tempSpawnZ = Float.NaN; + if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText("(nicht gesetzt)"); + }); + + inner.getChildren().addAll( + EditorUiHelper.bold("Temp. Spawn:"), tempSpawnCoordsLabel, clearTempBtn, + EditorUiHelper.bold("Perm. Spawn:"), permSpawnCoordsLabel, + new Separator()); + + inner.getChildren().addAll(EditorUiHelper.sectionTitle("Spiel starten"), new Separator()); + + Button playBtn = new Button("▶ Spielen"); + gamePlayBtn = playBtn; + playBtn.setMaxWidth(Double.MAX_VALUE); + playBtn.setStyle("-fx-background-color: #2d8a3e; -fx-text-fill: white; " + + "-fx-font-weight: bold; -fx-padding: 6 12 6 12;"); + playBtn.setTooltip(new Tooltip( + "Startet das Spiel mit dem temp. Spawnpunkt (bzw. gespeicherter Position)")); + playBtn.setOnAction(e -> launchGame()); + + Button newGameBtn = new Button("Neues Spiel"); + gameNewBtn = newGameBtn; + newGameBtn.setMaxWidth(Double.MAX_VALUE); + newGameBtn.setStyle("-fx-background-color: #7b3ea4; -fx-text-fill: white; " + + "-fx-font-weight: bold; -fx-padding: 6 12 6 12;"); + newGameBtn.setTooltip(new Tooltip( + "Startet ein neues Spiel am perm. Spawnpunkt mit Intro-Sequenz")); + newGameBtn.setOnAction(e -> launchNewGame()); + + Button fullStartBtn = new Button("▶ Vollst. Start"); + gameFullBtn = fullStartBtn; + fullStartBtn.setMaxWidth(Double.MAX_VALUE); + fullStartBtn.setStyle("-fx-background-color: #1a6080; -fx-text-fill: white; " + + "-fx-font-weight: bold; -fx-padding: 6 12 6 12;"); + fullStartBtn.setTooltip(new Tooltip( + "Startet das Spiel wie von außen: mit OAA-Logo, JME-Logo und Hauptmenü")); + fullStartBtn.setOnAction(e -> launchFullGame()); + + debugConsoleCB = new CheckBox("Debug-Konsole anzeigen"); + debugConsoleCB.setTooltip(new Tooltip( + "Öffnet bei Spielstart die Ausgabekonsole mit der Spielausgabe")); + + inner.getChildren().addAll(playBtn, newGameBtn, fullStartBtn, debugConsoleCB); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + // ── Launch ──────────────────────────────────────────────────────────────── + + public void launchGame() { + if (launchGameAfterSave) return; + launchGameAfterSave = true; + pendingNewGame = false; + pendingAutostart = true; + if (gamePlayBtn != null) { gamePlayBtn.setDisable(true); gamePlayBtn.setText("⏳ Startet…"); } + if (gameNewBtn != null) gameNewBtn.setDisable(true); + if (gameFullBtn != null) gameFullBtn.setDisable(true); + ctx.input().saveRequested = true; + ctx.setStatus().accept("Karte wird gespeichert, Spiel startet…"); + } + + public void launchNewGame() { + if (launchGameAfterSave) return; + launchGameAfterSave = true; + pendingNewGame = true; + pendingAutostart = true; + if (gameNewBtn != null) { gameNewBtn.setDisable(true); gameNewBtn.setText("⏳ Startet…"); } + if (gamePlayBtn != null) gamePlayBtn.setDisable(true); + if (gameFullBtn != null) gameFullBtn.setDisable(true); + ctx.input().saveRequested = true; + ctx.setStatus().accept("Karte wird gespeichert, Neues Spiel startet…"); + } + + public void launchFullGame() { + if (launchGameAfterSave) return; + launchGameAfterSave = true; + pendingNewGame = false; + pendingAutostart = false; + if (gameFullBtn != null) { gameFullBtn.setDisable(true); gameFullBtn.setText("⏳ Startet…"); } + if (gamePlayBtn != null) gamePlayBtn.setDisable(true); + if (gameNewBtn != null) gameNewBtn.setDisable(true); + ctx.input().saveRequested = true; + ctx.setStatus().accept("Karte wird gespeichert, vollständiger Spielstart…"); + } + + /** Called from EditorApp AnimationTimer after save completes. */ + public void onSaveComplete() { + if (!launchGameAfterSave) return; + launchGameAfterSave = false; + startGameProcess(); + } + + // ── Console buffer drain — called from AnimationTimer ───────────────────── + + public void flushConsoleBuffer() { + if (consoleBuffer.isEmpty()) return; + if (gameConsoleArea != null) { + StringBuilder sb = new StringBuilder(); + int count = 0; + String ln; + while (count < 200 && (ln = consoleBuffer.poll()) != null) { + sb.append(ln).append('\n'); + count++; + } + gameConsoleArea.appendText(sb.toString()); + gameConsoleArea.setScrollTop(Double.MAX_VALUE); + } else { + consoleBuffer.clear(); + } + } + + // ── Spawn coords update — called from AnimationTimer ────────────────────── + + public void updateSpawnFields(String info) { + if (info == null) return; + String[] p = info.split("\\|", -1); + if (p.length < 2) return; + if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText(tempSpawnCoordsText()); + } + + public void updatePermSpawnLabel() { + if (permSpawnCoordsLabel != null) permSpawnCoordsLabel.setText(permSpawnCoordsText()); + } + + // ── Internal ────────────────────────────────────────────────────────────── + + private String tempSpawnCoordsText() { + SharedInput in = ctx.input(); + if (Float.isNaN(in.tempSpawnX) || Float.isNaN(in.tempSpawnZ)) return "(nicht gesetzt)"; + return String.format("X=%.1f Z=%.1f Yaw=%.0f°", in.tempSpawnX, in.tempSpawnZ, in.tempSpawnYaw); + } + + private String permSpawnCoordsText() { + SharedInput in = ctx.input(); + if (Float.isNaN(in.permSpawnX) || Float.isNaN(in.permSpawnZ)) return "(nicht gesetzt)"; + return String.format("X=%.1f Z=%.1f Yaw=%.0f°", in.permSpawnX, in.permSpawnZ, in.permSpawnYaw); + } + + private void startGameProcess() { + final boolean isNewGame = pendingNewGame; + final boolean isAutostart = pendingAutostart; + pendingNewGame = false; + pendingAutostart = true; + Stage primary = ctx.stageSupplier().get(); + Platform.runLater(() -> { if (primary != null) primary.setIconified(true); }); + SharedInput in = ctx.input(); + new Thread(() -> { + try { + String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString(); + String classpath = System.getProperty("java.class.path"); + String libPath = System.getProperty("java.library.path", ""); + String projRoot = ProjectRoot.PATH.toString(); + + Path srcMapDir = ProjectRoot.PATH.resolve( + Paths.get("blight-map", "src", "main", "map")); + Path runDir = ProjectRoot.PATH.resolve("run"); + Path sessionDir = runDir.resolve("session-" + System.currentTimeMillis()); + copyDirectory(srcMapDir, sessionDir); + + try { + java.util.List sessions; + try (java.util.stream.Stream s = Files.list(runDir)) { + sessions = s.filter(p -> p.getFileName().toString().startsWith("session-")) + .sorted(java.util.Comparator.comparing(p -> p.getFileName().toString())) + .collect(java.util.stream.Collectors.toList()); + } + int keep = 3; + for (int i = 0; i < sessions.size() - keep; i++) { + deleteDirectoryRecursive(sessions.get(i)); + } + } catch (Exception e) { + // non-fatal cleanup failure + } + String sessionMapPath = sessionDir.resolve("blight_map.blm").toString(); + + java.util.List cmd = new java.util.ArrayList<>(List.of( + javaExe, + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.desktop/sun.awt=ALL-UNNAMED", + "-Djava.library.path=" + libPath, + "-Dblight.project.root=" + projRoot, + "-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath, + "-Dblight.autostart=" + isAutostart)); + + if (isNewGame) { + cmd.add("-Dblight.new.game=true"); + } else if (!Float.isNaN(in.tempSpawnX) && !Float.isNaN(in.tempSpawnZ)) { + cmd.add("-Dblight.temp.spawn.x=" + in.tempSpawnX); + cmd.add("-Dblight.temp.spawn.z=" + in.tempSpawnZ); + cmd.add("-Dblight.temp.spawn.yaw=" + in.tempSpawnYaw); + } + + cmd.addAll(List.of("-cp", classpath, "de.blight.game.BlightGame")); + + Process proc = new ProcessBuilder(cmd) + .directory(ProjectRoot.PATH.toFile()) + .redirectErrorStream(true) + .start(); + + Platform.runLater(() -> { + ctx.setStatus().accept(isNewGame ? "Neues Spiel gestartet" : "Spiel gestartet"); + if (gamePlayBtn != null) gamePlayBtn.setText("Laeuft…"); + if (gameNewBtn != null) gameNewBtn.setText("Laeuft…"); + if (gameFullBtn != null) gameFullBtn.setText("Laeuft…"); + if (debugConsoleCB != null && debugConsoleCB.isSelected()) openGameConsole(); + }); + + try (java.io.BufferedReader br = new java.io.BufferedReader( + new java.io.InputStreamReader(proc.getInputStream(), + java.nio.charset.StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + consoleBuffer.offer(line); + } + } + consoleBuffer.offer("--- Spiel beendet ---"); + Platform.runLater(() -> { + if (primary != null) primary.setIconified(false); + if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); } + if (gameNewBtn != null) { gameNewBtn.setText("Neues Spiel"); gameNewBtn.setDisable(false); } + if (gameFullBtn != null) { gameFullBtn.setText("▶ Vollst. Start"); gameFullBtn.setDisable(false); } + }); + + } catch (IOException ex) { + Platform.runLater(() -> { + ctx.setStatus().accept("Spielstart fehlgeschlagen: " + ex.getMessage()); + if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); } + if (gameNewBtn != null) { gameNewBtn.setText("Neues Spiel"); gameNewBtn.setDisable(false); } + if (gameFullBtn != null) { gameFullBtn.setText("▶ Vollst. Start"); gameFullBtn.setDisable(false); } + }); + } + }, "game-launcher").start(); + } + + public void openGameConsole() { + Stage primary = ctx.stageSupplier().get(); + if (gameConsoleStage == null) { + gameConsoleArea = new TextArea(); + gameConsoleArea.setEditable(false); + gameConsoleArea.setStyle( + "-fx-control-inner-background: #1e1e1e; -fx-text-fill: #d4d4d4;" + + " -fx-font-family: monospace; -fx-font-size: 11;"); + + Button clearBtn = new Button("Löschen"); + clearBtn.setOnAction(e -> gameConsoleArea.clear()); + + HBox toolbar = new HBox(5, clearBtn); + toolbar.setPadding(new Insets(4)); + + VBox layout = new VBox(toolbar, gameConsoleArea); + VBox.setVgrow(gameConsoleArea, Priority.ALWAYS); + + Scene scene = new Scene(layout, 800, 350); + gameConsoleStage = new Stage(); + gameConsoleStage.setTitle("Spiel-Konsole"); + gameConsoleStage.setScene(scene); + gameConsoleStage.initOwner(primary); + var screens = javafx.stage.Screen.getScreens(); + if (screens.size() > 1) { + javafx.geometry.Rectangle2D sb = screens.get(1).getVisualBounds(); + gameConsoleStage.setX(sb.getMinX()); + gameConsoleStage.setY(sb.getMinY()); + gameConsoleStage.setWidth(sb.getWidth()); + gameConsoleStage.setHeight(sb.getHeight()); + } else if (primary != null) { + gameConsoleStage.setX(primary.getX()); + gameConsoleStage.setY(primary.getY() + primary.getHeight()); + } + } else { + gameConsoleArea.clear(); + } + gameConsoleStage.show(); + gameConsoleStage.toFront(); + } + + private static void copyDirectory(Path src, Path dst) throws IOException { + try (java.util.stream.Stream walk = java.nio.file.Files.walk(src)) { + for (Path s : (Iterable) walk::iterator) { + Path d = dst.resolve(src.relativize(s)); + if (java.nio.file.Files.isDirectory(s)) { + java.nio.file.Files.createDirectories(d); + } else { + java.nio.file.Files.copy(s, d, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + } + } + } + + private static void deleteDirectoryRecursive(Path dir) throws IOException { + if (!Files.exists(dir)) return; + try (java.util.stream.Stream walk = Files.walk(dir)) { + walk.sorted(java.util.Comparator.reverseOrder()) + .forEach(p -> { try { Files.delete(p); } catch (IOException ignored) {} }); + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/terrain/TerrainPanelController.java b/blight-editor/src/main/java/de/blight/editor/toolset/terrain/TerrainPanelController.java new file mode 100644 index 0000000..fd2490c --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/terrain/TerrainPanelController.java @@ -0,0 +1,1323 @@ +package de.blight.editor.toolset.terrain; + +import de.blight.editor.SharedInput; +import de.blight.editor.TextureMapData; +import de.blight.editor.ToolContext; +import de.blight.editor.tool.*; +import de.blight.editor.ui.TextureChooser; +import de.blight.editor.ui.TextureDetailDialog; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.image.*; +import javafx.scene.layout.*; +import javafx.stage.FileChooser; + +import java.io.*; +import java.net.URL; +import java.nio.file.*; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class TerrainPanelController { + + private final ToolContext ctx; + private final SharedInput input; + private de.blight.editor.toolset.grass.GrassController grassController; + private final Runnable scheduleAutoSave; + private VBox toolPanelRef; // set at top of showToolParameters for self-refresh callbacks + + private static final com.google.gson.Gson TEX_GSON = new com.google.gson.Gson(); + + public TerrainPanelController(ToolContext ctx, Runnable scheduleAutoSave) { + this.ctx = ctx; + this.input = ctx.input(); + this.scheduleAutoSave = scheduleAutoSave; + } + + public void setGrassController(de.blight.editor.toolset.grass.GrassController gc) { + this.grassController = gc; + } + + // ── Kleine Helpers für Baum-Parameter-Panel ────────────────────────────── + + private static Label bold(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + return l; + } + + private boolean isObjectMode() { + return input.activeLayer == SharedInput.LAYER_OBJECTS + || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT + || input.activeLayer == SharedInput.LAYER_AUSWAHL; + } + + private static Label styledHint(String text) { + Label l = new Label(text); + l.setStyle("-fx-text-fill: #333333; -fx-font-size: 11;"); + return l; + } + + /** + * Wraps a Slider in an HBox with a synced TextField on the right. + * The slider's own valueProperty listeners fire normally; this just adds a text field. + */ + private static HBox withField(Slider slider, String fmt) { + slider.setMaxWidth(Double.MAX_VALUE); + TextField tf = new TextField(String.format(fmt, slider.getValue())); + tf.setPrefWidth(62); + tf.setMaxWidth(62); + tf.setStyle("-fx-font-size: 11;"); + slider.valueProperty().addListener((o, ov, nv) -> + tf.setText(String.format(fmt, nv.doubleValue()))); + Runnable commit = () -> { + try { + double v = Math.max(slider.getMin(), Math.min(slider.getMax(), + Double.parseDouble(tf.getText().replace(',', '.')))); + slider.setValue(v); + } catch (NumberFormatException ignored) { + tf.setText(String.format(fmt, slider.getValue())); + } + }; + tf.setOnAction(e -> commit.run()); + tf.focusedProperty().addListener((o, ov, focused) -> { if (!focused) commit.run(); }); + HBox row = new HBox(5, slider, tf); + HBox.setHgrow(slider, Priority.ALWAYS); + return row; + } + + private static VBox paramSlider(String label, double min, double max, double init, + Consumer setter) { + Label name = new Label(label); + name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + Slider slider = new Slider(min, max, init); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setMajorTickUnit((max - min) / 2.0); + slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue())); + return new VBox(3, name, withField(slider, "%.3f")); + } + + + + // ── EZ-Tree UI-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: #1a3a6a;"); + VBox.setMargin(l, new Insets(8, 0, 0, 0)); + return l; + } + + private static Spinner intSpinner(int min, int max, int init) { + Spinner sp = new Spinner<>(min, max, Math.max(min, Math.min(max, init))); + sp.setEditable(true); + sp.setMaxWidth(Double.MAX_VALUE); + return sp; + } + + private static VBox ezFloat(String label, double min, double max, double init, + java.util.function.Consumer setter) { + Label name = new Label(label); + name.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + Slider slider = new Slider(min, max, Math.max(min, Math.min(max, init))); + slider.setShowTickMarks(false); + slider.valueProperty().addListener((o, a, b) -> setter.accept(b.floatValue())); + return new VBox(2, name, withField(slider, "%.3f")); + } + + private static String pathToName(String path, String[] paths, String[] names) { + if (path == null) return names[0]; + for (int i = 0; i < paths.length; i++) { + if (path.equals(paths[i])) return names[i]; + } + return names[0]; + } + + public static void applyModeButtonStyle(ToggleButton btn, boolean active) { + btn.setStyle(active + ? "-fx-background-color: #4a90d9; -fx-text-fill: white; -fx-background-radius: 4;" + : "-fx-background-color: #d0d0d0; -fx-text-fill: #333; -fx-background-radius: 4;"); + } + + public void showToolParameters(VBox panel, EditorTool tool) { + toolPanelRef = panel; + panel.getChildren().clear(); + + Label toolTitle = new Label(tool.getName()); + toolTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111111;"); + panel.getChildren().addAll(toolTitle, new Separator()); + + // ── TextureTool: Radius/Stärke oben, dann 4×3-Grid + aktiver Slot ────── + if (tool instanceof de.blight.editor.tool.TextureTool tt) { + for (ToolParameter p : tt.getParameters()) { + Label name = new Label(p.getName()); + name.setStyle("-fx-text-fill: #111111;"); + Slider slider = new Slider(p.getMin(), p.getMax(), p.getValue()); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setMajorTickUnit((p.getMax() - p.getMin()) / 2); + slider.valueProperty().addListener((obs, oldV, newV) -> p.setValue(newV.doubleValue())); + panel.getChildren().add(new VBox(3, name, withField(slider, "%.3f"))); + } + panel.getChildren().add(new Separator()); + javafx.scene.Node texUI = buildTextureChoiceWithActiveSlotUI(tt.textureIndex); + VBox.setVgrow(texUI, Priority.ALWAYS); + panel.getChildren().add(texUI); + return; + } + + for (ChoiceToolParameter param : tool.getChoiceParameters()) { + Label nameLabel = new Label(param.getName()); + nameLabel.setStyle("-fx-text-fill: #111111;"); + + if (tool instanceof de.blight.editor.tool.TextureTool + && param == ((de.blight.editor.tool.TextureTool) tool).textureIndex) { + panel.getChildren().addAll(nameLabel, buildTextureChoiceUI(param)); + } else if (tool instanceof de.blight.editor.tool.VoxelTool vt2 + && param == vt2.mode) { + panel.getChildren().addAll(nameLabel, buildVoxelModeUI(param)); + } else if (param.getImagePaths() != null) { + String[] paths = param.getImagePaths(); + String[] labels = param.getChoices(); + ToggleGroup tg = new ToggleGroup(); + ToggleButton[] buttons = new ToggleButton[paths.length]; + HBox row = new HBox(3); + + for (int j = 0; j < paths.length; j++) { + final int index = j; + ToggleButton btn = new ToggleButton(); + btn.setToggleGroup(tg); + btn.setTooltip(new Tooltip(labels[j])); + btn.setMinSize(50, 50); + btn.setMaxSize(50, 50); + + String imgUrlStr = null; + URL resUrl = TerrainPanelController.class.getResource("/" + paths[j]); + if (resUrl != null) { + imgUrlStr = resUrl.toString(); + } else { + File imgFile = new File(paths[j]).getAbsoluteFile(); + if (imgFile.exists()) imgUrlStr = imgFile.toURI().toString(); + } + if (imgUrlStr != null) { + Image img = new Image(imgUrlStr, 38, 38, true, true); + if (!img.isError()) btn.setGraphic(new ImageView(img)); + else btn.setText(labels[j].substring(0, 2)); + } else { + btn.setText(labels[j].substring(0, 2)); + } + + boolean initially = (j == param.getSelectedIndex()); + btn.setSelected(initially); + applyModeButtonStyle(btn, initially); + btn.selectedProperty().addListener((obs, was, isNow) -> { + applyModeButtonStyle(btn, isNow); + if (isNow) param.setSelectedIndex(index); + }); + + buttons[j] = btn; + row.getChildren().add(btn); + } + + tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { + if (newT == null) tg.selectToggle(oldT); + }); + panel.getChildren().addAll(nameLabel, row); + } else { + ChoiceBox choiceBox = new ChoiceBox<>(); + choiceBox.getItems().addAll(param.getChoices()); + choiceBox.getSelectionModel().select(param.getSelectedIndex()); + choiceBox.setMaxWidth(Double.MAX_VALUE); + choiceBox.getSelectionModel().selectedIndexProperty().addListener( + (obs, oldV, newV) -> param.setSelectedIndex(newV.intValue())); + panel.getChildren().addAll(nameLabel, choiceBox); + } + } + + for (ToolParameter param : tool.getParameters()) { + Label name = new Label(param.getName()); + name.setStyle("-fx-text-fill: #111111;"); + + if (tool instanceof de.blight.editor.tool.VoxelTool vtR + && param == vtR.brushRadius) { + // Ganzzahliger Slider: snapToTicks mit majorTickUnit=1 → nur ganze Zahlen + int initR = Math.max(1, (int) Math.round(param.getValue())); + Slider slider = new Slider(1, 30, initR); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setMajorTickUnit(5); + slider.setMinorTickCount(4); + slider.setSnapToTicks(true); + slider.setBlockIncrement(1); + slider.valueProperty().addListener((obs, oldV, newV) -> + param.setValue(Math.round(newV.doubleValue()))); + panel.getChildren().add(new VBox(3, name, withField(slider, "%.0f"))); + + } else if (tool instanceof de.blight.editor.tool.VoxelTool vtP + && param == vtP.plateauTarget) { + // x.5-gestufter Slider: min=0.5, Schritt=1 → 0.5, 1.5, 2.5, … + double initSnap = Math.round(param.getValue() - 0.5) + 0.5; + initSnap = Math.max(0.5, Math.min(49.5, initSnap)); + Slider slider = new Slider(0.5, 49.5, initSnap); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setMajorTickUnit(5); + slider.setMinorTickCount(4); + slider.setSnapToTicks(true); + slider.setBlockIncrement(1.0); + slider.valueProperty().addListener((obs, oldV, newV) -> { + double sv = Math.round(newV.doubleValue() - 0.5) + 0.5; + param.setValue(sv); + }); + panel.getChildren().add(new VBox(3, name, withField(slider, "%.1f"))); + + } else { + Slider slider = new Slider(param.getMin(), param.getMax(), param.getValue()); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setMajorTickUnit((param.getMax() - param.getMin()) / 2); + slider.valueProperty().addListener((obs, oldV, newV) -> param.setValue(newV.doubleValue())); + panel.getChildren().add(new VBox(3, name, withField(slider, "%.3f"))); + } + } + + // Textur-Picker (nur beim GrassTool) + if (tool instanceof GrassTool) { + javafx.scene.Node grassUI = grassController.buildGrassTextureUI(); + VBox.setVgrow(grassUI, javafx.scene.layout.Priority.ALWAYS); + panel.getChildren().addAll(new Separator(), grassUI); + } + + // Voxel-Textur-Slots (nur beim VoxelTool) + if (tool instanceof de.blight.editor.tool.VoxelTool) { + panel.getChildren().addAll(new Separator(), buildVoxelTextureSlotsUI()); + } + + // Backen-Button (nur beim VoxelTool) + if (tool instanceof de.blight.editor.tool.VoxelTool) { + Label bakeStatus = new Label(""); + bakeStatus.setWrapText(true); + bakeStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); + + javafx.scene.control.ProgressBar bakeBar = new javafx.scene.control.ProgressBar(0); + bakeBar.setMaxWidth(Double.MAX_VALUE); + bakeBar.setVisible(false); + bakeBar.setManaged(false); + + Label bakeBarLabel = new Label(""); + bakeBarLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #444;"); + bakeBarLabel.setVisible(false); + bakeBarLabel.setManaged(false); + + // ── Bake-Parameter ──────────────────────────────────────────────── + Label smoothLbl = new Label(String.format("Glättung: %.1f", input.bakeSmoothStrength)); + smoothLbl.setStyle("-fx-font-size: 11;"); + javafx.scene.control.Slider smoothSlider = + new javafx.scene.control.Slider(0, 1, input.bakeSmoothStrength); + smoothSlider.setShowTickMarks(false); + smoothSlider.valueProperty().addListener((obs, o, n) -> { + input.bakeSmoothStrength = n.floatValue(); + smoothLbl.setText(String.format("Glättung: %.1f", n.floatValue())); + }); + + Label cliffLbl = new Label(String.format("Klippen-Noise: %.1f", input.bakeCliffNoiseStrength)); + cliffLbl.setStyle("-fx-font-size: 11;"); + javafx.scene.control.Slider cliffSlider = + new javafx.scene.control.Slider(0, 1, input.bakeCliffNoiseStrength); + cliffSlider.setShowTickMarks(false); + cliffSlider.valueProperty().addListener((obs, o, n) -> { + input.bakeCliffNoiseStrength = n.floatValue(); + cliffLbl.setText(String.format("Klippen-Noise: %.1f", n.floatValue())); + }); + + Button bakeBtn = new Button("Voxels backen (J3O)"); + bakeBtn.setMaxWidth(Double.MAX_VALUE); + bakeBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;"); + bakeBtn.setOnAction(e -> { + input.bakeVoxelsRequested = true; + bakeStatus.setText(""); + bakeBtn.setDisable(true); + bakeBar.setProgress(0); + bakeBar.setVisible(true); + bakeBar.setManaged(true); + bakeBarLabel.setVisible(true); + bakeBarLabel.setManaged(true); + bakeBarLabel.setText("Starte..."); + }); + + javafx.animation.Timeline bakePoller = new javafx.animation.Timeline( + new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { + int total = input.bakeTotal; + int done = input.bakeDone; + if (total > 0) { + bakeBar.setProgress((double) done / total); + bakeBarLabel.setText(done + " / " + total + " Chunks"); + } + String msg = input.bakeStatusMsg; + if (msg != null) { + input.bakeStatusMsg = null; + input.bakeTotal = 0; + input.bakeDone = 0; + bakeStatus.setText(msg); + bakeBar.setVisible(false); + bakeBar.setManaged(false); + bakeBarLabel.setVisible(false); + bakeBarLabel.setManaged(false); + bakeBtn.setDisable(false); + } + }) + ); + bakePoller.setCycleCount(javafx.animation.Animation.INDEFINITE); + bakePoller.play(); + + // ── Selektion: Löschen / Bake rückgängig (Modus "Baked selektieren") ── + Label selInfoLabel = new Label("Keine Auswahl"); + selInfoLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); + Label selHintLabel = new Label("LMK: Struktur wählen | RMK: Auswahl leeren"); + selHintLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #888;"); + selHintLabel.setWrapText(true); + + Button deleteBtn = new Button("Löschen"); + deleteBtn.setMaxWidth(Double.MAX_VALUE); + deleteBtn.setStyle("-fx-background-color: #ba4a4a; -fx-text-fill: white;"); + deleteBtn.setDisable(true); + deleteBtn.setOnAction(e -> { + int n = input.markedDeleteBakedKeys.size(); + javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.CONFIRMATION, + n + " Chunk" + (n != 1 ? "s" : "") + " unwiderruflich löschen?", + javafx.scene.control.ButtonType.YES, + javafx.scene.control.ButtonType.NO); + confirm.setTitle("Gebackene Chunks löschen"); + confirm.setHeaderText(null); + confirm.initOwner((javafx.stage.Stage) ctx.stageSupplier().get()); + confirm.showAndWait().ifPresent(btn -> { + if (btn == javafx.scene.control.ButtonType.YES) { + input.deleteMarkedBakedRequested = true; + } + }); + }); + + Button revertBtn = new Button("Bake rückgängig"); + revertBtn.setMaxWidth(Double.MAX_VALUE); + revertBtn.setStyle("-fx-background-color: #7a5a2a; -fx-text-fill: white;"); + revertBtn.setDisable(true); + revertBtn.setOnAction(e -> input.revertMarkedBakedRequested = true); + + javafx.animation.Timeline selPoller = new javafx.animation.Timeline( + new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { + int n = input.markedDeleteBakedKeys.size(); + boolean has = n > 0; + selInfoLabel.setText(has + ? n + " Chunk" + (n != 1 ? "s" : "") + " ausgewählt" + : "Keine Auswahl"); + deleteBtn.setDisable(!has); + revertBtn.setDisable(!has); + }) + ); + selPoller.setCycleCount(javafx.animation.Animation.INDEFINITE); + selPoller.play(); + + // ── Unterirdische Voxel bereinigen ────────────────────────────────── + Label cleanupStatus = new Label(""); + cleanupStatus.setWrapText(true); + cleanupStatus.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); + + Button cleanupBtn = new Button("Unterirdische Voxel bereinigen"); + cleanupBtn.setMaxWidth(Double.MAX_VALUE); + cleanupBtn.setOnAction(e -> { + input.cleanupUndergroundStatus = null; + input.cleanupUndergroundRequested = true; + cleanupBtn.setDisable(true); + cleanupStatus.setText("Läuft..."); + }); + + javafx.animation.Timeline cleanupPoller = new javafx.animation.Timeline( + new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), ev -> { + String msg = input.cleanupUndergroundStatus; + if (msg != null) { + input.cleanupUndergroundStatus = null; + cleanupStatus.setText(msg); + cleanupBtn.setDisable(false); + } + }) + ); + cleanupPoller.setCycleCount(javafx.animation.Animation.INDEFINITE); + cleanupPoller.play(); + + panel.getChildren().addAll(new Separator(), + smoothLbl, smoothSlider, cliffLbl, cliffSlider, + bakeBtn, bakeBar, bakeBarLabel, bakeStatus, + new Separator(), cleanupBtn, cleanupStatus, + new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn); + } + } + + // ── Gras-Textur-Picker (Multi-Slot) ────────────────────────────────────── + + // ── Textur-Slot-Konfigurator ────────────────────────────────────────────── + + /** Baut das 8-Slot-Thumbnail-Grid für das TextureTool. */ + private javafx.scene.Node buildTextureChoiceUI(ChoiceToolParameter param) { + String[] defaultsTerrain = { + "Textures/Terrain/splat/grass.jpg", + "Textures/Terrain/Rock2/rock.jpg", + "Textures/Terrain/splat/dirt.jpg", + "" + }; + + String[] allPaths = new String[12]; + String[] allLabels = new String[12]; + for (int i = 0; i < 4; i++) { + String p = input.terrainTexturePaths[i]; + allPaths[i] = (p != null && !p.isEmpty()) ? p : defaultsTerrain[i]; + allLabels[i] = "S" + (i + 1) + " " + labelFromPath(allPaths[i]); + } + for (int i = 0; i < 4; i++) { + String p = input.upperTexturePaths[i]; + allPaths[4 + i] = (p != null && !p.isEmpty()) ? p : ""; + allLabels[4 + i] = "S" + (i + 5) + " " + labelFromPath(allPaths[4 + i]); + } + for (int i = 0; i < 4; i++) { + String p = input.thirdTexturePaths[i]; + allPaths[8 + i] = (p != null && !p.isEmpty()) ? p : ""; + allLabels[8 + i] = "S" + (i + 9) + " " + labelFromPath(allPaths[8 + i]); + } + + ToggleGroup tg = new ToggleGroup(); + javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); + tile.setHgap(4); + tile.setVgap(4); + tile.setPrefColumns(4); + + for (int j = 0; j < 12; j++) { + final int idx = j; + String path = allPaths[j]; + + ToggleButton btn = new ToggleButton(); + btn.setToggleGroup(tg); + btn.setPrefSize(56, 76); + btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + btn.setTooltip(new Tooltip("Slot " + (j + 1) + ": " + (path.isEmpty() ? "(leer)" : path))); + + VBox content = new VBox(2); + content.setAlignment(Pos.CENTER); + + if (!path.isEmpty()) { + String imgUrl = resolveImageUrl(path); + if (imgUrl != null) { + Image img = new Image(imgUrl, 44, 44, true, true); + ImageView iv = new ImageView(img.isError() ? null : img); + iv.setFitWidth(44); + iv.setFitHeight(44); + content.getChildren().add(img.isError() ? emptySlotPlaceholder(44) : iv); + } else { + content.getChildren().add(emptySlotPlaceholder(44)); + } + } else { + content.getChildren().add(emptySlotPlaceholder(44)); + } + + Label lbl = new Label(allLabels[j]); + lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;"); + lbl.setMaxWidth(54); + content.getChildren().add(lbl); + + btn.setGraphic(content); + boolean initially = (j == param.getSelectedIndex()); + btn.setSelected(initially); + applyModeButtonStyle(btn, initially); + btn.selectedProperty().addListener((obs, was, isNow) -> { + applyModeButtonStyle(btn, isNow); + if (isNow) param.setSelectedIndex(idx); + }); + tile.getChildren().add(btn); + } + + tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { + if (newT == null) tg.selectToggle(oldT); + }); + return tile; + } + + private javafx.scene.Node buildVoxelModeUI(ChoiceToolParameter param) { + String[] labels = param.getChoices(); + String[] imgPaths = param.getImagePaths(); + ToggleGroup tg = new ToggleGroup(); + javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); + tile.setHgap(4); + tile.setVgap(4); + tile.setPrefColumns(6); + + for (int j = 0; j < labels.length; j++) { + final int idx = j; + String path = (imgPaths != null && j < imgPaths.length) ? imgPaths[j] : null; + + ToggleButton btn = new ToggleButton(); + btn.setToggleGroup(tg); + btn.setPrefSize(44, 56); + btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + btn.setTooltip(new Tooltip(labels[j])); + + VBox content = new VBox(2); + content.setAlignment(Pos.CENTER); + + boolean hasImage = false; + if (path != null) { + URL resUrl = TerrainPanelController.class.getResource("/" + path); + String imgUrl = resUrl != null ? resUrl.toString() : null; + if (imgUrl == null) { + File f = new File(path).getAbsoluteFile(); + if (f.exists()) imgUrl = f.toURI().toString(); + } + if (imgUrl != null) { + Image img = new Image(imgUrl, 32, 32, true, true); + if (!img.isError()) { + ImageView iv = new ImageView(img); + iv.setFitWidth(32); iv.setFitHeight(32); + content.getChildren().add(iv); + hasImage = true; + } + } + } + if (!hasImage) { + boolean isRemove = labels[j].equals("Entfernen"); + Label icon = new Label(isRemove ? "✕" : "+"); + icon.setStyle("-fx-font-size: 16; -fx-text-fill: " + (isRemove ? "#cc2222" : "#228822") + ";"); + content.getChildren().add(icon); + } + Label lbl = new Label(labels[j]); + lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;"); + content.getChildren().add(lbl); + btn.setGraphic(content); + + boolean initially = (j == param.getSelectedIndex()); + btn.setSelected(initially); + applyModeButtonStyle(btn, initially); + btn.selectedProperty().addListener((obs, was, isNow) -> { + applyModeButtonStyle(btn, isNow); + if (isNow) param.setSelectedIndex(idx); + }); + tile.getChildren().add(btn); + } + tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { + if (newT == null) tg.selectToggle(oldT); + }); + return tile; + } + + private VBox buildVoxelTextureSlotsUI() { + VBox box = new VBox(6); + box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); + Label title = new Label("Voxel-Texturen (aus Textur-Brush)"); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); + box.getChildren().add(title); + + String[] roleLabels = { "Flache Flächen", "Steile Wände", "Decke / Unterseite" }; + int[] current = { input.voxelFlatSlot, input.voxelSteepSlot, input.voxelCeilSlot }; + + // Alle 12 Textur-Slots aus dem Brush zusammenbauen (Slot 0-3 unten, 4-7 oben, 8-11 dritte) + String[] slotNames = new String[13]; // Index 0 = "(keiner)", 1..12 = Slot 0..11 + slotNames[0] = "(keiner)"; + for (int s = 0; s < 4; s++) { + String p = input.terrainTexturePaths[s]; + slotNames[s + 1] = "Slot " + s + (p != null && !p.isEmpty() + ? ": " + java.nio.file.Paths.get(p).getFileName() : " (leer)"); + } + for (int s = 0; s < 4; s++) { + String p = input.upperTexturePaths[s]; + slotNames[s + 5] = "Slot " + (s + 4) + (p != null && !p.isEmpty() + ? ": " + java.nio.file.Paths.get(p).getFileName() : " (leer)"); + } + for (int s = 0; s < 4; s++) { + String p = input.thirdTexturePaths[s]; + slotNames[s + 9] = "Slot " + (s + 8) + (p != null && !p.isEmpty() + ? ": " + java.nio.file.Paths.get(p).getFileName() : " (leer)"); + } + + for (int i = 0; i < 3; i++) { + final int roleIdx = i; + Label roleLbl = new Label(roleLabels[i]); + roleLbl.setStyle("-fx-text-fill: #444; -fx-font-size: 11;"); + + javafx.scene.control.ComboBox combo = new javafx.scene.control.ComboBox<>(); + combo.getItems().addAll(slotNames); + // current[i] ist -1..7 → ComboBox-Index 0..(current+1) + combo.getSelectionModel().select(Math.max(0, current[i] + 1)); + combo.setMaxWidth(Double.MAX_VALUE); + combo.setOnAction(ev -> { + int chosen = combo.getSelectionModel().getSelectedIndex() - 1; // -1 = keiner + switch (roleIdx) { + case 0 -> input.voxelFlatSlot = chosen; + case 1 -> input.voxelSteepSlot = chosen; + case 2 -> input.voxelCeilSlot = chosen; + } + input.voxelTexturesChanged = true; + }); + + VBox roleBox = new VBox(2, roleLbl, combo); + box.getChildren().add(roleBox); + } + return box; + } + + private javafx.scene.Node buildVoxelSplatSectionUI() { + VBox box = new VBox(4); + Label title = new Label("Voxel-Splatmap (Slots 2–4)"); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111; -fx-font-size: 11;"); + Label hint = new Label("Slot 1 (Basis) = Voxel-Flat-Slot oben"); + hint.setStyle("-fx-text-fill: #888; -fx-font-size: 10;"); + box.getChildren().addAll(title, hint); + + String[] slotLabels = {"Slot 2 (G)", "Slot 3 (B)", "Slot 4 (A)"}; + for (int i = 1; i <= 3; i++) { + final int si = i; + String[] paths = input.voxelSplatTexturePaths; + String cur = (paths != null && i < paths.length && paths[i] != null) ? paths[i] : ""; + Label nameLbl = new Label(slotLabels[i - 1] + ": " + labelFromPath(cur)); + nameLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #444;"); + nameLbl.setMaxWidth(180); + Button chooseBtn = new Button("..."); + chooseBtn.setOnAction(e -> { + de.blight.editor.ui.TextureChooser chooser = + new de.blight.editor.ui.TextureChooser(ctx.assetRoot(), false); + chooser.showAndWait().ifPresent(chosenPath -> { + String[] copy = input.voxelSplatTexturePaths.clone(); + copy[si] = chosenPath; + input.voxelSplatTexturePaths = copy; + input.voxelSplatTexturesChanged = true; + nameLbl.setText(slotLabels[si - 1] + ": " + labelFromPath(chosenPath)); + }); + }); + box.getChildren().add(new HBox(4, nameLbl, chooseBtn)); + } + return box; + } + + private javafx.scene.Node buildVoxelTextureChoiceUI(ChoiceToolParameter param) { + ToggleGroup tg = new ToggleGroup(); + javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); + tile.setHgap(4); + tile.setVgap(4); + tile.setPrefColumns(4); + + for (int j = 0; j < 4; j++) { + final int idx = j; + String path = (j < input.terrainTexturePaths.length + && input.terrainTexturePaths[j] != null + && !input.terrainTexturePaths[j].isEmpty()) + ? input.terrainTexturePaths[j] : ""; + + ToggleButton btn = new ToggleButton(); + btn.setToggleGroup(tg); + btn.setPrefSize(56, 76); + btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + btn.setTooltip(new Tooltip("Slot " + (j + 1) + ": " + (path.isEmpty() ? "(leer)" : path))); + + VBox content = new VBox(2); + content.setAlignment(Pos.CENTER); + + if (!path.isEmpty()) { + String imgUrl = resolveImageUrl(path); + if (imgUrl != null) { + Image img = new Image(imgUrl, 44, 44, true, true); + ImageView iv = new ImageView(img.isError() ? null : img); + iv.setFitWidth(44); + iv.setFitHeight(44); + content.getChildren().add(img.isError() ? emptySlotPlaceholder(44) : iv); + } else { + content.getChildren().add(emptySlotPlaceholder(44)); + } + } else { + content.getChildren().add(emptySlotPlaceholder(44)); + } + + Label lbl = new Label("S" + (j + 1) + " " + labelFromPath(path)); + lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;"); + lbl.setMaxWidth(54); + content.getChildren().add(lbl); + + btn.setGraphic(content); + boolean initially = (j == param.getSelectedIndex()); + btn.setSelected(initially); + applyModeButtonStyle(btn, initially); + btn.selectedProperty().addListener((obs, was, isNow) -> { + applyModeButtonStyle(btn, isNow); + if (isNow) param.setSelectedIndex(idx); + }); + tile.getChildren().add(btn); + } + + tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { + if (newT == null) tg.selectToggle(oldT); + }); + return tile; + } + + private javafx.scene.layout.Region emptySlotPlaceholder(double size) { + javafx.scene.layout.Region r = new javafx.scene.layout.Region(); + r.setPrefSize(size, size); + r.setMaxSize(size, size); + r.setStyle("-fx-background-color: #cccccc; -fx-border-color: #999; -fx-border-width: 1;"); + return r; + } + + private String resolveImageUrl(String path) { + if (path == null || path.isEmpty()) return null; + URL res = TerrainPanelController.class.getResource("/" + path); + if (res != null) return res.toString(); + File f = ctx.assetRoot().resolve(path).toFile(); + if (f.exists()) return f.toURI().toString(); + return null; + } + + private String labelFromPath(String path) { + if (path == null || path.isEmpty()) return "(leer)"; + Path p = ctx.assetRoot().resolve(path.replace('/', java.io.File.separatorChar)); + // Wenn die Datei in einem Textur-Set-Ordner liegt, den Ordnernamen verwenden + Path parent = p.getParent(); + if (parent != null && Files.exists(parent.resolve("textureset.json"))) { + return parent.getFileName().toString(); + } + String name = p.getFileName().toString(); + int dot = name.lastIndexOf('.'); + return dot > 0 ? name.substring(0, dot) : name; + } + + /** Baut die 4-Slot-Auswahl für Terrain (isUpper=false) oder Gebirge (isUpper=true). */ + private VBox buildTextureSlotsUI(boolean isUpper) { + VBox box = new VBox(5); + box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); + Label title = new Label(isUpper ? "Texturen Slots 5-8" : "Texturen Slots 1-4"); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); + box.getChildren().add(title); + + String[] paths = isUpper ? input.upperTexturePaths : input.terrainTexturePaths; + String[] defaults = isUpper + ? new String[]{"Textures/Terrain/Rock2/rock.jpg","","",""} + : new String[]{"Textures/Terrain/splat/grass.jpg","Textures/Terrain/Rock2/rock.jpg","Textures/Terrain/splat/dirt.jpg",""}; + + final int group = isUpper ? 1 : 0; + + for (int i = 0; i < 4; i++) { + final int slot = i; + final int globalIdx = (isUpper ? 4 : 0) + i; + + // ── Diffuse-Zeile ───────────────────────────────────────────────── + String current = (paths[i] != null && !paths[i].isEmpty()) + ? labelFromPath(paths[i]) + : (defaults[i].isEmpty() ? "(kein)" : labelFromPath(defaults[i]) + " (std)"); + + Label slotLbl = new Label("Slot " + (isUpper ? i + 5 : i + 1) + ": " + current); + slotLbl.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); + slotLbl.setMaxWidth(Double.MAX_VALUE); + slotLbl.setMinWidth(0); + slotLbl.setEllipsisString("…"); + + javafx.scene.control.Button chooseBtn = new javafx.scene.control.Button("Wählen"); + chooseBtn.setOnAction(ev -> openTexturePicker(slotLbl, slot, isUpper)); + + javafx.scene.control.Button clearBtn = new javafx.scene.control.Button("✕"); + clearBtn.setTooltip(new Tooltip("Auf Standard zurücksetzen")); + clearBtn.setOnAction(ev -> { + String[] p = isUpper ? input.upperTexturePaths : input.terrainTexturePaths; + p[slot] = ""; + if (isUpper) input.upperTexturesChanged = true; + else input.terrainTexturesChanged = true; + showToolParameters(toolPanelRef, input.activeTool); + }); + + HBox row = new HBox(4, slotLbl, chooseBtn, clearBtn); + HBox.setHgrow(slotLbl, Priority.ALWAYS); + row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + row.setOnContextMenuRequested(ev -> { + String tex = (isUpper ? input.upperTexturePaths : input.terrainTexturePaths)[slot]; + if (tex != null && !tex.isEmpty()) + showTextureSlotDetails(tex, slot, group, row, ev.getScreenX(), ev.getScreenY()); + ev.consume(); + }); + + box.getChildren().add(row); + } + return box; + } + + /** Öffnet den TextureChooser-Dialog und setzt den gewählten Pfad für den Slot. */ + private void openTexturePicker(Label slotLabel, int slot, boolean isUpper) { + TextureChooser chooser = new TextureChooser(ctx.assetRoot(), true); + if ((javafx.stage.Stage) ctx.stageSupplier().get() != null) chooser.initOwner((javafx.stage.Stage) ctx.stageSupplier().get()); + chooser.showAndWait().ifPresent(path -> { + String[] paths = isUpper ? input.upperTexturePaths : input.terrainTexturePaths; + paths[slot] = path; + if (isUpper) input.upperTexturesChanged = true; + else input.terrainTexturesChanged = true; + applyMetaToSlot(path, slot, isUpper ? 1 : 0); + showToolParameters(toolPanelRef, input.activeTool); + }); + } + + /** + * Liest die textureset.json für texRelPath und trägt Normal- und Displacement-Map + * automatisch in den entsprechenden Slot ein. + * group: 0 = terrain, 1 = upper, 2 = third + */ + private void applyMetaToSlot(String texRelPath, int slot, int group) { + if (texRelPath == null || texRelPath.isEmpty()) return; + Path absPath = ctx.assetRoot().resolve(texRelPath.replace('/', java.io.File.separatorChar)); + TextureMapData meta = loadTexMeta(absPath); + boolean changed = false; + + if (!meta.normalMap.isEmpty()) { + switch (group) { + case 0 -> { input.terrainNormalMapPaths[slot] = meta.normalMap; input.terrainNormalMapsChanged = true; } + case 1 -> { input.upperNormalMapPaths[slot] = meta.normalMap; input.upperNormalMapsChanged = true; } + case 2 -> { input.thirdNormalMapPaths[slot] = meta.normalMap; input.thirdNormalMapsChanged = true; } + } + changed = true; + } + if (!meta.displacementMap.isEmpty()) { + switch (group) { + case 0 -> input.terrainDisplacementMapPaths[slot] = meta.displacementMap; + case 1 -> input.upperDisplacementMapPaths[slot] = meta.displacementMap; + case 2 -> input.thirdDisplacementMapPaths[slot] = meta.displacementMap; + } + changed = true; + } + if (changed) input.voxelTexturesChanged = true; + } + + /** Baut die 4-Slot-Auswahl für die dritte Gruppe (Slots 9-12). */ + private VBox buildThirdTextureSlotsUI() { + VBox box = new VBox(5); + box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); + Label title = new Label("Texturen Slots 9-12"); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); + box.getChildren().add(title); + + String[] paths = input.thirdTexturePaths; + + for (int i = 0; i < 4; i++) { + final int slot = i; + final int globalIdx = 8 + i; + + String current = (paths[i] != null && !paths[i].isEmpty()) + ? labelFromPath(paths[i]) + : "(kein)"; + + Label slotLbl = new Label("Slot " + (i + 9) + ": " + current); + slotLbl.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); + slotLbl.setMaxWidth(Double.MAX_VALUE); + slotLbl.setMinWidth(0); + slotLbl.setEllipsisString("…"); + + javafx.scene.control.Button chooseBtn = new javafx.scene.control.Button("Wählen"); + chooseBtn.setOnAction(ev -> { + TextureChooser chooser = new TextureChooser(ctx.assetRoot(), true); + if ((javafx.stage.Stage) ctx.stageSupplier().get() != null) chooser.initOwner((javafx.stage.Stage) ctx.stageSupplier().get()); + chooser.showAndWait().ifPresent(p -> { + input.thirdTexturePaths[slot] = p; + input.thirdTexturesChanged = true; + applyMetaToSlot(p, slot, 2); + showToolParameters(toolPanelRef, input.activeTool); + }); + }); + + javafx.scene.control.Button clearBtn = new javafx.scene.control.Button("✕"); + clearBtn.setTooltip(new Tooltip("Auf Standard zurücksetzen")); + clearBtn.setOnAction(ev -> { + input.thirdTexturePaths[slot] = ""; + input.thirdTexturesChanged = true; + showToolParameters(toolPanelRef, input.activeTool); + }); + + HBox row = new HBox(4, slotLbl, chooseBtn, clearBtn); + HBox.setHgrow(slotLbl, Priority.ALWAYS); + row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + row.setOnContextMenuRequested(ev -> { + String tex = input.thirdTexturePaths[slot]; + if (tex != null && !tex.isEmpty()) + showTextureSlotDetails(tex, slot, 2, row, ev.getScreenX(), ev.getScreenY()); + ev.consume(); + }); + box.getChildren().add(row); + } + return box; + } + + private String texPathForSlot(int globalIdx) { + if (globalIdx < 4) return input.terrainTexturePaths[globalIdx]; + else if (globalIdx < 8) return input.upperTexturePaths[globalIdx - 4]; + else return input.thirdTexturePaths[globalIdx - 8]; + } + + private void setTexPathForSlot(int globalIdx, String path) { + if (globalIdx < 4) { + input.terrainTexturePaths[globalIdx] = path; + input.terrainTexturesChanged = true; + } else if (globalIdx < 8) { + input.upperTexturePaths[globalIdx - 4] = path; + input.upperTexturesChanged = true; + } else { + input.thirdTexturePaths[globalIdx - 8] = path; + input.thirdTexturesChanged = true; + } + } + + /** 4×3-Textur-Grid + darunter UV-Scale und Textur-Button für den aktiven Slot. */ + private javafx.scene.Node buildTextureChoiceWithActiveSlotUI(ChoiceToolParameter param) { + VBox outer = new VBox(6); + + // Aktiver-Slot-Bereich (unten) — wird bei Slot-Wechsel neu befüllt + VBox activeBox = new VBox(4); + activeBox.setPadding(new Insets(4, 0, 0, 0)); + + Runnable[] refresh = { null }; + refresh[0] = () -> { + activeBox.getChildren().clear(); + int sel = param.getSelectedIndex(); + String path = texPathForSlot(sel); + boolean hasTexture = path != null && !path.isEmpty(); + int group = sel < 4 ? 0 : sel < 8 ? 1 : 2; + int slotInGrp = sel % 4; + + Label scaleLbl = new Label("UV-Scale Slot " + (sel + 1) + " (m):"); + scaleLbl.setStyle("-fx-font-size: 11; -fx-text-fill: #333;"); + float curScale = (input.diffuseScales != null && sel < input.diffuseScales.length) + ? input.diffuseScales[sel] : 8f; + javafx.scene.control.Spinner scaleSpin = + new javafx.scene.control.Spinner<>(0.5, 1024.0, curScale, 0.5); + scaleSpin.setEditable(true); + scaleSpin.setMaxWidth(Double.MAX_VALUE); + scaleSpin.setDisable(!hasTexture); + scaleSpin.valueProperty().addListener((ob, ov, nv) -> { + if (nv == null) return; + input.diffuseScales[sel] = nv.floatValue(); + input.diffuseScalesChanged = true; + }); + + Button chooseBtn = new Button("Textur ändern..."); + chooseBtn.setMaxWidth(Double.MAX_VALUE); + chooseBtn.setDisable(!hasTexture); + chooseBtn.setOnAction(ev -> { + TextureChooser chooser = new TextureChooser(ctx.assetRoot(), true); + if ((javafx.stage.Stage) ctx.stageSupplier().get() != null) chooser.initOwner((javafx.stage.Stage) ctx.stageSupplier().get()); + chooser.showAndWait().ifPresent(p -> { + setTexPathForSlot(sel, p); + applyMetaToSlot(p, slotInGrp, group); + param.setSelectedIndex(sel); + showToolParameters(toolPanelRef, input.activeTool); + }); + }); + + activeBox.getChildren().addAll(new Separator(), scaleLbl, scaleSpin, chooseBtn); + }; + + // 4×3 Grid (wie buildTextureChoiceUI, aber mit Refresh-Hook) + String[] defaultsTerrain = { + "Textures/Terrain/splat/grass.jpg", "Textures/Terrain/Rock2/rock.jpg", + "Textures/Terrain/splat/dirt.jpg", "" + }; + String[] allPaths = new String[12]; + String[] allLabels = new String[12]; + for (int i = 0; i < 4; i++) { + String p = input.terrainTexturePaths[i]; + allPaths[i] = (p != null && !p.isEmpty()) ? p : defaultsTerrain[i]; + allLabels[i] = "S" + (i + 1) + " " + labelFromPath(allPaths[i]); + } + for (int i = 0; i < 4; i++) { + String p = input.upperTexturePaths[i]; + allPaths[4 + i] = (p != null && !p.isEmpty()) ? p : ""; + allLabels[4 + i] = "S" + (i + 5) + " " + labelFromPath(allPaths[4 + i]); + } + for (int i = 0; i < 4; i++) { + String p = input.thirdTexturePaths[i]; + allPaths[8 + i] = (p != null && !p.isEmpty()) ? p : ""; + allLabels[8 + i] = "S" + (i + 9) + " " + labelFromPath(allPaths[8 + i]); + } + + ToggleGroup tg = new ToggleGroup(); + javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); + tile.setHgap(4); tile.setVgap(4); tile.setPrefColumns(4); + + for (int j = 0; j < 12; j++) { + final int idx = j; + String path = allPaths[j]; + ToggleButton btn = new ToggleButton(); + btn.setToggleGroup(tg); + btn.setPrefSize(56, 76); + btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + btn.setTooltip(new Tooltip("Slot " + (j + 1) + ": " + (path.isEmpty() ? "(leer)" : path))); + VBox content = new VBox(2); + content.setAlignment(Pos.CENTER); + if (!path.isEmpty()) { + String imgUrl = resolveImageUrl(path); + if (imgUrl != null) { + Image img = new Image(imgUrl, 44, 44, true, true); + ImageView iv = new ImageView(img.isError() ? null : img); + iv.setFitWidth(44); iv.setFitHeight(44); + content.getChildren().add(img.isError() ? emptySlotPlaceholder(44) : iv); + } else { + content.getChildren().add(emptySlotPlaceholder(44)); + } + } else { + content.getChildren().add(emptySlotPlaceholder(44)); + } + Label lbl = new Label(allLabels[j]); + lbl.setStyle("-fx-font-size: 9; -fx-text-fill: #222;"); + lbl.setMaxWidth(54); + content.getChildren().add(lbl); + btn.setGraphic(content); + boolean initially = (j == param.getSelectedIndex()); + btn.setSelected(initially); + applyModeButtonStyle(btn, initially); + btn.selectedProperty().addListener((obs, was, isNow) -> { + applyModeButtonStyle(btn, isNow); + if (isNow) { param.setSelectedIndex(idx); refresh[0].run(); } + }); + tile.getChildren().add(btn); + } + tg.selectedToggleProperty().addListener((obs, oldT, newT) -> { + if (newT == null) tg.selectToggle(oldT); + }); + + refresh[0].run(); + outer.getChildren().addAll(tile, activeBox); + return outer; + } + + /** + * Normal-Map-Prioritätsliste: zeigt alle 12 Slots, lässt den Nutzer die Reihenfolge + * per ↑/↓ ändern. Slots ohne Normal-Map sind ausgegraut; ein Budget-Hinweis zeigt, + * wie viele Normal Maps tatsächlich an die GPU gebunden werden. + */ + private VBox buildNormalMapPriorityUI() { + VBox box = new VBox(5); + box.setPadding(new javafx.geometry.Insets(4, 0, 4, 0)); + + Label title = new Label("Normal Map Priorität"); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); + + // Budget-Hinweis (dynamisch berechnet) + Label budgetLbl = new Label(); + budgetLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #555;"); + budgetLbl.setWrapText(true); + + // Alle 12 Normalpfade (flat, Index 0-11 = Slot 1-12) + java.util.function.Supplier normPaths = () -> { + String[] p = new String[12]; + System.arraycopy(input.terrainNormalMapPaths, 0, p, 0, 4); + System.arraycopy(input.upperNormalMapPaths, 0, p, 4, 4); + System.arraycopy(input.thirdNormalMapPaths, 0, p, 8, 4); + return p; + }; + + // Slot-Labels (Slot 1-12) + java.util.function.Function slotLabel = idx -> "S" + (idx + 1); + + // Observable-Liste der globalen Slot-Indizes in Prioritätsreihenfolge + javafx.collections.ObservableList items = + javafx.collections.FXCollections.observableArrayList(); + for (int v : input.normalMapOrder) items.add(v); + + // Budget neu berechnen und Label aktualisieren + Runnable refreshBudget = () -> { + String[] np = normPaths.get(); + long withNorm = Arrays.stream(np).filter(s -> s != null && !s.isEmpty()).count(); + boolean hasUpper = Arrays.stream(input.upperTexturePaths).anyMatch(s -> s != null && !s.isEmpty()); + boolean hasThird = Arrays.stream(input.thirdTexturePaths).anyMatch(s -> s != null && !s.isEmpty()); + int alphaCost = 1 + (hasUpper ? 1 : 0) + (hasThird ? 1 : 0); + long diffCount = Arrays.stream(input.terrainTexturePaths).filter(s -> s != null && !s.isEmpty()).count() + + Arrays.stream(input.upperTexturePaths).filter(s -> s != null && !s.isEmpty()).count() + + Arrays.stream(input.thirdTexturePaths).filter(s -> s != null && !s.isEmpty()).count(); + int normBudget = Math.max(0, 16 - alphaCost - (int) diffCount); + budgetLbl.setText("Budget: " + normBudget + " Normal Map" + (normBudget != 1 ? "s" : "") + + " (von " + withNorm + " konfiguriert). Oben = höchste Prio."); + }; + refreshBudget.run(); + + // ListView + javafx.scene.control.ListView list = new javafx.scene.control.ListView<>(items); + list.setPrefHeight(200); + list.setMaxHeight(240); + list.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { + @Override + protected void updateItem(Integer idx, boolean empty) { + super.updateItem(idx, empty); + if (empty || idx == null) { setText(null); setStyle(""); return; } + String[] np = normPaths.get(); + String nm = (np[idx] != null && !np[idx].isEmpty()) ? labelFromPath(np[idx]) : "–"; + setText(slotLabel.apply(idx) + " → " + nm); + boolean hasNorm = np[idx] != null && !np[idx].isEmpty(); + setStyle(hasNorm ? "-fx-text-fill: #111;" : "-fx-text-fill: #aaa;"); + } + }); + + // Schreibe Reihenfolge zurück nach SharedInput + Runnable commit = () -> { + int[] order = new int[12]; + for (int i = 0; i < 12; i++) order[i] = items.get(i); + input.normalMapOrder = order; + input.normalMapOrderChanged = true; + refreshBudget.run(); + list.refresh(); + }; + + // Auf Standardreihenfolge zurücksetzen + javafx.scene.control.Button resetBtn = new javafx.scene.control.Button("Reset"); + resetBtn.setTooltip(new Tooltip("Reihenfolge auf 1–12 zurücksetzen")); + resetBtn.setOnAction(e -> { + items.setAll(0,1,2,3,4,5,6,7,8,9,10,11); + commit.run(); + }); + + // ↑ / ↓ Buttons + javafx.scene.control.Button upBtn = new javafx.scene.control.Button("↑"); + javafx.scene.control.Button downBtn = new javafx.scene.control.Button("↓"); + + upBtn.setOnAction(e -> { + int sel = list.getSelectionModel().getSelectedIndex(); + if (sel <= 0) return; + Integer tmp = items.get(sel - 1); + items.set(sel - 1, items.get(sel)); + items.set(sel, tmp); + list.getSelectionModel().select(sel - 1); + commit.run(); + }); + downBtn.setOnAction(e -> { + int sel = list.getSelectionModel().getSelectedIndex(); + if (sel < 0 || sel >= items.size() - 1) return; + Integer tmp = items.get(sel + 1); + items.set(sel + 1, items.get(sel)); + items.set(sel, tmp); + list.getSelectionModel().select(sel + 1); + commit.run(); + }); + + HBox btnRow = new HBox(4, upBtn, downBtn, resetBtn); + btnRow.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + + box.getChildren().addAll(title, budgetLbl, list, btnRow); + return box; + } + + + // ── Textur-Metadaten ───────────────────────────────────────────────────── + + private Path texMetaFile(Path colorTexPath) { + Path dir = Files.isDirectory(colorTexPath) ? colorTexPath : colorTexPath.getParent(); + return dir.resolve("textureset.json"); + } + + public TextureMapData loadTexMeta(Path colorTexPath) { + Path mf = texMetaFile(colorTexPath); + if (!Files.exists(mf)) return new TextureMapData(); + try (java.io.Reader r = Files.newBufferedReader(mf)) { + TextureMapData d = TEX_GSON.fromJson(r, TextureMapData.class); + return d != null ? d : new TextureMapData(); + } catch (IOException e) { + return new TextureMapData(); + } + } + + public void saveTexMeta(Path colorTexPath, TextureMapData meta) { + Path mf = texMetaFile(colorTexPath); + try { + Files.createDirectories(mf.getParent()); + try (java.io.Writer w = Files.newBufferedWriter(mf)) { + TEX_GSON.toJson(meta, w); + } + } catch (IOException ex) { + ctx.setStatus().accept("Fehler beim Speichern der Textur-Metadaten: " + ex.getMessage()); + } + } + + // ── Textur-Slot Details-Popup ───────────────────────────────────────────── + + private void showTextureSlotDetails(String texRelPath, int slot, int group, + javafx.scene.Node owner, double screenX, double screenY) { + Path texAbs = ctx.assetRoot().resolve(texRelPath.replace('/', java.io.File.separatorChar)); + TextureMapData meta = loadTexMeta(texAbs); + + String normPath = switch (group) { + case 0 -> input.terrainNormalMapPaths[slot]; + case 1 -> input.upperNormalMapPaths[slot]; + default -> input.thirdNormalMapPaths[slot]; + }; + + javafx.scene.layout.VBox box = new javafx.scene.layout.VBox(5); + box.setPadding(new Insets(10)); + box.setStyle("-fx-background-color:#fafafa; -fx-border-color:#888; -fx-border-width:1;" + + "-fx-effect:dropshadow(gaussian,rgba(0,0,0,0.25),8,0,2,2);"); + box.setMinWidth(270); + + addDetailRow(box, "🖼 Textur", texAbs); + + if (normPath != null && !normPath.isEmpty()) { + box.getChildren().add(new javafx.scene.control.Separator()); + addDetailRow(box, "🔵 Normal", ctx.assetRoot().resolve(normPath.replace('/', java.io.File.separatorChar))); + } + if (!meta.roughnessMap.isEmpty()) { + box.getChildren().add(new javafx.scene.control.Separator()); + addDetailRow(box, "🟤 Roughness", ctx.assetRoot().resolve(meta.roughnessMap.replace('/', java.io.File.separatorChar))); + } + if (!meta.aoMap.isEmpty()) { + box.getChildren().add(new javafx.scene.control.Separator()); + addDetailRow(box, "⬛ AO", ctx.assetRoot().resolve(meta.aoMap.replace('/', java.io.File.separatorChar))); + } + + javafx.stage.Popup popup = new javafx.stage.Popup(); + popup.getContent().add(box); + popup.setAutoHide(true); + if (owner.getScene() != null) + popup.show(owner.getScene().getWindow(), screenX, screenY); + } + + private static void addDetailRow(javafx.scene.layout.VBox box, String label, Path file) { + Label nameLbl = new Label(label + ": " + file.getFileName()); + nameLbl.setStyle("-fx-font-weight:bold; -fx-font-size:11;"); + box.getChildren().add(nameLbl); + + if (Files.exists(file)) { + StringBuilder sb = new StringBuilder(" "); + int[] dims = readImageDimensions(file); + if (dims != null) sb.append(dims[0]).append(" × ").append(dims[1]).append(" · "); + String ext = file.getFileName().toString(); + int dot = ext.lastIndexOf('.'); + if (dot >= 0) sb.append(ext.substring(dot + 1).toUpperCase()).append(" · "); + try { sb.append(formatFileSize(Files.size(file))); } catch (IOException ignored) { sb.append("?"); } + Label info = new Label(sb.toString()); + info.setStyle("-fx-text-fill:#555; -fx-font-size:10;"); + box.getChildren().add(info); + } else { + Label missing = new Label(" (Datei nicht gefunden)"); + missing.setStyle("-fx-text-fill:#c00; -fx-font-size:10;"); + box.getChildren().add(missing); + } + } + + private static int[] readImageDimensions(Path p) { + try (var iis = javax.imageio.ImageIO.createImageInputStream(p.toFile())) { + java.util.Iterator readers = javax.imageio.ImageIO.getImageReaders(iis); + if (readers.hasNext()) { + javax.imageio.ImageReader reader = readers.next(); + try { + reader.setInput(iis); + return new int[]{ reader.getWidth(0), reader.getHeight(0) }; + } finally { + reader.dispose(); + } + } + } catch (Exception ignored) {} + return null; + } + + private static String formatFileSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); + return String.format("%.1f MB", bytes / (1024.0 * 1024)); + } + +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java b/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java new file mode 100644 index 0000000..df5747c --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java @@ -0,0 +1,335 @@ +package de.blight.editor.toolset.topbar; + +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import javafx.geometry.Orientation; +import javafx.scene.control.*; +import javafx.scene.layout.*; + +public class TopBarController { + + public record Deps( + VBox toolPanel, + de.blight.editor.toolset.model.ModelEditorController modelEditorController, + de.blight.editor.toolset.asset.AssetTreeController assetController, + de.blight.editor.toolset.anim.AnimSetController animSetController, + de.blight.editor.toolset.content.ContentController contentController, + de.blight.editor.toolset.world.WorldPanelController worldPanelController, + de.blight.editor.toolset.voxel.VoxelController voxelController, + de.blight.editor.toolset.grass.GrassController grassController, + de.blight.editor.toolset.atmosphere.AtmosphereController atmosphereController, + de.blight.editor.toolset.water.WaterController waterController, + de.blight.editor.toolset.game.GameController gameController, + de.blight.editor.toolset.terrain.TerrainPanelController terrainController, + de.blight.editor.toolset.object.ObjectController objectController, + Runnable switchToVegetationGenerator, + Runnable switchToEzTree, + Runnable switchToTripo, + Runnable openCharacterEditor, + Runnable switchToThumbnailManager, + Runnable takeEditorScreenshot, + Runnable openSettings + ) {} + + private final SharedInput input; + + // ToggleButtons – pre-created so getters work before buildTop() is called + private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl"); + private final ToggleButton baseBtn = new ToggleButton("▲▼ Terrain"); + private final ToggleButton voxelMergedBtn = new ToggleButton("◈ Voxel"); + private final ToggleButton textureBtn = new ToggleButton(": : Textur"); + private final ToggleButton grassBtn = new ToggleButton("// Gras"); + private final ToggleButton stoneBtn = new ToggleButton("o Steine"); + private final ToggleButton objPlaceBtn = new ToggleButton("+ Objekte"); + private final ToggleButton atmosphaereBtn = new ToggleButton("* Atmosphare"); + private final ToggleButton wasserMergedBtn = new ToggleButton("~ Wasser"); + private final ToggleButton wegeBtn = new ToggleButton("-- Wege"); + private final ToggleButton zonenBtn = new ToggleButton("[ ] Zonen"); + private final ToggleButton playToolBtn = new ToggleButton("▶ Spielen"); + + // Menu items – pre-created + private final RadioMenuItem camOrbitItem = new RadioMenuItem("⊙ Orbit-Kamera (Alt+C)"); + private final RadioMenuItem camFreeItem = new RadioMenuItem("✈ FreeFly-Kamera (Alt+C)"); + private final CheckMenuItem dualWindowMenuItem = new CheckMenuItem("2-Fenster-Modus (Alt+W)"); + private final CheckMenuItem showGridItem = new CheckMenuItem("1m-Gitternetz (Alt+N)"); + private CheckMenuItem viewTopologyItem; + private boolean wireframeActive = false; + + private ToolBar worldToolBar; + private Label worldHintLabel; + + public TopBarController(SharedInput input) { + this.input = input; + + // Bold style + final String BOLD = "-fx-font-weight:bold;"; + auswahlBtn.setStyle(BOLD); + baseBtn.setStyle(BOLD); + voxelMergedBtn.setStyle(BOLD); + textureBtn.setStyle(BOLD); + grassBtn.setStyle(BOLD); + stoneBtn.setStyle(BOLD); + objPlaceBtn.setStyle(BOLD); + atmosphaereBtn.setStyle(BOLD); + wasserMergedBtn.setStyle(BOLD); + wegeBtn.setStyle(BOLD); + zonenBtn.setStyle(BOLD); + playToolBtn.setStyle(BOLD); + + // Mutually exclusive toggle group + ToggleGroup layerGroup = new ToggleGroup(); + auswahlBtn.setToggleGroup(layerGroup); + baseBtn.setToggleGroup(layerGroup); + voxelMergedBtn.setToggleGroup(layerGroup); + textureBtn.setToggleGroup(layerGroup); + grassBtn.setToggleGroup(layerGroup); + stoneBtn.setToggleGroup(layerGroup); + objPlaceBtn.setToggleGroup(layerGroup); + atmosphaereBtn.setToggleGroup(layerGroup); + wasserMergedBtn.setToggleGroup(layerGroup); + wegeBtn.setToggleGroup(layerGroup); + zonenBtn.setToggleGroup(layerGroup); + playToolBtn.setToggleGroup(layerGroup); + + // Cam-Menu toggle group + ToggleGroup camMenuGroup = new ToggleGroup(); + camOrbitItem.setToggleGroup(camMenuGroup); + camFreeItem.setToggleGroup(camMenuGroup); + camOrbitItem.setSelected(true); + + // Grid default + showGridItem.setSelected(true); + } + + // ── Getters ────────────────────────────────────────────────────────────── + + public ToggleButton getAuswahlBtn() { return auswahlBtn; } + public ToggleButton getBaseBtn() { return baseBtn; } + public ToggleButton getVoxelMergedBtn() { return voxelMergedBtn; } + public ToggleButton getTextureBtn() { return textureBtn; } + public ToggleButton getGrassBtn() { return grassBtn; } + public ToggleButton getStoneBtn() { return stoneBtn; } + public ToggleButton getObjPlaceBtn() { return objPlaceBtn; } + public ToggleButton getAtmosphaereBtn() { return atmosphaereBtn; } + public ToggleButton getWasserMergedBtn(){ return wasserMergedBtn; } + public ToggleButton getWegeBtn() { return wegeBtn; } + public ToggleButton getZonenBtn() { return zonenBtn; } + public ToggleButton getPlayToolBtn() { return playToolBtn; } + + public RadioMenuItem getCamOrbitItem() { return camOrbitItem; } + public RadioMenuItem getCamFreeItem() { return camFreeItem; } + public CheckMenuItem getDualWindowMenuItem() { return dualWindowMenuItem; } + public CheckMenuItem getShowGridItem() { return showGridItem; } + public CheckMenuItem getViewTopologyItem() { return viewTopologyItem; } + + public ToolBar getWorldToolBar() { return worldToolBar; } + public Label getWorldHintLabel(){ return worldHintLabel; } + + public boolean toggleWireframe() { + wireframeActive = !wireframeActive; + return wireframeActive; + } + + public void updateCamHintLabel() { + if (worldHintLabel != null) { worldHintLabel.setText(camHintText()); } + } + + public void applyShowGridPref(boolean value) { + showGridItem.setSelected(value); + } + + private String camHintText() { + String mode = (input.camMode == SharedInput.CAM_FREEFLY) ? "FreeFly" : "Orbit"; + return "WASD/QE: Kamera | Mitte-Drag: Drehen | Alt+C: Kamera wechseln | Kamera: " + mode; + } + + // ── Build ───────────────────────────────────────────────────────────────── + + public VBox buildTop(ToolContext ctx, Deps deps) { + MenuBar menuBar = new MenuBar(); + + // ── Datei-Menü ────────────────────────────────────────────────────── + Menu fileMenu = new Menu("Datei"); + MenuItem newItem = new MenuItem("Neue Karte"); + MenuItem saveItem = new MenuItem("Speichern"); + saveItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+S")); + saveItem.setOnAction(e -> { input.saveRequested = true; ctx.setStatus().accept("Speichern…"); }); + MenuItem screenshotItem = new MenuItem("Screenshot"); + screenshotItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Ctrl+F12")); + screenshotItem.setOnAction(e -> deps.takeEditorScreenshot().run()); + MenuItem settingsItem = new MenuItem("Einstellungen…"); + settingsItem.setOnAction(e -> deps.openSettings().run()); + MenuItem quitItem = new MenuItem("Beenden"); + quitItem.setOnAction(e -> javafx.application.Platform.exit()); + fileMenu.getItems().addAll(newItem, saveItem, new SeparatorMenuItem(), + screenshotItem, new SeparatorMenuItem(), settingsItem, + new SeparatorMenuItem(), quitItem); + + // ── Import-Menü ───────────────────────────────────────────────────── + Menu importMenu = new Menu("Import"); + MenuItem importModelLodItem = new MenuItem("Modell mit LOD-Editor…"); + MenuItem importTexItem = new MenuItem("Texturen…"); + MenuItem importTexZipItem = new MenuItem("Texturen-ZIP…"); + MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…"); + MenuItem importAudioItem = new MenuItem("Audio…"); + MenuItem importAnimItem = new MenuItem("Animationen…"); + MenuItem importCharModelItem = new MenuItem("Character-Modell (FBX)…"); + importModelLodItem.setOnAction(e -> deps.modelEditorController().openModelImport(ctx.stageSupplier().get())); + importTexItem.setOnAction(e -> deps.assetController().handleTextureImport(ctx.stageSupplier().get())); + importTexZipItem.setOnAction(e -> deps.assetController().handleZipTextureImport(ctx.stageSupplier().get())); + importTexSetItem.setOnAction(e -> deps.assetController().openTextureSetImport(ctx.stageSupplier().get())); + importAudioItem.setOnAction(e -> deps.assetController().handleAudioImport(ctx.stageSupplier().get())); + importAnimItem.setOnAction(e -> deps.animSetController().handleAnimationImport(ctx.stageSupplier().get())); + importCharModelItem.setOnAction(e -> deps.animSetController().handleCharacterModelImport(ctx.stageSupplier().get())); + importMenu.getItems().addAll(importModelLodItem, importTexItem, importTexZipItem, + importTexSetItem, importAudioItem, importAnimItem, + new SeparatorMenuItem(), importCharModelItem); + + // ── Werkzeuge-Menü ────────────────────────────────────────────────── + Menu toolsMenu = new Menu("Werkzeuge"); + MenuItem vegetationsItem = new MenuItem("Vegetations Generator"); + MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)"); + MenuItem tripoItem = new MenuItem("AI Modell-Generator (Tripo3D)"); + MenuItem animPrevItem = new MenuItem("Animationset Editor"); + MenuItem objEditorItem = new MenuItem("Model Editor"); + MenuItem thumbItem = new MenuItem("Thumbnail-Verwaltung"); + vegetationsItem.setOnAction(e -> deps.switchToVegetationGenerator().run()); + ezTreeItem.setOnAction(e -> deps.switchToEzTree().run()); + tripoItem.setOnAction(e -> deps.switchToTripo().run()); + animPrevItem.setOnAction(e -> deps.openCharacterEditor().run()); + objEditorItem.setOnAction(e -> deps.modelEditorController().openModelEditor(null, null)); + thumbItem.setOnAction(e -> deps.switchToThumbnailManager().run()); + toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem, + animPrevItem, objEditorItem, new SeparatorMenuItem(), thumbItem); + + // ── Ansicht-Menü ──────────────────────────────────────────────────── + Menu viewMenu = new Menu("Ansicht"); + MenuItem resetCam = new MenuItem("Kamera zurücksetzen (Alt+R)"); + resetCam.setOnAction(e -> input.resetCameraRequested.set(true)); + MenuItem viewTexture = new MenuItem("Textur (Alt+G)"); + MenuItem viewWireframe = new MenuItem("Drahtgitter (Alt+G)"); + viewTopologyItem = new CheckMenuItem("Topologie-Overlay (Alt+T)"); + viewTexture.setOnAction(e -> { + wireframeActive = false; + input.wireframeRequest = 2; + }); + viewWireframe.setOnAction(e -> { + wireframeActive = true; + input.wireframeRequest = 1; + }); + viewTopologyItem.setOnAction(e -> + input.topologyRequest = viewTopologyItem.isSelected() ? 1 : 2); + + camOrbitItem.setOnAction(e -> { input.camMode = SharedInput.CAM_ORBIT; updateCamHintLabel(); }); + camFreeItem.setOnAction(e -> { input.camMode = SharedInput.CAM_FREEFLY; updateCamHintLabel(); }); + + CheckMenuItem debugHeightItem = new CheckMenuItem("Höhen-Overlay (Alt+H)"); + debugHeightItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+H")); + debugHeightItem.setOnAction(e -> + input.debugHeightOverlayEnabled = debugHeightItem.isSelected()); + + dualWindowMenuItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+W")); + dualWindowMenuItem.setOnAction(e -> { + if (dualWindowMenuItem.isSelected()) { deps.contentController().enableDualWindowMode(); } + else { deps.contentController().disableDualWindowMode(); } + }); + deps.contentController().setDualWindowMenuItem(dualWindowMenuItem); + + showGridItem.setAccelerator(javafx.scene.input.KeyCombination.keyCombination("Alt+N")); + showGridItem.setOnAction(e -> input.showGrid = showGridItem.isSelected()); + + viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe, + new SeparatorMenuItem(), viewTopologyItem, + new SeparatorMenuItem(), debugHeightItem, + new SeparatorMenuItem(), showGridItem, + new SeparatorMenuItem(), camOrbitItem, camFreeItem); + + // ── Zeit-Menü ─────────────────────────────────────────────────────── + Menu zeitMenu = new Menu("Zeit"); + ToggleGroup zeitGroup = new ToggleGroup(); + int[] hours = {0, 3, 6, 9, 12, 15, 18, 21}; + for (int h : hours) { + RadioMenuItem item = new RadioMenuItem(String.format("%02d:00", h)); + item.setToggleGroup(zeitGroup); + if (h == 12) { item.setSelected(true); } + final float t = h / 24f; + item.setOnAction(e -> input.pendingTimeOfDay = t); + zeitMenu.getItems().add(item); + } + menuBar.getMenus().addAll(fileMenu, toolsMenu, importMenu, viewMenu, zeitMenu); + + // ── Toolbar-Buttons ───────────────────────────────────────────────── + auswahlBtn.setSelected(true); + input.activeLayer = SharedInput.LAYER_AUSWAHL; + + auswahlBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_AUSWAHL; + input.deselectAllOnAuswahl = true; + ctx.setRightPanel().accept(deps.worldPanelController().buildAuswahlPanel()); + }); + baseBtn.setOnAction(e -> { + input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue()); + input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue()); + input.heightTool.plateauHeight.setValue(input.voxelTool.plateauTarget.getValue()); + input.activeLayer = 0; input.activeTool = input.heightTool; + ctx.setRightPanel().accept(deps.toolPanel()); + deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); + }); + voxelMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.voxelController().buildPanel())); + textureBtn.setOnAction(e -> { + input.activeLayer = 4; input.activeTool = input.textureTool; + ctx.setRightPanel().accept(deps.toolPanel()); + deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); + }); + grassBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.grassController().buildPanel())); + stoneBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_STONE; + input.activeTool = input.stoneTool; + ctx.setRightPanel().accept(deps.toolPanel()); + deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); + }); + objPlaceBtn.setOnAction(e -> { + deps.assetController().expandModelsNode(); + deps.assetController().openOverlay(); + deps.objectController().activatePlaceMode(); + }); + atmosphaereBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.atmosphereController().buildPanel())); + wasserMergedBtn.setOnAction(e -> ctx.setRightPanel().accept(deps.waterController().buildPanel())); + wegeBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_PATH_NETWORK; + ctx.setRightPanel().accept(deps.worldPanelController().buildWegePanel()); + }); + zonenBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_ZONEN; + ctx.setRightPanel().accept(deps.worldPanelController().buildZonenPanel()); + }); + playToolBtn.setOnAction(e -> { + input.activeLayer = SharedInput.LAYER_PLAY_TOOL; + input.playToolMode = SharedInput.PlayToolMode.EDIT; + ctx.setRightPanel().accept(deps.gameController().buildPlayToolPanel()); + }); + + worldHintLabel = new Label(camHintText()); + worldHintLabel.setStyle("-fx-text-fill: #555;"); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + ToolBar toolBar = new ToolBar(); + toolBar.getItems().addAll( + auswahlBtn, + new Separator(Orientation.VERTICAL), baseBtn, voxelMergedBtn, + new Separator(Orientation.VERTICAL), textureBtn, grassBtn, stoneBtn, + new Separator(Orientation.VERTICAL), objPlaceBtn, + new Separator(Orientation.VERTICAL), atmosphaereBtn, + new Separator(Orientation.VERTICAL), wasserMergedBtn, wegeBtn, + new Separator(Orientation.VERTICAL), zonenBtn, + new Separator(Orientation.VERTICAL), playToolBtn, + new Separator(Orientation.VERTICAL), worldHintLabel, + new Separator(Orientation.VERTICAL), spacer, + deps.contentController().buildContentSwitchBtn()); + + worldToolBar = toolBar; + return new VBox(menuBar, toolBar); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/tripo/TripoController.java b/blight-editor/src/main/java/de/blight/editor/toolset/tripo/TripoController.java new file mode 100644 index 0000000..ad94be8 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/tripo/TripoController.java @@ -0,0 +1,282 @@ +package de.blight.editor.toolset.tripo; + +import de.blight.editor.ToolContext; +import de.blight.editor.toolset.EditorUiHelper; +import de.blight.editor.SharedInput; +import de.blight.editor.TripoGenerator; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class TripoController { + + private final ToolContext ctx; + + private ImageView tripoPreviewView; + private Label tripoStatusLabel; + private ProgressBar tripoProgressBar; + private Button tripoImportBtn; + private Button tripoImportRigBtn; + private boolean tripoGenerating = false; + private String tripoLastModelUrl; + private String tripoLastTaskId; + + public TripoController(ToolContext ctx) { + this.ctx = ctx; + } + + public void activate() { + ctx.setCurrentTool().accept("tripo"); + + StackPane previewCenter = new StackPane(); + previewCenter.setStyle("-fx-background-color: #1a1a2e;"); + tripoPreviewView = new ImageView(); + tripoPreviewView.setPreserveRatio(true); + tripoPreviewView.fitWidthProperty().bind(previewCenter.widthProperty()); + tripoPreviewView.fitHeightProperty().bind(previewCenter.heightProperty()); + Label placeholder = new Label("Vorschau erscheint hier nach der Generierung"); + placeholder.setStyle("-fx-text-fill: #888; -fx-font-size: 14;"); + previewCenter.getChildren().addAll(placeholder, tripoPreviewView); + ctx.setCenterView().accept(previewCenter); + ctx.setRightPanel().accept(buildTripoPanel()); + ctx.setMainToolbar().accept(buildTripoToolBar()); + } + + private ToolBar buildTripoToolBar() { + Button backBtn = new Button("← Welteneditor"); + backBtn.setOnAction(e -> ctx.switchToWorld().run()); + Label label = new Label("AI Modell-Generator · Tripo3D"); + label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + ToolBar tb = new ToolBar(); + tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); + return tb; + } + + private VBox buildTripoPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + inner.getChildren().addAll(EditorUiHelper.sectionTitle("Tripo3D API"), new Separator()); + Label apiHint = EditorUiHelper.styledHint("Schlüssel unter platform.tripo3d.ai → API Keys"); + apiHint.setWrapText(true); + TextField apiKeyField = new TextField(loadTripoApiKey()); + apiKeyField.setPromptText("tsk_xxxx…"); + apiKeyField.setStyle("-fx-font-family: monospace; -fx-font-size: 11;"); + inner.getChildren().addAll(new Label("API-Schlüssel:"), apiKeyField, apiHint); + + inner.getChildren().addAll(EditorUiHelper.sectionTitle("Modell beschreiben"), new Separator()); + TextArea promptArea = new TextArea(); + promptArea.setPromptText("z. B. \"Ein mittelalterlicher Ritter in Ruestung\"\n" + + "Tipp: Englische Prompts liefern meist bessere Ergebnisse."); + promptArea.setPrefRowCount(5); + promptArea.setWrapText(true); + inner.getChildren().addAll(new Label("Prompt:"), promptArea); + + TextField nameField = new TextField(); + nameField.setPromptText("dateiname (optional)"); + inner.getChildren().addAll(new Label("Dateiname:"), nameField); + + inner.getChildren().add(new Separator()); + Button generateBtn = new Button("Modell generieren"); + generateBtn.setMaxWidth(Double.MAX_VALUE); + generateBtn.setStyle("-fx-font-weight: bold;"); + generateBtn.setDisable(tripoGenerating); + + tripoProgressBar = new ProgressBar(tripoGenerating ? ProgressBar.INDETERMINATE_PROGRESS : 0); + tripoProgressBar.setMaxWidth(Double.MAX_VALUE); + tripoStatusLabel = new Label(tripoGenerating ? "Generierung läuft…" : "Bereit"); + tripoStatusLabel.setWrapText(true); + tripoStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #444;"); + inner.getChildren().addAll(generateBtn, tripoProgressBar, tripoStatusLabel); + + inner.getChildren().addAll(EditorUiHelper.sectionTitle("Import"), new Separator()); + tripoImportBtn = new Button("Importieren (GLB → j3o)"); + tripoImportRigBtn = new Button("Importieren + Skelett (Humanoid)"); + tripoImportBtn.setMaxWidth(Double.MAX_VALUE); + tripoImportRigBtn.setMaxWidth(Double.MAX_VALUE); + boolean hasPrev = tripoLastModelUrl != null && !tripoGenerating; + tripoImportBtn.setDisable(!hasPrev); + tripoImportRigBtn.setDisable(!hasPrev || tripoLastTaskId == null); + inner.getChildren().addAll(tripoImportBtn, tripoImportRigBtn); + inner.getChildren().add(EditorUiHelper.styledHint("Das Skelett-Modell eignet sich für Humanoid-Animationen.")); + + generateBtn.setOnAction(e -> { + if (tripoGenerating) return; + String apiKey = apiKeyField.getText().trim(); + String prompt = promptArea.getText().trim(); + if (apiKey.isEmpty()) { ctx.setStatus().accept("Bitte API-Schlüssel eingeben"); return; } + if (prompt.isEmpty()) { ctx.setStatus().accept("Bitte Prompt eingeben"); return; } + + saveTripoApiKey(apiKey); + tripoGenerating = true; + generateBtn.setDisable(true); + tripoImportBtn.setDisable(true); + tripoImportRigBtn.setDisable(true); + tripoProgressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + tripoStatusLabel.setText("Erstelle Aufgabe…"); + if (tripoPreviewView != null) tripoPreviewView.setImage(null); + + new Thread(() -> { + try { + String taskId = TripoGenerator.createTextToModelTask(apiKey, prompt); + tripoLastTaskId = taskId; + Platform.runLater(() -> tripoStatusLabel.setText("Generiere… (ID: " + taskId + ")")); + + pollUntilDone(apiKey, taskId, result -> { + tripoLastModelUrl = result.modelUrl(); + if (result.previewUrl() != null && tripoPreviewView != null) { + tripoPreviewView.setImage( + new javafx.scene.image.Image(result.previewUrl(), true)); + } + tripoProgressBar.setProgress(1.0); + tripoStatusLabel.setText("Fertig! Modell bereit zum Import."); + generateBtn.setDisable(false); + tripoImportBtn.setDisable(false); + tripoImportRigBtn.setDisable(false); + tripoGenerating = false; + }, err -> { + tripoProgressBar.setProgress(0); + tripoStatusLabel.setText("Fehler: " + err); + generateBtn.setDisable(false); + tripoGenerating = false; + }); + } catch (Exception ex) { + Platform.runLater(() -> { + tripoProgressBar.setProgress(0); + tripoStatusLabel.setText("Fehler: " + ex.getMessage()); + generateBtn.setDisable(false); + tripoGenerating = false; + }); + } + }).start(); + }); + + tripoImportBtn.setOnAction(e -> { + if (tripoLastModelUrl == null) return; + String name = nameField.getText().trim(); + if (name.isEmpty()) name = "tripo_" + System.currentTimeMillis(); + importTripoModel(tripoLastModelUrl, name); + }); + + tripoImportRigBtn.setOnAction(e -> { + if (tripoLastTaskId == null || tripoLastModelUrl == null) return; + String apiKey = apiKeyField.getText().trim(); + String baseName = nameField.getText().trim(); + if (baseName.isEmpty()) baseName = "tripo_" + System.currentTimeMillis(); + final String rigName = baseName + "_rigged"; + + tripoImportRigBtn.setDisable(true); + tripoProgressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + tripoStatusLabel.setText("Erstelle Skelett…"); + + final String taskId = tripoLastTaskId; + new Thread(() -> { + try { + String rigTaskId = TripoGenerator.createRigTask(apiKey, taskId); + Platform.runLater(() -> tripoStatusLabel.setText( + "Rigging… (ID: " + rigTaskId + ")")); + + pollUntilDone(apiKey, rigTaskId, result -> { + tripoProgressBar.setProgress(1.0); + tripoStatusLabel.setText("Skelett fertig! Importiere…"); + tripoImportRigBtn.setDisable(false); + importTripoModel(result.modelUrl(), rigName); + }, err -> { + tripoProgressBar.setProgress(0); + tripoStatusLabel.setText("Skelett-Fehler: " + err); + tripoImportRigBtn.setDisable(false); + }); + } catch (Exception ex) { + Platform.runLater(() -> { + tripoProgressBar.setProgress(0); + tripoStatusLabel.setText("Fehler: " + ex.getMessage()); + tripoImportRigBtn.setDisable(false); + }); + } + }).start(); + }); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(280); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + private void pollUntilDone(String apiKey, String taskId, + java.util.function.Consumer onSuccess, + java.util.function.Consumer onError) { + while (true) { + try { Thread.sleep(3_000); } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + Platform.runLater(() -> onError.accept("Unterbrochen")); + return; + } + try { + TripoGenerator.TaskResult r = TripoGenerator.pollTask(apiKey, taskId); + switch (r.status()) { + case "success" -> { Platform.runLater(() -> onSuccess.accept(r)); return; } + case "failed", "cancelled" -> { + Platform.runLater(() -> onError.accept(r.status())); return; + } + default -> { + int p = r.progress(); + Platform.runLater(() -> { + if (tripoProgressBar != null) tripoProgressBar.setProgress(p / 100.0); + if (tripoStatusLabel != null) tripoStatusLabel.setText("Generiere… " + p + "%"); + }); + } + } + } catch (java.io.IOException ex) { + Platform.runLater(() -> { + if (tripoStatusLabel != null) + tripoStatusLabel.setText("Prüfe… (" + ex.getMessage() + ")"); + }); + } + } + } + + private void importTripoModel(String url, String name) { + final String safeName = name.replaceAll("[^a-zA-Z0-9_\\-]", "_"); + new Thread(() -> { + try { + Platform.runLater(() -> ctx.setStatus().accept("Lade Modell herunter: " + safeName + "…")); + Path destDir = ctx.assetRoot().resolve("Models"); + Files.createDirectories(destDir); + Path destGlb = destDir.resolve(safeName + ".glb"); + TripoGenerator.downloadFile(url, destGlb); + + String assetPath = "Models/" + safeName + ".glb"; + Path destJ3o = destDir.resolve(safeName + ".j3o"); + ctx.input().modelConvertQueue.offer( + new SharedInput.ModelConvertRequest(assetPath, destJ3o, destGlb, true)); + + Platform.runLater(() -> ctx.setStatus().accept("Konvertiere " + safeName + ".glb → .j3o…")); + } catch (java.io.IOException ex) { + Platform.runLater(() -> ctx.setStatus().accept("Import-Fehler: " + ex.getMessage())); + } + }).start(); + } + + private static String loadTripoApiKey() { + return java.util.prefs.Preferences + .userNodeForPackage(TripoController.class) + .get("tripo_api_key", ""); + } + + private static void saveTripoApiKey(String key) { + java.util.prefs.Preferences + .userNodeForPackage(TripoController.class) + .put("tripo_api_key", key); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java new file mode 100644 index 0000000..68b6770 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java @@ -0,0 +1,422 @@ +package de.blight.editor.toolset.viewport; + +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import javafx.geometry.Insets; +import javafx.scene.image.ImageView; +import javafx.scene.image.WritableImage; +import javafx.scene.input.MouseButton; +import javafx.scene.layout.StackPane; + +public class ViewportController { + + private final SharedInput input; + private final WritableImage initialImage; + private final Runnable scheduleAutoSave; + + private ImageView viewport; + private javafx.scene.canvas.Canvas compassCanvas; + + private double prevDragX, prevDragY; + private double objDragPrevX, objDragPrevY; + private boolean objDragging = false; + private double editPressX, editPressY; + private int editPressAction; + private javafx.animation.Timeline editTimer; + + public ViewportController(SharedInput input, WritableImage jfxImage, Runnable scheduleAutoSave) { + this.input = input; + this.initialImage = jfxImage; + this.scheduleAutoSave = scheduleAutoSave; + } + + public ImageView getViewport() { return viewport; } + + public void setImage(WritableImage img) { + if (viewport != null) { viewport.setImage(img); } + } + + public StackPane buildViewport() { + viewport = new ImageView(initialImage); + viewport.setPreserveRatio(false); + viewport.setFocusTraversable(true); + + compassCanvas = new javafx.scene.canvas.Canvas(100, 100); + compassCanvas.setMouseTransparent(true); + StackPane.setAlignment(compassCanvas, javafx.geometry.Pos.BOTTOM_RIGHT); + StackPane.setMargin(compassCanvas, new Insets(0, 10, 10, 0)); + drawCompass(); + + StackPane pane = new StackPane(viewport, compassCanvas); + pane.setStyle("-fx-background-color: #1a1a2e;"); + + javafx.animation.PauseTransition resizeDebounce = + new javafx.animation.PauseTransition(javafx.util.Duration.millis(150)); + resizeDebounce.setOnFinished(ev -> { + int w = (int) pane.getWidth(); + int h = (int) pane.getHeight(); + if (w > 0 && h > 0) { input.resizeRequest.set(new int[]{w, h}); } + }); + + pane.widthProperty().addListener((o, oldW, newW) -> { + viewport.setFitWidth(newW.doubleValue()); + resizeDebounce.playFromStart(); + }); + pane.heightProperty().addListener((o, oldH, newH) -> { + viewport.setFitHeight(newH.doubleValue()); + resizeDebounce.playFromStart(); + }); + + // Camera-Rotation: Filter am StackPane (Capture-Phase) → feuert immer, + // auch wenn Overlay-Nodes (Tab-Button, Asset-Panel) das Event abfangen würden. + pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_PRESSED, e -> { + if (e.getButton() == MouseButton.MIDDLE + || (e.isPrimaryButtonDown() && e.isSecondaryButtonDown()) + || (e.getButton() == MouseButton.PRIMARY && e.isAltDown())) { + prevDragX = e.getX(); prevDragY = e.getY(); + } + }); + pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_DRAGGED, e -> { + boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); + boolean altLeft = e.isPrimaryButtonDown() && e.isAltDown(); + if (e.isMiddleButtonDown() || bothDown || altLeft) { + double dx = e.getX() - prevDragX; + double dy = e.getY() - prevDragY; + input.addMouseDelta((int) dx, (int) dy); + prevDragX = e.getX(); prevDragY = e.getY(); + if (bothDown) { stopEditTimer(); } + } + }); + + viewport.setOnMousePressed(e -> { + viewport.requestFocus(); + boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); + + if (isObjectMode()) { + if (e.getButton() == MouseButton.PRIMARY && !bothDown) { + input.objectClickQueue.offer( + new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, e.isShiftDown())); + objDragPrevX = e.getX(); objDragPrevY = e.getY(); + objDragging = true; + } else if (e.getButton() == MouseButton.SECONDARY && !bothDown) { + input.objectClickQueue.offer( + new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), true, false)); + objDragPrevX = e.getX(); objDragPrevY = e.getY(); + } + return; + } + + if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR) { + if (e.getButton() == MouseButton.PRIMARY) { + input.routinePointClickQueue.offer( + new SharedInput.RoutinePointClick((float) e.getX(), (float) e.getY())); + } else if (e.getButton() == MouseButton.SECONDARY) { + input.routineYawDragQueue.offer( + new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), false)); + } + return; + } + + if (input.activeLayer == SharedInput.LAYER_WATERFALL) { + if (e.getButton() == MouseButton.PRIMARY) { + input.waterfallClickQueue.offer( + new SharedInput.WaterfallClick((float)e.getX(), (float)e.getY(), false)); + } else if (e.getButton() == MouseButton.SECONDARY) { + input.waterfallClickQueue.offer( + new SharedInput.WaterfallClick((float)e.getX(), (float)e.getY(), true)); + } + return; + } + + if (input.activeLayer == SharedInput.LAYER_ITEMS) { + if (e.getButton() == MouseButton.PRIMARY && !bothDown) { + input.itemClickQueue.offer( + new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, false)); + } else if (e.getButton() == MouseButton.SECONDARY) { + input.itemClickQueue.offer( + new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), true, false)); + } + return; + } + + if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) { + if (e.getButton() == MouseButton.PRIMARY && !bothDown) { + input.pathNetworkClickQueue.offer( + new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), false)); + } else if (e.getButton() == MouseButton.SECONDARY && !bothDown) { + input.pathNetworkClickQueue.offer( + new SharedInput.PathNetworkClick((float)e.getX(), (float)e.getY(), true)); + } + return; + } + + if (input.activeLayer == SharedInput.LAYER_STONE + && input.stoneTool.brushRadius.getValue() == 1.0) { + // Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer + if (e.getButton() == MouseButton.SECONDARY && !bothDown) { + input.stoneEditQueue.offer( + new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), -1)); + } + } else if (bothDown) { + stopEditTimer(); + } else if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) { + boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_PLAY_TOOL + || input.activeLayer == SharedInput.LAYER_ZONEN + || input.activeLayer == SharedInput.LAYER_SOUND_AREAS + || input.activeLayer == SharedInput.LAYER_AREAS + || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES + || input.activeLayer == SharedInput.LAYER_WATER; + if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { + // Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam) + input.playToolClickQueue.offer( + new SharedInput.PlayToolClick((float) e.getX(), (float) e.getY())); + } else if (singleClickLayer) { + submitEdit(e.getX(), e.getY(), +1); + } else { + editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1; + submitEdit(editPressX, editPressY, editPressAction); + startEditTimer(); + } + } else if (e.getButton() == MouseButton.SECONDARY) { + boolean singleClickLayer = input.activeLayer == SharedInput.LAYER_ZONEN + || input.activeLayer == SharedInput.LAYER_SOUND_AREAS + || input.activeLayer == SharedInput.LAYER_AREAS + || input.activeLayer == SharedInput.LAYER_LOCATION_ZONES + || input.activeLayer == SharedInput.LAYER_WATER; + editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1; + submitEdit(editPressX, editPressY, editPressAction); + if (!singleClickLayer) { startEditTimer(); } + } + }); + + viewport.setOnMouseDragged(e -> { + if (isObjectMode()) { + if (objDragging && e.isPrimaryButtonDown() + && !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) { + float dx = (float)(e.getX() - objDragPrevX); + float dy = (float)(e.getY() - objDragPrevY); + input.objectDragQueue.offer(new SharedInput.ObjectDrag(dx, dy)); + objDragPrevX = e.getX(); objDragPrevY = e.getY(); + } else if (e.isSecondaryButtonDown() && !e.isPrimaryButtonDown() + && !e.isMiddleButtonDown() + && input.activeLayer == SharedInput.LAYER_OBJECTS) { + float dx = (float)(e.getX() - objDragPrevX); + input.objectRotateDragQueue.offer(new SharedInput.ObjectRotateDrag(dx)); + objDragPrevX = e.getX(); objDragPrevY = e.getY(); + } + return; + } + + if (input.activeLayer == SharedInput.LAYER_WATER && e.isPrimaryButtonDown()) { + input.waterDragQueue.offer(new SharedInput.WaterDrag((float) e.getX(), (float) e.getY())); + input.mouseScreenX = (float) e.getX(); + input.mouseScreenY = (float) e.getY(); + return; + } + + if (input.activeLayer == SharedInput.LAYER_WATERFALL && e.isPrimaryButtonDown()) { + input.waterfallDragQueue.offer(new SharedInput.WaterfallDrag((float) e.getX(), (float) e.getY())); + return; + } + + if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR && e.isSecondaryButtonDown()) { + input.routineYawDragQueue.offer( + new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), false)); + return; + } + + // Play-Tool EDIT: Drag-Events an JME3 weiterleiten + if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL + && input.playToolMode == SharedInput.PlayToolMode.EDIT + && e.isPrimaryButtonDown()) { + input.playToolDragQueue.offer( + new SharedInput.PlayToolDrag((float) e.getX(), (float) e.getY())); + return; + } + + if (e.isPrimaryButtonDown() || e.isSecondaryButtonDown()) { + editPressX = e.getX(); + editPressY = e.getY(); + input.mouseScreenX = (float) e.getX(); + input.mouseScreenY = (float) e.getY(); + } + }); + + viewport.setOnMouseReleased(e -> { + objDragging = false; + if (!isObjectMode()) { stopEditTimer(); } + if (input.activeLayer == SharedInput.LAYER_WATER) { + input.waterDragEnd = true; + } + if (input.activeLayer == SharedInput.LAYER_WATERFALL) { + input.waterfallDragEnd = true; + } + if (input.activeLayer == SharedInput.LAYER_ROUTINE_EDITOR) { + input.routineYawDragQueue.offer( + new SharedInput.RoutineYawDrag((float) e.getX(), (float) e.getY(), true)); + } + if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { + input.playToolMouseUp = true; + } + if (input.vertexSnapEnabled + && input.objectSelectionMode == SharedInput.SEL_MODE_VERTEX) { + input.vertexSnapTrigger = true; + } + }); + + pane.setOnMouseMoved(e -> { + input.mouseScreenX = (float) e.getX(); + input.mouseScreenY = (float) e.getY(); + }); + pane.setOnMouseExited(e -> input.mouseScreenX = -1f); + + viewport.setOnScroll(e -> { + int steps = (int) Math.signum(e.getDeltaY()); + if (e.isControlDown()) { steps *= 10; } + input.scrollAccum.addAndGet(steps); + }); + + return pane; + } + + private boolean isObjectMode() { + return input.activeLayer == SharedInput.LAYER_OBJECTS + || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT + || input.activeLayer == SharedInput.LAYER_AUSWAHL; + } + + private void startEditTimer() { + stopEditTimer(); + editTimer = new javafx.animation.Timeline( + new javafx.animation.KeyFrame(javafx.util.Duration.millis(50), + ev -> submitEdit(editPressX, editPressY, editPressAction)) + ); + editTimer.setCycleCount(javafx.animation.Timeline.INDEFINITE); + editTimer.play(); + } + + private void stopEditTimer() { + if (editTimer != null) { editTimer.stop(); editTimer = null; } + } + + private void submitEdit(double x, double y, int action) { + switch (input.activeLayer) { + case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action)); + case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action)); + case SharedInput.LAYER_GRASS_VERTEX -> { + input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); + if (action < 0) { input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); } + } + case SharedInput.LAYER_TERRAIN_GRASS -> { + input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); + if (action < 0) { input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); } + } + case 4 -> { + SharedInput.TextureEdit te = new SharedInput.TextureEdit((float) x, (float) y, action); + input.textureEditQueue.offer(te); + input.voxelTextureEditQueue.offer(te); + } + case SharedInput.LAYER_LIGHTS -> + input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0)); + case SharedInput.LAYER_EMITTERS -> + input.emitterClickQueue.offer(new SharedInput.EmitterClick((float) x, (float) y, action < 0)); + case SharedInput.LAYER_WATER -> + input.waterClickQueue.offer(new SharedInput.WaterClick((float) x, (float) y, action < 0)); + case SharedInput.LAYER_SOUND_AREAS -> + input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0)); + case SharedInput.LAYER_AREAS -> + input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0)); + case SharedInput.LAYER_LOCATION_ZONES -> + input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0)); + case SharedInput.LAYER_ZONEN -> { + input.soundAreaClickQueue.offer(new SharedInput.SoundAreaClick((float) x, (float) y, action < 0)); + input.areaClickQueue.offer(new SharedInput.AreaClick((float) x, (float) y, action < 0)); + input.locationZoneClickQueue.offer(new SharedInput.LocationZoneClick((float) x, (float) y, action < 0)); + } + case SharedInput.LAYER_VOXEL_CLIFF -> + input.voxelCliffClickQueue.offer(new SharedInput.VoxelCliffClick((float) x, (float) y, action < 0)); + case SharedInput.LAYER_PLAY_TOOL -> { + if (action > 0) { + input.playToolClickQueue.offer(new SharedInput.PlayToolClick((float) x, (float) y)); + } + } + case SharedInput.LAYER_VOXEL -> + input.voxelEditQueue.offer(new SharedInput.VoxelEdit((float) x, (float) y, action)); + case SharedInput.LAYER_STONE -> + input.stoneEditQueue.offer(new SharedInput.StoneEdit((float) x, (float) y, action)); + case SharedInput.LAYER_MODEL_INTERACTABLE -> { + if (action > 0) { + input.modelInteractableClickQueue.offer( + new SharedInput.ModelInteractableClick((float) x, (float) y)); + } + } + } + scheduleAutoSave.run(); + } + + public void drawCompass() { + if (compassCanvas == null) { return; } + final double S = compassCanvas.getWidth(); // 100 + final double cx = S / 2.0; + final double cy = S / 2.0; + final double R = cx - 3; // Radius bis zum Rand + + javafx.scene.canvas.GraphicsContext gc = compassCanvas.getGraphicsContext2D(); + gc.clearRect(0, 0, S, S); + + // Hintergrund: dunkler Kreis + gc.setFill(javafx.scene.paint.Color.rgb(12, 12, 24, 0.82)); + gc.fillOval(2, 2, S - 4, S - 4); + gc.setStroke(javafx.scene.paint.Color.rgb(100, 100, 160, 0.85)); + gc.setLineWidth(1.5); + gc.strokeOval(2, 2, S - 4, S - 4); + + // Rotierende Rose: -camYaw so dass die aktuelle Blickrichtung oben erscheint + gc.save(); + gc.translate(cx, cy); + gc.rotate(-input.camYaw); + + // Tick-Striche an 8 Positionen (45°-Schritte) + for (int i = 0; i < 8; i++) { + double a = Math.toRadians(i * 45.0); + double len = (i % 2 == 0) ? 9 : 5; + gc.setStroke(javafx.scene.paint.Color.rgb(160, 160, 200, 0.65)); + gc.setLineWidth(i % 2 == 0 ? 1.5 : 1.0); + gc.strokeLine( + Math.sin(a) * (R - len), -Math.cos(a) * (R - len), + Math.sin(a) * R, -Math.cos(a) * R); + } + + // Himmelsrichtungen (N/E/S/W), Text durch Gegenrotation immer lesbar + String[] dirs = {"N", "E", "S", "W"}; + double[] angles = {0.0, 90.0, 180.0, 270.0}; + for (int i = 0; i < 4; i++) { + double a = Math.toRadians(angles[i]); + double tx = Math.sin(a) * (R - 16); + double ty = -Math.cos(a) * (R - 16); + gc.save(); + gc.translate(tx, ty); + gc.rotate(input.camYaw); // Gegenrotation zur Rose → Buchstabe immer aufrecht + gc.setFill(i == 0 + ? javafx.scene.paint.Color.rgb(255, 80, 80) + : javafx.scene.paint.Color.rgb(210, 210, 230)); + gc.setFont(javafx.scene.text.Font.font( + "System", javafx.scene.text.FontWeight.BOLD, i == 0 ? 13 : 11)); + gc.fillText(dirs[i], -4.5, 5.0); + gc.restore(); + } + + gc.restore(); // Ende rotierende Rose + + // Fixer Richtungszeiger oben (gelbes Dreieck, zeigt immer die Blickrichtung) + gc.setFill(javafx.scene.paint.Color.rgb(255, 220, 60, 0.95)); + gc.fillPolygon( + new double[]{cx, cx - 5, cx + 5}, + new double[]{cy - R + 5, cy - R + 16, cy - R + 16}, + 3); + + // Mittelpunkt + gc.setFill(javafx.scene.paint.Color.rgb(200, 200, 240, 0.85)); + gc.fillOval(cx - 2.5, cy - 2.5, 5, 5); + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/world/WorldPanelController.java b/blight-editor/src/main/java/de/blight/editor/toolset/world/WorldPanelController.java new file mode 100644 index 0000000..efd13f5 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/world/WorldPanelController.java @@ -0,0 +1,511 @@ +package de.blight.editor.toolset.world; + +import de.blight.editor.ProjectRoot; +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import de.blight.editor.toolset.EditorUiHelper; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.*; + +/** + * Owns the world-zone side panels: + * Auswahl, Wege, Zonen (unified), SoundArea, Area, LocationZone. + */ +public class WorldPanelController { + + private final ToolContext ctx; + private final Runnable scheduleAutoSave; + + // Dynamic VBox areas rebuilt by update*Panel methods + private VBox auswahlDynamicContent; + private VBox zonenDynamicContent; + private VBox soundAreaDynamicContent; + private VBox areaDynamicContent; + private VBox locationZoneDynamicContent; + private Runnable zonenKindStyleUpdater; + + public WorldPanelController(ToolContext ctx, Runnable scheduleAutoSave) { + this.ctx = ctx; + this.scheduleAutoSave = scheduleAutoSave; + } + + // ── Public update methods called from EditorApp AnimationTimer ───────────── + + public void updateAuswahlItemPanel(String info) { + if (auswahlDynamicContent == null) return; + auswahlDynamicContent.getChildren().clear(); + if (info == null) { + Label noSel = new Label("Kein Element ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + auswahlDynamicContent.getChildren().add(noSel); + return; + } + String[] parts = info.split("\\|"); + String itemId = parts.length > 1 ? parts[1] : "?"; + Label idLabel = new Label("Item: " + itemId); + idLabel.setStyle("-fx-font-weight: bold;"); + Button deleteBtn = new Button("Löschen"); + deleteBtn.setMaxWidth(Double.MAX_VALUE); + deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;"); + deleteBtn.setOnAction(e -> ctx.input().deleteSelectedItemRequested = true); + auswahlDynamicContent.getChildren().addAll(idLabel, deleteBtn); + } + + public void updateZonenPanel(String kind) { + if (zonenDynamicContent == null) return; + zonenDynamicContent.getChildren().clear(); + switch (kind != null ? kind : "") { + case "area" -> { + VBox saved = areaDynamicContent; + areaDynamicContent = zonenDynamicContent; + updateAreaPanel(ctx.input().selectedAreaInfo); + areaDynamicContent = saved; + } + case "sound" -> { + VBox saved = soundAreaDynamicContent; + soundAreaDynamicContent = zonenDynamicContent; + updateSoundAreaPanel(ctx.input().selectedSoundAreaInfo); + soundAreaDynamicContent = saved; + } + case "location" -> { + VBox saved = locationZoneDynamicContent; + locationZoneDynamicContent = zonenDynamicContent; + updateLocationZonePanel(ctx.input().selectedLocationZoneInfo); + locationZoneDynamicContent = saved; + } + default -> { + Label noSel = new Label("Keine Zone ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + zonenDynamicContent.getChildren().add(noSel); + } + } + } + + public void runZonenKindStyleUpdater() { + if (zonenKindStyleUpdater != null) zonenKindStyleUpdater.run(); + } + + public void updateSoundAreaPanel(String info) { + if (soundAreaDynamicContent == null) return; + soundAreaDynamicContent.getChildren().clear(); + if (info == null) { + Label noSel = new Label("Kein Bereich ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + soundAreaDynamicContent.getChildren().add(noSel); + return; + } + String[] p = info.split("\\|", -1); + if (p.length < 4) return; + try { + int idx = Integer.parseInt(p[0]); + float volume = Float.parseFloat(p[2]); + boolean xfade = Boolean.parseBoolean(p[3]); + final String[] soundPath = {p[1]}; + final float[] vol = {volume}; + final boolean[] cf = {xfade}; + + Label soundLabel = new Label("Sound: " + (soundPath[0].isEmpty() ? "(keine)" : soundPath[0])); + soundLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;"); + soundLabel.setWrapText(true); + + Button browseBtn = new Button("Sound waehlen..."); + browseBtn.setMaxWidth(Double.MAX_VALUE); + browseBtn.setOnAction(e -> { + java.nio.file.Path assetRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources"); + new de.blight.editor.ui.SoundChooser(assetRoot, de.blight.editor.ui.SoundChooser.Mode.ALL) + .showAndWait() + .ifPresent(rel -> { + soundPath[0] = rel; + soundLabel.setText("Sound: " + rel); + sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]); + }); + }); + + Label volLbl = new Label("Lautstärke"); + volLbl.setStyle("-fx-font-size: 11;"); + Slider volSlider = new Slider(0, 1, vol[0]); + volSlider.setShowTickLabels(false); + volSlider.valueProperty().addListener((o, ov, nv) -> { + vol[0] = nv.floatValue(); + sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]); + }); + + CheckBox xfadeCB = new CheckBox("Loop-Crossfade"); + xfadeCB.setSelected(cf[0]); + xfadeCB.setOnAction(e -> { + cf[0] = xfadeCB.isSelected(); + sendSoundAreaUpdate(idx, soundPath[0], vol[0], cf[0]); + }); + + Button delBtn = new Button("Löschen"); + delBtn.setMaxWidth(Double.MAX_VALUE); + delBtn.setStyle("-fx-text-fill: #c0392b;"); + delBtn.setOnAction(e -> ctx.input().deleteSoundAreaRequested = true); + + soundAreaDynamicContent.getChildren().addAll( + soundLabel, browseBtn, new Separator(), + volLbl, EditorUiHelper.withField(volSlider, "%.2f"), xfadeCB, + new Separator(), delBtn); + + } catch (NumberFormatException ignored) {} + } + + public void updateAreaPanel(String info) { + if (areaDynamicContent == null) return; + areaDynamicContent.getChildren().clear(); + if (info == null) { + Label noSel = new Label("Keine Area ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + areaDynamicContent.getChildren().add(noSel); + return; + } + String[] p = info.split("\\|", 3); + if (p.length < 1) return; + try { + int idx = Integer.parseInt(p[0]); + String currentAreaId = p.length > 1 ? p[1] : ""; + java.util.List existingTriggers = + p.length > 2 + ? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2]) + : new java.util.ArrayList<>(); + + java.util.List areaIds = new java.util.ArrayList<>(); + areaIds.add(""); + try { + de.blight.common.AreaDefinitionIO.load().stream() + .map(de.blight.common.AreaDefinition::id) + .filter(s -> !s.isBlank()) + .forEach(areaIds::add); + } catch (Exception ignored) {} + + Label areaLbl = new Label("Verknüpfte Area:"); + areaLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); + + Label nameKeyLbl = new Label(currentAreaId.isBlank() ? "" : currentAreaId + ".name"); + nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;"); + + de.blight.editor.ui.TriggerListEditor[] editorHolder = {null}; + + ComboBox areaCombo = new ComboBox<>(); + areaCombo.getItems().setAll(areaIds); + areaCombo.setValue(areaIds.contains(currentAreaId) ? currentAreaId : ""); + areaCombo.setMaxWidth(Double.MAX_VALUE); + areaCombo.setPromptText("Area wählen…"); + areaCombo.valueProperty().addListener((obs, o, n) -> { + String aid = n != null ? n : ""; + nameKeyLbl.setText(aid.isBlank() ? "" : aid + ".name"); + java.util.List curTriggers = + editorHolder[0] != null + ? new java.util.ArrayList<>(editorHolder[0].getTriggers()) + : existingTriggers; + ctx.input().pendingArea.set(new de.blight.common.PlacedArea( + new float[0], new float[0], aid, curTriggers)); + scheduleAutoSave.run(); + }); + + Label triggerLbl = new Label("Trigger:"); + triggerLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); + + editorHolder[0] = new de.blight.editor.ui.TriggerListEditor(existingTriggers, () -> { + String aid = areaCombo.getValue() != null ? areaCombo.getValue() : currentAreaId; + ctx.input().pendingArea.set(new de.blight.common.PlacedArea( + new float[0], new float[0], aid, + new java.util.ArrayList<>(editorHolder[0].getTriggers()))); + scheduleAutoSave.run(); + }); + + Button delBtn = new Button("Löschen"); + delBtn.setMaxWidth(Double.MAX_VALUE); + delBtn.setStyle("-fx-text-fill: #c0392b;"); + delBtn.setOnAction(e -> { ctx.input().deleteAreaRequested = true; scheduleAutoSave.run(); }); + + areaDynamicContent.getChildren().addAll( + areaLbl, areaCombo, nameKeyLbl, + new Separator(), + triggerLbl, editorHolder[0], + new Separator(), + delBtn); + + } catch (NumberFormatException ignored) {} + } + + public void updateLocationZonePanel(String info) { + if (locationZoneDynamicContent == null) return; + locationZoneDynamicContent.getChildren().clear(); + if (info == null) { + Label noSel = new Label("Keine Location ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + locationZoneDynamicContent.getChildren().add(noSel); + return; + } + String[] p = info.split("\\|", 3); + if (p.length < 1) return; + try { + int idx = Integer.parseInt(p[0]); + String nameId = p.length > 1 ? p[1] : ""; + java.util.List existingTriggers = + p.length > 2 + ? de.blight.common.model.trigger.TriggerIO.deserializeList(p[2]) + : new java.util.ArrayList<>(); + + java.util.List locationIds = new java.util.ArrayList<>(); + locationIds.add(""); + try { + de.blight.common.LocationIO.load().stream() + .map(de.blight.common.model.Location::getId) + .filter(s -> !s.isBlank()) + .forEach(locationIds::add); + } catch (Exception ignored) {} + + Label nameLbl = new Label("Verknüpfte Location:"); + nameLbl.setStyle("-fx-font-size: 11; -fx-font-weight: bold;"); + + Label nameKeyLbl = new Label(nameId.isBlank() ? "" : nameId + ".name"); + nameKeyLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #888; -fx-font-style: italic;"); + + ComboBox locationCombo = new ComboBox<>(); + locationCombo.getItems().setAll(locationIds); + locationCombo.setValue(locationIds.contains(nameId) ? nameId : ""); + locationCombo.setMaxWidth(Double.MAX_VALUE); + locationCombo.setPromptText("Location wählen…"); + locationCombo.valueProperty().addListener((obs, o, n) -> { + String lid = n != null ? n : ""; + nameKeyLbl.setText(lid.isBlank() ? "" : lid + ".name"); + ctx.input().pendingLocationZone.set(new de.blight.common.PlacedLocationZone( + new float[0], new float[0], lid, existingTriggers)); + scheduleAutoSave.run(); + }); + + Button delBtn = new Button("Löschen"); + delBtn.setMaxWidth(Double.MAX_VALUE); + delBtn.setStyle("-fx-text-fill: #c0392b;"); + delBtn.setOnAction(e -> { ctx.input().deleteLocationZoneRequested = true; scheduleAutoSave.run(); }); + + locationZoneDynamicContent.getChildren().addAll( + nameLbl, locationCombo, nameKeyLbl, + new Separator(), + delBtn); + + } catch (NumberFormatException ignored) {} + } + + // ── Panel builders ───────────────────────────────────────────────────────── + + public VBox buildAuswahlPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + Label title = new Label("Auswahl"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); + panel.getChildren().addAll(title, new Separator()); + panel.getChildren().addAll( + EditorUiHelper.styledHint("L-Klick: Element auswählen"), + EditorUiHelper.styledHint(" → wechselt automatisch zum"), + EditorUiHelper.styledHint(" passenden Werkzeug"), + new Separator(), + EditorUiHelper.styledHint("Selektierbar:"), + EditorUiHelper.styledHint(" + Platzierte Objekte"), + EditorUiHelper.styledHint(" * Lichter / Emitter"), + EditorUiHelper.styledHint(" ~ Wasser / Wasserfall"), + EditorUiHelper.styledHint(" -- Wege / Knoten"), + EditorUiHelper.styledHint(" [ ] Zonen"), + EditorUiHelper.styledHint(" o Spawnpunkte (Items)"), + new Separator()); + auswahlDynamicContent = new VBox(6); + Label noSel = new Label("Kein Element ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + auswahlDynamicContent.getChildren().add(noSel); + panel.getChildren().add(auswahlDynamicContent); + return panel; + } + + public VBox buildWegePanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + inner.getChildren().addAll( + EditorUiHelper.sectionTitle("Wegnetz"), + new Separator(), + EditorUiHelper.styledHint("L-Klick: neuen Knoten setzen"), + EditorUiHelper.styledHint("L-Klick auf Knoten: auswählen"), + EditorUiHelper.styledHint(" (2 gewählte → Kante erzeugen)"), + EditorUiHelper.styledHint("L-Klick + gewählter Knoten:"), + EditorUiHelper.styledHint(" neuen Knoten + Kante"), + EditorUiHelper.styledHint("R-Klick auf Knoten: löschen"), + EditorUiHelper.styledHint("R-Klick auf Terrain: abwählen")); + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(260); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + public VBox buildZonenPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + final String STYLE_AREA_ON = "-fx-background-color: #e05050; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;"; + final String STYLE_AREA_OFF = "-fx-background-color: #b03030; -fx-text-fill: #ddd; -fx-font-weight: bold;"; + final String STYLE_SOUND_ON = "-fx-background-color: #e07818; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;"; + final String STYLE_SOUND_OFF = "-fx-background-color: #a05010; -fx-text-fill: #ddd; -fx-font-weight: bold;"; + final String STYLE_LOC_ON = "-fx-background-color: #c8a800; -fx-text-fill: white; -fx-font-weight: bold; -fx-border-color: white; -fx-border-width: 2;"; + final String STYLE_LOC_OFF = "-fx-background-color: #8a7200; -fx-text-fill: #ddd; -fx-font-weight: bold;"; + + Button areaKindBtn = new Button("+ Neue Area zeichnen"); + Button soundKindBtn = new Button("+ Neuen Sound zeichnen"); + Button locationKindBtn = new Button("+ Neue Location zeichnen"); + areaKindBtn.setMaxWidth(Double.MAX_VALUE); + soundKindBtn.setMaxWidth(Double.MAX_VALUE); + locationKindBtn.setMaxWidth(Double.MAX_VALUE); + + Runnable updateKindStyles = () -> { + String k = ctx.input().zonenNewKind; + areaKindBtn.setStyle("area".equals(k) ? STYLE_AREA_ON : STYLE_AREA_OFF); + soundKindBtn.setStyle("sound".equals(k) ? STYLE_SOUND_ON : STYLE_SOUND_OFF); + locationKindBtn.setStyle("location".equals(k) ? STYLE_LOC_ON : STYLE_LOC_OFF); + }; + updateKindStyles.run(); + zonenKindStyleUpdater = updateKindStyles; + + areaKindBtn.setOnAction(e -> { ctx.input().zonenNewKind = "area"; updateKindStyles.run(); }); + soundKindBtn.setOnAction(e -> { ctx.input().zonenNewKind = "sound"; updateKindStyles.run(); }); + locationKindBtn.setOnAction(e -> { ctx.input().zonenNewKind = "location"; updateKindStyles.run(); }); + + areaKindBtn.setTooltip(new Tooltip( + "Area: Musik-Zone mit Tag-, Nacht- und Kampf-Track.\nAreas dürfen sich nicht überschneiden.")); + soundKindBtn.setTooltip(new Tooltip( + "Sound: Ambient-Soundbereich mit wählbarer Audio-Datei,\nLautstärke und Loop-Crossfade.")); + locationKindBtn.setTooltip(new Tooltip( + "Location: Gameplay-Trigger-Zone, z.B. zum Starten\nvon Quests oder Dialogen beim Betreten.")); + + inner.getChildren().addAll( + EditorUiHelper.sectionTitle("Neue Zone"), + areaKindBtn, soundKindBtn, locationKindBtn, + new Separator(), + EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"), + EditorUiHelper.styledHint("R-Klick (beim Zeichnen) → letzten Punkt rückgängig"), + EditorUiHelper.styledHint("R-Klick (sonst) → Auswahl aufheben"), + EditorUiHelper.styledHint("ESC → Zeichnen abbrechen"), + EditorUiHelper.styledHint("Entf → Gewählte Zone löschen"), + new Separator(), + EditorUiHelper.sectionTitle("Gewählte Zone"), + new Separator()); + + zonenDynamicContent = new VBox(6); + Label noSel = new Label("Keine Zone ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + zonenDynamicContent.getChildren().add(noSel); + inner.getChildren().add(zonenDynamicContent); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + public VBox buildSoundAreaPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + inner.getChildren().addAll( + EditorUiHelper.sectionTitle("Sound-Bereiche"), + EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"), + EditorUiHelper.styledHint("R-Klick → Polygon schließen / Auswahl aufheben"), + EditorUiHelper.styledHint("Entf → Bereich löschen"), + new Separator(), + EditorUiHelper.sectionTitle("Gewählter Bereich"), + new Separator()); + soundAreaDynamicContent = new VBox(6); + Label noSel = new Label("Kein Bereich ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + soundAreaDynamicContent.getChildren().add(noSel); + inner.getChildren().add(soundAreaDynamicContent); + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + public VBox buildAreaPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + inner.getChildren().addAll( + EditorUiHelper.sectionTitle("Areas"), + EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"), + EditorUiHelper.styledHint("R-Klick → Polygon schließen / Auswahl aufheben"), + EditorUiHelper.styledHint("ESC → Zeichnen abbrechen"), + EditorUiHelper.styledHint("Entf → Area löschen"), + EditorUiHelper.styledHint("Areas dürfen sich nicht überschneiden"), + new Separator(), + EditorUiHelper.sectionTitle("Gewählte Area"), + new Separator()); + areaDynamicContent = new VBox(6); + Label noSel = new Label("Keine Area ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + areaDynamicContent.getChildren().add(noSel); + inner.getChildren().add(areaDynamicContent); + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + public VBox buildLocationZonePanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + inner.getChildren().addAll( + EditorUiHelper.sectionTitle("Locations"), + EditorUiHelper.styledHint("L-Klick → Polygon-Punkte setzen"), + EditorUiHelper.styledHint("R-Klick → Polygon schließen / Auswahl aufheben"), + EditorUiHelper.styledHint("ESC → Zeichnen abbrechen"), + EditorUiHelper.styledHint("Entf → Location löschen"), + EditorUiHelper.styledHint("Locations dürfen sich überschneiden"), + new Separator(), + EditorUiHelper.sectionTitle("Gewählte Location"), + new Separator()); + locationZoneDynamicContent = new VBox(6); + Label noSel = new Label("Keine Location ausgewählt"); + noSel.setStyle("-fx-text-fill: #888;"); + locationZoneDynamicContent.getChildren().add(noSel); + inner.getChildren().add(locationZoneDynamicContent); + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + return panel; + } + + // ── Internal ────────────────────────────────────────────────────────────── + + private void sendSoundAreaUpdate(int idx, String soundPath, float volume, boolean crossfade) { + SharedInput in = ctx.input(); + if (in.selectedSoundAreaInfo == null) return; + in.selectedSoundAreaInfo = idx + "|" + soundPath + "|" + volume + "|" + crossfade; + in.pendingSoundArea.set(new de.blight.common.PlacedSoundArea( + new float[0], new float[0], soundPath, volume, crossfade)); + scheduleAutoSave.run(); + } +} diff --git a/blight-map/src/main/map/blight_emitters.bpe b/blight-map/src/main/map/blight_emitters.bpe index 0f2c5cb..966e690 100644 --- a/blight-map/src/main/map/blight_emitters.bpe +++ b/blight-map/src/main/map/blight_emitters.bpe @@ -1 +1,2 @@ # x y z activationRadius texturePath imagesX imagesY startR startG startB startA endR endG endB endA startSize endSize velX velY velZ velVariation gravX gravY gravZ lowLife highLife maxParticles emitRate +0.93604 2.50000 -3.56553 20.00000 Effects/Explosion/flame.png 2 2 1.0000 0.7000 0.1000 1.0000 1.0000 0.1000 0.0000 0.0000 0.5000 1.2000 0.0000 2.5000 0.0000 0.5000 0.0000 -0.1000 0.0000 1.0000 3.0000 60 25.0000 diff --git a/blight-map/src/main/map/blight_grass.blg b/blight-map/src/main/map/blight_grass.blg index 8597292..14f3df9 100644 Binary files a/blight-map/src/main/map/blight_grass.blg and b/blight-map/src/main/map/blight_grass.blg differ diff --git a/blight-map/src/main/map/blight_grass_terrain.blgv b/blight-map/src/main/map/blight_grass_terrain.blgv index 468ee0c..50342bb 100644 Binary files a/blight-map/src/main/map/blight_grass_terrain.blgv and b/blight-map/src/main/map/blight_grass_terrain.blgv differ diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index 360e9ca..35d3069 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_lights.bll b/blight-map/src/main/map/blight_lights.bll index 36a87bd..df6aa3b 100644 --- a/blight-map/src/main/map/blight_lights.bll +++ b/blight-map/src/main/map/blight_lights.bll @@ -1 +1,3 @@ # x y z r g b intensity radius +-2.18687 3.00000 -5.43333 1.00000 1.00000 1.00000 1.00000 20.00000 +0.42349 3.00000 -10.67357 1.00000 1.00000 1.00000 1.00000 20.00000 diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index 9b07e0e..4700fb5 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_voxel_splat.bin b/blight-map/src/main/map/blight_voxel_splat.bin index adce6f4..585d4b9 100644 Binary files a/blight-map/src/main/map/blight_voxel_splat.bin and b/blight-map/src/main/map/blight_voxel_splat.bin differ diff --git a/blight-map/src/main/map/blight_water.blw b/blight-map/src/main/map/blight_water.blw index 710d40c..3dadb1c 100644 --- a/blight-map/src/main/map/blight_water.blw +++ b/blight-map/src/main/map/blight_water.blw @@ -1,2 +1,3 @@ # polygon_points waterHeight flowDegrees speed waveScale waveAmplitude transparency waterR waterG waterB deepR deepG deepB 155.13664,11.00000,-898.11578;120.48705,11.00000,-897.61938;107.05534,11.00000,-890.41797;96.71703,11.00000,-875.01575;98.34164,11.00000,-854.85681;182.64165,11.00000,-829.23749;195.95512,11.00000,-848.04095 11.00000 180.0 0.5000 0.008000 0.10000 0.15000 0.05000 0.25000 0.55000 0.02000 0.12000 0.30000 +219.67131,1.90000,-796.04559;197.17520,1.90000,-800.89313;192.60945,1.90000,-797.65149;190.13742,1.90000,-767.83209;221.88794,1.90000,-767.83209 1.90000 0.0 0.5000 0.008000 0.10000 0.15000 0.05000 0.25000 0.55000 0.02000 0.12000 0.30000 diff --git a/blight-map/src/main/map/chunks/chunk_09_01.blc b/blight-map/src/main/map/chunks/chunk_09_01.blc index 9d013f9..c53212e 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_01.blc and b/blight-map/src/main/map/chunks/chunk_09_01.blc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_08.blvc.prebake b/blight-map/src/main/map/chunks/voxel_16_0_08.blvc.prebake index 13a8ba1..0c9f3d1 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_08.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_16_0_08.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod0.j3o index 58b5622..d5487df 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod1.j3o index 8e897da..237878e 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o new file mode 100644 index 0000000..3ac62aa Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_08_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_08.blvc.prebake b/blight-map/src/main/map/chunks/voxel_16_m1_08.blvc.prebake index 9d66284..112e4ea 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_m1_08.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_16_m1_08.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod0.j3o index 658b9af..55d4425 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod1.j3o index 07c071f..8775902 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod2.j3o index d786551..f54fb1f 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod2.j3o and b/blight-map/src/main/map/chunks/voxel_16_m1_08_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_10.blvc.prebake b/blight-map/src/main/map/chunks/voxel_17_0_10.blvc.prebake new file mode 100644 index 0000000..945acbf Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_10.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod0.j3o new file mode 100644 index 0000000..0795e42 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod1.j3o new file mode 100644 index 0000000..f046c03 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod2.j3o new file mode 100644 index 0000000..5b6dbf4 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_0_10_baked_lod2.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_10.blvc.prebake b/blight-map/src/main/map/chunks/voxel_17_m1_10.blvc.prebake new file mode 100644 index 0000000..b58cbf8 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_10.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod0.j3o new file mode 100644 index 0000000..a6cbb77 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod1.j3o new file mode 100644 index 0000000..9ac6f32 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod2.j3o new file mode 100644 index 0000000..18faa37 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_17_m1_10_baked_lod2.j3o differ