Weiter am Kampfsystem gearbeitet - hier: Schwert und Schild
This commit is contained in:
@@ -204,19 +204,10 @@ public class EditorApp extends Application {
|
||||
|
||||
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 muss VOR buildTop() erstellt werden, damit es korrekt in Deps landet.
|
||||
// Alle Abhängigkeiten (objectController, animPreviewController etc.) sind bereits verfügbar.
|
||||
// Die Buttons (atmosphaereBtn, wasserMergedBtn, zonenBtn) sind im TopBarController
|
||||
// als final-Felder vordefiniert und unabhängig von buildTop().
|
||||
assetController = new de.blight.editor.toolset.asset.AssetTreeController(toolCtx,
|
||||
new de.blight.editor.toolset.asset.AssetTreeController.Deps(
|
||||
objectController, animPreviewController, animSetController,
|
||||
@@ -230,6 +221,20 @@ public class EditorApp extends Application {
|
||||
terrainController::loadTexMeta, terrainController::saveTexMeta,
|
||||
() -> currentTool, r -> pendingAfterSave = r
|
||||
));
|
||||
|
||||
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.buildPanel();
|
||||
assetController.setupOverlay(centerStack);
|
||||
|
||||
@@ -430,8 +435,25 @@ public class EditorApp extends Application {
|
||||
worldPanelController.updateLocationZonePanel(input.selectedLocationZoneInfo);
|
||||
}
|
||||
|
||||
// Items haben höchste Priorität: ein selektiertes Item verdrängt
|
||||
// alle konkurrierenden Auswahl-Treffer desselben Klicks.
|
||||
if (input.itemSelectionChanged && input.selectedItemInfo != null
|
||||
&& (input.activeLayer == SharedInput.LAYER_AUSWAHL
|
||||
|| input.activeLayer == SharedInput.LAYER_ITEMS_EDIT)) {
|
||||
input.itemSelectionChanged = false;
|
||||
input.auswahlZoneSelected = false;
|
||||
input.auswahlLightSelected = false;
|
||||
input.auswahlEmitterSelected = false;
|
||||
input.auswahlSoundSelected = false;
|
||||
input.auswahlWaterSelected = false;
|
||||
input.auswahlWaterfallSelected = false;
|
||||
log.info("[Auswahl→Tool] Item ausgewaehlt → Info='{}'", input.selectedItemInfo);
|
||||
root.setRight(worldPanelController.buildAuswahlItemEditPanel(input.selectedItemInfo));
|
||||
}
|
||||
|
||||
if (input.auswahlZoneSelected) {
|
||||
input.auswahlZoneSelected = false;
|
||||
log.info("[Auswahl→Tool] Zone ausgewaehlt → LAYER_ZONEN");
|
||||
input.activeLayer = SharedInput.LAYER_ZONEN;
|
||||
topBarController.getZonenBtn().setSelected(true);
|
||||
root.setRight(worldPanelController.buildZonenPanel());
|
||||
@@ -439,6 +461,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlLightSelected) {
|
||||
input.auswahlLightSelected = false;
|
||||
log.info("[Auswahl→Tool] Licht ausgewaehlt → Atmo-Tab, Info='{}'", input.selectedLightInfo);
|
||||
topBarController.getAtmosphaereBtn().setSelected(true);
|
||||
root.setRight(atmosphereController.buildPanel());
|
||||
atmosphereController.updateLightPanel(input.selectedLightInfo);
|
||||
@@ -446,6 +469,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlEmitterSelected) {
|
||||
input.auswahlEmitterSelected = false;
|
||||
log.info("[Auswahl→Tool] Emitter ausgewaehlt → Atmo-Tab/LAYER_EMITTERS, Info='{}'", input.selectedEmitterInfo);
|
||||
topBarController.getAtmosphaereBtn().setSelected(true);
|
||||
input.activeLayer = SharedInput.LAYER_EMITTERS;
|
||||
root.setRight(atmosphereController.buildPanel());
|
||||
@@ -454,6 +478,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlSoundSelected) {
|
||||
input.auswahlSoundSelected = false;
|
||||
log.info("[Auswahl→Tool] Sound ausgewaehlt → Atmo-Tab/LAYER_PLACED_SOUNDS, Info='{}'", input.selectedSoundInfo);
|
||||
topBarController.getAtmosphaereBtn().setSelected(true);
|
||||
input.activeLayer = SharedInput.LAYER_PLACED_SOUNDS;
|
||||
root.setRight(atmosphereController.buildPanel());
|
||||
@@ -462,6 +487,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlWaterSelected) {
|
||||
input.auswahlWaterSelected = false;
|
||||
log.info("[Auswahl→Tool] Wasser ausgewaehlt → LAYER_WATER");
|
||||
topBarController.getWasserMergedBtn().setSelected(true);
|
||||
input.activeLayer = SharedInput.LAYER_WATER;
|
||||
root.setRight(waterController.buildPanel());
|
||||
@@ -470,15 +496,22 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlWaterfallSelected) {
|
||||
input.auswahlWaterfallSelected = false;
|
||||
log.info("[Auswahl→Tool] Wasserfall ausgewaehlt → LAYER_WATERFALL");
|
||||
topBarController.getWasserMergedBtn().setSelected(true);
|
||||
input.activeLayer = SharedInput.LAYER_WATERFALL;
|
||||
root.setRight(waterController.buildPanel());
|
||||
waterController.updateWaterfallPanel(input.selectedWaterfallInfo);
|
||||
}
|
||||
|
||||
// itemSelectionChanged mit null = Item abgewählt → Auswahl-Panel wiederherstellen,
|
||||
// aber nur wenn noch im Auswahl-Kontext (kein anderes Tool geöffnet)
|
||||
if (input.itemSelectionChanged) {
|
||||
input.itemSelectionChanged = false;
|
||||
worldPanelController.updateAuswahlItemPanel(input.selectedItemInfo);
|
||||
if (input.activeLayer == SharedInput.LAYER_AUSWAHL
|
||||
|| input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) {
|
||||
log.info("[Auswahl→Tool] Item abgewaehlt → Auswahl-Panel");
|
||||
root.setRight(worldPanelController.buildAuswahlPanel());
|
||||
}
|
||||
}
|
||||
|
||||
if (input.zonenSelectionChanged) {
|
||||
@@ -952,7 +985,9 @@ public class EditorApp extends Application {
|
||||
input.deleteEmitterRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_PLACED_SOUNDS && input.selectedSoundInfo != null)
|
||||
input.deleteSoundRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_ITEMS && input.selectedItemInfo != null)
|
||||
else if ((input.activeLayer == SharedInput.LAYER_ITEMS
|
||||
|| input.activeLayer == SharedInput.LAYER_ITEMS_EDIT)
|
||||
&& input.selectedItemInfo != null)
|
||||
input.deleteSelectedItemRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
|
||||
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
|
||||
|
||||
@@ -970,6 +970,9 @@ public class SharedInput {
|
||||
/** activeLayer==21 → Item-Pickup auf die Karte platzieren */
|
||||
public static final int LAYER_ITEMS = 21;
|
||||
|
||||
/** activeLayer==36 → ausgewähltes Item per Gizmo verschieben/rotieren */
|
||||
public static final int LAYER_ITEMS_EDIT = 36;
|
||||
|
||||
/** JFX → JME: itemId des zu platzierenden Items (aus .item-Datei). */
|
||||
public volatile String pendingItemId = null;
|
||||
|
||||
@@ -1178,6 +1181,8 @@ public class SharedInput {
|
||||
public volatile float gripEditorRotZ = 0f;
|
||||
/** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */
|
||||
public volatile boolean gripEditorGizmoDirty = false;
|
||||
/** true → Gizmo ist ein 28cm-Unterarm-Pfeil (Schild); false → 15cm-Waffen-Pfeil. */
|
||||
public volatile boolean gripEditorIsShield = false;
|
||||
|
||||
/** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */
|
||||
public record SculptEdit(float screenX, float screenY, int action) {}
|
||||
@@ -1246,11 +1251,19 @@ public class SharedInput {
|
||||
public volatile boolean deselectAllOnAuswahl = false;
|
||||
/** JME3 → JavaFX: Zone wurde im Auswahl-Modus selektiert → Zonen-Werkzeug aktivieren. */
|
||||
public volatile boolean auswahlZoneSelected = false;
|
||||
/** JME3 → JavaFX: Item-Spawn im Auswahl-Modus selektiert. Format "idx|itemId|x|y|z" oder null. */
|
||||
/** JME3 → JavaFX: Item-Spawn im Auswahl-Modus selektiert. Format "idx|itemId|x|y|z|rotX|rotY|rotZ" oder null. */
|
||||
public volatile String selectedItemInfo = null;
|
||||
public volatile boolean itemSelectionChanged = false;
|
||||
/** JavaFX → JME3: Selektierten Item-Spawn löschen. */
|
||||
public volatile boolean deleteSelectedItemRequested = false;
|
||||
/** JavaFX → JME3: Position/Rotation des selektierten Items ändern. */
|
||||
public volatile boolean itemEditRequest = false;
|
||||
public volatile float itemEditX = 0f;
|
||||
public volatile float itemEditY = 0f;
|
||||
public volatile float itemEditZ = 0f;
|
||||
public volatile float itemEditRotX = 0f;
|
||||
public volatile float itemEditRotY = 0f;
|
||||
public volatile float itemEditRotZ = 0f;
|
||||
/** JME3-intern: Auswahl-Klick für ItemPlacementState (kein Objekt/keine Zone getroffen). */
|
||||
public final ConcurrentLinkedQueue<ObjectClick> auswahlClickQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3-intern: Auswahl-Klick für LightState (kein Objekt/keine Zone getroffen). */
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Verwaltet das aktive Werkzeug und leitet den AnimationTimer-Frame weiter.
|
||||
*
|
||||
@@ -8,6 +11,8 @@ package de.blight.editor;
|
||||
*/
|
||||
public class ToolRegistry {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ToolRegistry.class);
|
||||
|
||||
private ToolController active = null;
|
||||
|
||||
/**
|
||||
@@ -19,10 +24,12 @@ public class ToolRegistry {
|
||||
if (tool == active) {
|
||||
return;
|
||||
}
|
||||
String prev = active != null ? active.toolId() : "–";
|
||||
if (active != null) {
|
||||
active.deactivate();
|
||||
}
|
||||
active = tool;
|
||||
log.info("[Tool] {} → {}", prev, tool.toolId());
|
||||
tool.activate();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.math.Ray;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.editor.SharedInput;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Gemeinsame Basis für Zustandsklassen die Marker-Knoten verwalten und
|
||||
* Auswahl-Klicks via {@code auswahlXxxClickQueue} verarbeiten
|
||||
* (EmitterState, LightState, PlacedSoundState).
|
||||
*
|
||||
* Subklassen überschreiben {@link #onMarkerSelected(int)} um die eigene
|
||||
* Selektion auszuführen und das zugehörige SharedInput-Flag zu setzen.
|
||||
*/
|
||||
public abstract class AuswahlMarkerState extends BaseAppState {
|
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(getClass());
|
||||
protected final SharedInput input;
|
||||
protected SimpleApplication app;
|
||||
protected Camera cam;
|
||||
protected AssetManager assets;
|
||||
protected Node rootNode;
|
||||
protected TerrainQuad terrain;
|
||||
|
||||
protected final List<Node> markers = new ArrayList<>();
|
||||
|
||||
protected AuswahlMarkerState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) {
|
||||
this.terrain = terrain;
|
||||
}
|
||||
|
||||
protected final int pickMarker(Ray ray) {
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
markers.get(i).collideWith(ray, res);
|
||||
if (res.size() > 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected final void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
int hit = pickMarker(ray);
|
||||
log.info("[Auswahl] {}: Klick ({},{}), Marker getroffen: {}, Marker gesamt: {}",
|
||||
getClass().getSimpleName(), (int) click.screenX(), (int) click.screenY(),
|
||||
hit, markers.size());
|
||||
if (hit >= 0) {
|
||||
onMarkerSelected(hit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wird aufgerufen wenn ein Marker-Treffer gefunden wurde.
|
||||
* Subklasse selektiert den Eintrag und setzt das zugehörige auswahl*Selected-Flag.
|
||||
*/
|
||||
protected abstract void onMarkerSelected(int idx);
|
||||
}
|
||||
@@ -18,15 +18,10 @@ import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.common.PlacedEmitter;
|
||||
import de.blight.editor.SharedInput;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EmitterState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EmitterState.class);
|
||||
public class EmitterState extends AuswahlMarkerState {
|
||||
|
||||
private static final float MARKER_RADIUS = 0.4f;
|
||||
private static final float ACTIVATION_ALPHA = 0.07f;
|
||||
@@ -35,16 +30,8 @@ public class EmitterState extends BaseAppState {
|
||||
private static final String GEO_MARKER = "emitter_marker";
|
||||
private static final String GEO_ACTRADIUS = "activation_sphere";
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
// parallel lists: emitters[i] <-> markers[i] <-> particles[i]
|
||||
private final List<PlacedEmitter> emitters = new ArrayList<>();
|
||||
private final List<Node> markers = new ArrayList<>();
|
||||
private final List<ParticleEmitter> particles = new ArrayList<>();
|
||||
|
||||
private int selectedIdx = -1;
|
||||
@@ -52,19 +39,11 @@ public class EmitterState extends BaseAppState {
|
||||
private List<PlacedEmitter> pendingEmitters = null;
|
||||
|
||||
public EmitterState(SharedInput input) {
|
||||
this.input = input;
|
||||
super(input);
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application application) { clearAll(); }
|
||||
|
||||
@Override
|
||||
@@ -77,7 +56,11 @@ public class EmitterState extends BaseAppState {
|
||||
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||
@Override
|
||||
protected void onMarkerSelected(int idx) {
|
||||
selectEmitter(idx);
|
||||
input.auswahlEmitterSelected = true;
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -180,28 +163,6 @@ public class EmitterState extends BaseAppState {
|
||||
};
|
||||
}
|
||||
|
||||
private void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
int hit = pickMarker(ray);
|
||||
if (hit >= 0) {
|
||||
selectEmitter(hit);
|
||||
input.auswahlEmitterSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
private int pickMarker(Ray ray) {
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
markers.get(i).collideWith(ray, res);
|
||||
if (res.size() > 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectEmitter(int idx) {
|
||||
|
||||
@@ -4,12 +4,18 @@ import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.bounding.BoundingSphere;
|
||||
import com.jme3.bounding.BoundingVolume;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.shape.Box;
|
||||
import com.jme3.scene.shape.Cylinder;
|
||||
import com.jme3.scene.shape.Torus;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.common.PlacedItem;
|
||||
import de.blight.common.PlacedItemIO;
|
||||
@@ -21,6 +27,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -29,11 +36,31 @@ import java.util.Map;
|
||||
/**
|
||||
* Verwaltet die Platzierung von Items (Pickups) auf der Karte im Editor.
|
||||
* Aktiv wenn {@code input.activeLayer == LAYER_ITEMS}.
|
||||
* Auswahl-Modus (LAYER_AUSWAHL): Item auswählen → wechselt zu LAYER_ITEMS_EDIT.
|
||||
* LAYER_ITEMS_EDIT: Gizmo (Pfeile + Ringe) zum Verschieben/Rotieren.
|
||||
*/
|
||||
public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ItemPlacementState.class);
|
||||
|
||||
// ── Gizmo-Konstanten (identisch mit SceneObjectState) ────────────────────
|
||||
private static final ColorRGBA COL_X = new ColorRGBA(0.92f, 0.12f, 0.12f, 1f);
|
||||
private static final ColorRGBA COL_Y = new ColorRGBA(0.12f, 0.88f, 0.12f, 1f);
|
||||
private static final ColorRGBA COL_Z = new ColorRGBA(0.12f, 0.35f, 1.00f, 1f);
|
||||
|
||||
private static final float SHAFT_LEN = 3.2f;
|
||||
private static final float HEAD_LEN = 0.9f;
|
||||
private static final float SHAFT_RADIUS = 0.06f;
|
||||
private static final float HEAD_RADIUS = 0.22f;
|
||||
private static final float PX_PER_WE = 0.15f;
|
||||
private static final float RING_RADIUS = 4.5f;
|
||||
private static final float RING_TUBE = 0.25f;
|
||||
private static final float DEG_PER_PX = 0.8f;
|
||||
|
||||
private static final String HIGHLIGHT_NAME = "selectionHighlight";
|
||||
private static final ColorRGBA HIGHLIGHT_COLOR = new ColorRGBA(0.3f, 0.8f, 1.0f, 1f);
|
||||
|
||||
// ── Felder ────────────────────────────────────────────────────────────────
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
@@ -46,9 +73,16 @@ public class ItemPlacementState extends BaseAppState {
|
||||
private final Map<String, Item> itemDefs = new HashMap<>();
|
||||
private Node itemRoot;
|
||||
private Node previewNode;
|
||||
private int selectedItemIdx = -1;
|
||||
private int selectedItemIdx = -1;
|
||||
private Node selectedItemNode = null;
|
||||
|
||||
// Gizmo
|
||||
private Node gizmoNode;
|
||||
private Node arrowX, arrowY, arrowZ;
|
||||
private Node ringX, ringY, ringZ;
|
||||
private int activeGizmo = -1;
|
||||
private int activeRing = -1;
|
||||
|
||||
public ItemPlacementState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
@@ -66,7 +100,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
this.assets = app.getAssetManager();
|
||||
this.rootNode = this.app.getRootNode();
|
||||
|
||||
java.nio.file.Path itemDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("items");
|
||||
Path itemDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("items");
|
||||
for (Item it : ItemIO.loadAll(itemDir)) {
|
||||
if (it.getItemId() != null) itemDefs.put(it.getItemId(), it);
|
||||
}
|
||||
@@ -80,6 +114,11 @@ public class ItemPlacementState extends BaseAppState {
|
||||
prev.setMaterial(pm);
|
||||
prev.getMesh().setMode(com.jme3.scene.Mesh.Mode.Lines);
|
||||
previewNode.attachChild(prev);
|
||||
|
||||
gizmoNode = new Node("itemGizmo");
|
||||
buildGizmo();
|
||||
gizmoNode.setCullHint(Spatial.CullHint.Always);
|
||||
rootNode.attachChild(gizmoNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,6 +140,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
for (PlacedItem pi : items) {
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
n.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
|
||||
applyRot(n, pi.rotX(), pi.rotY(), pi.rotZ());
|
||||
itemRoot.attachChild(n);
|
||||
nodes.add(n);
|
||||
}
|
||||
@@ -113,7 +153,9 @@ public class ItemPlacementState extends BaseAppState {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {}
|
||||
protected void cleanup(Application app) {
|
||||
if (gizmoNode != null) gizmoNode.removeFromParent();
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -125,11 +167,15 @@ public class ItemPlacementState extends BaseAppState {
|
||||
reloadFromDisk();
|
||||
}
|
||||
|
||||
// Highlight entfernen sobald wir nicht mehr im Auswahl-Modus sind
|
||||
if (selectedItemIdx >= 0 && input.activeLayer != SharedInput.LAYER_AUSWAHL) {
|
||||
// Highlight entfernen sobald wir weder im Auswahl- noch im Edit-Modus sind
|
||||
if (selectedItemIdx >= 0
|
||||
&& input.activeLayer != SharedInput.LAYER_AUSWAHL
|
||||
&& input.activeLayer != SharedInput.LAYER_ITEMS_EDIT) {
|
||||
deselectItem();
|
||||
}
|
||||
|
||||
updateGizmoVisibility();
|
||||
|
||||
updatePreview();
|
||||
|
||||
SharedInput.ObjectClick click;
|
||||
@@ -142,6 +188,26 @@ public class ItemPlacementState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// LAYER_ITEMS_EDIT: Gizmo-Klicks und Drags
|
||||
if (input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) {
|
||||
SharedInput.ObjectClick objClick;
|
||||
while ((objClick = input.objectClickQueue.poll()) != null) {
|
||||
handleItemGizmoClick(objClick);
|
||||
}
|
||||
boolean hadDrags = false;
|
||||
SharedInput.ObjectDrag drag;
|
||||
while ((drag = input.objectDragQueue.poll()) != null) {
|
||||
handleItemGizmoDrag(drag);
|
||||
hadDrags = true;
|
||||
}
|
||||
if (hadDrags) {
|
||||
saveItems();
|
||||
}
|
||||
if (selectedItemIdx >= 0) {
|
||||
updateGizmoPosition();
|
||||
}
|
||||
}
|
||||
|
||||
SharedInput.ObjectClick auswahlClick;
|
||||
while ((auswahlClick = input.auswahlClickQueue.poll()) != null) {
|
||||
handleAuswahlClick(auswahlClick);
|
||||
@@ -155,8 +221,19 @@ public class ItemPlacementState extends BaseAppState {
|
||||
input.onObjectDeleted.run();
|
||||
}
|
||||
}
|
||||
|
||||
if (input.itemEditRequest) {
|
||||
input.itemEditRequest = false;
|
||||
if (selectedItemIdx >= 0) {
|
||||
applyItemEdit(selectedItemIdx,
|
||||
input.itemEditX, input.itemEditY, input.itemEditZ,
|
||||
input.itemEditRotX, input.itemEditRotY, input.itemEditRotZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Vorschau ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void updatePreview() {
|
||||
if (input.activeLayer != SharedInput.LAYER_ITEMS
|
||||
|| input.pendingItemId == null
|
||||
@@ -179,6 +256,8 @@ public class ItemPlacementState extends BaseAppState {
|
||||
previewNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
// ── Platzierung ───────────────────────────────────────────────────────────
|
||||
|
||||
private void handlePlace(SharedInput.ObjectClick click) {
|
||||
if (terrain == null || input.pendingItemId == null) return;
|
||||
float jx = click.screenX() * (float) input.viewportScaleX;
|
||||
@@ -188,30 +267,34 @@ public class ItemPlacementState extends BaseAppState {
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
PlacedItem pi = PlacedItem.create(input.pendingItemId, pt.x, pt.y, pt.z);
|
||||
items.add(pi);
|
||||
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
n.setLocalTranslation(pt.x, pt.y + 0.25f, pt.z);
|
||||
applyRot(n, 0f, 0f, 0f);
|
||||
itemRoot.attachChild(n);
|
||||
nodes.add(n);
|
||||
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})",
|
||||
pi.itemId(), pt.x, pt.y, pt.z);
|
||||
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", pi.itemId(), pt.x, pt.y, pt.z);
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auswahl-Klick (LAYER_AUSWAHL → Item-Picking) ─────────────────────────
|
||||
|
||||
private void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Ray ray = screenToRay(jmeX, jmeY);
|
||||
CollisionResults hits = new CollisionResults();
|
||||
itemRoot.collideWith(ray, hits);
|
||||
log.info("[Auswahl] ItemPlacementState: Klick ({},{}), Treffer: {}, Items gesamt: {}",
|
||||
(int) click.screenX(), (int) click.screenY(), hits.size(), items.size());
|
||||
if (hits.size() > 0) {
|
||||
Spatial geo = hits.getClosestCollision().getGeometry();
|
||||
Node parent = geo.getParent();
|
||||
@@ -219,44 +302,362 @@ public class ItemPlacementState extends BaseAppState {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
if (parent != null) {
|
||||
selectItem(nodes.indexOf(parent));
|
||||
int idx = nodes.indexOf(parent);
|
||||
log.info("[Auswahl] ItemPlacementState: Item {} ausgewaehlt (idx={})", items.get(idx).itemId(), idx);
|
||||
selectItem(idx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
deselectItem();
|
||||
}
|
||||
|
||||
// ── Gizmo-Klick (LAYER_ITEMS_EDIT) ───────────────────────────────────────
|
||||
|
||||
private void handleItemGizmoClick(SharedInput.ObjectClick click) {
|
||||
if (selectedItemIdx < 0) return;
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Ray ray = screenToRay(jmeX, jmeY);
|
||||
|
||||
if (input.objectEditTool == SharedInput.EDIT_TOOL_MOVE) {
|
||||
int hit = pickGizmo(ray);
|
||||
if (hit >= 0) {
|
||||
activeGizmo = hit;
|
||||
activeRing = -1;
|
||||
return;
|
||||
}
|
||||
} else if (input.objectEditTool == SharedInput.EDIT_TOOL_ROTATE) {
|
||||
int hit = pickRing(ray);
|
||||
if (hit >= 0) {
|
||||
activeRing = hit;
|
||||
activeGizmo = -1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
activeGizmo = -1;
|
||||
activeRing = -1;
|
||||
|
||||
// Klick außerhalb Gizmo: anderes Item oder leere Fläche?
|
||||
CollisionResults hits = new CollisionResults();
|
||||
itemRoot.collideWith(ray, hits);
|
||||
if (hits.size() > 0) {
|
||||
Spatial geo = hits.getClosestCollision().getGeometry();
|
||||
// Highlight-Klone ignorieren
|
||||
if (HIGHLIGHT_NAME.equals(geo.getParent() != null ? geo.getParent().getName() : "")) {
|
||||
return;
|
||||
}
|
||||
Node parent = geo.getParent();
|
||||
while (parent != null && !nodes.contains(parent)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
if (parent != null) {
|
||||
int idx = nodes.indexOf(parent);
|
||||
if (idx != selectedItemIdx) {
|
||||
selectItem(idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
deselectItem();
|
||||
}
|
||||
|
||||
// ── Gizmo-Drag (LAYER_ITEMS_EDIT) ────────────────────────────────────────
|
||||
|
||||
private void handleItemGizmoDrag(SharedInput.ObjectDrag drag) {
|
||||
if (selectedItemIdx < 0) return;
|
||||
switch (input.objectEditTool) {
|
||||
case SharedInput.EDIT_TOOL_MOVE -> handleMoveDrag(drag);
|
||||
case SharedInput.EDIT_TOOL_ROTATE -> handleRotateDrag(drag);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMoveDrag(SharedInput.ObjectDrag drag) {
|
||||
if (activeGizmo < 0 || selectedItemIdx < 0) return;
|
||||
Vector3f arrowDir = getArrowWorldDir(activeGizmo);
|
||||
Vector3f origin = gizmoNode.getWorldTranslation();
|
||||
Vector3f scrO = cam.getScreenCoordinates(origin);
|
||||
Vector3f scrT = cam.getScreenCoordinates(origin.add(arrowDir));
|
||||
float sdx = scrT.x - scrO.x;
|
||||
float sdy = -(scrT.y - scrO.y);
|
||||
float screenLen2 = sdx * sdx + sdy * sdy;
|
||||
float magnitude;
|
||||
if (screenLen2 < 0.01f) {
|
||||
magnitude = -drag.dy() * PX_PER_WE;
|
||||
} else {
|
||||
magnitude = (drag.dx() * sdx + drag.dy() * sdy) / screenLen2;
|
||||
}
|
||||
Vector3f delta = arrowDir.mult(magnitude);
|
||||
|
||||
Node n = nodes.get(selectedItemIdx);
|
||||
n.move(delta.x, delta.y, delta.z);
|
||||
|
||||
PlacedItem pi = items.get(selectedItemIdx);
|
||||
PlacedItem updated = pi.withTransform(
|
||||
pi.x() + delta.x, pi.y() + delta.y, pi.z() + delta.z,
|
||||
pi.rotX(), pi.rotY(), pi.rotZ());
|
||||
items.set(selectedItemIdx, updated);
|
||||
publishSelection(selectedItemIdx);
|
||||
updateGizmoPosition();
|
||||
}
|
||||
|
||||
private void handleRotateDrag(SharedInput.ObjectDrag drag) {
|
||||
if (activeRing < 0 || selectedItemIdx < 0) return;
|
||||
|
||||
PlacedItem pi = items.get(selectedItemIdx);
|
||||
Quaternion qObjY = new Quaternion().fromAngleAxis(pi.rotY(), Vector3f.UNIT_Y);
|
||||
Vector3f rotAxis = switch (activeRing) {
|
||||
case 0 -> qObjY.mult(new Vector3f(1, 0, 0));
|
||||
case 1 -> new Vector3f(0, 1, 0);
|
||||
default -> qObjY.mult(new Vector3f(0, 0, 1));
|
||||
};
|
||||
|
||||
Vector3f center = gizmoNode.getWorldTranslation();
|
||||
Vector3f toCam = cam.getLocation().subtract(center).normalizeLocal();
|
||||
Vector3f tangent = rotAxis.cross(toCam);
|
||||
float tLen = tangent.length();
|
||||
float angle;
|
||||
if (tLen < 1e-4f) {
|
||||
angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD;
|
||||
} else {
|
||||
tangent.divideLocal(tLen);
|
||||
Vector3f scrC = cam.getScreenCoordinates(center);
|
||||
Vector3f scrT = cam.getScreenCoordinates(center.add(tangent));
|
||||
float sdx = scrT.x - scrC.x;
|
||||
float sdy = -(scrT.y - scrC.y);
|
||||
float sl2 = sdx * sdx + sdy * sdy;
|
||||
if (sl2 < 0.5f) {
|
||||
angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD;
|
||||
} else {
|
||||
float proj = (drag.dx() * sdx + drag.dy() * sdy) / sl2;
|
||||
angle = proj / RING_RADIUS;
|
||||
}
|
||||
}
|
||||
|
||||
Node n = nodes.get(selectedItemIdx);
|
||||
Quaternion deltaRot = new Quaternion().fromAngleAxis(angle, rotAxis);
|
||||
n.setLocalRotation(deltaRot.mult(n.getLocalRotation()));
|
||||
|
||||
float[] angles = n.getLocalRotation().toAngles(null);
|
||||
PlacedItem updated = pi.withTransform(pi.x(), pi.y(), pi.z(), angles[0], angles[1], angles[2]);
|
||||
items.set(selectedItemIdx, updated);
|
||||
publishSelection(selectedItemIdx);
|
||||
}
|
||||
|
||||
// ── Selektion ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectItem(int idx) {
|
||||
deselectItem();
|
||||
selectedItemIdx = idx;
|
||||
selectedItemNode = nodes.get(idx);
|
||||
Geometry highlight = new Geometry("item_sel",
|
||||
new com.jme3.scene.shape.Box(0.28f, 0.28f, 0.28f));
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new com.jme3.math.ColorRGBA(1f, 0.6f, 0.1f, 1f));
|
||||
mat.getAdditionalRenderState().setWireframe(true);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
highlight.setMaterial(mat);
|
||||
highlight.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
|
||||
selectedItemNode.attachChild(highlight);
|
||||
PlacedItem pi = items.get(idx);
|
||||
input.selectedItemInfo = idx + "|" + pi.itemId() + "|" + pi.x() + "|" + pi.y() + "|" + pi.z();
|
||||
input.itemSelectionChanged = true;
|
||||
addSelectionHighlight(idx);
|
||||
publishSelection(idx);
|
||||
input.activeLayer = SharedInput.LAYER_ITEMS_EDIT;
|
||||
updateGizmoPosition();
|
||||
updateGizmoVisibility();
|
||||
}
|
||||
|
||||
private void deselectItem() {
|
||||
if (selectedItemNode != null) {
|
||||
Spatial h = selectedItemNode.getChild("item_sel");
|
||||
if (h != null) h.removeFromParent();
|
||||
removeSelectionHighlight(selectedItemIdx);
|
||||
selectedItemNode = null;
|
||||
}
|
||||
activeGizmo = -1;
|
||||
activeRing = -1;
|
||||
if (selectedItemIdx >= 0) {
|
||||
selectedItemIdx = -1;
|
||||
input.selectedItemInfo = null;
|
||||
input.itemSelectionChanged = true;
|
||||
}
|
||||
if (input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) {
|
||||
input.activeLayer = SharedInput.LAYER_AUSWAHL;
|
||||
}
|
||||
updateGizmoVisibility();
|
||||
}
|
||||
|
||||
private void publishSelection(int idx) {
|
||||
PlacedItem pi = items.get(idx);
|
||||
input.selectedItemInfo = idx + "|" + pi.itemId()
|
||||
+ "|" + pi.x() + "|" + pi.y() + "|" + pi.z()
|
||||
+ "|" + pi.rotX() + "|" + pi.rotY() + "|" + pi.rotZ();
|
||||
input.itemSelectionChanged = true;
|
||||
}
|
||||
|
||||
// ── Highlight ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void addSelectionHighlight(int idx) {
|
||||
Node itemNode = nodes.get(idx);
|
||||
removeSelectionHighlight(idx);
|
||||
Node highlight = new Node(HIGHLIGHT_NAME);
|
||||
for (Spatial child : new ArrayList<>(itemNode.getChildren())) {
|
||||
if (child == gizmoNode) continue;
|
||||
Spatial clone = child.clone();
|
||||
applySelectionMaterial(clone);
|
||||
highlight.attachChild(clone);
|
||||
}
|
||||
itemNode.attachChild(highlight);
|
||||
}
|
||||
|
||||
private void removeSelectionHighlight(int idx) {
|
||||
if (idx < 0 || idx >= nodes.size()) return;
|
||||
Spatial h = nodes.get(idx).getChild(HIGHLIGHT_NAME);
|
||||
if (h != null) h.removeFromParent();
|
||||
}
|
||||
|
||||
private void applySelectionMaterial(Spatial s) {
|
||||
if (s instanceof Geometry geo) {
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", HIGHLIGHT_COLOR);
|
||||
mat.getAdditionalRenderState().setWireframe(true);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
geo.setMaterial(mat);
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : new ArrayList<>(n.getChildren())) {
|
||||
applySelectionMaterial(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gizmo-Bau ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void buildGizmo() {
|
||||
gizmoNode.detachAllChildren();
|
||||
|
||||
arrowX = buildArrowNode(COL_X, "arrowX",
|
||||
new Vector3f(1, 0, 0),
|
||||
new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y));
|
||||
arrowY = buildArrowNode(COL_Y, "arrowY",
|
||||
new Vector3f(0, 1, 0),
|
||||
new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X));
|
||||
arrowZ = buildArrowNode(COL_Z, "arrowZ",
|
||||
new Vector3f(0, 0, -1),
|
||||
new Quaternion());
|
||||
gizmoNode.attachChild(arrowX);
|
||||
gizmoNode.attachChild(arrowY);
|
||||
gizmoNode.attachChild(arrowZ);
|
||||
|
||||
ringX = buildRingNode(COL_X, "ringX",
|
||||
new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y));
|
||||
ringY = buildRingNode(COL_Y, "ringY",
|
||||
new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X));
|
||||
ringZ = buildRingNode(COL_Z, "ringZ",
|
||||
new Quaternion());
|
||||
gizmoNode.attachChild(ringX);
|
||||
gizmoNode.attachChild(ringY);
|
||||
gizmoNode.attachChild(ringZ);
|
||||
}
|
||||
|
||||
private Node buildArrowNode(ColorRGBA color, String name, Vector3f dir, Quaternion geomRot) {
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
|
||||
Geometry shaft = new Geometry(name + "_shaft",
|
||||
new Cylinder(2, 10, SHAFT_RADIUS, SHAFT_LEN, false));
|
||||
shaft.setLocalRotation(geomRot);
|
||||
shaft.setLocalTranslation(dir.mult(SHAFT_LEN * 0.5f));
|
||||
shaft.setMaterial(mat);
|
||||
shaft.setQueueBucket(RenderQueue.Bucket.Translucent);
|
||||
|
||||
Geometry head = new Geometry(name + "_head",
|
||||
new Cylinder(2, 10, HEAD_RADIUS, HEAD_LEN, true));
|
||||
head.setLocalRotation(geomRot);
|
||||
head.setLocalTranslation(dir.mult(SHAFT_LEN + HEAD_LEN * 0.5f));
|
||||
head.setMaterial(mat);
|
||||
head.setQueueBucket(RenderQueue.Bucket.Translucent);
|
||||
|
||||
Node node = new Node(name);
|
||||
node.attachChild(shaft);
|
||||
node.attachChild(head);
|
||||
return node;
|
||||
}
|
||||
|
||||
private Node buildRingNode(ColorRGBA color, String name, Quaternion nodeRot) {
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
|
||||
Geometry ring = new Geometry(name + "_ring",
|
||||
new Torus(48, 10, RING_TUBE, RING_RADIUS));
|
||||
ring.setMaterial(mat);
|
||||
ring.setQueueBucket(RenderQueue.Bucket.Translucent);
|
||||
|
||||
Node node = new Node(name);
|
||||
node.setLocalRotation(nodeRot);
|
||||
node.attachChild(ring);
|
||||
return node;
|
||||
}
|
||||
|
||||
// ── Gizmo-Position / -Sichtbarkeit ────────────────────────────────────────
|
||||
|
||||
private void updateGizmoPosition() {
|
||||
if (selectedItemIdx < 0) return;
|
||||
Node n = nodes.get(selectedItemIdx);
|
||||
n.updateGeometricState();
|
||||
BoundingVolume bv = n.getWorldBound();
|
||||
Vector3f center;
|
||||
float radius;
|
||||
if (bv instanceof BoundingSphere bs) {
|
||||
center = bs.getCenter().clone();
|
||||
radius = bs.getRadius();
|
||||
} else if (bv instanceof BoundingBox bb) {
|
||||
center = bb.getCenter().clone();
|
||||
radius = bb.getExtent(null).length();
|
||||
} else {
|
||||
center = n.getWorldTranslation().clone();
|
||||
radius = 0.5f;
|
||||
}
|
||||
gizmoNode.setLocalTranslation(center);
|
||||
gizmoNode.setLocalScale(Math.max(0.01f, radius / RING_RADIUS));
|
||||
|
||||
float objRotY = items.get(selectedItemIdx).rotY();
|
||||
Quaternion qObjY = new Quaternion().fromAngleAxis(objRotY, Vector3f.UNIT_Y);
|
||||
ringX.setLocalRotation(qObjY.mult(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y)));
|
||||
ringY.setLocalRotation(qObjY.mult(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X)));
|
||||
ringZ.setLocalRotation(qObjY.mult(new Quaternion()));
|
||||
}
|
||||
|
||||
private void updateGizmoVisibility() {
|
||||
if (gizmoNode == null) return;
|
||||
boolean sel = selectedItemIdx >= 0 && input.activeLayer == SharedInput.LAYER_ITEMS_EDIT;
|
||||
boolean move = sel && input.objectEditTool == SharedInput.EDIT_TOOL_MOVE;
|
||||
boolean rotate = sel && input.objectEditTool == SharedInput.EDIT_TOOL_ROTATE;
|
||||
|
||||
Spatial.CullHint showArrows = move ? Spatial.CullHint.Inherit : Spatial.CullHint.Always;
|
||||
Spatial.CullHint showRings = rotate ? Spatial.CullHint.Inherit : Spatial.CullHint.Always;
|
||||
arrowX.setCullHint(showArrows); arrowY.setCullHint(showArrows); arrowZ.setCullHint(showArrows);
|
||||
ringX.setCullHint(showRings); ringY.setCullHint(showRings); ringZ.setCullHint(showRings);
|
||||
gizmoNode.setCullHint((move || rotate) ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
// ── Gizmo-Picking ─────────────────────────────────────────────────────────
|
||||
|
||||
private int pickGizmo(Ray ray) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
arrowX.collideWith(ray, hits); if (hits.size() > 0) return 0; hits.clear();
|
||||
arrowY.collideWith(ray, hits); if (hits.size() > 0) return 1; hits.clear();
|
||||
arrowZ.collideWith(ray, hits); if (hits.size() > 0) return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int pickRing(Ray ray) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
ringX.collideWith(ray, hits); if (hits.size() > 0) return 0; hits.clear();
|
||||
ringY.collideWith(ray, hits); if (hits.size() > 0) return 1; hits.clear();
|
||||
ringZ.collideWith(ray, hits); if (hits.size() > 0) return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private Vector3f getArrowWorldDir(int idx) {
|
||||
return switch (idx) {
|
||||
case 0 -> new Vector3f(1, 0, 0);
|
||||
case 1 -> new Vector3f(0, 1, 0);
|
||||
default -> new Vector3f(0, 0, -1);
|
||||
};
|
||||
}
|
||||
|
||||
// ── Edit / Löschen ────────────────────────────────────────────────────────
|
||||
|
||||
private void removeItem(int idx) {
|
||||
itemRoot.detachChild(nodes.get(idx));
|
||||
items.remove(idx);
|
||||
@@ -268,8 +669,39 @@ public class ItemPlacementState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyItemEdit(int idx, float x, float y, float z, float rotX, float rotY, float rotZ) {
|
||||
PlacedItem updated = items.get(idx).withTransform(x, y, z, rotX, rotY, rotZ);
|
||||
items.set(idx, updated);
|
||||
|
||||
Node n = nodes.get(idx);
|
||||
n.setLocalTranslation(x, y + 0.25f, z);
|
||||
applyRot(n, rotX, rotY, rotZ);
|
||||
|
||||
publishSelection(idx);
|
||||
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern nach Edit fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveItems() {
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
|
||||
private static void applyRot(Node n, float rotX, float rotY, float rotZ) {
|
||||
Quaternion q = new Quaternion();
|
||||
q.fromAngles(rotX, rotY, rotZ);
|
||||
n.setLocalRotation(q);
|
||||
}
|
||||
|
||||
private Node buildItemNode(String itemId) {
|
||||
Node n = new Node("item_" + itemId);
|
||||
n.setUserData("itemId", itemId);
|
||||
|
||||
@@ -21,7 +21,7 @@ import de.blight.editor.SharedInput;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LightState extends BaseAppState {
|
||||
public class LightState extends AuswahlMarkerState {
|
||||
|
||||
// ── Glühbirnen-Geometrie ──────────────────────────────────────────────────
|
||||
private static final float BULB_RADIUS = 0.38f;
|
||||
@@ -37,35 +37,19 @@ public class LightState extends BaseAppState {
|
||||
private static final ColorRGBA SEL_COLOR = new ColorRGBA(1f, 1f, 0f, 1f);
|
||||
private static final ColorRGBA SOCKET_COLOR = new ColorRGBA(0.22f, 0.22f, 0.22f, 1f);
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
// parallel lists: lights[i] <-> markers[i] <-> pointLights[i]
|
||||
private final List<PlacedLight> lights = new ArrayList<>();
|
||||
private final List<Node> markers = new ArrayList<>();
|
||||
private final List<PointLight> pointLights = new ArrayList<>();
|
||||
|
||||
private int selectedIdx = -1;
|
||||
private List<PlacedLight> pendingLights = null;
|
||||
|
||||
public LightState(SharedInput input) {
|
||||
this.input = input;
|
||||
super(input);
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
clearAll();
|
||||
@@ -80,7 +64,11 @@ public class LightState extends BaseAppState {
|
||||
}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||
@Override
|
||||
protected void onMarkerSelected(int idx) {
|
||||
selectLight(idx);
|
||||
input.auswahlLightSelected = true;
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -153,29 +141,6 @@ public class LightState extends BaseAppState {
|
||||
selectLight(lights.size() - 1);
|
||||
}
|
||||
|
||||
private void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
int hit = pickMarker(ray);
|
||||
if (hit >= 0) {
|
||||
selectLight(hit);
|
||||
input.auswahlLightSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Trifft den Glow-Halo oder den Bulb irgendeines Markers. */
|
||||
private int pickMarker(Ray ray) {
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
markers.get(i).collideWith(ray, res);
|
||||
if (res.size() > 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectLight(int idx) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.jme3.light.DirectionalLight;
|
||||
import com.jme3.light.LightProbe;
|
||||
import com.jme3.light.PointLight;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.MatParam;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
@@ -31,6 +32,7 @@ import com.jme3.anim.SkinningControl;
|
||||
import com.jme3.effect.ParticleEmitter;
|
||||
import com.jme3.effect.ParticleMesh;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.texture.Texture;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.util.ImpostorUtil;
|
||||
@@ -1093,6 +1095,17 @@ public class ModelEditorState extends BaseAppState {
|
||||
VertexBuffer vb = mesh.getBuffer(type);
|
||||
if (vb != null) vb.setUpdateNeeded();
|
||||
}
|
||||
// Texturen als update-needed markieren – verhindert schwarzes Rendering
|
||||
// wenn die GPU-Textur-Zustände durch vorherige Thumbnail-Renders beschädigt wurden.
|
||||
Material mat = g.getMaterial();
|
||||
if (mat != null) {
|
||||
for (MatParam param : mat.getParams()) {
|
||||
Object val = param.getValue();
|
||||
if (val instanceof Texture tex && tex.getImage() != null) {
|
||||
tex.getImage().setUpdateNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) markBuffersUpdateNeeded(child);
|
||||
}
|
||||
@@ -1520,20 +1533,33 @@ public class ModelEditorState extends BaseAppState {
|
||||
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
/** Zeichnet / aktualisiert den 15cm-Grip-Gizmo (orangefarbener Pfeil). */
|
||||
/** Zeichnet / aktualisiert den Grip-Gizmo: 15cm orange (Waffe) oder 28cm cyan (Schild/Unterarm). */
|
||||
public void rebuildGripGizmo() {
|
||||
if (gripGizmoNode == null) return;
|
||||
gripGizmoNode.detachAllChildren();
|
||||
|
||||
float shaftLen = 0.12f;
|
||||
float shaftRad = 0.008f;
|
||||
float headRad = 0.022f;
|
||||
float headLen = 0.03f; // 15cm gesamt: 12cm Schaft + 3cm Kopf
|
||||
boolean shieldMode = input.gripEditorIsShield;
|
||||
|
||||
// Waffe: 15cm orange | Schild: 28cm cyan
|
||||
float shaftLen = shieldMode ? 0.24f : 0.12f;
|
||||
float shaftRad = shieldMode ? 0.010f : 0.008f;
|
||||
float headRad = shieldMode ? 0.026f : 0.022f;
|
||||
float headLen = shieldMode ? 0.04f : 0.03f;
|
||||
|
||||
ColorRGBA shaftColor = shieldMode
|
||||
? new ColorRGBA(0f, 0.75f, 0.9f, 1f)
|
||||
: new ColorRGBA(1f, 0.55f, 0f, 1f);
|
||||
ColorRGBA headColor = shieldMode
|
||||
? new ColorRGBA(0f, 0.5f, 0.75f, 1f)
|
||||
: new ColorRGBA(1f, 0.3f, 0f, 1f);
|
||||
ColorRGBA axisColor = shieldMode
|
||||
? new ColorRGBA(0.6f, 1f, 0.6f, 1f)
|
||||
: new ColorRGBA(1f, 0.8f, 0.1f, 1f);
|
||||
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(1f, 0.55f, 0f, 1f));
|
||||
mat.setColor("Color", shaftColor);
|
||||
Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
matHead.setColor("Color", new ColorRGBA(1f, 0.3f, 0f, 1f));
|
||||
matHead.setColor("Color", headColor);
|
||||
|
||||
Node group = new Node("gripGroup");
|
||||
group.setLocalTranslation(gripPosX, gripPosY, gripPosZ);
|
||||
@@ -1545,8 +1571,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
com.jme3.math.FastMath.DEG_TO_RAD * gripRotZ);
|
||||
group.setLocalRotation(rot);
|
||||
|
||||
// Kleiner Würfel am Griffpunkt
|
||||
Geometry marker = new Geometry("gMarker", new Box(0.015f, 0.015f, 0.015f));
|
||||
float markerSize = shieldMode ? 0.018f : 0.015f;
|
||||
Geometry marker = new Geometry("gMarker", new Box(markerSize, markerSize, markerSize));
|
||||
marker.setMaterial(mat);
|
||||
|
||||
// Schaft entlang +Z
|
||||
@@ -1562,14 +1588,12 @@ public class ModelEditorState extends BaseAppState {
|
||||
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||
head.setLocalRotation(headRot);
|
||||
|
||||
// Griffachsen-Zylinder: runder Zylinder senkrecht zum Schaft (+Y),
|
||||
// zeigt die Roll-Richtung (ersetzt die frühere flache Klingenflosse).
|
||||
// JME3-Cylinder liegt standardmäßig auf +Z → -90° um X dreht ihn nach +Y.
|
||||
Material matFin = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
matFin.setColor("Color", new ColorRGBA(1f, 0.8f, 0.1f, 1f));
|
||||
float axisLen = 0.06f;
|
||||
// Querachsen-Zylinder entlang +Y: zeigt Roll-/Orientierungsachse
|
||||
Material matAxis = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
matAxis.setColor("Color", axisColor);
|
||||
float axisLen = shieldMode ? 0.08f : 0.06f;
|
||||
Geometry gripAxis = new Geometry("gGripAxis", new Cylinder(4, 8, 0.006f, axisLen, true));
|
||||
gripAxis.setMaterial(matFin);
|
||||
gripAxis.setMaterial(matAxis);
|
||||
Quaternion axisRot = new Quaternion();
|
||||
axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||
gripAxis.setLocalRotation(axisRot);
|
||||
|
||||
@@ -19,7 +19,7 @@ import de.blight.editor.SharedInput;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PlacedSoundState extends BaseAppState {
|
||||
public class PlacedSoundState extends AuswahlMarkerState {
|
||||
|
||||
private static final float MARKER_RADIUS = 0.30f;
|
||||
private static final float GLOW_RADIUS = MARKER_RADIUS * 2.4f;
|
||||
@@ -31,34 +31,18 @@ public class PlacedSoundState extends BaseAppState {
|
||||
|
||||
private static final float POS_EPSILON = 0.05f;
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
// parallel lists: sounds[i] <-> markers[i]
|
||||
private final List<PlacedSound> sounds = new ArrayList<>();
|
||||
private final List<Node> markers = new ArrayList<>();
|
||||
private final List<PlacedSound> sounds = new ArrayList<>();
|
||||
|
||||
private int selectedIdx = -1;
|
||||
private List<PlacedSound> pendingSounds = null;
|
||||
|
||||
public PlacedSoundState(SharedInput input) {
|
||||
this.input = input;
|
||||
super(input);
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
clearAll();
|
||||
@@ -74,7 +58,11 @@ public class PlacedSoundState extends BaseAppState {
|
||||
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||
@Override
|
||||
protected void onMarkerSelected(int idx) {
|
||||
selectSound(idx);
|
||||
input.auswahlSoundSelected = true;
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -145,28 +133,6 @@ public class PlacedSoundState extends BaseAppState {
|
||||
selectSound(sounds.size() - 1);
|
||||
}
|
||||
|
||||
private void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
int hit = pickMarker(ray);
|
||||
if (hit >= 0) {
|
||||
selectSound(hit);
|
||||
input.auswahlSoundSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
private int pickMarker(Ray ray) {
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
markers.get(i).collideWith(ray, res);
|
||||
if (res.size() > 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectSound(int idx) {
|
||||
|
||||
@@ -707,6 +707,9 @@ public class SceneObjectState extends BaseAppState {
|
||||
}
|
||||
if (closest == null) { deselectAll(); return; }
|
||||
int idx = findObjectIndexByNode(closest.getGeometry());
|
||||
log.info("[SceneObject] Klick ({},{}): Treffer='{}', idx={}, layer={}",
|
||||
(int)click.screenX(), (int)click.screenY(),
|
||||
closest.getGeometry().getName(), idx, input.activeLayer);
|
||||
if (idx >= 0) {
|
||||
selectObject(idx, click.shift());
|
||||
// Vorgefertigte Objekte (externe Modelle + Gruppen) können nicht sub-selektiert werden.
|
||||
@@ -723,6 +726,11 @@ public class SceneObjectState extends BaseAppState {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Ray traf etwas in objectRoot, aber kein bekanntes Objekt — nicht an
|
||||
// andere Layer-Queues weiterleiten (verhindert versehentliches Emitter-Select).
|
||||
log.info("[SceneObject] Geometrie '{}' in objectRoot aber nicht in objNodes — Klick ignoriert",
|
||||
closest.getGeometry().getName());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Terrain-Treffer – im Edit-Modus: zurück zu Place-Modus, deselektieren
|
||||
@@ -733,6 +741,8 @@ public class SceneObjectState extends BaseAppState {
|
||||
}
|
||||
// Auswahl-Modus: kein Objekt angeklickt → deselektieren + Zonen/Items prüfen lassen
|
||||
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) {
|
||||
log.info("[SceneObject] LAYER_AUSWAHL-Broadcast bei Klick ({},{}): kein SceneObject getroffen, {} Objekte im objectRoot",
|
||||
(int)click.screenX(), (int)click.screenY(), objNodes.size());
|
||||
deselectAll();
|
||||
float sx = click.screenX(), sy = click.screenY();
|
||||
input.areaClickQueue.offer(new SharedInput.AreaClick(sx, sy, false));
|
||||
|
||||
@@ -344,6 +344,7 @@ public class ContentController {
|
||||
ctx.setCurrentTool().accept("itemEditor");
|
||||
ToolBar tb = makeSingleWindowToolBar("Item-Manager");
|
||||
ctx.setMainToolbar().accept(tb);
|
||||
ctx.setLeftPanel().accept(buildContentTreePanel());
|
||||
de.blight.editor.ui.ItemEditorView view =
|
||||
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||
wireItemView(view);
|
||||
@@ -960,7 +961,7 @@ public class ContentController {
|
||||
|
||||
private void wireItemView(de.blight.editor.ui.ItemEditorView view) {
|
||||
view.setOnIdChanged(this::refreshContentItemTree);
|
||||
view.setGripEditorLauncher((modelPath, currentGrip, onSave) -> {
|
||||
view.setGripEditorLauncher((modelPath, currentGrip, isShield, onSave) -> {
|
||||
String itemId = view.getCurrentItemId();
|
||||
gripEditorController.setOnReturn(() -> {
|
||||
if (dualWindowMode) {
|
||||
@@ -970,7 +971,7 @@ public class ContentController {
|
||||
switchToItemEditor(itemId);
|
||||
}
|
||||
});
|
||||
gripEditorController.open(modelPath, currentGrip, onSave);
|
||||
gripEditorController.open(modelPath, currentGrip, isShield, onSave);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
private Consumer<ItemGrip> pendingOnSave;
|
||||
private Runnable onReturn;
|
||||
private boolean suppressSpinnerEvents = false;
|
||||
private boolean shieldMode = false;
|
||||
|
||||
public void setOnReturn(Runnable r) { this.onReturn = r; }
|
||||
|
||||
@@ -37,11 +38,13 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
// ── GripEditorLauncher ────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void open(String modelPath, ItemGrip currentGrip, Consumer<ItemGrip> onSave) {
|
||||
public void open(String modelPath, ItemGrip currentGrip, boolean isShield, Consumer<ItemGrip> onSave) {
|
||||
pendingOnSave = onSave;
|
||||
shieldMode = isShield;
|
||||
|
||||
SharedInput input = ctx.input();
|
||||
input.gripEditorActive = true;
|
||||
input.gripEditorActive = true;
|
||||
input.gripEditorIsShield = isShield;
|
||||
|
||||
// Grip-Werte aus Item übernehmen (oder Defaults)
|
||||
ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip();
|
||||
@@ -63,7 +66,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
ctx.setCenterView().accept(ctx.viewportSupplier().get());
|
||||
ctx.setCurrentTool().accept("gripEditor");
|
||||
ctx.setMainToolbar().accept(buildToolbar());
|
||||
ctx.setRightPanel().accept(buildPanel(g));
|
||||
ctx.setRightPanel().accept(buildPanel(g, isShield));
|
||||
ctx.setLeftPanel().accept(null);
|
||||
ctx.setBottomPanel().accept(null);
|
||||
}
|
||||
@@ -82,7 +85,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
|
||||
// ── Panel ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildPanel(ItemGrip g) {
|
||||
private VBox buildPanel(ItemGrip g, boolean isShield) {
|
||||
posXSpin = doubleSpin(g.getPosX(), -2.0, 2.0, 0.01, "m");
|
||||
posYSpin = doubleSpin(g.getPosY(), -2.0, 2.0, 0.01, "m");
|
||||
posZSpin = doubleSpin(g.getPosZ(), -2.0, 2.0, 0.01, "m");
|
||||
@@ -123,7 +126,9 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
row("Y:", rotYSpin),
|
||||
row("Z:", rotZSpin),
|
||||
new Separator(),
|
||||
styledHint("Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."),
|
||||
styledHint(isShield
|
||||
? "Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge."
|
||||
: "Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."),
|
||||
new Separator(),
|
||||
saveBtn,
|
||||
cancelBtn
|
||||
@@ -156,7 +161,9 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
grip.setRotY(rotYSpin.getValue().floatValue());
|
||||
grip.setRotZ(rotZSpin.getValue().floatValue());
|
||||
|
||||
ctx.input().gripEditorActive = false;
|
||||
SharedInput input = ctx.input();
|
||||
input.gripEditorActive = false;
|
||||
input.gripEditorIsShield = false;
|
||||
|
||||
Consumer<ItemGrip> save = pendingOnSave;
|
||||
Runnable ret = onReturn;
|
||||
@@ -168,7 +175,8 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
ctx.input().gripEditorActive = false;
|
||||
ctx.input().gripEditorActive = false;
|
||||
ctx.input().gripEditorIsShield = false;
|
||||
|
||||
Runnable ret = onReturn;
|
||||
pendingOnSave = null;
|
||||
|
||||
@@ -386,7 +386,8 @@ public class ViewportController {
|
||||
private boolean isObjectMode() {
|
||||
return input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT
|
||||
|| input.activeLayer == SharedInput.LAYER_AUSWAHL;
|
||||
|| input.activeLayer == SharedInput.LAYER_AUSWAHL
|
||||
|| input.activeLayer == SharedInput.LAYER_ITEMS_EDIT;
|
||||
}
|
||||
|
||||
private void startEditTimer() {
|
||||
|
||||
@@ -5,8 +5,10 @@ import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.ToolContext;
|
||||
import de.blight.editor.toolset.EditorUiHelper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
/**
|
||||
* Owns the world-zone side panels:
|
||||
@@ -41,15 +43,96 @@ public class WorldPanelController {
|
||||
auswahlDynamicContent.getChildren().add(noSel);
|
||||
return;
|
||||
}
|
||||
// Format: "idx|itemId|x|y|z|rotX|rotY|rotZ"
|
||||
String[] parts = info.split("\\|");
|
||||
String itemId = parts.length > 1 ? parts[1] : "?";
|
||||
float cx = parts.length > 2 ? parseF(parts[2]) : 0f;
|
||||
float cy = parts.length > 3 ? parseF(parts[3]) : 0f;
|
||||
float cz = parts.length > 4 ? parseF(parts[4]) : 0f;
|
||||
float cRotY = parts.length > 6 ? parseF(parts[6]) : 0f;
|
||||
|
||||
Label idLabel = new Label("Item: " + itemId);
|
||||
idLabel.setStyle("-fx-font-weight: bold;");
|
||||
|
||||
Spinner<Double> xSpin = posSpin(cx);
|
||||
Spinner<Double> ySpin = posSpin(cy);
|
||||
Spinner<Double> zSpin = posSpin(cz);
|
||||
Spinner<Double> rotYSpin = rotSpin(cRotY);
|
||||
|
||||
SharedInput input = ctx.input();
|
||||
Runnable sendEdit = () -> {
|
||||
input.itemEditX = xSpin.getValue().floatValue();
|
||||
input.itemEditY = ySpin.getValue().floatValue();
|
||||
input.itemEditZ = zSpin.getValue().floatValue();
|
||||
input.itemEditRotY = rotYSpin.getValue().floatValue();
|
||||
input.itemEditRequest = true;
|
||||
};
|
||||
xSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
ySpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
zSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
rotYSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
|
||||
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);
|
||||
deleteBtn.setOnAction(e -> input.deleteSelectedItemRequested = true);
|
||||
|
||||
auswahlDynamicContent.getChildren().addAll(
|
||||
idLabel,
|
||||
new Separator(),
|
||||
EditorUiHelper.sectionTitle("Position"),
|
||||
spinRow("X:", xSpin), spinRow("Y:", ySpin), spinRow("Z:", zSpin),
|
||||
new Separator(),
|
||||
EditorUiHelper.sectionTitle("Rotation"),
|
||||
spinRow("Y°:", rotYSpin),
|
||||
new Separator(),
|
||||
deleteBtn
|
||||
);
|
||||
}
|
||||
|
||||
private static float parseF(String s) {
|
||||
try { return Float.parseFloat(s); } catch (NumberFormatException e) { return 0f; }
|
||||
}
|
||||
|
||||
private static Spinner<Double> posSpin(float initial) {
|
||||
SpinnerValueFactory.DoubleSpinnerValueFactory vf =
|
||||
new SpinnerValueFactory.DoubleSpinnerValueFactory(-10000, 10000, initial, 0.5);
|
||||
vf.setConverter(new StringConverter<>() {
|
||||
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.2f", v); }
|
||||
@Override public Double fromString(String s) {
|
||||
try { return Double.parseDouble(s.replace(",", ".")); }
|
||||
catch (NumberFormatException e) { return vf.getValue(); }
|
||||
}
|
||||
});
|
||||
Spinner<Double> sp = new Spinner<>(vf);
|
||||
sp.setEditable(true);
|
||||
sp.setMaxWidth(Double.MAX_VALUE);
|
||||
return sp;
|
||||
}
|
||||
|
||||
private static Spinner<Double> rotSpin(float initial) {
|
||||
SpinnerValueFactory.DoubleSpinnerValueFactory vf =
|
||||
new SpinnerValueFactory.DoubleSpinnerValueFactory(-360, 360, initial, 5.0);
|
||||
vf.setConverter(new StringConverter<>() {
|
||||
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.1f°", v); }
|
||||
@Override public Double fromString(String s) {
|
||||
try { return Double.parseDouble(s.replace("°", "").replace(",", ".")); }
|
||||
catch (NumberFormatException e) { return vf.getValue(); }
|
||||
}
|
||||
});
|
||||
Spinner<Double> sp = new Spinner<>(vf);
|
||||
sp.setEditable(true);
|
||||
sp.setMaxWidth(Double.MAX_VALUE);
|
||||
return sp;
|
||||
}
|
||||
|
||||
private static HBox spinRow(String labelText, Spinner<Double> spinner) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(28);
|
||||
HBox.setHgrow(spinner, Priority.ALWAYS);
|
||||
HBox row = new HBox(6, lbl, spinner);
|
||||
row.setAlignment(Pos.CENTER_LEFT);
|
||||
return row;
|
||||
}
|
||||
|
||||
public void updateZonenPanel(String kind) {
|
||||
@@ -321,6 +404,108 @@ public class WorldPanelController {
|
||||
return panel;
|
||||
}
|
||||
|
||||
public VBox buildAuswahlItemEditPanel(String info) {
|
||||
String[] parts = info != null ? info.split("\\|") : new String[0];
|
||||
String itemId = parts.length > 1 ? parts[1] : "?";
|
||||
float cx = parts.length > 2 ? parseF(parts[2]) : 0f;
|
||||
float cy = parts.length > 3 ? parseF(parts[3]) : 0f;
|
||||
float cz = parts.length > 4 ? parseF(parts[4]) : 0f;
|
||||
// Format: idx|itemId|x|y|z|rotX|rotY|rotZ (rotX/rotZ in radians → convert to degrees)
|
||||
float cRotX = parts.length > 5 ? (float) Math.toDegrees(parseF(parts[5])) : 0f;
|
||||
float cRotY = parts.length > 6 ? (float) Math.toDegrees(parseF(parts[6])) : 0f;
|
||||
float cRotZ = parts.length > 7 ? (float) Math.toDegrees(parseF(parts[7])) : 0f;
|
||||
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
|
||||
Label titleLbl = new Label("Item-Pickup");
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
Label idLbl = new Label(itemId);
|
||||
idLbl.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
|
||||
|
||||
// Werkzeug-Umschalter Verschieben / Rotieren
|
||||
SharedInput input = ctx.input();
|
||||
Button moveBtn = new Button("Verschieben");
|
||||
Button rotateBtn = new Button("Rotieren");
|
||||
moveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
rotateBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
final String TOOL_ON = "-fx-background-color: #2980b9; -fx-text-fill: white; -fx-font-weight: bold;";
|
||||
final String TOOL_OFF = "-fx-background-color: #bdc3c7; -fx-text-fill: #555;";
|
||||
Runnable updateToolStyle = () -> {
|
||||
boolean mv = input.objectEditTool == SharedInput.EDIT_TOOL_MOVE;
|
||||
moveBtn.setStyle(mv ? TOOL_ON : TOOL_OFF);
|
||||
rotateBtn.setStyle(mv ? TOOL_OFF : TOOL_ON);
|
||||
};
|
||||
updateToolStyle.run();
|
||||
moveBtn.setOnAction(e -> {
|
||||
input.objectEditTool = SharedInput.EDIT_TOOL_MOVE;
|
||||
updateToolStyle.run();
|
||||
});
|
||||
rotateBtn.setOnAction(e -> {
|
||||
input.objectEditTool = SharedInput.EDIT_TOOL_ROTATE;
|
||||
updateToolStyle.run();
|
||||
});
|
||||
HBox toolRow = new HBox(6, moveBtn, rotateBtn);
|
||||
toolRow.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(moveBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(rotateBtn, Priority.ALWAYS);
|
||||
|
||||
Spinner<Double> xSpin = posSpin(cx);
|
||||
Spinner<Double> ySpin = posSpin(cy);
|
||||
Spinner<Double> zSpin = posSpin(cz);
|
||||
Spinner<Double> rotXSpin = rotSpin(cRotX);
|
||||
Spinner<Double> rotYSpin = rotSpin(cRotY);
|
||||
Spinner<Double> rotZSpin = rotSpin(cRotZ);
|
||||
|
||||
Runnable sendEdit = () -> {
|
||||
input.itemEditX = xSpin.getValue().floatValue();
|
||||
input.itemEditY = ySpin.getValue().floatValue();
|
||||
input.itemEditZ = zSpin.getValue().floatValue();
|
||||
input.itemEditRotX = (float) Math.toRadians(rotXSpin.getValue());
|
||||
input.itemEditRotY = (float) Math.toRadians(rotYSpin.getValue());
|
||||
input.itemEditRotZ = (float) Math.toRadians(rotZSpin.getValue());
|
||||
input.itemEditRequest = true;
|
||||
};
|
||||
xSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
ySpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
zSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
rotXSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
rotYSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
rotZSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
|
||||
Button deleteBtn = new Button("Loeschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white; -fx-font-weight: bold;");
|
||||
deleteBtn.setOnAction(e -> input.deleteSelectedItemRequested = true);
|
||||
|
||||
inner.getChildren().addAll(
|
||||
titleLbl, idLbl, new Separator(),
|
||||
toolRow,
|
||||
new Separator(),
|
||||
EditorUiHelper.sectionTitle("Position"),
|
||||
spinRow("X:", xSpin), spinRow("Y:", ySpin), spinRow("Z:", zSpin),
|
||||
new Separator(),
|
||||
EditorUiHelper.sectionTitle("Rotation"),
|
||||
spinRow("X°:", rotXSpin), spinRow("Y°:", rotYSpin), spinRow("Z°:", rotZSpin),
|
||||
new Separator(),
|
||||
deleteBtn,
|
||||
new Separator(),
|
||||
EditorUiHelper.styledHint("Klick auf leere Flaeche"),
|
||||
EditorUiHelper.styledHint(" → Abwaehlen")
|
||||
);
|
||||
|
||||
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 buildWegePanel() {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
|
||||
@@ -14,7 +14,6 @@ import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class AreaEditorView extends BorderPane {
|
||||
@@ -234,50 +233,11 @@ public class AreaEditorView extends BorderPane {
|
||||
|
||||
// ── Localisation defaults ──────────────────────────────────────────────────
|
||||
|
||||
private void writeDefaultLocalisation(String fullId) {
|
||||
private static void writeDefaultLocalisation(String fullId) {
|
||||
if (fullId == null || fullId.isBlank()) return;
|
||||
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
|
||||
if (shortId.isBlank()) return;
|
||||
String key = fullId + ".name";
|
||||
String defaultName = EditorNaming.toDisplayName(shortId);
|
||||
|
||||
Path langSrc = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang");
|
||||
Path[] langMirrors = {
|
||||
ProjectRoot.resolve("blight-lang", "bin", "main", "lang"),
|
||||
ProjectRoot.resolve("blight-lang", "build", "resources", "main", "lang"),
|
||||
};
|
||||
for (String locale : new String[]{"de", "en"}) {
|
||||
Path src = langSrc.resolve("messages_" + locale + ".properties");
|
||||
try {
|
||||
writeOrUpdateKey(src, key, defaultName);
|
||||
for (Path mirrorDir : langMirrors) {
|
||||
Path mirror = mirrorDir.resolve("messages_" + locale + ".properties");
|
||||
if (Files.exists(mirror)) {
|
||||
Files.copy(src, mirror, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// non-critical
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeOrUpdateKey(Path file, String key, String value) throws IOException {
|
||||
if (!Files.exists(file)) return;
|
||||
String needle = key + "=";
|
||||
String needleSpace = key + " =";
|
||||
java.util.List<String> lines = new java.util.ArrayList<>(Files.readAllLines(file, StandardCharsets.UTF_8));
|
||||
boolean found = false;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (lines.get(i).stripLeading().startsWith(needle) || lines.get(i).stripLeading().startsWith(needleSpace)) {
|
||||
lines.set(i, key + "=" + value);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) lines.add(key + "=" + value);
|
||||
Files.writeString(file, String.join("\n", lines) + "\n",
|
||||
StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
LangDefaults.writeOrUpdate(fullId + ".name", EditorNaming.toDisplayName(shortId));
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -48,6 +48,7 @@ public class ItemEditorView extends BorderPane {
|
||||
private Spinner<Integer> goldSpinner;
|
||||
private Spinner<Integer> damageSpinner;
|
||||
private VBox weaponSection;
|
||||
private VBox shieldSection;
|
||||
private TextField modelRefField;
|
||||
private VBox formContainer;
|
||||
private CheckBox consumableCheck;
|
||||
@@ -55,7 +56,7 @@ public class ItemEditorView extends BorderPane {
|
||||
private VBox effectsRows;
|
||||
|
||||
public interface GripEditorLauncher {
|
||||
void open(String modelPath, ItemGrip currentGrip, java.util.function.Consumer<ItemGrip> onSave);
|
||||
void open(String modelPath, ItemGrip currentGrip, boolean isShield, java.util.function.Consumer<ItemGrip> onSave);
|
||||
}
|
||||
|
||||
private Runnable onIdChanged;
|
||||
@@ -156,7 +157,6 @@ public class ItemEditorView extends BorderPane {
|
||||
if (wasFocused && !isFocused) onIdCommitted();
|
||||
});
|
||||
idField.setOnAction(e -> onIdCommitted());
|
||||
idField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
catCombo = new ComboBox<>();
|
||||
catCombo.getItems().addAll(ItemCategory.values());
|
||||
@@ -246,6 +246,20 @@ public class ItemEditorView extends BorderPane {
|
||||
weaponSection.setVisible(false);
|
||||
weaponSection.setManaged(false);
|
||||
|
||||
// ── Schild-Sektion ────────────────────────────────────────────────────
|
||||
Button shieldGripBtn = new Button("Unterarm-Griff definieren...");
|
||||
shieldGripBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
shieldGripBtn.setStyle("-fx-background-color: #2a4a5a; -fx-text-fill: #aaddff; -fx-cursor: hand;");
|
||||
shieldGripBtn.setOnAction(e -> openShieldGripEditor());
|
||||
|
||||
shieldSection = new VBox(6,
|
||||
new Separator(),
|
||||
sectionTitle("Schild-Griff"),
|
||||
shieldGripBtn
|
||||
);
|
||||
shieldSection.setVisible(false);
|
||||
shieldSection.setManaged(false);
|
||||
|
||||
modelRefField = new TextField();
|
||||
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
|
||||
modelRefField.setEditable(false);
|
||||
@@ -300,6 +314,7 @@ public class ItemEditorView extends BorderPane {
|
||||
row("Wert (Gold):", goldSpinner),
|
||||
row("Modell:", modelFullRow),
|
||||
weaponSection,
|
||||
shieldSection,
|
||||
new Separator(),
|
||||
consumableCheck,
|
||||
consumablesSection
|
||||
@@ -310,7 +325,16 @@ public class ItemEditorView extends BorderPane {
|
||||
private void openGripEditor() {
|
||||
if (current == null || gripEditorLauncher == null) return;
|
||||
String modelPath = modelRefField.getText().trim();
|
||||
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), grip -> {
|
||||
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), false, grip -> {
|
||||
current.setGrip(grip);
|
||||
persistCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
private void openShieldGripEditor() {
|
||||
if (current == null || gripEditorLauncher == null) return;
|
||||
String modelPath = modelRefField.getText().trim();
|
||||
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), true, grip -> {
|
||||
current.setGrip(grip);
|
||||
persistCurrent();
|
||||
});
|
||||
@@ -319,10 +343,12 @@ public class ItemEditorView extends BorderPane {
|
||||
private void updateWeaponSectionVisibility() {
|
||||
ItemCategory cat = catCombo.getValue();
|
||||
ItemSubCategory sub = subCatCombo.getValue();
|
||||
boolean isWeapon = cat == ItemCategory.WEAPON
|
||||
&& sub != ItemSubCategory.SHIELD;
|
||||
boolean isShield = cat == ItemCategory.WEAPON && sub == ItemSubCategory.SHIELD;
|
||||
boolean isWeapon = cat == ItemCategory.WEAPON && !isShield;
|
||||
weaponSection.setVisible(isWeapon);
|
||||
weaponSection.setManaged(isWeapon);
|
||||
shieldSection.setVisible(isShield);
|
||||
shieldSection.setManaged(isShield);
|
||||
}
|
||||
|
||||
// ── Modell-Auswahl ────────────────────────────────────────────────────────
|
||||
@@ -333,14 +359,29 @@ public class ItemEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
private void onIdCommitted() {
|
||||
String id = idField.getText().trim();
|
||||
if (id.isBlank()) return;
|
||||
nameLabel.setText(id + ".name");
|
||||
descLabel.setText(id + ".description");
|
||||
scheduleSave();
|
||||
String newId = idField.getText().trim();
|
||||
if (newId.isBlank()) return;
|
||||
nameLabel.setText(newId + ".name");
|
||||
descLabel.setText(newId + ".description");
|
||||
// Alte Datei löschen, wenn die ID geändert wurde
|
||||
if (current != null) {
|
||||
String oldId = current.getItemId();
|
||||
if (oldId != null && !oldId.isBlank() && !oldId.equals(newId)) {
|
||||
try { ItemIO.delete(oldId, itemDir); } catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
persistCurrent();
|
||||
writeDefaultLocalisation(newId);
|
||||
if (onIdChanged != null) onIdChanged.run();
|
||||
}
|
||||
|
||||
// ── Localisation defaults ─────────────────────────────────────────────────
|
||||
|
||||
private static void writeDefaultLocalisation(String itemId) {
|
||||
if (itemId == null || itemId.isBlank()) return;
|
||||
LangDefaults.writeIfAbsent(itemId + ".name", EditorNaming.toDisplayName(itemId));
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.editor.ProjectRoot;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Schreibt Default-Einträge in die Sprachdateien (messages_de/en.properties)
|
||||
* inklusive der Build-Mirror-Verzeichnisse.
|
||||
*/
|
||||
public final class LangDefaults {
|
||||
|
||||
private static final Path LANG_SRC = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang");
|
||||
private static final Path[] MIRRORS = {
|
||||
ProjectRoot.resolve("blight-lang", "bin", "main", "lang"),
|
||||
ProjectRoot.resolve("blight-lang", "build", "resources", "main", "lang"),
|
||||
};
|
||||
private static final String[] LOCALES = {"de", "en"};
|
||||
|
||||
private LangDefaults() {}
|
||||
|
||||
/**
|
||||
* Fügt {@code key=value} in alle Locale-Dateien ein, sofern der Schlüssel
|
||||
* dort noch nicht vorhanden ist. Bestehende Übersetzungen werden nicht
|
||||
* überschrieben.
|
||||
*/
|
||||
public static void writeIfAbsent(String key, String value) {
|
||||
forEach(key, value, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt {@code key=value} in alle Locale-Dateien; überschreibt einen
|
||||
* vorhandenen Eintrag oder hängt ihn ans Ende, falls er fehlt.
|
||||
*/
|
||||
public static void writeOrUpdate(String key, String value) {
|
||||
forEach(key, value, true);
|
||||
}
|
||||
|
||||
// ── intern ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static void forEach(String key, String value, boolean overwrite) {
|
||||
for (String locale : LOCALES) {
|
||||
Path src = LANG_SRC.resolve("messages_" + locale + ".properties");
|
||||
try {
|
||||
writeToFile(src, key, value, overwrite);
|
||||
for (Path mirrorDir : MIRRORS) {
|
||||
Path mirror = mirrorDir.resolve("messages_" + locale + ".properties");
|
||||
if (Files.exists(mirror)) {
|
||||
Files.copy(src, mirror, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeToFile(Path file, String key, String value, boolean overwrite)
|
||||
throws IOException {
|
||||
if (!Files.exists(file)) return;
|
||||
String needle = key + "=";
|
||||
String needleSpace = key + " =";
|
||||
List<String> lines = new ArrayList<>(Files.readAllLines(file, StandardCharsets.UTF_8));
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String stripped = lines.get(i).stripLeading();
|
||||
if (stripped.startsWith(needle) || stripped.startsWith(needleSpace)) {
|
||||
if (!overwrite) return;
|
||||
lines.set(i, key + "=" + value);
|
||||
Files.writeString(file, String.join("\n", lines) + "\n",
|
||||
StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
return;
|
||||
}
|
||||
}
|
||||
lines.add(key + "=" + value);
|
||||
Files.writeString(file, String.join("\n", lines) + "\n",
|
||||
StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package de.blight.editor.ui;
|
||||
import de.blight.common.LocationIO;
|
||||
import de.blight.common.model.Location;
|
||||
import de.blight.common.model.TextReference;
|
||||
import de.blight.editor.ProjectRoot;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -15,8 +14,6 @@ import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.List;
|
||||
|
||||
public class LocationEditorView extends BorderPane {
|
||||
@@ -194,50 +191,11 @@ public class LocationEditorView extends BorderPane {
|
||||
|
||||
// ── Localisation defaults ──────────────────────────────────────────────────
|
||||
|
||||
private void writeDefaultLocalisation(String fullId) {
|
||||
private static void writeDefaultLocalisation(String fullId) {
|
||||
if (fullId == null || fullId.isBlank()) return;
|
||||
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
|
||||
if (shortId.isBlank()) return;
|
||||
String key = fullId + ".name";
|
||||
String defaultName = EditorNaming.toDisplayName(shortId);
|
||||
|
||||
Path langSrc = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang");
|
||||
Path[] langMirrors = {
|
||||
ProjectRoot.resolve("blight-lang", "bin", "main", "lang"),
|
||||
ProjectRoot.resolve("blight-lang", "build", "resources", "main", "lang"),
|
||||
};
|
||||
for (String locale : new String[]{"de", "en"}) {
|
||||
Path src = langSrc.resolve("messages_" + locale + ".properties");
|
||||
try {
|
||||
writeOrUpdateKey(src, key, defaultName);
|
||||
for (Path mirrorDir : langMirrors) {
|
||||
Path mirror = mirrorDir.resolve("messages_" + locale + ".properties");
|
||||
if (Files.exists(mirror)) {
|
||||
Files.copy(src, mirror, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// non-critical
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeOrUpdateKey(Path file, String key, String value) throws IOException {
|
||||
if (!Files.exists(file)) return;
|
||||
String needle = key + "=";
|
||||
String needleSpace = key + " =";
|
||||
java.util.List<String> lines = new java.util.ArrayList<>(Files.readAllLines(file, StandardCharsets.UTF_8));
|
||||
boolean found = false;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (lines.get(i).stripLeading().startsWith(needle) || lines.get(i).stripLeading().startsWith(needleSpace)) {
|
||||
lines.set(i, key + "=" + value);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) lines.add(key + "=" + value);
|
||||
Files.writeString(file, String.join("\n", lines) + "\n",
|
||||
StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
LangDefaults.writeOrUpdate(fullId + ".name", EditorNaming.toDisplayName(shortId));
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user