Weiter am Kampfsystem gearbeitet - hier: Schwert und Schild

This commit is contained in:
2026-09-20 22:17:29 +02:00
parent 836b5d3d99
commit ef061b2726
49 changed files with 1463 additions and 366 deletions

View File

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

View File

@@ -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). */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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