Compare commits

...

2 Commits

66 changed files with 1815 additions and 478 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -9,7 +9,17 @@
"T-Pose", "T-Pose",
"Walking", "Walking",
"Jumping", "Jumping",
"Sword And Shield Slash" "Sword And Shield Slash",
"Sword And Shield Idle",
"Sword And Shield Run",
"Sword And Shield Walk",
"Sword And Shield Attack",
"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": { "actionMap": {
"DEFAULT": "Idle", "DEFAULT": "Idle",
@@ -22,7 +32,16 @@
"IDLE": "Idle", "IDLE": "Idle",
"JUMP": "Jumping", "JUMP": "Jumping",
"SWORD_ATTACK_1": "Sword And Shield Slash", "SWORD_ATTACK_1": "Sword And Shield Slash",
"SWORD_ATTACK_2": "Sword And Shield Slash" "SWORD_IDLE": "Sword And Shield Idle",
"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_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", "previewModelPath": "Characters/Models/hero.j3o",
"animOffsets": { "animOffsets": {
@@ -67,5 +86,16 @@
"SIT_UP", "SIT_UP",
"IDLE" "IDLE"
] ]
} },
"snapClips": [
"Idle",
"Walking",
"Running",
"Sprint",
"Jumping",
"Sword And Shield Idle",
"Sword And Shield Run",
"Sword And Shield Walk",
"Sword And Shield Attack"
]
} }

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

@@ -1,4 +1,4 @@
#Tue Sep 15 12:31:37 CEST 2026 #Sun Sep 20 15:31:32 CEST 2026
attachedEmitters.count=0 attachedEmitters.count=0
attachedLights.count=0 attachedLights.count=0
attachedSounds.count=0 attachedSounds.count=0

View File

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

View File

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

@@ -11,12 +11,12 @@
"worthGold": 0, "worthGold": 0,
"damage": 0, "damage": 0,
"grip": { "grip": {
"posX": 0.24, "posX": 0.37,
"posY": 0.05, "posY": 0.08,
"posZ": 0.0, "posZ": 0.0,
"rotX": 0.0, "rotX": 0.0,
"rotY": -90.0, "rotY": -90.0,
"rotZ": 0.0 "rotZ": 90.0
}, },
"modelRef": { "modelRef": {
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o" "path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"

View File

@@ -3,10 +3,15 @@ package de.blight.common;
import java.util.UUID; import java.util.UUID;
/** Ein Item-Pickup das auf der Spielwelt liegt (Kartendaten). */ /** 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) { 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(); Path p = getPath();
Files.createDirectories(p.getParent()); Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) { 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(); w.newLine();
for (PlacedItem it : items) { for (PlacedItem it : items) {
w.write(String.format(Locale.ROOT, "%s\t%s\t%.5f\t%.5f\t%.5f%n", 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.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; if (line.isEmpty() || line.startsWith("#")) continue;
String[] f = line.split("\t", -1); String[] f = line.split("\t", -1);
try { try {
if (f.length >= 5) { if (f.length >= 8) {
// Aktuelles Format: uuid itemId x y z // Aktuelles Format: uuid itemId x y z rotX rotY rotZ
list.add(new PlacedItem(f[0], f[1], 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) { } else if (f.length == 4) {
// Altes Format: itemId x y z → UUID deterministisch aus Inhalt ableiten // Altes Format: itemId x y z → UUID deterministisch aus Inhalt ableiten
String deterministicId = UUID.nameUUIDFromBytes( String deterministicId = UUID.nameUUIDFromBytes(
("legacy:" + f[0] + ":" + f[1] + ":" + f[2] + ":" + f[3]) ("legacy:" + f[0] + ":" + f[1] + ":" + f[2] + ":" + f[3])
.getBytes(StandardCharsets.UTF_8)).toString(); .getBytes(StandardCharsets.UTF_8)).toString();
list.add(new PlacedItem(deterministicId, f[0], 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) {} } 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); private static final Map<ItemCategory, List<ItemSubCategory>> mapping = new EnumMap<ItemCategory, List<ItemSubCategory>>(ItemCategory.class);
static { 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(GEAR, List.of(RING, NECKLACE, ARMOR, HELM, SHIELD));
mapping.put(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM)); mapping.put(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM));
mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM)); mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM));

View File

@@ -204,19 +204,10 @@ public class EditorApp extends Application {
toolPanel = buildToolPanel(); toolPanel = buildToolPanel();
topBar = topBarController.buildTop(toolCtx, new de.blight.editor.toolset.topbar.TopBarController.Deps( // assetController muss VOR buildTop() erstellt werden, damit es korrekt in Deps landet.
toolPanel, // Alle Abhängigkeiten (objectController, animPreviewController etc.) sind bereits verfügbar.
modelEditorController, assetController, animSetController, // Die Buttons (atmosphaereBtn, wasserMergedBtn, zonenBtn) sind im TopBarController
contentController, worldPanelController, voxelController, // als final-Felder vordefiniert und unabhängig von buildTop().
grassController, atmosphereController, waterController,
gameController, terrainController, objectController,
this::switchToVegetationGenerator, this::switchToEzTree,
this::switchToTripo, this::openCharacterEditor,
this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings
));
root.setTop(topBar);
root.setRight(worldPanelController.buildAuswahlPanel());
assetController = new de.blight.editor.toolset.asset.AssetTreeController(toolCtx, assetController = new de.blight.editor.toolset.asset.AssetTreeController(toolCtx,
new de.blight.editor.toolset.asset.AssetTreeController.Deps( new de.blight.editor.toolset.asset.AssetTreeController.Deps(
objectController, animPreviewController, animSetController, objectController, animPreviewController, animSetController,
@@ -230,6 +221,20 @@ public class EditorApp extends Application {
terrainController::loadTexMeta, terrainController::saveTexMeta, terrainController::loadTexMeta, terrainController::saveTexMeta,
() -> currentTool, r -> pendingAfterSave = r () -> 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.buildPanel();
assetController.setupOverlay(centerStack); assetController.setupOverlay(centerStack);
@@ -430,8 +435,25 @@ public class EditorApp extends Application {
worldPanelController.updateLocationZonePanel(input.selectedLocationZoneInfo); 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) { if (input.auswahlZoneSelected) {
input.auswahlZoneSelected = false; input.auswahlZoneSelected = false;
log.info("[Auswahl→Tool] Zone ausgewaehlt → LAYER_ZONEN");
input.activeLayer = SharedInput.LAYER_ZONEN; input.activeLayer = SharedInput.LAYER_ZONEN;
topBarController.getZonenBtn().setSelected(true); topBarController.getZonenBtn().setSelected(true);
root.setRight(worldPanelController.buildZonenPanel()); root.setRight(worldPanelController.buildZonenPanel());
@@ -439,6 +461,7 @@ public class EditorApp extends Application {
if (input.auswahlLightSelected) { if (input.auswahlLightSelected) {
input.auswahlLightSelected = false; input.auswahlLightSelected = false;
log.info("[Auswahl→Tool] Licht ausgewaehlt → Atmo-Tab, Info='{}'", input.selectedLightInfo);
topBarController.getAtmosphaereBtn().setSelected(true); topBarController.getAtmosphaereBtn().setSelected(true);
root.setRight(atmosphereController.buildPanel()); root.setRight(atmosphereController.buildPanel());
atmosphereController.updateLightPanel(input.selectedLightInfo); atmosphereController.updateLightPanel(input.selectedLightInfo);
@@ -446,6 +469,7 @@ public class EditorApp extends Application {
if (input.auswahlEmitterSelected) { if (input.auswahlEmitterSelected) {
input.auswahlEmitterSelected = false; input.auswahlEmitterSelected = false;
log.info("[Auswahl→Tool] Emitter ausgewaehlt → Atmo-Tab/LAYER_EMITTERS, Info='{}'", input.selectedEmitterInfo);
topBarController.getAtmosphaereBtn().setSelected(true); topBarController.getAtmosphaereBtn().setSelected(true);
input.activeLayer = SharedInput.LAYER_EMITTERS; input.activeLayer = SharedInput.LAYER_EMITTERS;
root.setRight(atmosphereController.buildPanel()); root.setRight(atmosphereController.buildPanel());
@@ -454,6 +478,7 @@ public class EditorApp extends Application {
if (input.auswahlSoundSelected) { if (input.auswahlSoundSelected) {
input.auswahlSoundSelected = false; input.auswahlSoundSelected = false;
log.info("[Auswahl→Tool] Sound ausgewaehlt → Atmo-Tab/LAYER_PLACED_SOUNDS, Info='{}'", input.selectedSoundInfo);
topBarController.getAtmosphaereBtn().setSelected(true); topBarController.getAtmosphaereBtn().setSelected(true);
input.activeLayer = SharedInput.LAYER_PLACED_SOUNDS; input.activeLayer = SharedInput.LAYER_PLACED_SOUNDS;
root.setRight(atmosphereController.buildPanel()); root.setRight(atmosphereController.buildPanel());
@@ -462,6 +487,7 @@ public class EditorApp extends Application {
if (input.auswahlWaterSelected) { if (input.auswahlWaterSelected) {
input.auswahlWaterSelected = false; input.auswahlWaterSelected = false;
log.info("[Auswahl→Tool] Wasser ausgewaehlt → LAYER_WATER");
topBarController.getWasserMergedBtn().setSelected(true); topBarController.getWasserMergedBtn().setSelected(true);
input.activeLayer = SharedInput.LAYER_WATER; input.activeLayer = SharedInput.LAYER_WATER;
root.setRight(waterController.buildPanel()); root.setRight(waterController.buildPanel());
@@ -470,15 +496,22 @@ public class EditorApp extends Application {
if (input.auswahlWaterfallSelected) { if (input.auswahlWaterfallSelected) {
input.auswahlWaterfallSelected = false; input.auswahlWaterfallSelected = false;
log.info("[Auswahl→Tool] Wasserfall ausgewaehlt → LAYER_WATERFALL");
topBarController.getWasserMergedBtn().setSelected(true); topBarController.getWasserMergedBtn().setSelected(true);
input.activeLayer = SharedInput.LAYER_WATERFALL; input.activeLayer = SharedInput.LAYER_WATERFALL;
root.setRight(waterController.buildPanel()); root.setRight(waterController.buildPanel());
waterController.updateWaterfallPanel(input.selectedWaterfallInfo); 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) { if (input.itemSelectionChanged) {
input.itemSelectionChanged = false; 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) { if (input.zonenSelectionChanged) {
@@ -952,7 +985,9 @@ public class EditorApp extends Application {
input.deleteEmitterRequested = true; input.deleteEmitterRequested = true;
else if (input.activeLayer == SharedInput.LAYER_PLACED_SOUNDS && input.selectedSoundInfo != null) else if (input.activeLayer == SharedInput.LAYER_PLACED_SOUNDS && input.selectedSoundInfo != null)
input.deleteSoundRequested = true; 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; input.deleteSelectedItemRequested = true;
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) { else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); } if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }

View File

@@ -970,6 +970,9 @@ public class SharedInput {
/** activeLayer==21 → Item-Pickup auf die Karte platzieren */ /** activeLayer==21 → Item-Pickup auf die Karte platzieren */
public static final int LAYER_ITEMS = 21; 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). */ /** JFX → JME: itemId des zu platzierenden Items (aus .item-Datei). */
public volatile String pendingItemId = null; public volatile String pendingItemId = null;
@@ -1178,6 +1181,8 @@ public class SharedInput {
public volatile float gripEditorRotZ = 0f; public volatile float gripEditorRotZ = 0f;
/** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */ /** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */
public volatile boolean gripEditorGizmoDirty = false; 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. */ /** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */
public record SculptEdit(float screenX, float screenY, int action) {} public record SculptEdit(float screenX, float screenY, int action) {}
@@ -1246,11 +1251,19 @@ public class SharedInput {
public volatile boolean deselectAllOnAuswahl = false; public volatile boolean deselectAllOnAuswahl = false;
/** JME3 → JavaFX: Zone wurde im Auswahl-Modus selektiert → Zonen-Werkzeug aktivieren. */ /** JME3 → JavaFX: Zone wurde im Auswahl-Modus selektiert → Zonen-Werkzeug aktivieren. */
public volatile boolean auswahlZoneSelected = false; 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 String selectedItemInfo = null;
public volatile boolean itemSelectionChanged = false; public volatile boolean itemSelectionChanged = false;
/** JavaFX → JME3: Selektierten Item-Spawn löschen. */ /** JavaFX → JME3: Selektierten Item-Spawn löschen. */
public volatile boolean deleteSelectedItemRequested = false; 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). */ /** JME3-intern: Auswahl-Klick für ItemPlacementState (kein Objekt/keine Zone getroffen). */
public final ConcurrentLinkedQueue<ObjectClick> auswahlClickQueue = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<ObjectClick> auswahlClickQueue = new ConcurrentLinkedQueue<>();
/** JME3-intern: Auswahl-Klick für LightState (kein Objekt/keine Zone getroffen). */ /** JME3-intern: Auswahl-Klick für LightState (kein Objekt/keine Zone getroffen). */

View File

@@ -1,5 +1,8 @@
package de.blight.editor; package de.blight.editor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Verwaltet das aktive Werkzeug und leitet den AnimationTimer-Frame weiter. * Verwaltet das aktive Werkzeug und leitet den AnimationTimer-Frame weiter.
* *
@@ -8,6 +11,8 @@ package de.blight.editor;
*/ */
public class ToolRegistry { public class ToolRegistry {
private static final Logger log = LoggerFactory.getLogger(ToolRegistry.class);
private ToolController active = null; private ToolController active = null;
/** /**
@@ -19,10 +24,12 @@ public class ToolRegistry {
if (tool == active) { if (tool == active) {
return; return;
} }
String prev = active != null ? active.toolId() : "–";
if (active != null) { if (active != null) {
active.deactivate(); active.deactivate();
} }
active = tool; active = tool;
log.info("[Tool] {} → {}", prev, tool.toolId());
tool.activate(); 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.common.PlacedEmitter;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class EmitterState extends BaseAppState { public class EmitterState extends AuswahlMarkerState {
private static final Logger log = LoggerFactory.getLogger(EmitterState.class);
private static final float MARKER_RADIUS = 0.4f; private static final float MARKER_RADIUS = 0.4f;
private static final float ACTIVATION_ALPHA = 0.07f; 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_MARKER = "emitter_marker";
private static final String GEO_ACTRADIUS = "activation_sphere"; 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] // parallel lists: emitters[i] <-> markers[i] <-> particles[i]
private final List<PlacedEmitter> emitters = new ArrayList<>(); private final List<PlacedEmitter> emitters = new ArrayList<>();
private final List<Node> markers = new ArrayList<>();
private final List<ParticleEmitter> particles = new ArrayList<>(); private final List<ParticleEmitter> particles = new ArrayList<>();
private int selectedIdx = -1; private int selectedIdx = -1;
@@ -52,19 +39,11 @@ public class EmitterState extends BaseAppState {
private List<PlacedEmitter> pendingEmitters = null; private List<PlacedEmitter> pendingEmitters = null;
public EmitterState(SharedInput input) { public EmitterState(SharedInput input) {
this.input = input; super(input);
} }
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── 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 protected void cleanup(Application application) { clearAll(); }
@Override @Override
@@ -77,7 +56,11 @@ public class EmitterState extends BaseAppState {
@Override protected void onDisable() {} @Override protected void onDisable() {}
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; } @Override
protected void onMarkerSelected(int idx) {
selectEmitter(idx);
input.auswahlEmitterSelected = true;
}
// ── Update ──────────────────────────────────────────────────────────────── // ── 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 ───────────────────────────────────────────────────────────── // ── Selection ─────────────────────────────────────────────────────────────
private void selectEmitter(int idx) { 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.SimpleApplication;
import com.jme3.app.state.BaseAppState; import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager; 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.collision.CollisionResults;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.math.*; import com.jme3.math.*;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.*; import com.jme3.scene.*;
import com.jme3.scene.shape.Box; 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 com.jme3.terrain.geomipmap.TerrainQuad;
import de.blight.common.PlacedItem; import de.blight.common.PlacedItem;
import de.blight.common.PlacedItemIO; import de.blight.common.PlacedItemIO;
@@ -21,6 +27,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -29,11 +36,31 @@ import java.util.Map;
/** /**
* Verwaltet die Platzierung von Items (Pickups) auf der Karte im Editor. * Verwaltet die Platzierung von Items (Pickups) auf der Karte im Editor.
* Aktiv wenn {@code input.activeLayer == LAYER_ITEMS}. * 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 { public class ItemPlacementState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(ItemPlacementState.class); 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 final SharedInput input;
private SimpleApplication app; private SimpleApplication app;
private Camera cam; private Camera cam;
@@ -46,9 +73,16 @@ public class ItemPlacementState extends BaseAppState {
private final Map<String, Item> itemDefs = new HashMap<>(); private final Map<String, Item> itemDefs = new HashMap<>();
private Node itemRoot; private Node itemRoot;
private Node previewNode; private Node previewNode;
private int selectedItemIdx = -1; private int selectedItemIdx = -1;
private Node selectedItemNode = null; 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) { public ItemPlacementState(SharedInput input) {
this.input = input; this.input = input;
} }
@@ -66,7 +100,7 @@ public class ItemPlacementState extends BaseAppState {
this.assets = app.getAssetManager(); this.assets = app.getAssetManager();
this.rootNode = this.app.getRootNode(); 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)) { for (Item it : ItemIO.loadAll(itemDir)) {
if (it.getItemId() != null) itemDefs.put(it.getItemId(), it); if (it.getItemId() != null) itemDefs.put(it.getItemId(), it);
} }
@@ -80,6 +114,11 @@ public class ItemPlacementState extends BaseAppState {
prev.setMaterial(pm); prev.setMaterial(pm);
prev.getMesh().setMode(com.jme3.scene.Mesh.Mode.Lines); prev.getMesh().setMode(com.jme3.scene.Mesh.Mode.Lines);
previewNode.attachChild(prev); previewNode.attachChild(prev);
gizmoNode = new Node("itemGizmo");
buildGizmo();
gizmoNode.setCullHint(Spatial.CullHint.Always);
rootNode.attachChild(gizmoNode);
} }
@Override @Override
@@ -101,6 +140,7 @@ public class ItemPlacementState extends BaseAppState {
for (PlacedItem pi : items) { for (PlacedItem pi : items) {
Node n = buildItemNode(pi.itemId()); Node n = buildItemNode(pi.itemId());
n.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z()); n.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
applyRot(n, pi.rotX(), pi.rotY(), pi.rotZ());
itemRoot.attachChild(n); itemRoot.attachChild(n);
nodes.add(n); nodes.add(n);
} }
@@ -113,7 +153,9 @@ public class ItemPlacementState extends BaseAppState {
} }
@Override @Override
protected void cleanup(Application app) {} protected void cleanup(Application app) {
if (gizmoNode != null) gizmoNode.removeFromParent();
}
// ── Update ──────────────────────────────────────────────────────────────── // ── Update ────────────────────────────────────────────────────────────────
@@ -125,11 +167,15 @@ public class ItemPlacementState extends BaseAppState {
reloadFromDisk(); reloadFromDisk();
} }
// Highlight entfernen sobald wir nicht mehr im Auswahl-Modus sind // Highlight entfernen sobald wir weder im Auswahl- noch im Edit-Modus sind
if (selectedItemIdx >= 0 && input.activeLayer != SharedInput.LAYER_AUSWAHL) { if (selectedItemIdx >= 0
&& input.activeLayer != SharedInput.LAYER_AUSWAHL
&& input.activeLayer != SharedInput.LAYER_ITEMS_EDIT) {
deselectItem(); deselectItem();
} }
updateGizmoVisibility();
updatePreview(); updatePreview();
SharedInput.ObjectClick click; 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; SharedInput.ObjectClick auswahlClick;
while ((auswahlClick = input.auswahlClickQueue.poll()) != null) { while ((auswahlClick = input.auswahlClickQueue.poll()) != null) {
handleAuswahlClick(auswahlClick); handleAuswahlClick(auswahlClick);
@@ -155,8 +221,19 @@ public class ItemPlacementState extends BaseAppState {
input.onObjectDeleted.run(); 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() { private void updatePreview() {
if (input.activeLayer != SharedInput.LAYER_ITEMS if (input.activeLayer != SharedInput.LAYER_ITEMS
|| input.pendingItemId == null || input.pendingItemId == null
@@ -179,6 +256,8 @@ public class ItemPlacementState extends BaseAppState {
previewNode.setCullHint(Spatial.CullHint.Inherit); previewNode.setCullHint(Spatial.CullHint.Inherit);
} }
// ── Platzierung ───────────────────────────────────────────────────────────
private void handlePlace(SharedInput.ObjectClick click) { private void handlePlace(SharedInput.ObjectClick click) {
if (terrain == null || input.pendingItemId == null) return; if (terrain == null || input.pendingItemId == null) return;
float jx = click.screenX() * (float) input.viewportScaleX; float jx = click.screenX() * (float) input.viewportScaleX;
@@ -188,30 +267,34 @@ public class ItemPlacementState extends BaseAppState {
terrain.collideWith(ray, hits); terrain.collideWith(ray, hits);
if (hits.size() == 0) return; if (hits.size() == 0) return;
Vector3f pt = hits.getClosestCollision().getContactPoint(); Vector3f pt = hits.getClosestCollision().getContactPoint();
PlacedItem pi = PlacedItem.create(input.pendingItemId, pt.x, pt.y, pt.z); PlacedItem pi = PlacedItem.create(input.pendingItemId, pt.x, pt.y, pt.z);
items.add(pi); items.add(pi);
Node n = buildItemNode(pi.itemId()); Node n = buildItemNode(pi.itemId());
n.setLocalTranslation(pt.x, pt.y + 0.25f, pt.z); n.setLocalTranslation(pt.x, pt.y + 0.25f, pt.z);
applyRot(n, 0f, 0f, 0f);
itemRoot.attachChild(n); itemRoot.attachChild(n);
nodes.add(n); nodes.add(n);
try { try {
PlacedItemIO.save(items); PlacedItemIO.save(items);
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", pi.itemId(), pt.x, pt.y, pt.z);
pi.itemId(), pt.x, pt.y, pt.z);
} catch (IOException e) { } catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage()); log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
} }
} }
// ── Auswahl-Klick (LAYER_AUSWAHL → Item-Picking) ─────────────────────────
private void handleAuswahlClick(SharedInput.ObjectClick click) { private void handleAuswahlClick(SharedInput.ObjectClick click) {
float jmeX = click.screenX() * (float) input.viewportScaleX; float jmeX = click.screenX() * (float) input.viewportScaleX;
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
Ray ray = screenToRay(jmeX, jmeY); Ray ray = screenToRay(jmeX, jmeY);
CollisionResults hits = new CollisionResults(); CollisionResults hits = new CollisionResults();
itemRoot.collideWith(ray, hits); 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) { if (hits.size() > 0) {
Spatial geo = hits.getClosestCollision().getGeometry(); Spatial geo = hits.getClosestCollision().getGeometry();
Node parent = geo.getParent(); Node parent = geo.getParent();
@@ -219,44 +302,362 @@ public class ItemPlacementState extends BaseAppState {
parent = parent.getParent(); parent = parent.getParent();
} }
if (parent != null) { 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; return;
} }
} }
deselectItem(); 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) { private void selectItem(int idx) {
deselectItem(); deselectItem();
selectedItemIdx = idx; selectedItemIdx = idx;
selectedItemNode = nodes.get(idx); selectedItemNode = nodes.get(idx);
Geometry highlight = new Geometry("item_sel", addSelectionHighlight(idx);
new com.jme3.scene.shape.Box(0.28f, 0.28f, 0.28f)); publishSelection(idx);
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); input.activeLayer = SharedInput.LAYER_ITEMS_EDIT;
mat.setColor("Color", new com.jme3.math.ColorRGBA(1f, 0.6f, 0.1f, 1f)); updateGizmoPosition();
mat.getAdditionalRenderState().setWireframe(true); updateGizmoVisibility();
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;
} }
private void deselectItem() { private void deselectItem() {
if (selectedItemNode != null) { if (selectedItemNode != null) {
Spatial h = selectedItemNode.getChild("item_sel"); removeSelectionHighlight(selectedItemIdx);
if (h != null) h.removeFromParent();
selectedItemNode = null; selectedItemNode = null;
} }
activeGizmo = -1;
activeRing = -1;
if (selectedItemIdx >= 0) { if (selectedItemIdx >= 0) {
selectedItemIdx = -1; selectedItemIdx = -1;
input.selectedItemInfo = null; input.selectedItemInfo = null;
input.itemSelectionChanged = true; 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) { private void removeItem(int idx) {
itemRoot.detachChild(nodes.get(idx)); itemRoot.detachChild(nodes.get(idx));
items.remove(idx); items.remove(idx);
@@ -268,8 +669,39 @@ public class ItemPlacementState extends BaseAppState {
} }
} }
private void applyItemEdit(int idx, float x, float y, float z, float rotX, float rotY, float rotZ) {
PlacedItem updated = items.get(idx).withTransform(x, y, z, rotX, rotY, rotZ);
items.set(idx, updated);
Node n = nodes.get(idx);
n.setLocalTranslation(x, y + 0.25f, z);
applyRot(n, rotX, rotY, rotZ);
publishSelection(idx);
try {
PlacedItemIO.save(items);
} catch (IOException e) {
log.warn("[ItemPlacement] Speichern nach Edit fehlgeschlagen: {}", e.getMessage());
}
}
private void saveItems() {
try {
PlacedItemIO.save(items);
} catch (IOException e) {
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
}
}
// ── Hilfsmethoden ──────────────────────────────────────────────────────── // ── 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) { private Node buildItemNode(String itemId) {
Node n = new Node("item_" + itemId); Node n = new Node("item_" + itemId);
n.setUserData("itemId", itemId); n.setUserData("itemId", itemId);

View File

@@ -21,7 +21,7 @@ import de.blight.editor.SharedInput;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class LightState extends BaseAppState { public class LightState extends AuswahlMarkerState {
// ── Glühbirnen-Geometrie ────────────────────────────────────────────────── // ── Glühbirnen-Geometrie ──────────────────────────────────────────────────
private static final float BULB_RADIUS = 0.38f; 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 SEL_COLOR = new ColorRGBA(1f, 1f, 0f, 1f);
private static final ColorRGBA SOCKET_COLOR = new ColorRGBA(0.22f, 0.22f, 0.22f, 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] // parallel lists: lights[i] <-> markers[i] <-> pointLights[i]
private final List<PlacedLight> lights = new ArrayList<>(); private final List<PlacedLight> lights = new ArrayList<>();
private final List<Node> markers = new ArrayList<>();
private final List<PointLight> pointLights = new ArrayList<>(); private final List<PointLight> pointLights = new ArrayList<>();
private int selectedIdx = -1; private int selectedIdx = -1;
private List<PlacedLight> pendingLights = null; private List<PlacedLight> pendingLights = null;
public LightState(SharedInput input) { public LightState(SharedInput input) {
this.input = input; super(input);
} }
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
cam = app.getCamera();
assets = app.getAssetManager();
rootNode = app.getRootNode();
}
@Override @Override
protected void cleanup(Application application) { protected void cleanup(Application application) {
clearAll(); clearAll();
@@ -80,7 +64,11 @@ public class LightState extends BaseAppState {
} }
@Override protected void onDisable() {} @Override protected void onDisable() {}
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; } @Override
protected void onMarkerSelected(int idx) {
selectLight(idx);
input.auswahlLightSelected = true;
}
// ── Update ──────────────────────────────────────────────────────────────── // ── Update ────────────────────────────────────────────────────────────────
@@ -153,29 +141,6 @@ public class LightState extends BaseAppState {
selectLight(lights.size() - 1); 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 ───────────────────────────────────────────────────────────── // ── Selection ─────────────────────────────────────────────────────────────
private void selectLight(int idx) { 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.LightProbe;
import com.jme3.light.PointLight; import com.jme3.light.PointLight;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.MatParam;
import com.jme3.math.*; import com.jme3.math.*;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager; import com.jme3.renderer.RenderManager;
@@ -31,6 +32,7 @@ import com.jme3.anim.SkinningControl;
import com.jme3.effect.ParticleEmitter; import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh; import com.jme3.effect.ParticleMesh;
import com.jme3.material.RenderState; import com.jme3.material.RenderState;
import com.jme3.texture.Texture;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.util.ImpostorUtil; import de.blight.editor.util.ImpostorUtil;
@@ -1093,6 +1095,17 @@ public class ModelEditorState extends BaseAppState {
VertexBuffer vb = mesh.getBuffer(type); VertexBuffer vb = mesh.getBuffer(type);
if (vb != null) vb.setUpdateNeeded(); 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) { } else if (s instanceof Node n) {
for (Spatial child : n.getChildren()) markBuffersUpdateNeeded(child); for (Spatial child : n.getChildren()) markBuffersUpdateNeeded(child);
} }
@@ -1520,20 +1533,33 @@ public class ModelEditorState extends BaseAppState {
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); 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() { public void rebuildGripGizmo() {
if (gripGizmoNode == null) return; if (gripGizmoNode == null) return;
gripGizmoNode.detachAllChildren(); gripGizmoNode.detachAllChildren();
float shaftLen = 0.12f; boolean shieldMode = input.gripEditorIsShield;
float shaftRad = 0.008f;
float headRad = 0.022f; // Waffe: 15cm orange | Schild: 28cm cyan
float headLen = 0.03f; // 15cm gesamt: 12cm Schaft + 3cm Kopf 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"); 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"); 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"); Node group = new Node("gripGroup");
group.setLocalTranslation(gripPosX, gripPosY, gripPosZ); group.setLocalTranslation(gripPosX, gripPosY, gripPosZ);
@@ -1545,8 +1571,8 @@ public class ModelEditorState extends BaseAppState {
com.jme3.math.FastMath.DEG_TO_RAD * gripRotZ); com.jme3.math.FastMath.DEG_TO_RAD * gripRotZ);
group.setLocalRotation(rot); group.setLocalRotation(rot);
// Kleiner Würfel am Griffpunkt float markerSize = shieldMode ? 0.018f : 0.015f;
Geometry marker = new Geometry("gMarker", new Box(0.015f, 0.015f, 0.015f)); Geometry marker = new Geometry("gMarker", new Box(markerSize, markerSize, markerSize));
marker.setMaterial(mat); marker.setMaterial(mat);
// Schaft entlang +Z // Schaft entlang +Z
@@ -1562,17 +1588,20 @@ public class ModelEditorState extends BaseAppState {
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X); headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
head.setLocalRotation(headRot); head.setLocalRotation(headRot);
// Klingenflosse: dünnes flaches Rechteck senkrecht zum Schaft entlang +Y, // Querachsen-Zylinder entlang +Y: zeigt Roll-/Orientierungsachse
// damit Rotation um Z (Roll) sichtbar ist. Breite 6cm, Dicke 2mm, entlang Schaft. Material matAxis = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
Material matFin = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); matAxis.setColor("Color", axisColor);
matFin.setColor("Color", new ColorRGBA(1f, 0.8f, 0.1f, 1f)); float axisLen = shieldMode ? 0.08f : 0.06f;
Geometry fin = new Geometry("gFin", new Box(0.001f, 0.03f, shaftLen * 0.5f)); Geometry gripAxis = new Geometry("gGripAxis", new Cylinder(4, 8, 0.006f, axisLen, true));
fin.setMaterial(matFin); gripAxis.setMaterial(matAxis);
fin.setLocalTranslation(0f, 0.03f, shaftLen * 0.5f); Quaternion axisRot = new Quaternion();
axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
gripAxis.setLocalRotation(axisRot);
gripAxis.setLocalTranslation(0f, axisLen * 0.5f, 0f);
group.attachChild(marker); group.attachChild(marker);
group.attachChild(shaft); group.attachChild(shaft);
group.attachChild(fin); group.attachChild(gripAxis);
group.attachChild(head); group.attachChild(head);
gripGizmoNode.attachChild(group); gripGizmoNode.attachChild(group);
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit); gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);

View File

@@ -19,7 +19,7 @@ import de.blight.editor.SharedInput;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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 MARKER_RADIUS = 0.30f;
private static final float GLOW_RADIUS = MARKER_RADIUS * 2.4f; 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 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] // parallel lists: sounds[i] <-> markers[i]
private final List<PlacedSound> sounds = new ArrayList<>(); private final List<PlacedSound> sounds = new ArrayList<>();
private final List<Node> markers = new ArrayList<>();
private int selectedIdx = -1; private int selectedIdx = -1;
private List<PlacedSound> pendingSounds = null; private List<PlacedSound> pendingSounds = null;
public PlacedSoundState(SharedInput input) { public PlacedSoundState(SharedInput input) {
this.input = input; super(input);
} }
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
cam = app.getCamera();
assets = app.getAssetManager();
rootNode = app.getRootNode();
}
@Override @Override
protected void cleanup(Application application) { protected void cleanup(Application application) {
clearAll(); clearAll();
@@ -74,7 +58,11 @@ public class PlacedSoundState extends BaseAppState {
@Override protected void onDisable() {} @Override protected void onDisable() {}
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; } @Override
protected void onMarkerSelected(int idx) {
selectSound(idx);
input.auswahlSoundSelected = true;
}
// ── Update ──────────────────────────────────────────────────────────────── // ── Update ────────────────────────────────────────────────────────────────
@@ -145,28 +133,6 @@ public class PlacedSoundState extends BaseAppState {
selectSound(sounds.size() - 1); 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 ───────────────────────────────────────────────────────────── // ── Selection ─────────────────────────────────────────────────────────────
private void selectSound(int idx) { private void selectSound(int idx) {

View File

@@ -707,6 +707,9 @@ public class SceneObjectState extends BaseAppState {
} }
if (closest == null) { deselectAll(); return; } if (closest == null) { deselectAll(); return; }
int idx = findObjectIndexByNode(closest.getGeometry()); 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) { if (idx >= 0) {
selectObject(idx, click.shift()); selectObject(idx, click.shift());
// Vorgefertigte Objekte (externe Modelle + Gruppen) können nicht sub-selektiert werden. // Vorgefertigte Objekte (externe Modelle + Gruppen) können nicht sub-selektiert werden.
@@ -723,6 +726,11 @@ public class SceneObjectState extends BaseAppState {
} }
return; 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 // 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 // Auswahl-Modus: kein Objekt angeklickt → deselektieren + Zonen/Items prüfen lassen
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) { 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(); deselectAll();
float sx = click.screenX(), sy = click.screenY(); float sx = click.screenX(), sy = click.screenY();
input.areaClickQueue.offer(new SharedInput.AreaClick(sx, sy, false)); input.areaClickQueue.offer(new SharedInput.AreaClick(sx, sy, false));

View File

@@ -51,6 +51,7 @@ public class AnimSetController implements ToolController {
private ListView<String> animSetPartsListView; private ListView<String> animSetPartsListView;
private java.util.Map<String, de.blight.game.animation.AnimSet.SubClipDef> private java.util.Map<String, de.blight.game.animation.AnimSet.SubClipDef>
editableSubClips = new java.util.LinkedHashMap<>(); editableSubClips = new java.util.LinkedHashMap<>();
private java.util.Set<String> editableSnapClips = new java.util.LinkedHashSet<>();
private ListView<String> animSetSequenceListView; private ListView<String> animSetSequenceListView;
private java.util.Map<String, java.util.List<String>> private java.util.Map<String, java.util.List<String>>
editableSequences = new java.util.LinkedHashMap<>(); editableSequences = new java.util.LinkedHashMap<>();
@@ -76,6 +77,16 @@ public class AnimSetController implements ToolController {
animSetCurrentName = setName; animSetCurrentName = setName;
animSetCurrentDir = setDir; animSetCurrentDir = setDir;
currentAnimSet = animSet; currentAnimSet = animSet;
editableSnapClips = new java.util.LinkedHashSet<>(animSet.getSnapClips());
// Migration: wenn snapClips noch leer, Fallback-Clips (LOCOMOTION_ACTIONS) einmalig übernehmen
// damit der Checkbox-Zustand die tatsächliche Snap-Konfiguration widerspiegelt.
if (editableSnapClips.isEmpty()) {
java.util.Set<String> locoFallback = java.util.Set.of(
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
animSet.getActionMap().forEach((action, clip) -> {
if (locoFallback.contains(action)) editableSnapClips.add(clip);
});
}
} }
@Override @Override
@@ -337,17 +348,14 @@ public class AnimSetController implements ToolController {
animSetClipListView.setPrefHeight(180); animSetClipListView.setPrefHeight(180);
editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips()); editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips());
editableSequences = new java.util.LinkedHashMap<>(animSet.getSequences()); editableSequences = new java.util.LinkedHashMap<>(animSet.getSequences());
// editableSnapClips wird bereits in openWithSet gesetzt (inkl. Migration)
ctx.input().animPreviewClipOffsets.clear(); ctx.input().animPreviewClipOffsets.clear();
for (var en : animSet.getAnimOffsets().entrySet()) { for (var en : animSet.getAnimOffsets().entrySet()) {
de.blight.game.animation.AnimOffset o = en.getValue(); de.blight.game.animation.AnimOffset o = en.getValue();
ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz}); ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
} }
java.util.Set<String> locomotionActions = java.util.Set.of(
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
ctx.input().animPreviewLocomotionClips.clear(); ctx.input().animPreviewLocomotionClips.clear();
animSet.getActionMap().forEach((action, clip) -> { ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
if (locomotionActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip);
});
Path animRootDir = ctx.assetRoot().resolve("Characters"); Path animRootDir = ctx.assetRoot().resolve("Characters");
@@ -454,10 +462,41 @@ public class AnimSetController implements ToolController {
autoSaveAnimSet(); autoSaveAnimSet();
}); });
CheckBox snapCB = new CheckBox("Root-Motion entfernen (auf der Stelle)");
snapCB.setDisable(true);
snapCB.setTooltip(new Tooltip("Bewegt sich die Animation durch den Raum (Walk/Run)?\nMit dieser Option bleibt der Charakter an Ort und Stelle\nund das Spiel übernimmt die Fortbewegung."));
final boolean[] snapCbUpdating = {false};
animSetClipListView.getSelectionModel().selectedItemProperty()
.addListener((obs, ov, nv) -> {
snapCB.setDisable(nv == null);
if (nv != null) {
snapCbUpdating[0] = true;
snapCB.setSelected(editableSnapClips.contains(nv));
snapCbUpdating[0] = false;
}
});
snapCB.selectedProperty().addListener((obs, ov, nv) -> {
if (snapCbUpdating[0]) return;
String sel = animSetClipListView.getSelectionModel().getSelectedItem();
if (sel == null) return;
if (nv) {
editableSnapClips.add(sel);
} else {
editableSnapClips.remove(sel);
}
ctx.input().animPreviewLocomotionClips.clear();
ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
// Clip neu laden damit Snap-Änderung sofort in der Vorschau sichtbar wird
ctx.input().animImportQueue.offer(findAnimClipPath(sel));
autoSaveAnimSet();
});
HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn); HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn);
HBox.setHgrow(addClipBtn, Priority.ALWAYS); HBox.setHgrow(addClipBtn, Priority.ALWAYS);
HBox.setHgrow(removeClipBtn, Priority.ALWAYS); HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
inner.getChildren().addAll(animSetClipListView, clipBtns); inner.getChildren().addAll(animSetClipListView, snapCB, clipBtns);
// ── Clip-Teile (Sub-Clips) ───────────────────────────────────────────── // ── Clip-Teile (Sub-Clips) ─────────────────────────────────────────────
inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator()); inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator());
@@ -1172,12 +1211,9 @@ public class AnimSetController implements ToolController {
de.blight.game.animation.AnimOffset o = en.getValue(); de.blight.game.animation.AnimOffset o = en.getValue();
ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz}); ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
} }
java.util.Set<String> locoActions = java.util.Set.of( animSet.setSnapClips(new java.util.LinkedHashSet<>(editableSnapClips));
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
ctx.input().animPreviewLocomotionClips.clear(); ctx.input().animPreviewLocomotionClips.clear();
animSet.getActionMap().forEach((action, clip) -> { ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
if (locoActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip);
});
if (animSetModelCombo != null && animSetModelCombo.getValue() != null if (animSetModelCombo != null && animSetModelCombo.getValue() != null
&& !animSetModelCombo.getValue().isBlank()) { && !animSetModelCombo.getValue().isBlank()) {
animSet.setPreviewModelPath(animSetModelCombo.getValue()); animSet.setPreviewModelPath(animSetModelCombo.getValue());

View File

@@ -344,6 +344,7 @@ public class ContentController {
ctx.setCurrentTool().accept("itemEditor"); ctx.setCurrentTool().accept("itemEditor");
ToolBar tb = makeSingleWindowToolBar("Item-Manager"); ToolBar tb = makeSingleWindowToolBar("Item-Manager");
ctx.setMainToolbar().accept(tb); ctx.setMainToolbar().accept(tb);
ctx.setLeftPanel().accept(buildContentTreePanel());
de.blight.editor.ui.ItemEditorView view = de.blight.editor.ui.ItemEditorView view =
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
wireItemView(view); wireItemView(view);
@@ -960,7 +961,7 @@ public class ContentController {
private void wireItemView(de.blight.editor.ui.ItemEditorView view) { private void wireItemView(de.blight.editor.ui.ItemEditorView view) {
view.setOnIdChanged(this::refreshContentItemTree); view.setOnIdChanged(this::refreshContentItemTree);
view.setGripEditorLauncher((modelPath, currentGrip, onSave) -> { view.setGripEditorLauncher((modelPath, currentGrip, isShield, onSave) -> {
String itemId = view.getCurrentItemId(); String itemId = view.getCurrentItemId();
gripEditorController.setOnReturn(() -> { gripEditorController.setOnReturn(() -> {
if (dualWindowMode) { if (dualWindowMode) {
@@ -970,7 +971,7 @@ public class ContentController {
switchToItemEditor(itemId); 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 Consumer<ItemGrip> pendingOnSave;
private Runnable onReturn; private Runnable onReturn;
private boolean suppressSpinnerEvents = false; private boolean suppressSpinnerEvents = false;
private boolean shieldMode = false;
public void setOnReturn(Runnable r) { this.onReturn = r; } public void setOnReturn(Runnable r) { this.onReturn = r; }
@@ -37,11 +38,13 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
// ── GripEditorLauncher ──────────────────────────────────────────────────── // ── GripEditorLauncher ────────────────────────────────────────────────────
@Override @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; pendingOnSave = onSave;
shieldMode = isShield;
SharedInput input = ctx.input(); SharedInput input = ctx.input();
input.gripEditorActive = true; input.gripEditorActive = true;
input.gripEditorIsShield = isShield;
// Grip-Werte aus Item übernehmen (oder Defaults) // Grip-Werte aus Item übernehmen (oder Defaults)
ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip(); ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip();
@@ -63,7 +66,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
ctx.setCenterView().accept(ctx.viewportSupplier().get()); ctx.setCenterView().accept(ctx.viewportSupplier().get());
ctx.setCurrentTool().accept("gripEditor"); ctx.setCurrentTool().accept("gripEditor");
ctx.setMainToolbar().accept(buildToolbar()); ctx.setMainToolbar().accept(buildToolbar());
ctx.setRightPanel().accept(buildPanel(g)); ctx.setRightPanel().accept(buildPanel(g, isShield));
ctx.setLeftPanel().accept(null); ctx.setLeftPanel().accept(null);
ctx.setBottomPanel().accept(null); ctx.setBottomPanel().accept(null);
} }
@@ -82,7 +85,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
// ── Panel ───────────────────────────────────────────────────────────────── // ── Panel ─────────────────────────────────────────────────────────────────
private VBox buildPanel(ItemGrip g) { private VBox buildPanel(ItemGrip g, boolean isShield) {
posXSpin = doubleSpin(g.getPosX(), -2.0, 2.0, 0.01, "m"); posXSpin = doubleSpin(g.getPosX(), -2.0, 2.0, 0.01, "m");
posYSpin = doubleSpin(g.getPosY(), -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"); 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("Y:", rotYSpin),
row("Z:", rotZSpin), row("Z:", rotZSpin),
new Separator(), 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(), new Separator(),
saveBtn, saveBtn,
cancelBtn cancelBtn
@@ -156,7 +161,9 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
grip.setRotY(rotYSpin.getValue().floatValue()); grip.setRotY(rotYSpin.getValue().floatValue());
grip.setRotZ(rotZSpin.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; Consumer<ItemGrip> save = pendingOnSave;
Runnable ret = onReturn; Runnable ret = onReturn;
@@ -168,7 +175,8 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
} }
private void onCancel() { private void onCancel() {
ctx.input().gripEditorActive = false; ctx.input().gripEditorActive = false;
ctx.input().gripEditorIsShield = false;
Runnable ret = onReturn; Runnable ret = onReturn;
pendingOnSave = null; pendingOnSave = null;

View File

@@ -386,7 +386,8 @@ public class ViewportController {
private boolean isObjectMode() { private boolean isObjectMode() {
return input.activeLayer == SharedInput.LAYER_OBJECTS return input.activeLayer == SharedInput.LAYER_OBJECTS
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT || 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() { private void startEditTimer() {

View File

@@ -5,8 +5,10 @@ import de.blight.editor.SharedInput;
import de.blight.editor.ToolContext; import de.blight.editor.ToolContext;
import de.blight.editor.toolset.EditorUiHelper; import de.blight.editor.toolset.EditorUiHelper;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.util.StringConverter;
/** /**
* Owns the world-zone side panels: * Owns the world-zone side panels:
@@ -41,15 +43,96 @@ public class WorldPanelController {
auswahlDynamicContent.getChildren().add(noSel); auswahlDynamicContent.getChildren().add(noSel);
return; return;
} }
// Format: "idx|itemId|x|y|z|rotX|rotY|rotZ"
String[] parts = info.split("\\|"); String[] parts = info.split("\\|");
String itemId = parts.length > 1 ? parts[1] : "?"; 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); Label idLabel = new Label("Item: " + itemId);
idLabel.setStyle("-fx-font-weight: bold;"); 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"); Button deleteBtn = new Button("Löschen");
deleteBtn.setMaxWidth(Double.MAX_VALUE); deleteBtn.setMaxWidth(Double.MAX_VALUE);
deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;"); deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;");
deleteBtn.setOnAction(e -> ctx.input().deleteSelectedItemRequested = true); deleteBtn.setOnAction(e -> input.deleteSelectedItemRequested = true);
auswahlDynamicContent.getChildren().addAll(idLabel, deleteBtn);
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) { public void updateZonenPanel(String kind) {
@@ -321,6 +404,108 @@ public class WorldPanelController {
return panel; 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() { public VBox buildWegePanel() {
VBox inner = new VBox(8); VBox inner = new VBox(8);
inner.setPadding(new Insets(10)); inner.setPadding(new Insets(10));

View File

@@ -14,7 +14,6 @@ import javafx.scene.layout.*;
import javafx.util.Duration; import javafx.util.Duration;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*; import java.nio.file.*;
public class AreaEditorView extends BorderPane { public class AreaEditorView extends BorderPane {
@@ -234,50 +233,11 @@ public class AreaEditorView extends BorderPane {
// ── Localisation defaults ────────────────────────────────────────────────── // ── Localisation defaults ──────────────────────────────────────────────────
private void writeDefaultLocalisation(String fullId) { private static void writeDefaultLocalisation(String fullId) {
if (fullId == null || fullId.isBlank()) return; if (fullId == null || fullId.isBlank()) return;
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId; String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
if (shortId.isBlank()) return; if (shortId.isBlank()) return;
String key = fullId + ".name"; LangDefaults.writeOrUpdate(fullId + ".name", EditorNaming.toDisplayName(shortId));
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);
} }
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────

View File

@@ -48,6 +48,7 @@ public class ItemEditorView extends BorderPane {
private Spinner<Integer> goldSpinner; private Spinner<Integer> goldSpinner;
private Spinner<Integer> damageSpinner; private Spinner<Integer> damageSpinner;
private VBox weaponSection; private VBox weaponSection;
private VBox shieldSection;
private TextField modelRefField; private TextField modelRefField;
private VBox formContainer; private VBox formContainer;
private CheckBox consumableCheck; private CheckBox consumableCheck;
@@ -55,7 +56,7 @@ public class ItemEditorView extends BorderPane {
private VBox effectsRows; private VBox effectsRows;
public interface GripEditorLauncher { 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; private Runnable onIdChanged;
@@ -156,7 +157,6 @@ public class ItemEditorView extends BorderPane {
if (wasFocused && !isFocused) onIdCommitted(); if (wasFocused && !isFocused) onIdCommitted();
}); });
idField.setOnAction(e -> onIdCommitted()); idField.setOnAction(e -> onIdCommitted());
idField.textProperty().addListener((obs, o, n) -> scheduleSave());
catCombo = new ComboBox<>(); catCombo = new ComboBox<>();
catCombo.getItems().addAll(ItemCategory.values()); catCombo.getItems().addAll(ItemCategory.values());
@@ -246,6 +246,20 @@ public class ItemEditorView extends BorderPane {
weaponSection.setVisible(false); weaponSection.setVisible(false);
weaponSection.setManaged(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 = new TextField();
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)"); modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
modelRefField.setEditable(false); modelRefField.setEditable(false);
@@ -300,6 +314,7 @@ public class ItemEditorView extends BorderPane {
row("Wert (Gold):", goldSpinner), row("Wert (Gold):", goldSpinner),
row("Modell:", modelFullRow), row("Modell:", modelFullRow),
weaponSection, weaponSection,
shieldSection,
new Separator(), new Separator(),
consumableCheck, consumableCheck,
consumablesSection consumablesSection
@@ -310,7 +325,16 @@ public class ItemEditorView extends BorderPane {
private void openGripEditor() { private void openGripEditor() {
if (current == null || gripEditorLauncher == null) return; if (current == null || gripEditorLauncher == null) return;
String modelPath = modelRefField.getText().trim(); 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); current.setGrip(grip);
persistCurrent(); persistCurrent();
}); });
@@ -319,10 +343,12 @@ public class ItemEditorView extends BorderPane {
private void updateWeaponSectionVisibility() { private void updateWeaponSectionVisibility() {
ItemCategory cat = catCombo.getValue(); ItemCategory cat = catCombo.getValue();
ItemSubCategory sub = subCatCombo.getValue(); ItemSubCategory sub = subCatCombo.getValue();
boolean isWeapon = cat == ItemCategory.WEAPON boolean isShield = cat == ItemCategory.WEAPON && sub == ItemSubCategory.SHIELD;
&& sub != ItemSubCategory.SHIELD; boolean isWeapon = cat == ItemCategory.WEAPON && !isShield;
weaponSection.setVisible(isWeapon); weaponSection.setVisible(isWeapon);
weaponSection.setManaged(isWeapon); weaponSection.setManaged(isWeapon);
shieldSection.setVisible(isShield);
shieldSection.setManaged(isShield);
} }
// ── Modell-Auswahl ──────────────────────────────────────────────────────── // ── Modell-Auswahl ────────────────────────────────────────────────────────
@@ -333,14 +359,29 @@ public class ItemEditorView extends BorderPane {
} }
private void onIdCommitted() { private void onIdCommitted() {
String id = idField.getText().trim(); String newId = idField.getText().trim();
if (id.isBlank()) return; if (newId.isBlank()) return;
nameLabel.setText(id + ".name"); nameLabel.setText(newId + ".name");
descLabel.setText(id + ".description"); descLabel.setText(newId + ".description");
scheduleSave(); // 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(); 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 ───────────────────────────────────────────────────────────── // ── Auto-save ─────────────────────────────────────────────────────────────
private void scheduleSave() { 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.LocationIO;
import de.blight.common.model.Location; import de.blight.common.model.Location;
import de.blight.common.model.TextReference; import de.blight.common.model.TextReference;
import de.blight.editor.ProjectRoot;
import javafx.animation.PauseTransition; import javafx.animation.PauseTransition;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@@ -15,8 +14,6 @@ import javafx.scene.layout.*;
import javafx.util.Duration; import javafx.util.Duration;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List; import java.util.List;
public class LocationEditorView extends BorderPane { public class LocationEditorView extends BorderPane {
@@ -194,50 +191,11 @@ public class LocationEditorView extends BorderPane {
// ── Localisation defaults ────────────────────────────────────────────────── // ── Localisation defaults ──────────────────────────────────────────────────
private void writeDefaultLocalisation(String fullId) { private static void writeDefaultLocalisation(String fullId) {
if (fullId == null || fullId.isBlank()) return; if (fullId == null || fullId.isBlank()) return;
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId; String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
if (shortId.isBlank()) return; if (shortId.isBlank()) return;
String key = fullId + ".name"; LangDefaults.writeOrUpdate(fullId + ".name", EditorNaming.toDisplayName(shortId));
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);
} }
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────

View File

@@ -38,12 +38,36 @@ public class AnimPlayer {
private LinearBlendSpace blendSpace; private LinearBlendSpace blendSpace;
private String pendingClip; private String pendingClip;
private float blendElapsed; private float blendElapsed;
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) { public AnimPlayer(AnimComposer composer, SkinningControl skinning) {
this.composer = composer; this.composer = composer;
this.skinning = skinning; 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).
*
* @param startTimeOffset Clip startet ab dieser Zeit statt bei 0; 0 = von Anfang
*/
public boolean play(String clipName, float transition, float startTimeOffset) {
this.pendingStartOffset = startTimeOffset;
return play(clipName, transition);
}
/** /**
* Spielt einen Clip ab. * Spielt einen Clip ab.
* Bei laufendem Clip und {@code transition > 0}: nahtloser Übergang * Bei laufendem Clip und {@code transition > 0}: nahtloser Übergang
@@ -82,6 +106,7 @@ public class AnimPlayer {
blendSpace = space; blendSpace = space;
pendingClip = clipName; pendingClip = clipName;
blendElapsed = 0f; blendElapsed = 0f;
blendDuration = Math.max(0.01f, transition);
currentAction = blend; currentAction = blend;
currentClip = null; currentClip = null;
log.debug("[AnimPlayer] Blend → '{}'", clipName); log.debug("[AnimPlayer] Blend → '{}'", clipName);
@@ -101,7 +126,7 @@ public class AnimPlayer {
public void update(float tpf) { public void update(float tpf) {
if (pendingClip == null || blendSpace == null) return; if (pendingClip == null || blendSpace == null) return;
blendElapsed += tpf; blendElapsed += tpf;
float bt = Math.min(1f, blendElapsed / BLEND_DURATION); float bt = Math.min(1f, blendElapsed / blendDuration);
blendSpace.setValue(bt); blendSpace.setValue(bt);
if (bt >= 1f) finishBlend(); if (bt >= 1f) finishBlend();
} }
@@ -129,13 +154,40 @@ public class AnimPlayer {
private boolean directPlay(String clipName) { private boolean directPlay(String clipName) {
try { try {
Action act = composer.action(clipName); // Diagnose: zeigt warum length=0 entsteht
if (act instanceof BlendableAction ba) ba.setTransitionLength(0.0); com.jme3.anim.AnimClip rawClip = composer.getAnimClip(clipName);
if (rawClip != null) {
com.jme3.anim.AnimTrack[] rawTracks = rawClip.getTracks();
if (rawTracks != null && rawTracks.length > 0 && rawTracks[0] instanceof com.jme3.anim.TransformTrack tt) {
float[] times = tt.getTimes();
log.info("[AnimDiag] '{}' clipLen={}s tracks={} t0Len={}s times=[{}..{}] n={}",
clipName, rawClip.getLength(), rawTracks.length, tt.getLength(),
times != null && times.length > 0 ? times[0] : -1,
times != null && times.length > 0 ? times[times.length-1] : -1,
times != null ? times.length : 0);
} else {
log.info("[AnimDiag] '{}' clipLen={}s tracks={}", clipName, rawClip.getLength(),
rawTracks != null ? rawTracks.length : -1);
}
} else {
log.info("[AnimDiag] '{}' → clip NOT in animClipMap!", clipName);
}
currentAction = composer.setCurrentAction(clipName); currentAction = composer.setCurrentAction(clipName);
currentClip = clipName; currentClip = clipName;
pendingClip = null; pendingClip = null;
blendSpace = null; blendSpace = null;
log.debug("[AnimPlayer] direktPlay → '{}'", clipName); double len = currentAction != null ? currentAction.getLength() : -1;
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; return currentAction != null;
} catch (Exception e) { } catch (Exception e) {
log.warn("[AnimPlayer] direktPlay('{}') fehlgeschlagen: {}", clipName, e.getMessage()); log.warn("[AnimPlayer] direktPlay('{}') fehlgeschlagen: {}", clipName, e.getMessage());
@@ -153,16 +205,20 @@ public class AnimPlayer {
if (composer.hasAction("__blend__")) composer.removeAction("__blend__"); if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
} catch (Exception ignored) {} } catch (Exception ignored) {}
try { try {
Action next = composer.action(clip);
if (next instanceof BlendableAction ba) ba.setTransitionLength(0.0);
currentAction = composer.setCurrentAction(clip); currentAction = composer.setCurrentAction(clip);
currentClip = clip; currentClip = clip;
double len = currentAction != null ? currentAction.getLength() : 0.0; double len = currentAction != null ? currentAction.getLength() : 0.0;
// AnimLayer.setTime: kein extra controlUpdate, kein loop=false-Seiteneffekt. if (pendingReversed && currentAction != null && len > 0.01) {
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung). currentAction.setSpeed(-1f);
if (len > 0.01 && elapsed > 0) { composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len);
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(elapsed % len); pendingReversed = false;
} else if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) {
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
float t = (pendingStartOffset + elapsed) % (float) len;
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t);
pendingStartOffset = 0f;
} }
pendingReversed = false;
log.debug("[AnimPlayer] finishBlend → '{}'", clip); log.debug("[AnimPlayer] finishBlend → '{}'", clip);
} catch (Exception e) { } catch (Exception e) {
log.warn("[AnimPlayer] finishBlend('{}') fehlgeschlagen: {}", clip, e.getMessage()); log.warn("[AnimPlayer] finishBlend('{}') fehlgeschlagen: {}", clip, e.getMessage());

View File

@@ -7,8 +7,10 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* Beschreibt ein Animations-Set: eine Liste von Clip-Namen sowie die * Beschreibt ein Animations-Set: eine Liste von Clip-Namen sowie die
@@ -32,6 +34,8 @@ public class AnimSet {
private Map<String, SubClipDef> subClips = new LinkedHashMap<>(); private Map<String, SubClipDef> subClips = new LinkedHashMap<>();
/** Ablauf-Definitionen: Name → geordnete Liste von Aktions-Namen (aus actionMap). */ /** Ablauf-Definitionen: Name → geordnete Liste von Aktions-Namen (aus actionMap). */
private Map<String, List<String>> sequences = new LinkedHashMap<>(); private Map<String, List<String>> sequences = new LinkedHashMap<>();
/** Clip-Namen die snapRootBoneXZ erhalten sollen (Root-Motion entfernen, Bewegung auf der Stelle). */
private Set<String> snapClips = new LinkedHashSet<>();
/** Beschreibt einen Sub-Clip: Zeitfenster [start, end] innerhalb eines kombinierten Quell-Clips. */ /** Beschreibt einen Sub-Clip: Zeitfenster [start, end] innerhalb eines kombinierten Quell-Clips. */
public static class SubClipDef { public static class SubClipDef {
@@ -60,6 +64,10 @@ public class AnimSet {
return sequences != null ? sequences : new LinkedHashMap<>(); return sequences != null ? sequences : new LinkedHashMap<>();
} }
public void setSequences(Map<String, List<String>> sequences) { this.sequences = sequences; } public void setSequences(Map<String, List<String>> sequences) { this.sequences = sequences; }
public Set<String> getSnapClips() {
return snapClips != null ? snapClips : new LinkedHashSet<>();
}
public void setSnapClips(Set<String> snapClips) { this.snapClips = snapClips; }
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */ /** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
public void save(Path setDir, String setName) throws IOException { public void save(Path setDir, String setName) throws IOException {

View File

@@ -34,18 +34,21 @@ public enum AnimationAction {
// ── Schwert ─────────────────────────────────────────────────────────────── // ── Schwert ───────────────────────────────────────────────────────────────
SWORD_ATTACK_1, SWORD_ATTACK_1,
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN, SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK,
SWORD_ATTACK_2, SWORD_HEAVY_ATTACK, 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, SWORD_IDLE, SWORD_WALK, SWORD_RUN,
// ── Axt ─────────────────────────────────────────────────────────────────── // ── Axt ───────────────────────────────────────────────────────────────────
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_HIT_TAKEN, AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
AXE_ATTACK_1, AXE_ATTACK_2, AXE_HEAVY_ATTACK, 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, AXE_IDLE, AXE_WALK, AXE_RUN,
// ── Zweihänder ──────────────────────────────────────────────────────────── // ── 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_HEAVY_ATTACK, 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, TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
// ── Bogen ───────────────────────────────────────────────────────────────── // ── Bogen ─────────────────────────────────────────────────────────────────
@@ -96,32 +99,44 @@ public enum AnimationAction {
case SWORD_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Schwert)"); case SWORD_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Schwert)");
case SWORD_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Schwert)"); case SWORD_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Schwert)");
case SWORD_BLOCK -> TextRegistry.resolve(null, key, "Block (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_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)");
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)"); case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (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_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)");
case SWORD_RUN -> TextRegistry.resolve(null, key, "Laufen (Schwert)"); case SWORD_RUN -> TextRegistry.resolve(null, key, "Laufen (Schwert)");
case AXE_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Axt)"); case AXE_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Axt)");
case AXE_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Axt)"); case AXE_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Axt)");
case AXE_BLOCK -> TextRegistry.resolve(null, key, "Block (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_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Axt)");
case AXE_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Axt)"); case AXE_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Axt)");
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)"); case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)");
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (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_WALK -> TextRegistry.resolve(null, key, "Gehen (Axt)");
case AXE_RUN -> TextRegistry.resolve(null, key, "Laufen (Axt)"); case AXE_RUN -> TextRegistry.resolve(null, key, "Laufen (Axt)");
case TWO_HAND_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Zweihänder)"); 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_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Zweihänder)");
case TWO_HAND_BLOCK -> TextRegistry.resolve(null, key, "Block (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_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_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Zweihänder)");
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)"); case TWO_HAND_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Zweihänder)");
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (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_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)");
case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)"); case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)");

View File

@@ -43,10 +43,6 @@ public class AnimationLibrary extends BaseAppState {
/** Clips die snapRootBoneXZ erhalten (Locomotion: Walk, Run, Sprint, …). */ /** Clips die snapRootBoneXZ erhalten (Locomotion: Walk, Run, Sprint, …). */
private final Set<String> snapClipNames = new LinkedHashSet<>(); private final Set<String> snapClipNames = new LinkedHashSet<>();
/** Standard-Locomotion-Aktionen, deren gemappte Clips snapRootBoneXZ erhalten. */
private static final Set<String> LOCOMOTION_ACTIONS = Set.of(
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT"
);
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
@@ -103,6 +99,14 @@ public class AnimationLibrary extends BaseAppState {
if (ac.getAnimClip(clipName) != null) return true; // bereits vorhanden if (ac.getAnimClip(clipName) != null) return true; // bereits vorhanden
{
com.jme3.anim.AnimTrack<?>[] st = src.getTracks();
int sn0 = 0;
if (st != null && st.length > 0 && st[0] instanceof TransformTrack stt)
sn0 = stt.getTimes() != null ? stt.getTimes().length : 0;
log.info("[AnimLib] applyTo pre-retarget '{}': srcLen={}s srcTracks={} srcT0Frames={}",
clipName, src.getLength(), st != null ? st.length : 0, sn0);
}
AnimClip target; AnimClip target;
if (srcArm != null && srcArm != sc.getArmature()) { if (srcArm != null && srcArm != sc.getArmature()) {
// Immer retarget() aufrufen – auch bei gleichen Knochen-Namen. // Immer retarget() aufrufen – auch bei gleichen Knochen-Namen.
@@ -131,8 +135,10 @@ public class AnimationLibrary extends BaseAppState {
} }
ac.addAnimClip(target); ac.addAnimClip(target);
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName()); int trackCount = target.getTracks() != null ? target.getTracks().length : 0;
if (clipName.equals("sit_down")) { log.info("[AnimLib] Clip '{}' → AnimComposer '{}': length={}s tracks={}",
clipName, model.getName(), target.getLength(), trackCount);
if (clipName.equals("sit_down") || target.getLength() == 0) {
dumpClipTracks(target); dumpClipTracks(target);
} }
return true; return true;
@@ -221,10 +227,8 @@ public class AnimationLibrary extends BaseAppState {
snapClipNames.clear(); snapClipNames.clear();
try { try {
AnimSet set = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName); AnimSet set = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName);
set.getActionMap().forEach((action, clip) -> { snapClipNames.addAll(set.getSnapClips());
if (LOCOMOTION_ACTIONS.contains(action)) snapClipNames.add(clip); log.info("[AnimLib] snapRootBoneXZ für Clips: {}", snapClipNames);
});
log.info("[AnimLib] snapRootBoneXZ für Locomotion-Clips: {}", snapClipNames);
} catch (Exception e) { } catch (Exception e) {
log.warn("[AnimLib] AnimSet '{}' nicht ladbar für Locomotion-Snap: {}", animSetName, e.getMessage()); log.warn("[AnimLib] AnimSet '{}' nicht ladbar für Locomotion-Snap: {}", animSetName, e.getMessage());
} }
@@ -320,9 +324,9 @@ public class AnimationLibrary extends BaseAppState {
} }
private void loadClipFromFile(Path file) { private void loadClipFromFile(Path file) {
String fileName = file.getFileName().toString(); String fileName = file.getFileName().toString();
String clipName = fileName.replaceFirst("\\.j3o$", ""); String fileClipKey = fileName.replaceFirst("\\.j3o$", "");
String assetKey = "Characters/clips/" + fileName; String assetKey = "Characters/clips/" + fileName;
try { try {
Spatial loaded = assetManager.loadModel(assetKey); Spatial loaded = assetManager.loadModel(assetKey);
@@ -334,11 +338,37 @@ public class AnimationLibrary extends BaseAppState {
} }
Armature armature = sc != null ? sc.getArmature() : null; Armature armature = sc != null ? sc.getArmature() : null;
AnimClip bestClip = null;
for (String name : ac.getAnimClipsNames()) { for (String name : ac.getAnimClipsNames()) {
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name); com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
clips.put(name, animClip); clips.put(name, animClip);
if (armature != null) armatures.put(name, armature); if (armature != null) armatures.put(name, armature);
log.info("[AnimLib] Clip geladen: '{}' aus {}", name, assetKey); com.jme3.anim.AnimTrack<?>[] rawTracks = animClip.getTracks();
int n0 = 0;
if (rawTracks != null && rawTracks.length > 0 && rawTracks[0] instanceof TransformTrack tt0) {
n0 = tt0.getTimes() != null ? tt0.getTimes().length : 0;
}
log.info("[AnimLib] Clip geladen: '{}' aus {} | clipLen={}s tracks={} t0Frames={}",
name, assetKey, animClip.getLength(),
rawTracks != null ? rawTracks.length : 0, n0);
if (bestClip == null || animClip.getLength() > bestClip.getLength()) {
bestClip = animClip;
}
}
// Kanonischen Dateiname-Key auf den längsten Clip mappen.
// Verhindert dass ein leerer Null-Frame-Clip einen guten Clip überschreibt,
// wenn die j3o mehrere Clips mit leicht unterschiedlichen Namen enthält
// (z.B. "clip_Sword And Shield Slash" vs. "Sword And Shield Slash").
if (bestClip != null) {
AnimClip current = clips.get(fileClipKey);
if (current == null || bestClip.getLength() > current.getLength()) {
clips.put(fileClipKey, bestClip);
if (armature != null) armatures.put(fileClipKey, armature);
if (!fileClipKey.equals(bestClip.getName())) {
log.info("[AnimLib] Key '{}' → '{}' (clipLen={}s, besser)",
fileClipKey, bestClip.getName(), bestClip.getLength());
}
}
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("[AnimLib] Fehler beim Laden von {}: {}", assetKey, e.getMessage()); log.warn("[AnimLib] Fehler beim Laden von {}: {}", assetKey, e.getMessage());

View File

@@ -81,11 +81,15 @@ public final class RetargetingSystem {
if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint j) if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint j)
srcTrackMap.put(j.getName(), tt); srcTrackMap.put(j.getName(), tt);
// Reference keyframe times // Reference keyframe times – pick the longest array (some bind-pose tracks have only 1 frame)
float[] times = null; float[] times = null;
for (String name : nameMap.keySet()) { for (String name : nameMap.keySet()) {
TransformTrack tt = srcTrackMap.get(name); TransformTrack tt = srcTrackMap.get(name);
if (tt != null) { times = tt.getTimes(); break; } if (tt != null && tt.getTimes() != null) {
if (times == null || tt.getTimes().length > times.length) {
times = tt.getTimes();
}
}
} }
if (times == null || times.length == 0) { if (times == null || times.length == 0) {
log.warn("[Retarget] Keine Keyframe-Zeiten für '{}'", sourceClip.getName()); log.warn("[Retarget] Keine Keyframe-Zeiten für '{}'", sourceClip.getName());
@@ -398,18 +402,10 @@ public final class RetargetingSystem {
// ── Same-rig detection & redirect ──────────────────────────────────────── // ── Same-rig detection & redirect ────────────────────────────────────────
/** True nur wenn alle Namen exakt selbst-gemappt UND alle Bind-Posen übereinstimmen. */ /** True wenn alle Namen exakt selbst-gemappt sind (gleiche Knochen-Namen → gleicher Rig-Typ). */
private static boolean isSameRig(Map<String, String> nameMap, Armature src, Armature dst) { private static boolean isSameRig(Map<String, String> nameMap, Armature src, Armature dst) {
for (var e : nameMap.entrySet()) { for (var e : nameMap.entrySet()) {
if (!e.getKey().equals(e.getValue())) return false; if (!e.getKey().equals(e.getValue())) return false;
Joint srcJ = src.getJoint(e.getKey());
Joint dstJ = dst.getJoint(e.getValue());
if (srcJ == null || dstJ == null) continue;
Quaternion sb = srcJ.getInitialTransform() != null
? srcJ.getInitialTransform().getRotation() : new Quaternion();
Quaternion db = dstJ.getInitialTransform() != null
? dstJ.getInitialTransform().getRotation() : new Quaternion();
if (Math.abs(sb.dot(db)) < 0.9999f) return false;
} }
return true; return true;
} }

View File

@@ -34,10 +34,20 @@ public class PlayerInputControl {
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class); private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
private static final float MOVE_SPEED = 0.07f; private static final float MOVE_SPEED = 0.07f;
private static final float SPRINT_MULT = 1.5f; private static final float SPRINT_MULT = 1.5f;
private static final float WALK_MULT = 0.5f; private static final float WALK_MULT = 0.5f;
private static final float ROTATE_SPEED = 10f; private static final float ROTATE_SPEED = 10f;
/** Kürzere Überblend-Zeit für Kampf-Animationen (Angriff fühlt sich "snappy" an). */
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
// bereits verdrahtet — nur die Konstanten reaktivieren und in handleCombatAttack() einsetzen.
private static final String ACT_DRAW_MELEE = "DrawMeleeWeapon"; private static final String ACT_DRAW_MELEE = "DrawMeleeWeapon";
private static final String ACT_DRAW_RANGED = "DrawRangedWeapon"; private static final String ACT_DRAW_RANGED = "DrawRangedWeapon";
@@ -59,16 +69,27 @@ public class PlayerInputControl {
private boolean forward, backward, left, right, sprint, walk; private boolean forward, backward, left, right, sprint, walk;
private boolean paused = false; private boolean paused = false;
private enum BlockPhase { NONE, WAITING, ENTERING, HOLDING, EXITING }
// ── Kampf-Zustand ───────────────────────────────────────────────────────── // ── Kampf-Zustand ─────────────────────────────────────────────────────────
private MainCharacter mainCharacter = null; private MainCharacter mainCharacter = null;
private boolean weaponDrawn = false; private boolean weaponDrawn = false;
private ItemSubCategory drawnWeaponType = null; private ItemSubCategory drawnWeaponType = null;
private boolean isAiming = false; private boolean isAiming = false;
private boolean meleeAttackToggle = false;
private boolean rightMouseHeld = false; private boolean rightMouseHeld = false;
private boolean combatAnimActive = false; private BlockPhase blockPhase = BlockPhase.NONE;
private float combatAnimRemaining = 0f; private float blockPhaseTimer = 0f;
private Runnable combatAnimCallback = null; 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. */
private boolean comboWindowActive = false;
private float comboWindowRemaining = 0f;
/** Letzter Angriff war ein kritischer Treffer (Kombo-Schritt 3). Für das Schadenssystem. */
private boolean lastAttackCritical = false;
private AnimationLibrary animLib; private AnimationLibrary animLib;
private String animSetName; private String animSetName;
@@ -121,6 +142,8 @@ public class PlayerInputControl {
private int jumpFrames = 0; private int jumpFrames = 0;
private int groundGraceFrames = 0; private int groundGraceFrames = 0;
private double nextTransitionLength = 0.0; private double nextTransitionLength = 0.0;
private float nextStartOffset = 0f;
private boolean nextPlayReversed = false;
private boolean pickupActive = false; private boolean pickupActive = false;
private float pickupRemaining = 0f; private float pickupRemaining = 0f;
@@ -241,6 +264,7 @@ public class PlayerInputControl {
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; } public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
public boolean isWeaponDrawn() { return weaponDrawn; } public boolean isWeaponDrawn() { return weaponDrawn; }
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; } public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
public boolean isLastAttackCritical() { return lastAttackCritical; }
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; } public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
public boolean isPaused() { return paused; } public boolean isPaused() { return paused; }
@@ -253,8 +277,11 @@ public class PlayerInputControl {
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
stopWading(); stopWading();
rightMouseHeld = false; rightMouseHeld = false;
blockPhase = BlockPhase.NONE;
combatAnimActive = false; combatAnimActive = false;
combatAnimCallback = null; combatAnimCallback = null;
comboStep = 0;
comboWindowActive = false;
} }
} }
@@ -657,20 +684,45 @@ public class PlayerInputControl {
// Kampf-Anim-Timer // Kampf-Anim-Timer
if (combatAnimActive) { if (combatAnimActive) {
combatAnimRemaining -= tpf; combatAnimRemaining -= tpf;
physicsChar.setWalkDirection(Vector3f.ZERO);
if (combatAnimRemaining <= 0f) { if (combatAnimRemaining <= 0f) {
combatAnimActive = false; combatAnimActive = false;
currentAnim = null; currentAnim = null;
nextTransitionLength = COMBAT_BLEND;
Runnable cb = combatAnimCallback; Runnable cb = combatAnimCallback;
combatAnimCallback = null; combatAnimCallback = null;
boolean skipWindow = combatAnimSkipComboWindow;
combatAnimSkipComboWindow = false;
if (cb != null) cb.run(); if (cb != null) cb.run();
if (!skipWindow && !combatAnimActive && isMeleeType(drawnWeaponType)) {
comboWindowActive = true;
comboWindowRemaining = COMBO_WINDOW;
log.info("[Combat] Kombo-Fenster geöffnet (step={} next={})", comboStep, comboStep);
}
} else {
pollFootsteps(tpf);
return;
} }
} }
// Kombo-Fenster: läuft nach einem Nahkampfangriff; verfällt es → Kombo-Reset
if (comboWindowActive) {
comboWindowRemaining -= tpf;
if (comboWindowRemaining <= 0f) {
comboWindowActive = false;
comboStep = 0;
log.info("[Combat] Kombo-Fenster abgelaufen → Reset");
}
}
updateBlockPhase(tpf);
// Animations-Auswahl // Animations-Auswahl
if (jumpFrames > 0) jumpFrames--; if (jumpFrames > 0) jumpFrames--;
if (groundGraceFrames > 0) groundGraceFrames--; if (groundGraceFrames > 0) groundGraceFrames--;
if (!combatAnimActive) { // Block-Phase verwaltet ihre eigenen Animationen.
if (!combatAnimActive && !isBlocking()) {
AnimationAction target; AnimationAction target;
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) { if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP; target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
@@ -684,10 +736,6 @@ public class PlayerInputControl {
target = resolveWeaponAction(target); target = resolveWeaponAction(target);
if (weaponDrawn && rightMouseHeld && isMeleeType(drawnWeaponType)) {
target = getMeleeBlockAction(drawnWeaponType);
}
if (target != currentAnim) { if (target != currentAnim) {
playAction(target); playAction(target);
currentAnim = target; currentAnim = target;
@@ -707,7 +755,8 @@ public class PlayerInputControl {
} }
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action); String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName); log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName);
if (runningClip != null) nextTransitionLength = 0.15; // Negativer Wert = Hard-Cut angefordert (kein Blend); 0 = noch nicht gesetzt → Default 0.15s
if (runningClip != null && nextTransitionLength == 0.0) nextTransitionLength = 0.15;
if (clip != null && tryPlay(clip)) return; if (clip != null && tryPlay(clip)) return;
if (action != AnimationAction.DEFAULT) { if (action != AnimationAction.DEFAULT) {
String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT); String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT);
@@ -755,6 +804,7 @@ public class PlayerInputControl {
if (drawnWeaponType == null) { weaponDrawn = false; return; } if (drawnWeaponType == null) { weaponDrawn = false; return; }
isAiming = false; isAiming = false;
rightMouseHeld = false; rightMouseHeld = false;
blockPhase = BlockPhase.NONE;
AnimationAction sheathAnim = getSheathAction(drawnWeaponType); AnimationAction sheathAnim = getSheathAction(drawnWeaponType);
float dur = resolveClipLength(sheathAnim, 0.5f); float dur = resolveClipLength(sheathAnim, 0.5f);
requestAnimation(sheathAnim, dur, () -> { requestAnimation(sheathAnim, dur, () -> {
@@ -765,16 +815,25 @@ public class PlayerInputControl {
} }
private void handleCombatAttack() { private void handleCombatAttack() {
if (!weaponDrawn || blockingAnimActive || combatAnimActive) return; if (!weaponDrawn || blockingAnimActive || combatAnimActive || isBlocking()) {
log.info("[Combat] BLOCKIERT: weaponDrawn={} blockingAnim={} combatAnim={} blocking={}",
weaponDrawn, blockingAnimActive, combatAnimActive, isBlocking());
return;
}
if (isMeleeType(drawnWeaponType)) { if (isMeleeType(drawnWeaponType)) {
AnimationAction atk = meleeAttackToggle int step = comboStep;
? getMeleeAttack2Action(drawnWeaponType) boolean isComboFollow = comboWindowActive; // war Kombo-Fenster offen?
: getMeleeAttack1Action(drawnWeaponType); AnimationAction atk = getMeleeComboAction(drawnWeaponType, step);
meleeAttackToggle = !meleeAttackToggle; lastAttackCritical = (step == 3);
comboStep = (step + 1) % 4;
comboWindowActive = false;
float dur = resolveClipLength(atk, 0.6f); float dur = resolveClipLength(atk, 0.6f);
// Kombo-Folgeschlag: kurzer Blend für sauberen Übergang; erster Schlag: Hard-Cut
nextTransitionLength = isComboFollow ? COMBAT_BLEND : -1.0;
log.info("[Combat] Kombo-Schritt {}: action={} dur={}s kritisch={}",
step, atk, dur, lastAttackCritical);
combatAnimActive = true; combatAnimActive = true;
// Blend-out zu Idle startet BLEND_DURATION früher → kein sichtbares Einfrieren am letzten Frame combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
combatAnimRemaining = Math.max(0.1f, dur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
combatAnimCallback = null; combatAnimCallback = null;
playAction(atk); playAction(atk);
currentAnim = atk; currentAnim = atk;
@@ -785,14 +844,16 @@ public class PlayerInputControl {
float fireDur = resolveClipLength(fireAnim, 0.5f); float fireDur = resolveClipLength(fireAnim, 0.5f);
float reloadDur = resolveClipLength(reloadAnim, 0.8f); float reloadDur = resolveClipLength(reloadAnim, 0.8f);
combatAnimActive = true; combatAnimActive = true;
combatAnimRemaining = Math.max(0.1f, fireDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION); combatAnimRemaining = Math.max(0.1f, fireDur - COMBAT_BLEND);
combatAnimCallback = () -> { combatAnimCallback = () -> {
combatAnimActive = true; combatAnimActive = true;
combatAnimRemaining = Math.max(0.1f, reloadDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION); combatAnimRemaining = Math.max(0.1f, reloadDur - COMBAT_BLEND);
combatAnimCallback = null; combatAnimCallback = null;
nextTransitionLength = COMBAT_BLEND;
playAction(reloadAnim); playAction(reloadAnim);
currentAnim = reloadAnim; currentAnim = reloadAnim;
}; };
nextTransitionLength = COMBAT_BLEND;
playAction(fireAnim); playAction(fireAnim);
currentAnim = fireAnim; currentAnim = fireAnim;
} }
@@ -801,12 +862,103 @@ public class PlayerInputControl {
private void handleCombatAlt(boolean isPressed) { private void handleCombatAlt(boolean isPressed) {
if (!weaponDrawn || blockingAnimActive) return; if (!weaponDrawn || blockingAnimActive) return;
if (isMeleeType(drawnWeaponType)) { 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) { } else if (isRangedType(drawnWeaponType) && isPressed) {
isAiming = !isAiming; 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) { private AnimationAction resolveWeaponAction(AnimationAction base) {
if (!weaponDrawn || drawnWeaponType == null) return base; if (!weaponDrawn || drawnWeaponType == null) return base;
return switch (base) { return switch (base) {
@@ -909,6 +1061,23 @@ public class PlayerInputControl {
}; };
} }
private static AnimationAction getMeleeAttack3Action(ItemSubCategory s) {
return switch (s) {
case AXE -> AnimationAction.AXE_ATTACK_3;
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_ATTACK_3;
default -> AnimationAction.SWORD_ATTACK_3;
};
}
/** Kombo-Sequenz: 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK3 (kritisch). */
private static AnimationAction getMeleeComboAction(ItemSubCategory type, int step) {
return switch (step) {
case 1 -> getMeleeAttack2Action(type);
case 3 -> getMeleeAttack3Action(type);
default -> getMeleeAttack1Action(type);
};
}
private static AnimationAction getMeleeBlockAction(ItemSubCategory s) { private static AnimationAction getMeleeBlockAction(ItemSubCategory s) {
return switch (s) { return switch (s) {
case AXE -> AnimationAction.AXE_BLOCK; case AXE -> AnimationAction.AXE_BLOCK;
@@ -917,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) { private static AnimationAction getRangedDrawAction(ItemSubCategory s) {
return switch (s) { return switch (s) {
case CROSSBOW -> AnimationAction.CROSSBOW_DRAW; case CROSSBOW -> AnimationAction.CROSSBOW_DRAW;
@@ -1185,10 +1386,17 @@ public class PlayerInputControl {
if (animPlayer == null) { if (animPlayer == null) {
animPlayer = new AnimPlayer(animComposer, skinningControl); animPlayer = new AnimPlayer(animComposer, skinningControl);
} }
float transLen = (float) nextTransitionLength; float transLen = (float) nextTransitionLength;
float startOff = nextStartOffset;
boolean reversed = nextPlayReversed;
nextTransitionLength = 0.0; nextTransitionLength = 0.0;
boolean ok = animPlayer.play(clip, transLen); nextStartOffset = 0f;
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) { if (ok) {
runningClip = animPlayer.getCurrentClip(); runningClip = animPlayer.getCurrentClip();
applyAnimOffset(clip); applyAnimOffset(clip);

View File

@@ -27,30 +27,54 @@ public class WeaponAttachmentControl extends AbstractControl {
private static final Logger log = LoggerFactory.getLogger(WeaponAttachmentControl.class); private static final Logger log = LoggerFactory.getLogger(WeaponAttachmentControl.class);
private static final String BONE_SPINE = "mixamorig:Spine2"; private static final String BONE_SPINE = "mixamorig:Spine2";
private static final String BONE_HAND = "mixamorig:RightHand"; 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 DEG_TO_RAD = (float) Math.PI / 180f;
private static final float HALF_PI = (float) Math.PI * 0.5f; private static final float HALF_PI = (float) Math.PI * 0.5f;
// Grip-Zielposition im Spine2-lokalen Raum (Meter): Griff ~40cm über dem Brustbein private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.05f, 0.40f, -0.08f); /** 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 AssetManager assetManager;
private final PlayerInputControl playerInput; private final PlayerInputControl playerInput;
private final MainCharacter mc; private final MainCharacter mc;
private com.jme3.anim.SkinningControl skinCtrl; private com.jme3.anim.SkinningControl skinCtrl;
private Node backNode; // Spine2-Attachment-Node (im skalierten Char-Modell) private Node backNode;
private Node handNode; // RightHand-Attachment-Node private Node handNode;
private Node palmNodeRef;
private int palmOffsetFrameDelay = 0;
// Bind-Pose-Rotation des RightHand-Knochens in Character-lokalem Raum (T-Pose).
// Wird einmalig beim Initialisieren aus dem InverseBindMatrix abgeleitet.
// Erlaubt, die Grip-Rotation aus dem Modell-Editor direkt im Spiel zu verwenden.
private Quaternion handBindRotInChar; private Quaternion handBindRotInChar;
// Offset Handgelenk → Fingerknöchel in Hand-Bone-Local-Space.
// Wird im 2. Frame aus den Runtime-Node-Positionen berechnet (skalierungs-unabhängig).
private Vector3f palmOffsetLocal;
private Item lastEquippedWeapon; private Item lastEquippedWeapon;
private boolean lastDrawnState = false; private boolean lastDrawnState = false;
private Spatial weaponSpatial; private Spatial weaponSpatial;
private boolean gripDiagLogged = false;
private Item lastEquippedShield;
private Spatial shieldSpatial;
private Node leftForeArmNode;
private Quaternion leftForeArmBindRotInChar;
public WeaponAttachmentControl(AssetManager assetManager, public WeaponAttachmentControl(AssetManager assetManager,
PlayerInputControl playerInput, PlayerInputControl playerInput,
@@ -62,44 +86,68 @@ public class WeaponAttachmentControl extends AbstractControl {
@Override @Override
protected void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
// Lazy init: SkinningControl erst nach setAnimationContext() verfügbar
if (skinCtrl == null) { if (skinCtrl == null) {
skinCtrl = playerInput.getSkinningControl(); skinCtrl = playerInput.getSkinningControl();
if (skinCtrl == null) return; if (skinCtrl == null) return;
try { try {
backNode = skinCtrl.getAttachmentsNode(BONE_SPINE); backNode = skinCtrl.getAttachmentsNode(BONE_SPINE);
log.info("[WeaponAttach] backNode ({}): {}", BONE_SPINE, backNode);
} catch (Exception e) { } catch (Exception e) {
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_SPINE, e.getMessage()); log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_SPINE, e.getMessage());
} }
try { try {
handNode = skinCtrl.getAttachmentsNode(BONE_HAND); handNode = skinCtrl.getAttachmentsNode(BONE_HAND);
log.info("[WeaponAttach] handNode ({}): {}", BONE_HAND, handNode);
} catch (Exception e) { } catch (Exception e) {
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_HAND, e.getMessage()); log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_HAND, e.getMessage());
} }
handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND); handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND);
log.info("[WeaponAttach] handBindRotInChar: {}", handBindRotInChar); try {
palmNodeRef = skinCtrl.getAttachmentsNode(BONE_PALM);
palmOffsetFrameDelay = 2; // SkinningControl muss mindestens 1x laufen
} 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);
}
// Palm-Offset verzögert berechnen (SkinningControl muss mindestens 1x gelaufen sein)
if (palmOffsetFrameDelay > 0 && palmNodeRef != null && handNode != null) {
palmOffsetFrameDelay--;
if (palmOffsetFrameDelay == 0) {
palmOffsetLocal = computePalmOffsetFromNodes(handNode, palmNodeRef);
log.info("[WeaponAttach] palmOffsetLocal (from nodes)={}", palmOffsetLocal);
}
} }
Item current = mc.getEquippedMeleeWeapon(); Item current = mc.getEquippedMeleeWeapon();
Item shield = mc.getEquippedShield();
boolean drawn = playerInput.isWeaponDrawn(); boolean drawn = playerInput.isWeaponDrawn();
if (current != lastEquippedWeapon) { if (current != lastEquippedWeapon) {
log.info("[WeaponAttach] Waffe geändert: {} → {}",
lastEquippedWeapon != null ? lastEquippedWeapon.getItemId() : "null",
current != null ? current.getItemId() : "null");
detachWeapon(); detachWeapon();
lastEquippedWeapon = current; lastEquippedWeapon = current;
if (current != null) { if (current != null) loadWeapon(current);
loadWeapon(current);
}
} }
if (weaponSpatial != null) { if (weaponSpatial != null) {
lastDrawnState = drawn; lastDrawnState = drawn;
updateWeaponTransform(drawn, current != null ? current.getGrip() : null); 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 ──────────────────────────────────────────────────────────────── // ── Laden ────────────────────────────────────────────────────────────────
@@ -111,12 +159,8 @@ public class WeaponAttachmentControl extends AbstractControl {
} }
try { try {
weaponSpatial = assetManager.loadModel(item.getModelRef().getPath()); weaponSpatial = assetManager.loadModel(item.getModelRef().getPath());
// Wichtig: Waffe direkt am character-Node hängen (ohne geerbten 0.02-Scale
// aus der Charakter-Modell-Hierarchie). Position/Rotation wird jedes Frame
// aus den Bone-World-Transforms berechnet.
((Node) spatial).attachChild(weaponSpatial); ((Node) spatial).attachChild(weaponSpatial);
log.info("[WeaponAttach] Waffe geladen und an character-Node gehängt: {}", log.info("[WeaponAttach] Waffe geladen: {}", item.getModelRef().getPath());
item.getModelRef().getPath());
} catch (Exception e) { } catch (Exception e) {
log.warn("[WeaponAttach] Waffenmodell nicht ladbar ({}): {}", log.warn("[WeaponAttach] Waffenmodell nicht ladbar ({}): {}",
item.getModelRef().getPath(), e.getMessage()); item.getModelRef().getPath(), e.getMessage());
@@ -130,31 +174,23 @@ public class WeaponAttachmentControl extends AbstractControl {
Node boneNode = drawn ? handNode : backNode; Node boneNode = drawn ? handNode : backNode;
if (boneNode == null) return; if (boneNode == null) return;
// Bone-World-Transform (korrekt, auch wenn Eltern skaliert sind)
Vector3f boneWorldPos = boneNode.getWorldTranslation();
Quaternion boneWorldRot = boneNode.getWorldRotation();
// In den lokalen Raum des character-Nodes transformieren
// (character hat in der Regel nur Translation + Rotation, keinen Scale)
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse(); Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
Vector3f boneInChar = charWorldRotInv.mult( Vector3f charWorldPos = spatial.getWorldTranslation();
boneWorldPos.subtract(spatial.getWorldTranslation()));
Quaternion boneRotInChar = charWorldRotInv.mult(boneWorldRot); Vector3f boneInChar = charWorldRotInv.mult(
boneNode.getWorldTranslation().subtract(charWorldPos));
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
if (drawn) { if (drawn) {
applyHandTransform(boneInChar, boneRotInChar, grip); Vector3f attachInChar = (palmOffsetLocal != null)
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
: boneInChar;
applyHandTransform(attachInChar, boneRotInChar, grip);
} else { } else {
applyBackTransform(boneInChar, boneRotInChar, grip); applyBackTransform(boneInChar, boneRotInChar, grip);
} }
} }
/**
* Waffe in der Hand: Griff-Punkt soll am Handknochen liegen, Rotation aus ItemGrip.
*
* Die Grip-Rotation ist im Character-/Welt-Raum definiert (wie im Grip-Editor sichtbar).
* Der Animations-Delta (Bewegung relativ zur Bind-Pose) wird zusätzlich aufaddiert,
* damit die Waffe den Handknochen durch alle Animationen folgt.
*/
private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
Quaternion gripRot = (grip != null) Quaternion gripRot = (grip != null)
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ()) ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ())
@@ -163,21 +199,65 @@ public class WeaponAttachmentControl extends AbstractControl {
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO; : Vector3f.ZERO;
// animDelta = wie weit der Knochen sich von der Bind-Pose bewegt hat.
// In der Bind-Pose (T-Pose / Idle) ist animDelta ≈ identity → weaponRot ≈ gripRot.
// Während Angriffsanimationen dreht animDelta die Waffe mit der Hand mit.
Quaternion animDelta = (handBindRotInChar != null) Quaternion animDelta = (handBindRotInChar != null)
? boneRotInChar.mult(handBindRotInChar.inverse()) ? boneRotInChar.mult(handBindRotInChar.inverse())
: boneRotInChar; : boneRotInChar;
Quaternion weaponRot = animDelta.mult(gripRot.inverse());
Quaternion weaponRot = animDelta.mult(gripRot); if (!gripDiagLogged && weaponSpatial != null) {
gripDiagLogged = true;
float[] wa = weaponRot.toAngles(null);
Vector3f bx = weaponRot.mult(Vector3f.UNIT_X);
Vector3f by = weaponRot.mult(Vector3f.UNIT_Y);
Vector3f bz = weaponRot.mult(Vector3f.UNIT_Z);
float[] ad = animDelta.toAngles(null);
float[] gr = gripRot.toAngles(null);
log.info("[GripDiag] animDelta(°): x={} y={} z={}",
Math.round(Math.toDegrees(ad[0])), Math.round(Math.toDegrees(ad[1])), Math.round(Math.toDegrees(ad[2])));
log.info("[GripDiag] gripRot(°): x={} y={} z={}",
Math.round(Math.toDegrees(gr[0])), Math.round(Math.toDegrees(gr[1])), Math.round(Math.toDegrees(gr[2])));
log.info("[GripDiag] weaponRot(°): x={} y={} z={}",
Math.round(Math.toDegrees(wa[0])), Math.round(Math.toDegrees(wa[1])), Math.round(Math.toDegrees(wa[2])));
log.info("[GripDiag] Waffe +X in CharSpace: ({},{},{})", round1(bx.x), round1(bx.y), round1(bx.z));
log.info("[GripDiag] Waffe +Y in CharSpace: ({},{},{})", round1(by.x), round1(by.y), round1(by.z));
log.info("[GripDiag] Waffe +Z in CharSpace: ({},{},{})", round1(bz.x), round1(bz.y), round1(bz.z));
// Vergleich RightHand vs RightHandMiddle1 – welcher Bone gibt die bessere Orientierung?
Quaternion charWorldRotInv2 = spatial.getWorldRotation().inverse();
if (palmNodeRef != null) {
Quaternion palmRotInChar = charWorldRotInv2.mult(palmNodeRef.getWorldRotation());
Quaternion palmBind = extractJointBindRot(skinCtrl, BONE_PALM);
Quaternion palmDelta = (palmBind != null) ? palmRotInChar.mult(palmBind.inverse()) : palmRotInChar;
float[] pd = palmDelta.toAngles(null);
Vector3f px = palmDelta.mult(Vector3f.UNIT_X);
Vector3f py = palmDelta.mult(Vector3f.UNIT_Y);
log.info("[GripDiag] palmDelta(°) [Middle1]: x={} y={} z={}",
Math.round(Math.toDegrees(pd[0])), Math.round(Math.toDegrees(pd[1])), Math.round(Math.toDegrees(pd[2])));
log.info("[GripDiag] palm +X in CharSpace: ({},{},{})", round1(px.x), round1(px.y), round1(px.z));
log.info("[GripDiag] palm +Y in CharSpace: ({},{},{})", round1(py.x), round1(py.y), round1(py.z));
}
}
// Waffenposition: Griff-Punkt liegt am Knochen-Ursprung Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos));
weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET));
weaponSpatial.setLocalRotation(weaponRot); weaponSpatial.setLocalRotation(weaponRot);
weaponSpatial.setLocalTranslation(bonePosInChar.subtract(weaponRot.mult(gripPos))); weaponSpatial.setLocalTranslation(weaponPos);
}
private static float round1(float v) { return Math.round(v * 10f) / 10f; }
/**
* Berechnet den Offset Handgelenk → Fingerknöchel in Hand-Bone-Local-Space
* direkt aus den World-Positionen der Attachment-Nodes.
* Dieser Ansatz umgeht alle InverseBindMatrix-Skalierungsprobleme.
* Muss aufgerufen werden NACHDEM SkinningControl mindestens einmal gelaufen ist.
*/
private static Vector3f computePalmOffsetFromNodes(Node handNode, Node palmNode) {
Vector3f deltaWorld = palmNode.getWorldTranslation().subtract(handNode.getWorldTranslation());
// Delta in Hand-Bone-Local-Space ausdrücken (rotiert mit der Hand mit)
Quaternion handWorldRotInv = handNode.getWorldRotation().inverse();
return handWorldRotInv.mult(deltaWorld);
} }
/** Liest die Bind-Pose-Rotation eines Knochens (Model-Raum ≈ Character-lokal) aus dem InverseBindMatrix. */
private static Quaternion extractJointBindRot(com.jme3.anim.SkinningControl sc, String jointName) { private static Quaternion extractJointBindRot(com.jme3.anim.SkinningControl sc, String jointName) {
com.jme3.anim.Armature arm = sc.getArmature(); com.jme3.anim.Armature arm = sc.getArmature();
if (arm == null) return null; if (arm == null) return null;
@@ -190,24 +270,82 @@ public class WeaponAttachmentControl extends AbstractControl {
return bindMat.toRotationQuat().normalizeLocal(); return bindMat.toRotationQuat().normalizeLocal();
} }
/**
* Waffe am Rücken: BACK_GRIP_OFFSET über Spine2, Klinge zeigt nach unten.
*/
private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
// fromAngles(0, 0, HALF_PI): Spine2-FBX-Flip kompensiert, Klinge zeigt nach unten Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
Quaternion weaponRot = boneRotInChar.mult(backRot); Quaternion weaponRot = boneRotInChar.mult(backRot);
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO;
weaponSpatial.setLocalRotation(weaponRot);
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
}
// Zielposition des Griff-Punktes: BACK_GRIP_OFFSET im Knochen-lokalen Raum // ── Schild ────────────────────────────────────────────────────────────────
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
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) Vector3f gripPos = (grip != null)
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
: Vector3f.ZERO; : 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)));
}
// weaponPos + weaponRot * gripPos = gripTarget private void applyShieldBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
weaponSpatial.setLocalRotation(weaponRot); // 180° um Y: Modell-+X → Spine-X (Griff nach links), Modell-+Z → Spine-−Z (flach am Rücken)
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos))); 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 ──────────────────────────────────────────────────────────────── // ── Detach ────────────────────────────────────────────────────────────────
@@ -220,6 +358,13 @@ public class WeaponAttachmentControl extends AbstractControl {
lastDrawnState = false; lastDrawnState = false;
} }
private void detachShield() {
if (shieldSpatial != null) {
shieldSpatial.removeFromParent();
shieldSpatial = null;
}
}
@Override @Override
protected void controlRender(RenderManager rm, ViewPort vp) {} 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) -> { private final ActionListener interactListener = (name, isPressed, tpf) -> {
if (!isPressed || phase != Phase.IDLE) return; if (!isPressed || phase != Phase.IDLE) return;
int idx = findNearestInRange(); if (hoveredIdx >= 0) startApproach(hoveredIdx);
if (idx >= 0) startApproach(idx);
}; };
private final ActionListener getUpListener = (name, isPressed, tpf) -> { 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()); 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 ──────────────────────────────────────────────────────────── // ── Annäherung ────────────────────────────────────────────────────────────

View File

@@ -153,6 +153,9 @@ public class WorldItemsState extends BaseAppState {
for (PlacedItem pi : items) { for (PlacedItem pi : items) {
Spatial s = buildVisual(pi); Spatial s = buildVisual(pi);
s.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z()); 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()); s.setUserData("itemId", pi.itemId());
itemsRoot.attachChild(s); itemsRoot.attachChild(s);
visuals.add(s); visuals.add(s);

View File

@@ -243,3 +243,7 @@ location.silas.name=Silas
location.silas_.name=Silas location.silas_.name=Silas
location.silas_haus.name=Silas Haus location.silas_haus.name=Silas Haus
location.silas_heim.name=Silas Heim 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_.name=Silas
location.silas_haus.name=Silas Haus location.silas_haus.name=Silas Haus
location.silas_heim.name=Silas Heim 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.description
dialog.silas.wer_seid_ihr.textmainchar dialog.silas.wer_seid_ihr.textmainchar
dialog.silas.wer_seid_ihr.textnpc dialog.silas.wer_seid_ihr.textnpc
einfacher_schild.description
einfacher_schild.name
erzmoss.description erzmoss.description
erzmoss.name erzmoss.name
hero.name hero.name
@@ -138,6 +140,10 @@ location.silas_
location.silas_haus location.silas_haus
location.silas_heim location.silas_heim
location.test 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.description
quest.blight_wuehler.name quest.blight_wuehler.name
quest.blight_wuehler.success quest.blight_wuehler.success

View File

@@ -1,2 +1,3 @@
# uuid itemId x y z # uuid itemId x y z rotX rotY rotZ
fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693 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