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

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -14,7 +14,12 @@
"Sword And Shield Run",
"Sword And Shield Walk",
"Sword And Shield Attack",
"Sword And Shield Slash 2"
"Sword And Shield Slash 2",
"Sword And Shield Attack Counterattack",
"Sword And Shield Block",
"Sword And Shield Block Idle",
"Sword And Shield Block Impact",
"Sword And Shield Impact"
],
"actionMap": {
"DEFAULT": "Idle",
@@ -31,7 +36,12 @@
"SWORD_WALK": "Sword And Shield Walk",
"SWORD_RUN": "Sword And Shield Run",
"SWORD_ATTACK_3": "Sword And Shield Attack",
"SWORD_ATTACK_2": "Sword And Shield Slash 2"
"SWORD_ATTACK_2": "Sword And Shield Slash 2",
"SWORD_BLOCK": "Sword And Shield Block Idle",
"SWORD_IDLE_TO_BLOCK": "Sword And Shield Block",
"SWORD_BLOCK_IMPACT": "Sword And Shield Block Impact",
"SWORD_IDLE_IMPACT": "Sword And Shield Impact",
"SWORD_COUNTERATTACK": "Sword And Shield Attack Counterattack"
},
"previewModelPath": "Characters/Models/hero.j3o",
"animOffsets": {

View File

@@ -0,0 +1,30 @@
#Sun Sep 20 18:14:13 CEST 2026
attachedEmitters.count=0
attachedLights.count=0
attachedSounds.count=0
castShadow=true
category=
cullDistance=120.0
footstepSurface=
interactableOffsetX=0.0
interactableOffsetY=0.5
interactableOffsetZ=0.0
interactableRotY=0.0
interactableType=NONE
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
modelCategory=NONE
name=medieval round shield 3d model
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=1.0
scaleY=1.0
scaleZ=1.0
solid=true
tags=
uniformScale=true

View File

@@ -9,6 +9,7 @@
"id": "blutagave.description"
},
"worthGold": 25,
"damage": 0,
"modelRef": {
"path": "Models/plants/usable/blutagave.j3o"
},

View File

@@ -9,6 +9,7 @@
"id": "erzmoss.description"
},
"worthGold": 100,
"damage": 0,
"modelRef": {
"path": "Models/plants/usable/erzmoss.j3o"
},

View File

@@ -0,0 +1,25 @@
{
"itemId": "einfacher_schild",
"category": "WEAPON",
"subCategory": "SHIELD",
"name": {
"id": "einfacher_schild.name"
},
"description": {
"id": "einfacher_schild.description"
},
"worthGold": 0,
"damage": 0,
"grip": {
"posX": -0.12,
"posY": 0.4,
"posZ": -0.13,
"rotX": 90.0,
"rotY": 0.0,
"rotZ": 90.0
},
"modelRef": {
"path": "Models/waffen/schilder/einfacher schild.j3o"
},
"consumable": false
}

View File

@@ -3,10 +3,15 @@ package de.blight.common;
import java.util.UUID;
/** Ein Item-Pickup das auf der Spielwelt liegt (Kartendaten). */
public record PlacedItem(String uuid, String itemId, float x, float y, float z) {
public record PlacedItem(String uuid, String itemId, float x, float y, float z, float rotX, float rotY, float rotZ) {
/** Erzeugt ein neues PlacedItem mit automatisch generierter UUID. */
/** Erzeugt ein neues PlacedItem mit automatisch generierter UUID (alle Rotationen = 0). */
public static PlacedItem create(String itemId, float x, float y, float z) {
return new PlacedItem(UUID.randomUUID().toString(), itemId, x, y, z);
return new PlacedItem(UUID.randomUUID().toString(), itemId, x, y, z, 0f, 0f, 0f);
}
/** Gibt eine Kopie mit geänderter Position/Rotation zurück. */
public PlacedItem withTransform(float nx, float ny, float nz, float nRotX, float nRotY, float nRotZ) {
return new PlacedItem(uuid, itemId, nx, ny, nz, nRotX, nRotY, nRotZ);
}
}

View File

@@ -25,11 +25,11 @@ public final class PlacedItemIO {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# uuid\titemId\tx\ty\tz");
w.write("# uuid\titemId\tx\ty\tz\trotX\trotY\trotZ");
w.newLine();
for (PlacedItem it : items) {
w.write(String.format(Locale.ROOT, "%s\t%s\t%.5f\t%.5f\t%.5f%n",
it.uuid(), it.itemId(), it.x(), it.y(), it.z()));
w.write(String.format(Locale.ROOT, "%s\t%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f%n",
it.uuid(), it.itemId(), it.x(), it.y(), it.z(), it.rotX(), it.rotY(), it.rotZ()));
}
}
}
@@ -43,17 +43,29 @@ public final class PlacedItemIO {
if (line.isEmpty() || line.startsWith("#")) continue;
String[] f = line.split("\t", -1);
try {
if (f.length >= 5) {
// Aktuelles Format: uuid itemId x y z
if (f.length >= 8) {
// Aktuelles Format: uuid itemId x y z rotX rotY rotZ
list.add(new PlacedItem(f[0], f[1],
Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4])));
Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]),
Float.parseFloat(f[5]), Float.parseFloat(f[6]), Float.parseFloat(f[7])));
} else if (f.length >= 6) {
// Vorherige Version: uuid itemId x y z rotY (kein rotX/rotZ)
list.add(new PlacedItem(f[0], f[1],
Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]),
0f, Float.parseFloat(f[5]), 0f));
} else if (f.length >= 5) {
// uuid itemId x y z (kein rot)
list.add(new PlacedItem(f[0], f[1],
Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]),
0f, 0f, 0f));
} else if (f.length == 4) {
// Altes Format: itemId x y z → UUID deterministisch aus Inhalt ableiten
String deterministicId = UUID.nameUUIDFromBytes(
("legacy:" + f[0] + ":" + f[1] + ":" + f[2] + ":" + f[3])
.getBytes(StandardCharsets.UTF_8)).toString();
list.add(new PlacedItem(deterministicId, f[0],
Float.parseFloat(f[1]), Float.parseFloat(f[2]), Float.parseFloat(f[3])));
Float.parseFloat(f[1]), Float.parseFloat(f[2]), Float.parseFloat(f[3]),
0f, 0f, 0f));
}
} catch (NumberFormatException ignored) {}
}

View File

@@ -39,7 +39,7 @@ public class ItemCategoryManager {
private static final Map<ItemCategory, List<ItemSubCategory>> mapping = new EnumMap<ItemCategory, List<ItemSubCategory>>(ItemCategory.class);
static {
mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, BOW, CROSSBOW, MAGIC_CRYSTAL));
mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, BOW, CROSSBOW, MAGIC_CRYSTAL, SHIELD));
mapping.put(GEAR, List.of(RING, NECKLACE, ARMOR, HELM, SHIELD));
mapping.put(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM));
mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM));

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;
@@ -49,6 +76,13 @@ public class ItemPlacementState extends BaseAppState {
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,7 +221,18 @@ 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
@@ -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;
@@ -194,24 +273,28 @@ public class ItemPlacementState extends BaseAppState {
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,43 +302,361 @@ 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));
@@ -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 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.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;
@@ -169,6 +176,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
private void onCancel() {
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 ───────────────────────────────────────────────────────────────

View File

@@ -41,12 +41,23 @@ public class AnimPlayer {
private float blendDuration;
/** Einsprungzeit: Clip startet bei diesem Zeitwert statt bei 0 (überspringt z.B. das Ausholen). */
private float pendingStartOffset;
/** Wenn true: Clip rückwärts abspielen (speed=-1, Start am Ende). */
private boolean pendingReversed;
public AnimPlayer(AnimComposer composer, SkinningControl skinning) {
this.composer = composer;
this.skinning = skinning;
}
/**
* Spielt einen Clip rückwärts ab (speed=-1, startet am Ende).
* Geeignet für IDLE_TO_BLOCK rückwärts beim Loslassen der Blocktaste.
*/
public boolean playReversed(String clipName, float transition) {
this.pendingReversed = true;
return play(clipName, transition);
}
/**
* Spielt einen Clip ab, optional ab einem Zeitversatz (z.B. Ausholen überspringen).
*
@@ -167,10 +178,15 @@ public class AnimPlayer {
pendingClip = null;
blendSpace = null;
double len = currentAction != null ? currentAction.getLength() : -1;
if (pendingStartOffset > 0f && currentAction != null && len > 0.01) {
if (pendingReversed && currentAction != null && len > 0.01) {
currentAction.setSpeed(-1f);
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len);
pendingReversed = false;
} else if (pendingStartOffset > 0f && currentAction != null && len > 0.01) {
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(pendingStartOffset % (float) len);
}
pendingStartOffset = 0f;
pendingReversed = false;
log.info("[AnimPlayer] direktPlay → '{}' length={}s", clipName, len);
return currentAction != null;
} catch (Exception e) {
@@ -192,13 +208,17 @@ public class AnimPlayer {
currentAction = composer.setCurrentAction(clip);
currentClip = clip;
double len = currentAction != null ? currentAction.getLength() : 0.0;
// AnimLayer.setTime: kein extra controlUpdate, kein loop=false-Seiteneffekt.
if (pendingReversed && currentAction != null && len > 0.01) {
currentAction.setSpeed(-1f);
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len);
pendingReversed = false;
} else if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) {
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) {
float t = (pendingStartOffset + elapsed) % (float) len;
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t);
pendingStartOffset = 0f;
}
pendingReversed = false;
log.debug("[AnimPlayer] finishBlend → '{}'", clip);
} catch (Exception e) {
log.warn("[AnimPlayer] finishBlend('{}') fehlgeschlagen: {}", clip, e.getMessage());

View File

@@ -34,18 +34,21 @@ public enum AnimationAction {
// ── Schwert ───────────────────────────────────────────────────────────────
SWORD_ATTACK_1,
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN,
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK,
SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,
SWORD_IDLE_TO_BLOCK, SWORD_COUNTERATTACK, SWORD_BLOCK_IMPACT, SWORD_IDLE_IMPACT,
SWORD_IDLE, SWORD_WALK, SWORD_RUN,
// ── Axt ───────────────────────────────────────────────────────────────────
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_HIT_TAKEN,
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
AXE_ATTACK_1, AXE_ATTACK_2, AXE_ATTACK_3, AXE_HEAVY_ATTACK,
AXE_IDLE_TO_BLOCK, AXE_COUNTERATTACK, AXE_BLOCK_IMPACT, AXE_IDLE_IMPACT,
AXE_IDLE, AXE_WALK, AXE_RUN,
// ── Zweihänder ────────────────────────────────────────────────────────────
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_HIT_TAKEN,
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK,
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK,
TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_COUNTERATTACK, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT,
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
// ── Bogen ─────────────────────────────────────────────────────────────────
@@ -96,10 +99,13 @@ public enum AnimationAction {
case SWORD_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Schwert)");
case SWORD_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Schwert)");
case SWORD_BLOCK -> TextRegistry.resolve(null, key, "Block (Schwert)");
case SWORD_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)");
case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
case SWORD_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Schwert)");
case SWORD_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Schwert)");
case SWORD_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Schwert)");
case SWORD_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
case SWORD_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)");
case SWORD_RUN -> TextRegistry.resolve(null, key, "Laufen (Schwert)");
@@ -107,11 +113,14 @@ public enum AnimationAction {
case AXE_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Axt)");
case AXE_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Axt)");
case AXE_BLOCK -> TextRegistry.resolve(null, key, "Block (Axt)");
case AXE_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Axt)");
case AXE_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Axt)");
case AXE_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Axt)");
case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)");
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
case AXE_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Axt)");
case AXE_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Axt)");
case AXE_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Axt)");
case AXE_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Axt)");
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
case AXE_WALK -> TextRegistry.resolve(null, key, "Gehen (Axt)");
case AXE_RUN -> TextRegistry.resolve(null, key, "Laufen (Axt)");
@@ -119,11 +128,14 @@ public enum AnimationAction {
case TWO_HAND_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Zweihänder)");
case TWO_HAND_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Zweihänder)");
case TWO_HAND_BLOCK -> TextRegistry.resolve(null, key, "Block (Zweihänder)");
case TWO_HAND_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
case TWO_HAND_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Zweihänder)");
case TWO_HAND_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Zweihänder)");
case TWO_HAND_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Zweihänder)");
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
case TWO_HAND_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Zweihänder)");
case TWO_HAND_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Zweihänder)");
case TWO_HAND_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Zweihänder)");
case TWO_HAND_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)");
case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)");

View File

@@ -42,6 +42,8 @@ public class PlayerInputControl {
private static final float COMBAT_BLEND = 0.05f;
/** Zeit in Sekunden nach einem Angriff, in der der nächste Kombo-Schlag gedrückt werden kann. */
private static final float COMBO_WINDOW = 0.6f;
/** Maximale Haltezeit für einen Kurz-Klick (Konterangriff); darüber = Block-Eingabe. */
private static final float BLOCK_TAP_MAX = 0.20f;
// Combo-Overlap-Utility: MELEE_COMBO_ACTIVE + COMBO_ATK2_OFFSET hier eintragen,
// sobald die Animationen in Blender angeglichen wurden (passende Übergangsposen).
// AnimPlayer.play(clip, transition, startOffset) und nextStartOffset in tryPlay() sind
@@ -67,15 +69,20 @@ public class PlayerInputControl {
private boolean forward, backward, left, right, sprint, walk;
private boolean paused = false;
private enum BlockPhase { NONE, WAITING, ENTERING, HOLDING, EXITING }
// ── Kampf-Zustand ─────────────────────────────────────────────────────────
private MainCharacter mainCharacter = null;
private boolean weaponDrawn = false;
private ItemSubCategory drawnWeaponType = null;
private boolean isAiming = false;
private boolean rightMouseHeld = false;
private BlockPhase blockPhase = BlockPhase.NONE;
private float blockPhaseTimer = 0f;
private boolean combatAnimActive = false;
private float combatAnimRemaining = 0f;
private Runnable combatAnimCallback = null;
private boolean combatAnimSkipComboWindow = false;
/** Aktueller Schritt in der Kombo-Sequenz (0–3): 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK3-kritisch. */
private int comboStep = 0;
/** Zeitfenster nach einem Angriff, in dem der nächste Kombo-Schlag akzeptiert wird. */
@@ -136,6 +143,7 @@ public class PlayerInputControl {
private int groundGraceFrames = 0;
private double nextTransitionLength = 0.0;
private float nextStartOffset = 0f;
private boolean nextPlayReversed = false;
private boolean pickupActive = false;
private float pickupRemaining = 0f;
@@ -269,6 +277,7 @@ public class PlayerInputControl {
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
stopWading();
rightMouseHeld = false;
blockPhase = BlockPhase.NONE;
combatAnimActive = false;
combatAnimCallback = null;
comboStep = 0;
@@ -682,8 +691,10 @@ public class PlayerInputControl {
nextTransitionLength = COMBAT_BLEND;
Runnable cb = combatAnimCallback;
combatAnimCallback = null;
boolean skipWindow = combatAnimSkipComboWindow;
combatAnimSkipComboWindow = false;
if (cb != null) cb.run();
if (!combatAnimActive && isMeleeType(drawnWeaponType)) {
if (!skipWindow && !combatAnimActive && isMeleeType(drawnWeaponType)) {
comboWindowActive = true;
comboWindowRemaining = COMBO_WINDOW;
log.info("[Combat] Kombo-Fenster geöffnet (step={} next={})", comboStep, comboStep);
@@ -704,13 +715,14 @@ public class PlayerInputControl {
}
}
updateBlockPhase(tpf);
// Animations-Auswahl
if (jumpFrames > 0) jumpFrames--;
if (groundGraceFrames > 0) groundGraceFrames--;
// Während des Kombo-Fensters läuft die letzte Angriffs-Animation weiter —
// kein Wechsel zu IDLE/WALK, sonst entsteht ein sichtbares Zucken beim nächsten Schlag.
if (!combatAnimActive && !comboWindowActive) {
// Block-Phase verwaltet ihre eigenen Animationen.
if (!combatAnimActive && !isBlocking()) {
AnimationAction target;
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
@@ -724,10 +736,6 @@ public class PlayerInputControl {
target = resolveWeaponAction(target);
if (weaponDrawn && rightMouseHeld && isMeleeType(drawnWeaponType)) {
target = getMeleeBlockAction(drawnWeaponType);
}
if (target != currentAnim) {
playAction(target);
currentAnim = target;
@@ -796,6 +804,7 @@ public class PlayerInputControl {
if (drawnWeaponType == null) { weaponDrawn = false; return; }
isAiming = false;
rightMouseHeld = false;
blockPhase = BlockPhase.NONE;
AnimationAction sheathAnim = getSheathAction(drawnWeaponType);
float dur = resolveClipLength(sheathAnim, 0.5f);
requestAnimation(sheathAnim, dur, () -> {
@@ -806,9 +815,9 @@ public class PlayerInputControl {
}
private void handleCombatAttack() {
if (!weaponDrawn || blockingAnimActive || combatAnimActive) {
log.info("[Combat] BLOCKIERT: weaponDrawn={} blockingAnim={} combatAnim={}",
weaponDrawn, blockingAnimActive, combatAnimActive);
if (!weaponDrawn || blockingAnimActive || combatAnimActive || isBlocking()) {
log.info("[Combat] BLOCKIERT: weaponDrawn={} blockingAnim={} combatAnim={} blocking={}",
weaponDrawn, blockingAnimActive, combatAnimActive, isBlocking());
return;
}
if (isMeleeType(drawnWeaponType)) {
@@ -853,12 +862,103 @@ public class PlayerInputControl {
private void handleCombatAlt(boolean isPressed) {
if (!weaponDrawn || blockingAnimActive) return;
if (isMeleeType(drawnWeaponType)) {
rightMouseHeld = isPressed;
if (isPressed) {
if (blockPhase == BlockPhase.NONE) {
blockPhase = BlockPhase.WAITING;
blockPhaseTimer = 0f;
}
} else {
switch (blockPhase) {
case WAITING -> {
blockPhase = BlockPhase.NONE;
playCounterAttack();
}
case ENTERING, HOLDING -> startBlockExit();
default -> {}
}
}
} else if (isRangedType(drawnWeaponType) && isPressed) {
isAiming = !isAiming;
}
}
private void updateBlockPhase(float tpf) {
switch (blockPhase) {
case WAITING -> {
blockPhaseTimer += tpf;
if (blockPhaseTimer >= BLOCK_TAP_MAX) {
if (hasShieldEquipped()) {
startBlockEnter();
} else {
blockPhase = BlockPhase.NONE;
}
}
}
case ENTERING -> {
blockPhaseTimer -= tpf;
if (blockPhaseTimer <= 0f) {
blockPhase = BlockPhase.HOLDING;
AnimationAction blockAct = getMeleeBlockAction(drawnWeaponType);
nextTransitionLength = COMBAT_BLEND;
playAction(blockAct);
currentAnim = blockAct;
}
}
case EXITING -> {
blockPhaseTimer -= tpf;
if (blockPhaseTimer <= 0f) {
blockPhase = BlockPhase.NONE;
currentAnim = null;
}
}
default -> {}
}
}
private void startBlockEnter() {
blockPhase = BlockPhase.ENTERING;
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
float dur = resolveClipLength(idleToBlock, 0.5f);
blockPhaseTimer = dur;
nextTransitionLength = COMBAT_BLEND;
playAction(idleToBlock);
currentAnim = idleToBlock;
}
private void startBlockExit() {
blockPhase = BlockPhase.EXITING;
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
float dur = resolveClipLength(idleToBlock, 0.5f);
blockPhaseTimer = dur;
nextTransitionLength = COMBAT_BLEND;
nextPlayReversed = true;
playAction(idleToBlock);
currentAnim = idleToBlock;
}
private void playCounterAttack() {
if (combatAnimActive) return;
AnimationAction counterAct = getMeleeCounterAttackAction(drawnWeaponType);
float dur = resolveClipLength(counterAct, 0.7f);
nextTransitionLength = COMBAT_BLEND;
combatAnimActive = true;
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
combatAnimCallback = null;
combatAnimSkipComboWindow = true;
playAction(counterAct);
currentAnim = counterAct;
}
public boolean isBlocking() {
return blockPhase == BlockPhase.ENTERING
|| blockPhase == BlockPhase.HOLDING
|| blockPhase == BlockPhase.EXITING;
}
private boolean hasShieldEquipped() {
return mainCharacter != null && mainCharacter.getEquippedShield() != null;
}
private AnimationAction resolveWeaponAction(AnimationAction base) {
if (!weaponDrawn || drawnWeaponType == null) return base;
return switch (base) {
@@ -986,6 +1086,38 @@ public class PlayerInputControl {
};
}
private static AnimationAction getMeleeIdleToBlockAction(ItemSubCategory s) {
return switch (s) {
case AXE -> AnimationAction.AXE_IDLE_TO_BLOCK;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_TO_BLOCK;
default -> AnimationAction.SWORD_IDLE_TO_BLOCK;
};
}
private static AnimationAction getMeleeCounterAttackAction(ItemSubCategory s) {
return switch (s) {
case AXE -> AnimationAction.AXE_COUNTERATTACK;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_COUNTERATTACK;
default -> AnimationAction.SWORD_COUNTERATTACK;
};
}
public static AnimationAction getMeleeBlockImpactAction(ItemSubCategory s) {
return switch (s) {
case AXE -> AnimationAction.AXE_BLOCK_IMPACT;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_BLOCK_IMPACT;
default -> AnimationAction.SWORD_BLOCK_IMPACT;
};
}
public static AnimationAction getMeleeIdleImpactAction(ItemSubCategory s) {
return switch (s) {
case AXE -> AnimationAction.AXE_IDLE_IMPACT;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_IDLE_IMPACT;
default -> AnimationAction.SWORD_IDLE_IMPACT;
};
}
private static AnimationAction getRangedDrawAction(ItemSubCategory s) {
return switch (s) {
case CROSSBOW -> AnimationAction.CROSSBOW_DRAW;
@@ -1256,10 +1388,15 @@ public class PlayerInputControl {
}
float transLen = (float) nextTransitionLength;
float startOff = nextStartOffset;
boolean reversed = nextPlayReversed;
nextTransitionLength = 0.0;
nextStartOffset = 0f;
boolean ok = animPlayer.play(clip, transLen, startOff);
log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED");
nextPlayReversed = false;
boolean ok = reversed
? animPlayer.playReversed(clip, transLen)
: animPlayer.play(clip, transLen, startOff);
log.info("[Anim] AnimPlayer.{}('{}') transLen={} → {}", reversed ? "playReversed" : "play",
clip, transLen, ok ? "OK" : "FAILED");
if (ok) {
runningClip = animPlayer.getCurrentClip();
applyAnimOffset(clip);

View File

@@ -30,6 +30,7 @@ public class WeaponAttachmentControl extends AbstractControl {
private static final String BONE_SPINE = "mixamorig:Spine2";
private static final String BONE_HAND = "mixamorig:RightHand";
private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
private static final float HALF_PI = (float) Math.PI * 0.5f;
@@ -37,6 +38,18 @@ public class WeaponAttachmentControl extends AbstractControl {
/** Zusatz-Versatz beim gezückten Schwert in lokalem Waffen-Space (X=entlang Klinge, Y=entlang Griff, Z=senkrecht zur Schneide). */
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.0f, 0.05f);
/** Schild am Rücken: Griff unterhalb der Schultern, parallel zum Boden, Schild über dem Schwert. */
private static final Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f);
// Schild am Arm (gezückt): Rotation relativ zur Bind-Pose des Unterarm-Knochens.
// Werte: 0f = keine Drehung, HALF_PI = +90°, -HALF_PI = -90°
private static final float SHIELD_ARM_ROT_X = -HALF_PI;
private static final float SHIELD_ARM_ROT_Y = 0f;
private static final float SHIELD_ARM_ROT_Z = 0f;
// Verschiebung entlang des Unterarms Richtung Ellbogen (lokale −Y des LeftHand-Bones).
private static final float SHIELD_ARM_ELBOW_OFFSET = 0.25f;
private final AssetManager assetManager;
private final PlayerInputControl playerInput;
private final MainCharacter mc;
@@ -57,6 +70,12 @@ public class WeaponAttachmentControl extends AbstractControl {
private Spatial weaponSpatial;
private boolean gripDiagLogged = false;
private Item lastEquippedShield;
private Spatial shieldSpatial;
private Node leftForeArmNode;
private Quaternion leftForeArmBindRotInChar;
public WeaponAttachmentControl(AssetManager assetManager,
PlayerInputControl playerInput,
MainCharacter mc) {
@@ -87,6 +106,12 @@ public class WeaponAttachmentControl extends AbstractControl {
} catch (Exception e) {
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage());
}
try {
leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM);
} catch (Exception e) {
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage());
}
leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM);
log.info("[WeaponAttach] handBindRotInChar={}", handBindRotInChar);
}
@@ -100,6 +125,7 @@ public class WeaponAttachmentControl extends AbstractControl {
}
Item current = mc.getEquippedMeleeWeapon();
Item shield = mc.getEquippedShield();
boolean drawn = playerInput.isWeaponDrawn();
if (current != lastEquippedWeapon) {
@@ -112,6 +138,16 @@ public class WeaponAttachmentControl extends AbstractControl {
lastDrawnState = drawn;
updateWeaponTransform(drawn, current != null ? current.getGrip() : null);
}
if (shield != lastEquippedShield) {
detachShield();
lastEquippedShield = shield;
if (shield != null) loadShield(shield);
}
if (shieldSpatial != null) {
updateShieldTransform(drawn, shield);
}
}
// ── Laden ────────────────────────────────────────────────────────────────
@@ -245,6 +281,73 @@ public class WeaponAttachmentControl extends AbstractControl {
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
}
// ── Schild ────────────────────────────────────────────────────────────────
private void loadShield(Item item) {
if (item.getModelRef() == null || item.getModelRef().getPath() == null) {
log.warn("[WeaponAttach] Kein modelRef an Schild '{}'.", item.getItemId());
return;
}
try {
shieldSpatial = assetManager.loadModel(item.getModelRef().getPath());
((Node) spatial).attachChild(shieldSpatial);
log.info("[WeaponAttach] Schild geladen: {}", item.getModelRef().getPath());
} catch (Exception e) {
log.warn("[WeaponAttach] Schildmodell nicht ladbar ({}): {}",
item.getModelRef().getPath(), e.getMessage());
shieldSpatial = null;
}
}
private void updateShieldTransform(boolean drawn, Item shield) {
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
Vector3f charWorldPos = spatial.getWorldTranslation();
ItemGrip grip = shield != null ? shield.getGrip() : null;
if (drawn && leftForeArmNode != null) {
Vector3f boneInChar = charWorldRotInv.mult(
leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation());
applyShieldArmTransform(boneInChar, boneRotInChar, grip);
shieldSpatial.setCullHint(Spatial.CullHint.Inherit);
} else if (!drawn && backNode != null) {
Vector3f boneInChar = charWorldRotInv.mult(
backNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(backNode.getWorldRotation());
applyShieldBackTransform(boneInChar, boneRotInChar, grip);
shieldSpatial.setCullHint(Spatial.CullHint.Inherit);
} else {
shieldSpatial.setCullHint(Spatial.CullHint.Always);
}
}
private void applyShieldArmTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
Quaternion animDelta = (leftForeArmBindRotInChar != null)
? boneRotInChar.mult(leftForeArmBindRotInChar.inverse())
: boneRotInChar;
Quaternion armRot = new Quaternion().fromAngles(SHIELD_ARM_ROT_X, SHIELD_ARM_ROT_Y, SHIELD_ARM_ROT_Z);
Quaternion shieldRot = animDelta.mult(armRot);
Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO;
// Ankerpunkt Richtung Ellbogen verschieben (lokale −Y des LeftHand-Bones)
Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -SHIELD_ARM_ELBOW_OFFSET, 0f)));
shieldSpatial.setLocalRotation(shieldRot);
shieldSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
}
private void applyShieldBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
// 180° um Y: Modell-+X → Spine-X (Griff nach links), Modell-+Z → Spine-−Z (flach am Rücken)
Quaternion backRot = new Quaternion().fromAngles(0f, (float) Math.PI, 0f);
Quaternion shieldRot = boneRotInChar.mult(backRot);
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(SHIELD_BACK_OFFSET));
Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO;
shieldSpatial.setLocalRotation(shieldRot);
shieldSpatial.setLocalTranslation(gripTarget.subtract(shieldRot.mult(gripPos)));
}
// ── Detach ────────────────────────────────────────────────────────────────
private void detachWeapon() {
@@ -255,6 +358,13 @@ public class WeaponAttachmentControl extends AbstractControl {
lastDrawnState = false;
}
private void detachShield() {
if (shieldSpatial != null) {
shieldSpatial.removeFromParent();
shieldSpatial = null;
}
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {}
}

View File

@@ -186,8 +186,7 @@ public class WorldInteractableState extends BaseAppState {
private final ActionListener interactListener = (name, isPressed, tpf) -> {
if (!isPressed || phase != Phase.IDLE) return;
int idx = findNearestInRange();
if (idx >= 0) startApproach(idx);
if (hoveredIdx >= 0) startApproach(hoveredIdx);
};
private final ActionListener getUpListener = (name, isPressed, tpf) -> {
@@ -247,22 +246,6 @@ public class WorldInteractableState extends BaseAppState {
return new Vector3f(e.worldX(), e.worldY(), e.worldZ());
}
// ── Suche nächstes Interactable ───────────────────────────────────────────
private int findNearestInRange() {
Vector3f pos = physicsChar.getPhysicsLocation();
int bestIdx = -1;
float bestDist = Float.MAX_VALUE;
for (int i = 0; i < entries.size(); i++) {
InteractableEntry e = entries.get(i);
float dx = e.worldX() - pos.x;
float dz = e.worldZ() - pos.z;
float d = (float) Math.sqrt(dx * dx + dz * dz);
float range = (e.type() == InteractableType.BENCH) ? BENCH_RANGE : BED_RANGE;
if (d < range && d < bestDist) { bestDist = d; bestIdx = i; }
}
return bestIdx;
}
// ── Annäherung ────────────────────────────────────────────────────────────

View File

@@ -153,6 +153,9 @@ public class WorldItemsState extends BaseAppState {
for (PlacedItem pi : items) {
Spatial s = buildVisual(pi);
s.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
com.jme3.math.Quaternion q = new com.jme3.math.Quaternion();
q.fromAngles(pi.rotX(), pi.rotY(), pi.rotZ());
s.setLocalRotation(q);
s.setUserData("itemId", pi.itemId());
itemsRoot.attachChild(s);
visuals.add(s);

View File

@@ -243,3 +243,7 @@ location.silas.name=Silas
location.silas_.name=Silas
location.silas_haus.name=Silas Haus
location.silas_heim.name=Silas Heim
einfacher_schild.name=Einfacher Schild
rostiges_kurzschwert.name=Rostiges Kurzschwert
blutagave.name=Blutagave
erzmoss.name=Erzmoss

View File

@@ -98,3 +98,7 @@ location.silas.name=Silas
location.silas_.name=Silas
location.silas_haus.name=Silas Haus
location.silas_heim.name=Silas Heim
einfacher_schild.name=Simple Shield
rostiges_kurzschwert.name=Rusty Short Sword
blutagave.name=Blood Agave
erzmoss.name=Ore Moss

View File

@@ -126,6 +126,8 @@ dialog.silas.wer_bist_du.textnpc
dialog.silas.wer_seid_ihr.description
dialog.silas.wer_seid_ihr.textmainchar
dialog.silas.wer_seid_ihr.textnpc
einfacher_schild.description
einfacher_schild.name
erzmoss.description
erzmoss.name
hero.name
@@ -138,6 +140,10 @@ location.silas_
location.silas_haus
location.silas_heim
location.test
neues_item_1789920920760.description
neues_item_1789920920760.name
neues_item_1789920asdf920760.description
neues_item_1789920asdf920760.name
quest.blight_wuehler.description
quest.blight_wuehler.name
quest.blight_wuehler.success

View File

@@ -1,2 +1,3 @@
# uuid itemId x y z
fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693
# uuid itemId x y z rotX rotY rotZ
fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693 0.00000 0.00000 0.00000
feebed68-92b8-4458-9c7c-b1b781c1eba5 einfacher_schild -4.45856 1.72895 -3.67467 -0.14178 0.64812 0.00000