Compare commits
2 Commits
bdaeed056c
...
ef061b2726
| Author | SHA1 | Date | |
|---|---|---|---|
| ef061b2726 | |||
| 836b5d3d99 |
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Attack.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Attack.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Block Idle.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Block Idle.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Block Impact.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Block Impact.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Block.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Block.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Idle.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Idle.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Impact.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Impact.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Run.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Run.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Slash 2.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Slash 2.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Walk.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Walk.fbx
Normal file
Binary file not shown.
BIN
assets/imported/models/medieval round shield 3d model.glb
Normal file
BIN
assets/imported/models/medieval round shield 3d model.glb
Normal file
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,7 +9,17 @@
|
||||
"T-Pose",
|
||||
"Walking",
|
||||
"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": {
|
||||
"DEFAULT": "Idle",
|
||||
@@ -22,7 +32,16 @@
|
||||
"IDLE": "Idle",
|
||||
"JUMP": "Jumping",
|
||||
"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",
|
||||
"animOffsets": {
|
||||
@@ -67,5 +86,16 @@
|
||||
"SIT_UP",
|
||||
"IDLE"
|
||||
]
|
||||
}
|
||||
},
|
||||
"snapClips": [
|
||||
"Idle",
|
||||
"Walking",
|
||||
"Running",
|
||||
"Sprint",
|
||||
"Jumping",
|
||||
"Sword And Shield Idle",
|
||||
"Sword And Shield Run",
|
||||
"Sword And Shield Walk",
|
||||
"Sword And Shield Attack"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
#Tue Sep 15 12:31:37 CEST 2026
|
||||
#Sun Sep 20 15:31:32 CEST 2026
|
||||
attachedEmitters.count=0
|
||||
attachedLights.count=0
|
||||
attachedSounds.count=0
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"id": "blutagave.description"
|
||||
},
|
||||
"worthGold": 25,
|
||||
"damage": 0,
|
||||
"modelRef": {
|
||||
"path": "Models/plants/usable/blutagave.j3o"
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"id": "erzmoss.description"
|
||||
},
|
||||
"worthGold": 100,
|
||||
"damage": 0,
|
||||
"modelRef": {
|
||||
"path": "Models/plants/usable/erzmoss.j3o"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -11,12 +11,12 @@
|
||||
"worthGold": 0,
|
||||
"damage": 0,
|
||||
"grip": {
|
||||
"posX": 0.24,
|
||||
"posY": 0.05,
|
||||
"posX": 0.37,
|
||||
"posY": 0.08,
|
||||
"posZ": 0.0,
|
||||
"rotX": 0.0,
|
||||
"rotY": -90.0,
|
||||
"rotZ": 0.0
|
||||
"rotZ": 90.0
|
||||
},
|
||||
"modelRef": {
|
||||
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"
|
||||
|
||||
@@ -3,10 +3,15 @@ package de.blight.common;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Ein Item-Pickup das auf der Spielwelt liegt (Kartendaten). */
|
||||
public record PlacedItem(String uuid, String itemId, float x, float y, float z) {
|
||||
public record PlacedItem(String uuid, String itemId, float x, float y, float z, float rotX, float rotY, float rotZ) {
|
||||
|
||||
/** Erzeugt ein neues PlacedItem mit automatisch generierter UUID. */
|
||||
/** Erzeugt ein neues PlacedItem mit automatisch generierter UUID (alle Rotationen = 0). */
|
||||
public static PlacedItem create(String itemId, float x, float y, float z) {
|
||||
return new PlacedItem(UUID.randomUUID().toString(), itemId, x, y, z);
|
||||
return new PlacedItem(UUID.randomUUID().toString(), itemId, x, y, z, 0f, 0f, 0f);
|
||||
}
|
||||
|
||||
/** Gibt eine Kopie mit geänderter Position/Rotation zurück. */
|
||||
public PlacedItem withTransform(float nx, float ny, float nz, float nRotX, float nRotY, float nRotZ) {
|
||||
return new PlacedItem(uuid, itemId, nx, ny, nz, nRotX, nRotY, nRotZ);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,11 +25,11 @@ public final class PlacedItemIO {
|
||||
Path p = getPath();
|
||||
Files.createDirectories(p.getParent());
|
||||
try (BufferedWriter w = Files.newBufferedWriter(p)) {
|
||||
w.write("# uuid\titemId\tx\ty\tz");
|
||||
w.write("# uuid\titemId\tx\ty\tz\trotX\trotY\trotZ");
|
||||
w.newLine();
|
||||
for (PlacedItem it : items) {
|
||||
w.write(String.format(Locale.ROOT, "%s\t%s\t%.5f\t%.5f\t%.5f%n",
|
||||
it.uuid(), it.itemId(), it.x(), it.y(), it.z()));
|
||||
w.write(String.format(Locale.ROOT, "%s\t%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f%n",
|
||||
it.uuid(), it.itemId(), it.x(), it.y(), it.z(), it.rotX(), it.rotY(), it.rotZ()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,17 +43,29 @@ public final class PlacedItemIO {
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
String[] f = line.split("\t", -1);
|
||||
try {
|
||||
if (f.length >= 5) {
|
||||
// Aktuelles Format: uuid itemId x y z
|
||||
if (f.length >= 8) {
|
||||
// Aktuelles Format: uuid itemId x y z rotX rotY rotZ
|
||||
list.add(new PlacedItem(f[0], f[1],
|
||||
Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4])));
|
||||
Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]),
|
||||
Float.parseFloat(f[5]), Float.parseFloat(f[6]), Float.parseFloat(f[7])));
|
||||
} else if (f.length >= 6) {
|
||||
// Vorherige Version: uuid itemId x y z rotY (kein rotX/rotZ)
|
||||
list.add(new PlacedItem(f[0], f[1],
|
||||
Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]),
|
||||
0f, Float.parseFloat(f[5]), 0f));
|
||||
} else if (f.length >= 5) {
|
||||
// uuid itemId x y z (kein rot)
|
||||
list.add(new PlacedItem(f[0], f[1],
|
||||
Float.parseFloat(f[2]), Float.parseFloat(f[3]), Float.parseFloat(f[4]),
|
||||
0f, 0f, 0f));
|
||||
} else if (f.length == 4) {
|
||||
// Altes Format: itemId x y z → UUID deterministisch aus Inhalt ableiten
|
||||
String deterministicId = UUID.nameUUIDFromBytes(
|
||||
("legacy:" + f[0] + ":" + f[1] + ":" + f[2] + ":" + f[3])
|
||||
.getBytes(StandardCharsets.UTF_8)).toString();
|
||||
list.add(new PlacedItem(deterministicId, f[0],
|
||||
Float.parseFloat(f[1]), Float.parseFloat(f[2]), Float.parseFloat(f[3])));
|
||||
Float.parseFloat(f[1]), Float.parseFloat(f[2]), Float.parseFloat(f[3]),
|
||||
0f, 0f, 0f));
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class ItemCategoryManager {
|
||||
private static final Map<ItemCategory, List<ItemSubCategory>> mapping = new EnumMap<ItemCategory, List<ItemSubCategory>>(ItemCategory.class);
|
||||
|
||||
static {
|
||||
mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, BOW, CROSSBOW, MAGIC_CRYSTAL));
|
||||
mapping.put(WEAPON, List.of(SWORD, TWO_HANDED_SWORD, STAFF, HALBERD, AXE, BOW, CROSSBOW, MAGIC_CRYSTAL, SHIELD));
|
||||
mapping.put(GEAR, List.of(RING, NECKLACE, ARMOR, HELM, SHIELD));
|
||||
mapping.put(CONSUMABLES, List.of(POTION, PERMANENT_POTION, FOOD, MAGICAL_ITEM));
|
||||
mapping.put(QUEST_ITEMS, List.of(QUEST_ITEM));
|
||||
|
||||
@@ -204,19 +204,10 @@ public class EditorApp extends Application {
|
||||
|
||||
toolPanel = buildToolPanel();
|
||||
|
||||
topBar = topBarController.buildTop(toolCtx, new de.blight.editor.toolset.topbar.TopBarController.Deps(
|
||||
toolPanel,
|
||||
modelEditorController, assetController, animSetController,
|
||||
contentController, worldPanelController, voxelController,
|
||||
grassController, atmosphereController, waterController,
|
||||
gameController, terrainController, objectController,
|
||||
this::switchToVegetationGenerator, this::switchToEzTree,
|
||||
this::switchToTripo, this::openCharacterEditor,
|
||||
this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings
|
||||
));
|
||||
root.setTop(topBar);
|
||||
root.setRight(worldPanelController.buildAuswahlPanel());
|
||||
|
||||
// assetController muss VOR buildTop() erstellt werden, damit es korrekt in Deps landet.
|
||||
// Alle Abhängigkeiten (objectController, animPreviewController etc.) sind bereits verfügbar.
|
||||
// Die Buttons (atmosphaereBtn, wasserMergedBtn, zonenBtn) sind im TopBarController
|
||||
// als final-Felder vordefiniert und unabhängig von buildTop().
|
||||
assetController = new de.blight.editor.toolset.asset.AssetTreeController(toolCtx,
|
||||
new de.blight.editor.toolset.asset.AssetTreeController.Deps(
|
||||
objectController, animPreviewController, animSetController,
|
||||
@@ -230,6 +221,20 @@ public class EditorApp extends Application {
|
||||
terrainController::loadTexMeta, terrainController::saveTexMeta,
|
||||
() -> currentTool, r -> pendingAfterSave = r
|
||||
));
|
||||
|
||||
topBar = topBarController.buildTop(toolCtx, new de.blight.editor.toolset.topbar.TopBarController.Deps(
|
||||
toolPanel,
|
||||
modelEditorController, assetController, animSetController,
|
||||
contentController, worldPanelController, voxelController,
|
||||
grassController, atmosphereController, waterController,
|
||||
gameController, terrainController, objectController,
|
||||
this::switchToVegetationGenerator, this::switchToEzTree,
|
||||
this::switchToTripo, this::openCharacterEditor,
|
||||
this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings
|
||||
));
|
||||
root.setTop(topBar);
|
||||
root.setRight(worldPanelController.buildAuswahlPanel());
|
||||
|
||||
assetController.buildPanel();
|
||||
assetController.setupOverlay(centerStack);
|
||||
|
||||
@@ -430,8 +435,25 @@ public class EditorApp extends Application {
|
||||
worldPanelController.updateLocationZonePanel(input.selectedLocationZoneInfo);
|
||||
}
|
||||
|
||||
// Items haben höchste Priorität: ein selektiertes Item verdrängt
|
||||
// alle konkurrierenden Auswahl-Treffer desselben Klicks.
|
||||
if (input.itemSelectionChanged && input.selectedItemInfo != null
|
||||
&& (input.activeLayer == SharedInput.LAYER_AUSWAHL
|
||||
|| input.activeLayer == SharedInput.LAYER_ITEMS_EDIT)) {
|
||||
input.itemSelectionChanged = false;
|
||||
input.auswahlZoneSelected = false;
|
||||
input.auswahlLightSelected = false;
|
||||
input.auswahlEmitterSelected = false;
|
||||
input.auswahlSoundSelected = false;
|
||||
input.auswahlWaterSelected = false;
|
||||
input.auswahlWaterfallSelected = false;
|
||||
log.info("[Auswahl→Tool] Item ausgewaehlt → Info='{}'", input.selectedItemInfo);
|
||||
root.setRight(worldPanelController.buildAuswahlItemEditPanel(input.selectedItemInfo));
|
||||
}
|
||||
|
||||
if (input.auswahlZoneSelected) {
|
||||
input.auswahlZoneSelected = false;
|
||||
log.info("[Auswahl→Tool] Zone ausgewaehlt → LAYER_ZONEN");
|
||||
input.activeLayer = SharedInput.LAYER_ZONEN;
|
||||
topBarController.getZonenBtn().setSelected(true);
|
||||
root.setRight(worldPanelController.buildZonenPanel());
|
||||
@@ -439,6 +461,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlLightSelected) {
|
||||
input.auswahlLightSelected = false;
|
||||
log.info("[Auswahl→Tool] Licht ausgewaehlt → Atmo-Tab, Info='{}'", input.selectedLightInfo);
|
||||
topBarController.getAtmosphaereBtn().setSelected(true);
|
||||
root.setRight(atmosphereController.buildPanel());
|
||||
atmosphereController.updateLightPanel(input.selectedLightInfo);
|
||||
@@ -446,6 +469,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlEmitterSelected) {
|
||||
input.auswahlEmitterSelected = false;
|
||||
log.info("[Auswahl→Tool] Emitter ausgewaehlt → Atmo-Tab/LAYER_EMITTERS, Info='{}'", input.selectedEmitterInfo);
|
||||
topBarController.getAtmosphaereBtn().setSelected(true);
|
||||
input.activeLayer = SharedInput.LAYER_EMITTERS;
|
||||
root.setRight(atmosphereController.buildPanel());
|
||||
@@ -454,6 +478,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlSoundSelected) {
|
||||
input.auswahlSoundSelected = false;
|
||||
log.info("[Auswahl→Tool] Sound ausgewaehlt → Atmo-Tab/LAYER_PLACED_SOUNDS, Info='{}'", input.selectedSoundInfo);
|
||||
topBarController.getAtmosphaereBtn().setSelected(true);
|
||||
input.activeLayer = SharedInput.LAYER_PLACED_SOUNDS;
|
||||
root.setRight(atmosphereController.buildPanel());
|
||||
@@ -462,6 +487,7 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlWaterSelected) {
|
||||
input.auswahlWaterSelected = false;
|
||||
log.info("[Auswahl→Tool] Wasser ausgewaehlt → LAYER_WATER");
|
||||
topBarController.getWasserMergedBtn().setSelected(true);
|
||||
input.activeLayer = SharedInput.LAYER_WATER;
|
||||
root.setRight(waterController.buildPanel());
|
||||
@@ -470,15 +496,22 @@ public class EditorApp extends Application {
|
||||
|
||||
if (input.auswahlWaterfallSelected) {
|
||||
input.auswahlWaterfallSelected = false;
|
||||
log.info("[Auswahl→Tool] Wasserfall ausgewaehlt → LAYER_WATERFALL");
|
||||
topBarController.getWasserMergedBtn().setSelected(true);
|
||||
input.activeLayer = SharedInput.LAYER_WATERFALL;
|
||||
root.setRight(waterController.buildPanel());
|
||||
waterController.updateWaterfallPanel(input.selectedWaterfallInfo);
|
||||
}
|
||||
|
||||
// itemSelectionChanged mit null = Item abgewählt → Auswahl-Panel wiederherstellen,
|
||||
// aber nur wenn noch im Auswahl-Kontext (kein anderes Tool geöffnet)
|
||||
if (input.itemSelectionChanged) {
|
||||
input.itemSelectionChanged = false;
|
||||
worldPanelController.updateAuswahlItemPanel(input.selectedItemInfo);
|
||||
if (input.activeLayer == SharedInput.LAYER_AUSWAHL
|
||||
|| input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) {
|
||||
log.info("[Auswahl→Tool] Item abgewaehlt → Auswahl-Panel");
|
||||
root.setRight(worldPanelController.buildAuswahlPanel());
|
||||
}
|
||||
}
|
||||
|
||||
if (input.zonenSelectionChanged) {
|
||||
@@ -952,7 +985,9 @@ public class EditorApp extends Application {
|
||||
input.deleteEmitterRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_PLACED_SOUNDS && input.selectedSoundInfo != null)
|
||||
input.deleteSoundRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_ITEMS && input.selectedItemInfo != null)
|
||||
else if ((input.activeLayer == SharedInput.LAYER_ITEMS
|
||||
|| input.activeLayer == SharedInput.LAYER_ITEMS_EDIT)
|
||||
&& input.selectedItemInfo != null)
|
||||
input.deleteSelectedItemRequested = true;
|
||||
else if (input.activeLayer == SharedInput.LAYER_SOUND_AREAS) {
|
||||
if (input.zoneVertexIsSelected) { input.deleteZoneVertexRequested = true; scheduleAutoSave(); }
|
||||
|
||||
@@ -970,6 +970,9 @@ public class SharedInput {
|
||||
/** activeLayer==21 → Item-Pickup auf die Karte platzieren */
|
||||
public static final int LAYER_ITEMS = 21;
|
||||
|
||||
/** activeLayer==36 → ausgewähltes Item per Gizmo verschieben/rotieren */
|
||||
public static final int LAYER_ITEMS_EDIT = 36;
|
||||
|
||||
/** JFX → JME: itemId des zu platzierenden Items (aus .item-Datei). */
|
||||
public volatile String pendingItemId = null;
|
||||
|
||||
@@ -1178,6 +1181,8 @@ public class SharedInput {
|
||||
public volatile float gripEditorRotZ = 0f;
|
||||
/** Gesetzt vom JFX-Thread wenn Spinner geändert → JME baut Gizmo neu. */
|
||||
public volatile boolean gripEditorGizmoDirty = false;
|
||||
/** true → Gizmo ist ein 28cm-Unterarm-Pfeil (Schild); false → 15cm-Waffen-Pfeil. */
|
||||
public volatile boolean gripEditorIsShield = false;
|
||||
|
||||
/** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */
|
||||
public record SculptEdit(float screenX, float screenY, int action) {}
|
||||
@@ -1246,11 +1251,19 @@ public class SharedInput {
|
||||
public volatile boolean deselectAllOnAuswahl = false;
|
||||
/** JME3 → JavaFX: Zone wurde im Auswahl-Modus selektiert → Zonen-Werkzeug aktivieren. */
|
||||
public volatile boolean auswahlZoneSelected = false;
|
||||
/** JME3 → JavaFX: Item-Spawn im Auswahl-Modus selektiert. Format "idx|itemId|x|y|z" oder null. */
|
||||
/** JME3 → JavaFX: Item-Spawn im Auswahl-Modus selektiert. Format "idx|itemId|x|y|z|rotX|rotY|rotZ" oder null. */
|
||||
public volatile String selectedItemInfo = null;
|
||||
public volatile boolean itemSelectionChanged = false;
|
||||
/** JavaFX → JME3: Selektierten Item-Spawn löschen. */
|
||||
public volatile boolean deleteSelectedItemRequested = false;
|
||||
/** JavaFX → JME3: Position/Rotation des selektierten Items ändern. */
|
||||
public volatile boolean itemEditRequest = false;
|
||||
public volatile float itemEditX = 0f;
|
||||
public volatile float itemEditY = 0f;
|
||||
public volatile float itemEditZ = 0f;
|
||||
public volatile float itemEditRotX = 0f;
|
||||
public volatile float itemEditRotY = 0f;
|
||||
public volatile float itemEditRotZ = 0f;
|
||||
/** JME3-intern: Auswahl-Klick für ItemPlacementState (kein Objekt/keine Zone getroffen). */
|
||||
public final ConcurrentLinkedQueue<ObjectClick> auswahlClickQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3-intern: Auswahl-Klick für LightState (kein Objekt/keine Zone getroffen). */
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.blight.editor;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Verwaltet das aktive Werkzeug und leitet den AnimationTimer-Frame weiter.
|
||||
*
|
||||
@@ -8,6 +11,8 @@ package de.blight.editor;
|
||||
*/
|
||||
public class ToolRegistry {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ToolRegistry.class);
|
||||
|
||||
private ToolController active = null;
|
||||
|
||||
/**
|
||||
@@ -19,10 +24,12 @@ public class ToolRegistry {
|
||||
if (tool == active) {
|
||||
return;
|
||||
}
|
||||
String prev = active != null ? active.toolId() : "–";
|
||||
if (active != null) {
|
||||
active.deactivate();
|
||||
}
|
||||
active = tool;
|
||||
log.info("[Tool] {} → {}", prev, tool.toolId());
|
||||
tool.activate();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.math.Ray;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.editor.SharedInput;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Gemeinsame Basis für Zustandsklassen die Marker-Knoten verwalten und
|
||||
* Auswahl-Klicks via {@code auswahlXxxClickQueue} verarbeiten
|
||||
* (EmitterState, LightState, PlacedSoundState).
|
||||
*
|
||||
* Subklassen überschreiben {@link #onMarkerSelected(int)} um die eigene
|
||||
* Selektion auszuführen und das zugehörige SharedInput-Flag zu setzen.
|
||||
*/
|
||||
public abstract class AuswahlMarkerState extends BaseAppState {
|
||||
|
||||
protected final Logger log = LoggerFactory.getLogger(getClass());
|
||||
protected final SharedInput input;
|
||||
protected SimpleApplication app;
|
||||
protected Camera cam;
|
||||
protected AssetManager assets;
|
||||
protected Node rootNode;
|
||||
protected TerrainQuad terrain;
|
||||
|
||||
protected final List<Node> markers = new ArrayList<>();
|
||||
|
||||
protected AuswahlMarkerState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) {
|
||||
this.terrain = terrain;
|
||||
}
|
||||
|
||||
protected final int pickMarker(Ray ray) {
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
markers.get(i).collideWith(ray, res);
|
||||
if (res.size() > 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
protected final void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
int hit = pickMarker(ray);
|
||||
log.info("[Auswahl] {}: Klick ({},{}), Marker getroffen: {}, Marker gesamt: {}",
|
||||
getClass().getSimpleName(), (int) click.screenX(), (int) click.screenY(),
|
||||
hit, markers.size());
|
||||
if (hit >= 0) {
|
||||
onMarkerSelected(hit);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wird aufgerufen wenn ein Marker-Treffer gefunden wurde.
|
||||
* Subklasse selektiert den Eintrag und setzt das zugehörige auswahl*Selected-Flag.
|
||||
*/
|
||||
protected abstract void onMarkerSelected(int idx);
|
||||
}
|
||||
@@ -18,15 +18,10 @@ import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.common.PlacedEmitter;
|
||||
import de.blight.editor.SharedInput;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class EmitterState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EmitterState.class);
|
||||
public class EmitterState extends AuswahlMarkerState {
|
||||
|
||||
private static final float MARKER_RADIUS = 0.4f;
|
||||
private static final float ACTIVATION_ALPHA = 0.07f;
|
||||
@@ -35,16 +30,8 @@ public class EmitterState extends BaseAppState {
|
||||
private static final String GEO_MARKER = "emitter_marker";
|
||||
private static final String GEO_ACTRADIUS = "activation_sphere";
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
// parallel lists: emitters[i] <-> markers[i] <-> particles[i]
|
||||
private final List<PlacedEmitter> emitters = new ArrayList<>();
|
||||
private final List<Node> markers = new ArrayList<>();
|
||||
private final List<ParticleEmitter> particles = new ArrayList<>();
|
||||
|
||||
private int selectedIdx = -1;
|
||||
@@ -52,19 +39,11 @@ public class EmitterState extends BaseAppState {
|
||||
private List<PlacedEmitter> pendingEmitters = null;
|
||||
|
||||
public EmitterState(SharedInput input) {
|
||||
this.input = input;
|
||||
super(input);
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application application) { clearAll(); }
|
||||
|
||||
@Override
|
||||
@@ -77,7 +56,11 @@ public class EmitterState extends BaseAppState {
|
||||
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||
@Override
|
||||
protected void onMarkerSelected(int idx) {
|
||||
selectEmitter(idx);
|
||||
input.auswahlEmitterSelected = true;
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -180,28 +163,6 @@ public class EmitterState extends BaseAppState {
|
||||
};
|
||||
}
|
||||
|
||||
private void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
int hit = pickMarker(ray);
|
||||
if (hit >= 0) {
|
||||
selectEmitter(hit);
|
||||
input.auswahlEmitterSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
private int pickMarker(Ray ray) {
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
markers.get(i).collideWith(ray, res);
|
||||
if (res.size() > 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectEmitter(int idx) {
|
||||
|
||||
@@ -4,12 +4,18 @@ import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.bounding.BoundingSphere;
|
||||
import com.jme3.bounding.BoundingVolume;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.shape.Box;
|
||||
import com.jme3.scene.shape.Cylinder;
|
||||
import com.jme3.scene.shape.Torus;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import de.blight.common.PlacedItem;
|
||||
import de.blight.common.PlacedItemIO;
|
||||
@@ -21,6 +27,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -29,11 +36,31 @@ import java.util.Map;
|
||||
/**
|
||||
* Verwaltet die Platzierung von Items (Pickups) auf der Karte im Editor.
|
||||
* Aktiv wenn {@code input.activeLayer == LAYER_ITEMS}.
|
||||
* Auswahl-Modus (LAYER_AUSWAHL): Item auswählen → wechselt zu LAYER_ITEMS_EDIT.
|
||||
* LAYER_ITEMS_EDIT: Gizmo (Pfeile + Ringe) zum Verschieben/Rotieren.
|
||||
*/
|
||||
public class ItemPlacementState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ItemPlacementState.class);
|
||||
|
||||
// ── Gizmo-Konstanten (identisch mit SceneObjectState) ────────────────────
|
||||
private static final ColorRGBA COL_X = new ColorRGBA(0.92f, 0.12f, 0.12f, 1f);
|
||||
private static final ColorRGBA COL_Y = new ColorRGBA(0.12f, 0.88f, 0.12f, 1f);
|
||||
private static final ColorRGBA COL_Z = new ColorRGBA(0.12f, 0.35f, 1.00f, 1f);
|
||||
|
||||
private static final float SHAFT_LEN = 3.2f;
|
||||
private static final float HEAD_LEN = 0.9f;
|
||||
private static final float SHAFT_RADIUS = 0.06f;
|
||||
private static final float HEAD_RADIUS = 0.22f;
|
||||
private static final float PX_PER_WE = 0.15f;
|
||||
private static final float RING_RADIUS = 4.5f;
|
||||
private static final float RING_TUBE = 0.25f;
|
||||
private static final float DEG_PER_PX = 0.8f;
|
||||
|
||||
private static final String HIGHLIGHT_NAME = "selectionHighlight";
|
||||
private static final ColorRGBA HIGHLIGHT_COLOR = new ColorRGBA(0.3f, 0.8f, 1.0f, 1f);
|
||||
|
||||
// ── Felder ────────────────────────────────────────────────────────────────
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
@@ -46,9 +73,16 @@ public class ItemPlacementState extends BaseAppState {
|
||||
private final Map<String, Item> itemDefs = new HashMap<>();
|
||||
private Node itemRoot;
|
||||
private Node previewNode;
|
||||
private int selectedItemIdx = -1;
|
||||
private int selectedItemIdx = -1;
|
||||
private Node selectedItemNode = null;
|
||||
|
||||
// Gizmo
|
||||
private Node gizmoNode;
|
||||
private Node arrowX, arrowY, arrowZ;
|
||||
private Node ringX, ringY, ringZ;
|
||||
private int activeGizmo = -1;
|
||||
private int activeRing = -1;
|
||||
|
||||
public ItemPlacementState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
@@ -66,7 +100,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
this.assets = app.getAssetManager();
|
||||
this.rootNode = this.app.getRootNode();
|
||||
|
||||
java.nio.file.Path itemDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("items");
|
||||
Path itemDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("items");
|
||||
for (Item it : ItemIO.loadAll(itemDir)) {
|
||||
if (it.getItemId() != null) itemDefs.put(it.getItemId(), it);
|
||||
}
|
||||
@@ -80,6 +114,11 @@ public class ItemPlacementState extends BaseAppState {
|
||||
prev.setMaterial(pm);
|
||||
prev.getMesh().setMode(com.jme3.scene.Mesh.Mode.Lines);
|
||||
previewNode.attachChild(prev);
|
||||
|
||||
gizmoNode = new Node("itemGizmo");
|
||||
buildGizmo();
|
||||
gizmoNode.setCullHint(Spatial.CullHint.Always);
|
||||
rootNode.attachChild(gizmoNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,6 +140,7 @@ public class ItemPlacementState extends BaseAppState {
|
||||
for (PlacedItem pi : items) {
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
n.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
|
||||
applyRot(n, pi.rotX(), pi.rotY(), pi.rotZ());
|
||||
itemRoot.attachChild(n);
|
||||
nodes.add(n);
|
||||
}
|
||||
@@ -113,7 +153,9 @@ public class ItemPlacementState extends BaseAppState {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {}
|
||||
protected void cleanup(Application app) {
|
||||
if (gizmoNode != null) gizmoNode.removeFromParent();
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -125,11 +167,15 @@ public class ItemPlacementState extends BaseAppState {
|
||||
reloadFromDisk();
|
||||
}
|
||||
|
||||
// Highlight entfernen sobald wir nicht mehr im Auswahl-Modus sind
|
||||
if (selectedItemIdx >= 0 && input.activeLayer != SharedInput.LAYER_AUSWAHL) {
|
||||
// Highlight entfernen sobald wir weder im Auswahl- noch im Edit-Modus sind
|
||||
if (selectedItemIdx >= 0
|
||||
&& input.activeLayer != SharedInput.LAYER_AUSWAHL
|
||||
&& input.activeLayer != SharedInput.LAYER_ITEMS_EDIT) {
|
||||
deselectItem();
|
||||
}
|
||||
|
||||
updateGizmoVisibility();
|
||||
|
||||
updatePreview();
|
||||
|
||||
SharedInput.ObjectClick click;
|
||||
@@ -142,6 +188,26 @@ public class ItemPlacementState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// LAYER_ITEMS_EDIT: Gizmo-Klicks und Drags
|
||||
if (input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) {
|
||||
SharedInput.ObjectClick objClick;
|
||||
while ((objClick = input.objectClickQueue.poll()) != null) {
|
||||
handleItemGizmoClick(objClick);
|
||||
}
|
||||
boolean hadDrags = false;
|
||||
SharedInput.ObjectDrag drag;
|
||||
while ((drag = input.objectDragQueue.poll()) != null) {
|
||||
handleItemGizmoDrag(drag);
|
||||
hadDrags = true;
|
||||
}
|
||||
if (hadDrags) {
|
||||
saveItems();
|
||||
}
|
||||
if (selectedItemIdx >= 0) {
|
||||
updateGizmoPosition();
|
||||
}
|
||||
}
|
||||
|
||||
SharedInput.ObjectClick auswahlClick;
|
||||
while ((auswahlClick = input.auswahlClickQueue.poll()) != null) {
|
||||
handleAuswahlClick(auswahlClick);
|
||||
@@ -155,8 +221,19 @@ public class ItemPlacementState extends BaseAppState {
|
||||
input.onObjectDeleted.run();
|
||||
}
|
||||
}
|
||||
|
||||
if (input.itemEditRequest) {
|
||||
input.itemEditRequest = false;
|
||||
if (selectedItemIdx >= 0) {
|
||||
applyItemEdit(selectedItemIdx,
|
||||
input.itemEditX, input.itemEditY, input.itemEditZ,
|
||||
input.itemEditRotX, input.itemEditRotY, input.itemEditRotZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Vorschau ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void updatePreview() {
|
||||
if (input.activeLayer != SharedInput.LAYER_ITEMS
|
||||
|| input.pendingItemId == null
|
||||
@@ -179,6 +256,8 @@ public class ItemPlacementState extends BaseAppState {
|
||||
previewNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
// ── Platzierung ───────────────────────────────────────────────────────────
|
||||
|
||||
private void handlePlace(SharedInput.ObjectClick click) {
|
||||
if (terrain == null || input.pendingItemId == null) return;
|
||||
float jx = click.screenX() * (float) input.viewportScaleX;
|
||||
@@ -188,30 +267,34 @@ public class ItemPlacementState extends BaseAppState {
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) return;
|
||||
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
||||
PlacedItem pi = PlacedItem.create(input.pendingItemId, pt.x, pt.y, pt.z);
|
||||
items.add(pi);
|
||||
|
||||
Node n = buildItemNode(pi.itemId());
|
||||
n.setLocalTranslation(pt.x, pt.y + 0.25f, pt.z);
|
||||
applyRot(n, 0f, 0f, 0f);
|
||||
itemRoot.attachChild(n);
|
||||
nodes.add(n);
|
||||
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})",
|
||||
pi.itemId(), pt.x, pt.y, pt.z);
|
||||
log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", pi.itemId(), pt.x, pt.y, pt.z);
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auswahl-Klick (LAYER_AUSWAHL → Item-Picking) ─────────────────────────
|
||||
|
||||
private void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Ray ray = screenToRay(jmeX, jmeY);
|
||||
CollisionResults hits = new CollisionResults();
|
||||
itemRoot.collideWith(ray, hits);
|
||||
log.info("[Auswahl] ItemPlacementState: Klick ({},{}), Treffer: {}, Items gesamt: {}",
|
||||
(int) click.screenX(), (int) click.screenY(), hits.size(), items.size());
|
||||
if (hits.size() > 0) {
|
||||
Spatial geo = hits.getClosestCollision().getGeometry();
|
||||
Node parent = geo.getParent();
|
||||
@@ -219,44 +302,362 @@ public class ItemPlacementState extends BaseAppState {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
if (parent != null) {
|
||||
selectItem(nodes.indexOf(parent));
|
||||
int idx = nodes.indexOf(parent);
|
||||
log.info("[Auswahl] ItemPlacementState: Item {} ausgewaehlt (idx={})", items.get(idx).itemId(), idx);
|
||||
selectItem(idx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
deselectItem();
|
||||
}
|
||||
|
||||
// ── Gizmo-Klick (LAYER_ITEMS_EDIT) ───────────────────────────────────────
|
||||
|
||||
private void handleItemGizmoClick(SharedInput.ObjectClick click) {
|
||||
if (selectedItemIdx < 0) return;
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Ray ray = screenToRay(jmeX, jmeY);
|
||||
|
||||
if (input.objectEditTool == SharedInput.EDIT_TOOL_MOVE) {
|
||||
int hit = pickGizmo(ray);
|
||||
if (hit >= 0) {
|
||||
activeGizmo = hit;
|
||||
activeRing = -1;
|
||||
return;
|
||||
}
|
||||
} else if (input.objectEditTool == SharedInput.EDIT_TOOL_ROTATE) {
|
||||
int hit = pickRing(ray);
|
||||
if (hit >= 0) {
|
||||
activeRing = hit;
|
||||
activeGizmo = -1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
activeGizmo = -1;
|
||||
activeRing = -1;
|
||||
|
||||
// Klick außerhalb Gizmo: anderes Item oder leere Fläche?
|
||||
CollisionResults hits = new CollisionResults();
|
||||
itemRoot.collideWith(ray, hits);
|
||||
if (hits.size() > 0) {
|
||||
Spatial geo = hits.getClosestCollision().getGeometry();
|
||||
// Highlight-Klone ignorieren
|
||||
if (HIGHLIGHT_NAME.equals(geo.getParent() != null ? geo.getParent().getName() : "")) {
|
||||
return;
|
||||
}
|
||||
Node parent = geo.getParent();
|
||||
while (parent != null && !nodes.contains(parent)) {
|
||||
parent = parent.getParent();
|
||||
}
|
||||
if (parent != null) {
|
||||
int idx = nodes.indexOf(parent);
|
||||
if (idx != selectedItemIdx) {
|
||||
selectItem(idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
deselectItem();
|
||||
}
|
||||
|
||||
// ── Gizmo-Drag (LAYER_ITEMS_EDIT) ────────────────────────────────────────
|
||||
|
||||
private void handleItemGizmoDrag(SharedInput.ObjectDrag drag) {
|
||||
if (selectedItemIdx < 0) return;
|
||||
switch (input.objectEditTool) {
|
||||
case SharedInput.EDIT_TOOL_MOVE -> handleMoveDrag(drag);
|
||||
case SharedInput.EDIT_TOOL_ROTATE -> handleRotateDrag(drag);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMoveDrag(SharedInput.ObjectDrag drag) {
|
||||
if (activeGizmo < 0 || selectedItemIdx < 0) return;
|
||||
Vector3f arrowDir = getArrowWorldDir(activeGizmo);
|
||||
Vector3f origin = gizmoNode.getWorldTranslation();
|
||||
Vector3f scrO = cam.getScreenCoordinates(origin);
|
||||
Vector3f scrT = cam.getScreenCoordinates(origin.add(arrowDir));
|
||||
float sdx = scrT.x - scrO.x;
|
||||
float sdy = -(scrT.y - scrO.y);
|
||||
float screenLen2 = sdx * sdx + sdy * sdy;
|
||||
float magnitude;
|
||||
if (screenLen2 < 0.01f) {
|
||||
magnitude = -drag.dy() * PX_PER_WE;
|
||||
} else {
|
||||
magnitude = (drag.dx() * sdx + drag.dy() * sdy) / screenLen2;
|
||||
}
|
||||
Vector3f delta = arrowDir.mult(magnitude);
|
||||
|
||||
Node n = nodes.get(selectedItemIdx);
|
||||
n.move(delta.x, delta.y, delta.z);
|
||||
|
||||
PlacedItem pi = items.get(selectedItemIdx);
|
||||
PlacedItem updated = pi.withTransform(
|
||||
pi.x() + delta.x, pi.y() + delta.y, pi.z() + delta.z,
|
||||
pi.rotX(), pi.rotY(), pi.rotZ());
|
||||
items.set(selectedItemIdx, updated);
|
||||
publishSelection(selectedItemIdx);
|
||||
updateGizmoPosition();
|
||||
}
|
||||
|
||||
private void handleRotateDrag(SharedInput.ObjectDrag drag) {
|
||||
if (activeRing < 0 || selectedItemIdx < 0) return;
|
||||
|
||||
PlacedItem pi = items.get(selectedItemIdx);
|
||||
Quaternion qObjY = new Quaternion().fromAngleAxis(pi.rotY(), Vector3f.UNIT_Y);
|
||||
Vector3f rotAxis = switch (activeRing) {
|
||||
case 0 -> qObjY.mult(new Vector3f(1, 0, 0));
|
||||
case 1 -> new Vector3f(0, 1, 0);
|
||||
default -> qObjY.mult(new Vector3f(0, 0, 1));
|
||||
};
|
||||
|
||||
Vector3f center = gizmoNode.getWorldTranslation();
|
||||
Vector3f toCam = cam.getLocation().subtract(center).normalizeLocal();
|
||||
Vector3f tangent = rotAxis.cross(toCam);
|
||||
float tLen = tangent.length();
|
||||
float angle;
|
||||
if (tLen < 1e-4f) {
|
||||
angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD;
|
||||
} else {
|
||||
tangent.divideLocal(tLen);
|
||||
Vector3f scrC = cam.getScreenCoordinates(center);
|
||||
Vector3f scrT = cam.getScreenCoordinates(center.add(tangent));
|
||||
float sdx = scrT.x - scrC.x;
|
||||
float sdy = -(scrT.y - scrC.y);
|
||||
float sl2 = sdx * sdx + sdy * sdy;
|
||||
if (sl2 < 0.5f) {
|
||||
angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD;
|
||||
} else {
|
||||
float proj = (drag.dx() * sdx + drag.dy() * sdy) / sl2;
|
||||
angle = proj / RING_RADIUS;
|
||||
}
|
||||
}
|
||||
|
||||
Node n = nodes.get(selectedItemIdx);
|
||||
Quaternion deltaRot = new Quaternion().fromAngleAxis(angle, rotAxis);
|
||||
n.setLocalRotation(deltaRot.mult(n.getLocalRotation()));
|
||||
|
||||
float[] angles = n.getLocalRotation().toAngles(null);
|
||||
PlacedItem updated = pi.withTransform(pi.x(), pi.y(), pi.z(), angles[0], angles[1], angles[2]);
|
||||
items.set(selectedItemIdx, updated);
|
||||
publishSelection(selectedItemIdx);
|
||||
}
|
||||
|
||||
// ── Selektion ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectItem(int idx) {
|
||||
deselectItem();
|
||||
selectedItemIdx = idx;
|
||||
selectedItemNode = nodes.get(idx);
|
||||
Geometry highlight = new Geometry("item_sel",
|
||||
new com.jme3.scene.shape.Box(0.28f, 0.28f, 0.28f));
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new com.jme3.math.ColorRGBA(1f, 0.6f, 0.1f, 1f));
|
||||
mat.getAdditionalRenderState().setWireframe(true);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
highlight.setMaterial(mat);
|
||||
highlight.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Transparent);
|
||||
selectedItemNode.attachChild(highlight);
|
||||
PlacedItem pi = items.get(idx);
|
||||
input.selectedItemInfo = idx + "|" + pi.itemId() + "|" + pi.x() + "|" + pi.y() + "|" + pi.z();
|
||||
input.itemSelectionChanged = true;
|
||||
addSelectionHighlight(idx);
|
||||
publishSelection(idx);
|
||||
input.activeLayer = SharedInput.LAYER_ITEMS_EDIT;
|
||||
updateGizmoPosition();
|
||||
updateGizmoVisibility();
|
||||
}
|
||||
|
||||
private void deselectItem() {
|
||||
if (selectedItemNode != null) {
|
||||
Spatial h = selectedItemNode.getChild("item_sel");
|
||||
if (h != null) h.removeFromParent();
|
||||
removeSelectionHighlight(selectedItemIdx);
|
||||
selectedItemNode = null;
|
||||
}
|
||||
activeGizmo = -1;
|
||||
activeRing = -1;
|
||||
if (selectedItemIdx >= 0) {
|
||||
selectedItemIdx = -1;
|
||||
input.selectedItemInfo = null;
|
||||
input.itemSelectionChanged = true;
|
||||
}
|
||||
if (input.activeLayer == SharedInput.LAYER_ITEMS_EDIT) {
|
||||
input.activeLayer = SharedInput.LAYER_AUSWAHL;
|
||||
}
|
||||
updateGizmoVisibility();
|
||||
}
|
||||
|
||||
private void publishSelection(int idx) {
|
||||
PlacedItem pi = items.get(idx);
|
||||
input.selectedItemInfo = idx + "|" + pi.itemId()
|
||||
+ "|" + pi.x() + "|" + pi.y() + "|" + pi.z()
|
||||
+ "|" + pi.rotX() + "|" + pi.rotY() + "|" + pi.rotZ();
|
||||
input.itemSelectionChanged = true;
|
||||
}
|
||||
|
||||
// ── Highlight ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void addSelectionHighlight(int idx) {
|
||||
Node itemNode = nodes.get(idx);
|
||||
removeSelectionHighlight(idx);
|
||||
Node highlight = new Node(HIGHLIGHT_NAME);
|
||||
for (Spatial child : new ArrayList<>(itemNode.getChildren())) {
|
||||
if (child == gizmoNode) continue;
|
||||
Spatial clone = child.clone();
|
||||
applySelectionMaterial(clone);
|
||||
highlight.attachChild(clone);
|
||||
}
|
||||
itemNode.attachChild(highlight);
|
||||
}
|
||||
|
||||
private void removeSelectionHighlight(int idx) {
|
||||
if (idx < 0 || idx >= nodes.size()) return;
|
||||
Spatial h = nodes.get(idx).getChild(HIGHLIGHT_NAME);
|
||||
if (h != null) h.removeFromParent();
|
||||
}
|
||||
|
||||
private void applySelectionMaterial(Spatial s) {
|
||||
if (s instanceof Geometry geo) {
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", HIGHLIGHT_COLOR);
|
||||
mat.getAdditionalRenderState().setWireframe(true);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
geo.setMaterial(mat);
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : new ArrayList<>(n.getChildren())) {
|
||||
applySelectionMaterial(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gizmo-Bau ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void buildGizmo() {
|
||||
gizmoNode.detachAllChildren();
|
||||
|
||||
arrowX = buildArrowNode(COL_X, "arrowX",
|
||||
new Vector3f(1, 0, 0),
|
||||
new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y));
|
||||
arrowY = buildArrowNode(COL_Y, "arrowY",
|
||||
new Vector3f(0, 1, 0),
|
||||
new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X));
|
||||
arrowZ = buildArrowNode(COL_Z, "arrowZ",
|
||||
new Vector3f(0, 0, -1),
|
||||
new Quaternion());
|
||||
gizmoNode.attachChild(arrowX);
|
||||
gizmoNode.attachChild(arrowY);
|
||||
gizmoNode.attachChild(arrowZ);
|
||||
|
||||
ringX = buildRingNode(COL_X, "ringX",
|
||||
new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y));
|
||||
ringY = buildRingNode(COL_Y, "ringY",
|
||||
new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X));
|
||||
ringZ = buildRingNode(COL_Z, "ringZ",
|
||||
new Quaternion());
|
||||
gizmoNode.attachChild(ringX);
|
||||
gizmoNode.attachChild(ringY);
|
||||
gizmoNode.attachChild(ringZ);
|
||||
}
|
||||
|
||||
private Node buildArrowNode(ColorRGBA color, String name, Vector3f dir, Quaternion geomRot) {
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
|
||||
Geometry shaft = new Geometry(name + "_shaft",
|
||||
new Cylinder(2, 10, SHAFT_RADIUS, SHAFT_LEN, false));
|
||||
shaft.setLocalRotation(geomRot);
|
||||
shaft.setLocalTranslation(dir.mult(SHAFT_LEN * 0.5f));
|
||||
shaft.setMaterial(mat);
|
||||
shaft.setQueueBucket(RenderQueue.Bucket.Translucent);
|
||||
|
||||
Geometry head = new Geometry(name + "_head",
|
||||
new Cylinder(2, 10, HEAD_RADIUS, HEAD_LEN, true));
|
||||
head.setLocalRotation(geomRot);
|
||||
head.setLocalTranslation(dir.mult(SHAFT_LEN + HEAD_LEN * 0.5f));
|
||||
head.setMaterial(mat);
|
||||
head.setQueueBucket(RenderQueue.Bucket.Translucent);
|
||||
|
||||
Node node = new Node(name);
|
||||
node.attachChild(shaft);
|
||||
node.attachChild(head);
|
||||
return node;
|
||||
}
|
||||
|
||||
private Node buildRingNode(ColorRGBA color, String name, Quaternion nodeRot) {
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setDepthTest(false);
|
||||
|
||||
Geometry ring = new Geometry(name + "_ring",
|
||||
new Torus(48, 10, RING_TUBE, RING_RADIUS));
|
||||
ring.setMaterial(mat);
|
||||
ring.setQueueBucket(RenderQueue.Bucket.Translucent);
|
||||
|
||||
Node node = new Node(name);
|
||||
node.setLocalRotation(nodeRot);
|
||||
node.attachChild(ring);
|
||||
return node;
|
||||
}
|
||||
|
||||
// ── Gizmo-Position / -Sichtbarkeit ────────────────────────────────────────
|
||||
|
||||
private void updateGizmoPosition() {
|
||||
if (selectedItemIdx < 0) return;
|
||||
Node n = nodes.get(selectedItemIdx);
|
||||
n.updateGeometricState();
|
||||
BoundingVolume bv = n.getWorldBound();
|
||||
Vector3f center;
|
||||
float radius;
|
||||
if (bv instanceof BoundingSphere bs) {
|
||||
center = bs.getCenter().clone();
|
||||
radius = bs.getRadius();
|
||||
} else if (bv instanceof BoundingBox bb) {
|
||||
center = bb.getCenter().clone();
|
||||
radius = bb.getExtent(null).length();
|
||||
} else {
|
||||
center = n.getWorldTranslation().clone();
|
||||
radius = 0.5f;
|
||||
}
|
||||
gizmoNode.setLocalTranslation(center);
|
||||
gizmoNode.setLocalScale(Math.max(0.01f, radius / RING_RADIUS));
|
||||
|
||||
float objRotY = items.get(selectedItemIdx).rotY();
|
||||
Quaternion qObjY = new Quaternion().fromAngleAxis(objRotY, Vector3f.UNIT_Y);
|
||||
ringX.setLocalRotation(qObjY.mult(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_Y)));
|
||||
ringY.setLocalRotation(qObjY.mult(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X)));
|
||||
ringZ.setLocalRotation(qObjY.mult(new Quaternion()));
|
||||
}
|
||||
|
||||
private void updateGizmoVisibility() {
|
||||
if (gizmoNode == null) return;
|
||||
boolean sel = selectedItemIdx >= 0 && input.activeLayer == SharedInput.LAYER_ITEMS_EDIT;
|
||||
boolean move = sel && input.objectEditTool == SharedInput.EDIT_TOOL_MOVE;
|
||||
boolean rotate = sel && input.objectEditTool == SharedInput.EDIT_TOOL_ROTATE;
|
||||
|
||||
Spatial.CullHint showArrows = move ? Spatial.CullHint.Inherit : Spatial.CullHint.Always;
|
||||
Spatial.CullHint showRings = rotate ? Spatial.CullHint.Inherit : Spatial.CullHint.Always;
|
||||
arrowX.setCullHint(showArrows); arrowY.setCullHint(showArrows); arrowZ.setCullHint(showArrows);
|
||||
ringX.setCullHint(showRings); ringY.setCullHint(showRings); ringZ.setCullHint(showRings);
|
||||
gizmoNode.setCullHint((move || rotate) ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
// ── Gizmo-Picking ─────────────────────────────────────────────────────────
|
||||
|
||||
private int pickGizmo(Ray ray) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
arrowX.collideWith(ray, hits); if (hits.size() > 0) return 0; hits.clear();
|
||||
arrowY.collideWith(ray, hits); if (hits.size() > 0) return 1; hits.clear();
|
||||
arrowZ.collideWith(ray, hits); if (hits.size() > 0) return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int pickRing(Ray ray) {
|
||||
CollisionResults hits = new CollisionResults();
|
||||
ringX.collideWith(ray, hits); if (hits.size() > 0) return 0; hits.clear();
|
||||
ringY.collideWith(ray, hits); if (hits.size() > 0) return 1; hits.clear();
|
||||
ringZ.collideWith(ray, hits); if (hits.size() > 0) return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private Vector3f getArrowWorldDir(int idx) {
|
||||
return switch (idx) {
|
||||
case 0 -> new Vector3f(1, 0, 0);
|
||||
case 1 -> new Vector3f(0, 1, 0);
|
||||
default -> new Vector3f(0, 0, -1);
|
||||
};
|
||||
}
|
||||
|
||||
// ── Edit / Löschen ────────────────────────────────────────────────────────
|
||||
|
||||
private void removeItem(int idx) {
|
||||
itemRoot.detachChild(nodes.get(idx));
|
||||
items.remove(idx);
|
||||
@@ -268,8 +669,39 @@ public class ItemPlacementState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyItemEdit(int idx, float x, float y, float z, float rotX, float rotY, float rotZ) {
|
||||
PlacedItem updated = items.get(idx).withTransform(x, y, z, rotX, rotY, rotZ);
|
||||
items.set(idx, updated);
|
||||
|
||||
Node n = nodes.get(idx);
|
||||
n.setLocalTranslation(x, y + 0.25f, z);
|
||||
applyRot(n, rotX, rotY, rotZ);
|
||||
|
||||
publishSelection(idx);
|
||||
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern nach Edit fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void saveItems() {
|
||||
try {
|
||||
PlacedItemIO.save(items);
|
||||
} catch (IOException e) {
|
||||
log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||
|
||||
private static void applyRot(Node n, float rotX, float rotY, float rotZ) {
|
||||
Quaternion q = new Quaternion();
|
||||
q.fromAngles(rotX, rotY, rotZ);
|
||||
n.setLocalRotation(q);
|
||||
}
|
||||
|
||||
private Node buildItemNode(String itemId) {
|
||||
Node n = new Node("item_" + itemId);
|
||||
n.setUserData("itemId", itemId);
|
||||
|
||||
@@ -21,7 +21,7 @@ import de.blight.editor.SharedInput;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class LightState extends BaseAppState {
|
||||
public class LightState extends AuswahlMarkerState {
|
||||
|
||||
// ── Glühbirnen-Geometrie ──────────────────────────────────────────────────
|
||||
private static final float BULB_RADIUS = 0.38f;
|
||||
@@ -37,35 +37,19 @@ public class LightState extends BaseAppState {
|
||||
private static final ColorRGBA SEL_COLOR = new ColorRGBA(1f, 1f, 0f, 1f);
|
||||
private static final ColorRGBA SOCKET_COLOR = new ColorRGBA(0.22f, 0.22f, 0.22f, 1f);
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
// parallel lists: lights[i] <-> markers[i] <-> pointLights[i]
|
||||
private final List<PlacedLight> lights = new ArrayList<>();
|
||||
private final List<Node> markers = new ArrayList<>();
|
||||
private final List<PointLight> pointLights = new ArrayList<>();
|
||||
|
||||
private int selectedIdx = -1;
|
||||
private List<PlacedLight> pendingLights = null;
|
||||
|
||||
public LightState(SharedInput input) {
|
||||
this.input = input;
|
||||
super(input);
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
clearAll();
|
||||
@@ -80,7 +64,11 @@ public class LightState extends BaseAppState {
|
||||
}
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||
@Override
|
||||
protected void onMarkerSelected(int idx) {
|
||||
selectLight(idx);
|
||||
input.auswahlLightSelected = true;
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -153,29 +141,6 @@ public class LightState extends BaseAppState {
|
||||
selectLight(lights.size() - 1);
|
||||
}
|
||||
|
||||
private void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
int hit = pickMarker(ray);
|
||||
if (hit >= 0) {
|
||||
selectLight(hit);
|
||||
input.auswahlLightSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Trifft den Glow-Halo oder den Bulb irgendeines Markers. */
|
||||
private int pickMarker(Ray ray) {
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
markers.get(i).collideWith(ray, res);
|
||||
if (res.size() > 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectLight(int idx) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.jme3.light.DirectionalLight;
|
||||
import com.jme3.light.LightProbe;
|
||||
import com.jme3.light.PointLight;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.MatParam;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
@@ -31,6 +32,7 @@ import com.jme3.anim.SkinningControl;
|
||||
import com.jme3.effect.ParticleEmitter;
|
||||
import com.jme3.effect.ParticleMesh;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.texture.Texture;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.util.ImpostorUtil;
|
||||
@@ -1093,6 +1095,17 @@ public class ModelEditorState extends BaseAppState {
|
||||
VertexBuffer vb = mesh.getBuffer(type);
|
||||
if (vb != null) vb.setUpdateNeeded();
|
||||
}
|
||||
// Texturen als update-needed markieren – verhindert schwarzes Rendering
|
||||
// wenn die GPU-Textur-Zustände durch vorherige Thumbnail-Renders beschädigt wurden.
|
||||
Material mat = g.getMaterial();
|
||||
if (mat != null) {
|
||||
for (MatParam param : mat.getParams()) {
|
||||
Object val = param.getValue();
|
||||
if (val instanceof Texture tex && tex.getImage() != null) {
|
||||
tex.getImage().setUpdateNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) markBuffersUpdateNeeded(child);
|
||||
}
|
||||
@@ -1520,20 +1533,33 @@ public class ModelEditorState extends BaseAppState {
|
||||
input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
/** Zeichnet / aktualisiert den 15cm-Grip-Gizmo (orangefarbener Pfeil). */
|
||||
/** Zeichnet / aktualisiert den Grip-Gizmo: 15cm orange (Waffe) oder 28cm cyan (Schild/Unterarm). */
|
||||
public void rebuildGripGizmo() {
|
||||
if (gripGizmoNode == null) return;
|
||||
gripGizmoNode.detachAllChildren();
|
||||
|
||||
float shaftLen = 0.12f;
|
||||
float shaftRad = 0.008f;
|
||||
float headRad = 0.022f;
|
||||
float headLen = 0.03f; // 15cm gesamt: 12cm Schaft + 3cm Kopf
|
||||
boolean shieldMode = input.gripEditorIsShield;
|
||||
|
||||
// Waffe: 15cm orange | Schild: 28cm cyan
|
||||
float shaftLen = shieldMode ? 0.24f : 0.12f;
|
||||
float shaftRad = shieldMode ? 0.010f : 0.008f;
|
||||
float headRad = shieldMode ? 0.026f : 0.022f;
|
||||
float headLen = shieldMode ? 0.04f : 0.03f;
|
||||
|
||||
ColorRGBA shaftColor = shieldMode
|
||||
? new ColorRGBA(0f, 0.75f, 0.9f, 1f)
|
||||
: new ColorRGBA(1f, 0.55f, 0f, 1f);
|
||||
ColorRGBA headColor = shieldMode
|
||||
? new ColorRGBA(0f, 0.5f, 0.75f, 1f)
|
||||
: new ColorRGBA(1f, 0.3f, 0f, 1f);
|
||||
ColorRGBA axisColor = shieldMode
|
||||
? new ColorRGBA(0.6f, 1f, 0.6f, 1f)
|
||||
: new ColorRGBA(1f, 0.8f, 0.1f, 1f);
|
||||
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(1f, 0.55f, 0f, 1f));
|
||||
mat.setColor("Color", shaftColor);
|
||||
Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
matHead.setColor("Color", new ColorRGBA(1f, 0.3f, 0f, 1f));
|
||||
matHead.setColor("Color", headColor);
|
||||
|
||||
Node group = new Node("gripGroup");
|
||||
group.setLocalTranslation(gripPosX, gripPosY, gripPosZ);
|
||||
@@ -1545,8 +1571,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
com.jme3.math.FastMath.DEG_TO_RAD * gripRotZ);
|
||||
group.setLocalRotation(rot);
|
||||
|
||||
// Kleiner Würfel am Griffpunkt
|
||||
Geometry marker = new Geometry("gMarker", new Box(0.015f, 0.015f, 0.015f));
|
||||
float markerSize = shieldMode ? 0.018f : 0.015f;
|
||||
Geometry marker = new Geometry("gMarker", new Box(markerSize, markerSize, markerSize));
|
||||
marker.setMaterial(mat);
|
||||
|
||||
// Schaft entlang +Z
|
||||
@@ -1562,17 +1588,20 @@ public class ModelEditorState extends BaseAppState {
|
||||
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||
head.setLocalRotation(headRot);
|
||||
|
||||
// Klingenflosse: dünnes flaches Rechteck senkrecht zum Schaft entlang +Y,
|
||||
// damit Rotation um Z (Roll) sichtbar ist. Breite 6cm, Dicke 2mm, entlang Schaft.
|
||||
Material matFin = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
matFin.setColor("Color", new ColorRGBA(1f, 0.8f, 0.1f, 1f));
|
||||
Geometry fin = new Geometry("gFin", new Box(0.001f, 0.03f, shaftLen * 0.5f));
|
||||
fin.setMaterial(matFin);
|
||||
fin.setLocalTranslation(0f, 0.03f, shaftLen * 0.5f);
|
||||
// Querachsen-Zylinder entlang +Y: zeigt Roll-/Orientierungsachse
|
||||
Material matAxis = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
matAxis.setColor("Color", axisColor);
|
||||
float axisLen = shieldMode ? 0.08f : 0.06f;
|
||||
Geometry gripAxis = new Geometry("gGripAxis", new Cylinder(4, 8, 0.006f, axisLen, true));
|
||||
gripAxis.setMaterial(matAxis);
|
||||
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(shaft);
|
||||
group.attachChild(fin);
|
||||
group.attachChild(gripAxis);
|
||||
group.attachChild(head);
|
||||
gripGizmoNode.attachChild(group);
|
||||
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
|
||||
@@ -19,7 +19,7 @@ import de.blight.editor.SharedInput;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PlacedSoundState extends BaseAppState {
|
||||
public class PlacedSoundState extends AuswahlMarkerState {
|
||||
|
||||
private static final float MARKER_RADIUS = 0.30f;
|
||||
private static final float GLOW_RADIUS = MARKER_RADIUS * 2.4f;
|
||||
@@ -31,34 +31,18 @@ public class PlacedSoundState extends BaseAppState {
|
||||
|
||||
private static final float POS_EPSILON = 0.05f;
|
||||
|
||||
private final SharedInput input;
|
||||
private SimpleApplication app;
|
||||
private Camera cam;
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
private TerrainQuad terrain;
|
||||
|
||||
// parallel lists: sounds[i] <-> markers[i]
|
||||
private final List<PlacedSound> sounds = new ArrayList<>();
|
||||
private final List<Node> markers = new ArrayList<>();
|
||||
private final List<PlacedSound> sounds = new ArrayList<>();
|
||||
|
||||
private int selectedIdx = -1;
|
||||
private List<PlacedSound> pendingSounds = null;
|
||||
|
||||
public PlacedSoundState(SharedInput input) {
|
||||
this.input = input;
|
||||
super(input);
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
cam = app.getCamera();
|
||||
assets = app.getAssetManager();
|
||||
rootNode = app.getRootNode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
clearAll();
|
||||
@@ -74,7 +58,11 @@ public class PlacedSoundState extends BaseAppState {
|
||||
|
||||
@Override protected void onDisable() {}
|
||||
|
||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||
@Override
|
||||
protected void onMarkerSelected(int idx) {
|
||||
selectSound(idx);
|
||||
input.auswahlSoundSelected = true;
|
||||
}
|
||||
|
||||
// ── Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -145,28 +133,6 @@ public class PlacedSoundState extends BaseAppState {
|
||||
selectSound(sounds.size() - 1);
|
||||
}
|
||||
|
||||
private void handleAuswahlClick(SharedInput.ObjectClick click) {
|
||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||
int hit = pickMarker(ray);
|
||||
if (hit >= 0) {
|
||||
selectSound(hit);
|
||||
input.auswahlSoundSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
private int pickMarker(Ray ray) {
|
||||
for (int i = 0; i < markers.size(); i++) {
|
||||
CollisionResults res = new CollisionResults();
|
||||
markers.get(i).collideWith(ray, res);
|
||||
if (res.size() > 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ── Selection ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void selectSound(int idx) {
|
||||
|
||||
@@ -707,6 +707,9 @@ public class SceneObjectState extends BaseAppState {
|
||||
}
|
||||
if (closest == null) { deselectAll(); return; }
|
||||
int idx = findObjectIndexByNode(closest.getGeometry());
|
||||
log.info("[SceneObject] Klick ({},{}): Treffer='{}', idx={}, layer={}",
|
||||
(int)click.screenX(), (int)click.screenY(),
|
||||
closest.getGeometry().getName(), idx, input.activeLayer);
|
||||
if (idx >= 0) {
|
||||
selectObject(idx, click.shift());
|
||||
// Vorgefertigte Objekte (externe Modelle + Gruppen) können nicht sub-selektiert werden.
|
||||
@@ -723,6 +726,11 @@ public class SceneObjectState extends BaseAppState {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Ray traf etwas in objectRoot, aber kein bekanntes Objekt — nicht an
|
||||
// andere Layer-Queues weiterleiten (verhindert versehentliches Emitter-Select).
|
||||
log.info("[SceneObject] Geometrie '{}' in objectRoot aber nicht in objNodes — Klick ignoriert",
|
||||
closest.getGeometry().getName());
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Terrain-Treffer – im Edit-Modus: zurück zu Place-Modus, deselektieren
|
||||
@@ -733,6 +741,8 @@ public class SceneObjectState extends BaseAppState {
|
||||
}
|
||||
// Auswahl-Modus: kein Objekt angeklickt → deselektieren + Zonen/Items prüfen lassen
|
||||
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) {
|
||||
log.info("[SceneObject] LAYER_AUSWAHL-Broadcast bei Klick ({},{}): kein SceneObject getroffen, {} Objekte im objectRoot",
|
||||
(int)click.screenX(), (int)click.screenY(), objNodes.size());
|
||||
deselectAll();
|
||||
float sx = click.screenX(), sy = click.screenY();
|
||||
input.areaClickQueue.offer(new SharedInput.AreaClick(sx, sy, false));
|
||||
|
||||
@@ -51,6 +51,7 @@ public class AnimSetController implements ToolController {
|
||||
private ListView<String> animSetPartsListView;
|
||||
private java.util.Map<String, de.blight.game.animation.AnimSet.SubClipDef>
|
||||
editableSubClips = new java.util.LinkedHashMap<>();
|
||||
private java.util.Set<String> editableSnapClips = new java.util.LinkedHashSet<>();
|
||||
private ListView<String> animSetSequenceListView;
|
||||
private java.util.Map<String, java.util.List<String>>
|
||||
editableSequences = new java.util.LinkedHashMap<>();
|
||||
@@ -76,6 +77,16 @@ public class AnimSetController implements ToolController {
|
||||
animSetCurrentName = setName;
|
||||
animSetCurrentDir = setDir;
|
||||
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
|
||||
@@ -337,17 +348,14 @@ public class AnimSetController implements ToolController {
|
||||
animSetClipListView.setPrefHeight(180);
|
||||
editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips());
|
||||
editableSequences = new java.util.LinkedHashMap<>(animSet.getSequences());
|
||||
// editableSnapClips wird bereits in openWithSet gesetzt (inkl. Migration)
|
||||
ctx.input().animPreviewClipOffsets.clear();
|
||||
for (var en : animSet.getAnimOffsets().entrySet()) {
|
||||
de.blight.game.animation.AnimOffset o = en.getValue();
|
||||
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();
|
||||
animSet.getActionMap().forEach((action, clip) -> {
|
||||
if (locomotionActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip);
|
||||
});
|
||||
ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
|
||||
|
||||
Path animRootDir = ctx.assetRoot().resolve("Characters");
|
||||
|
||||
@@ -454,10 +462,41 @@ public class AnimSetController implements ToolController {
|
||||
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.setHgrow(addClipBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
|
||||
inner.getChildren().addAll(animSetClipListView, clipBtns);
|
||||
inner.getChildren().addAll(animSetClipListView, snapCB, clipBtns);
|
||||
|
||||
// ── Clip-Teile (Sub-Clips) ─────────────────────────────────────────────
|
||||
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();
|
||||
ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
||||
}
|
||||
java.util.Set<String> locoActions = java.util.Set.of(
|
||||
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
|
||||
animSet.setSnapClips(new java.util.LinkedHashSet<>(editableSnapClips));
|
||||
ctx.input().animPreviewLocomotionClips.clear();
|
||||
animSet.getActionMap().forEach((action, clip) -> {
|
||||
if (locoActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip);
|
||||
});
|
||||
ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
|
||||
if (animSetModelCombo != null && animSetModelCombo.getValue() != null
|
||||
&& !animSetModelCombo.getValue().isBlank()) {
|
||||
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
||||
|
||||
@@ -344,6 +344,7 @@ public class ContentController {
|
||||
ctx.setCurrentTool().accept("itemEditor");
|
||||
ToolBar tb = makeSingleWindowToolBar("Item-Manager");
|
||||
ctx.setMainToolbar().accept(tb);
|
||||
ctx.setLeftPanel().accept(buildContentTreePanel());
|
||||
de.blight.editor.ui.ItemEditorView view =
|
||||
new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items"));
|
||||
wireItemView(view);
|
||||
@@ -960,7 +961,7 @@ public class ContentController {
|
||||
|
||||
private void wireItemView(de.blight.editor.ui.ItemEditorView view) {
|
||||
view.setOnIdChanged(this::refreshContentItemTree);
|
||||
view.setGripEditorLauncher((modelPath, currentGrip, onSave) -> {
|
||||
view.setGripEditorLauncher((modelPath, currentGrip, isShield, onSave) -> {
|
||||
String itemId = view.getCurrentItemId();
|
||||
gripEditorController.setOnReturn(() -> {
|
||||
if (dualWindowMode) {
|
||||
@@ -970,7 +971,7 @@ public class ContentController {
|
||||
switchToItemEditor(itemId);
|
||||
}
|
||||
});
|
||||
gripEditorController.open(modelPath, currentGrip, onSave);
|
||||
gripEditorController.open(modelPath, currentGrip, isShield, onSave);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
private Consumer<ItemGrip> pendingOnSave;
|
||||
private Runnable onReturn;
|
||||
private boolean suppressSpinnerEvents = false;
|
||||
private boolean shieldMode = false;
|
||||
|
||||
public void setOnReturn(Runnable r) { this.onReturn = r; }
|
||||
|
||||
@@ -37,11 +38,13 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
// ── GripEditorLauncher ────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public void open(String modelPath, ItemGrip currentGrip, Consumer<ItemGrip> onSave) {
|
||||
public void open(String modelPath, ItemGrip currentGrip, boolean isShield, Consumer<ItemGrip> onSave) {
|
||||
pendingOnSave = onSave;
|
||||
shieldMode = isShield;
|
||||
|
||||
SharedInput input = ctx.input();
|
||||
input.gripEditorActive = true;
|
||||
input.gripEditorActive = true;
|
||||
input.gripEditorIsShield = isShield;
|
||||
|
||||
// Grip-Werte aus Item übernehmen (oder Defaults)
|
||||
ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip();
|
||||
@@ -63,7 +66,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
ctx.setCenterView().accept(ctx.viewportSupplier().get());
|
||||
ctx.setCurrentTool().accept("gripEditor");
|
||||
ctx.setMainToolbar().accept(buildToolbar());
|
||||
ctx.setRightPanel().accept(buildPanel(g));
|
||||
ctx.setRightPanel().accept(buildPanel(g, isShield));
|
||||
ctx.setLeftPanel().accept(null);
|
||||
ctx.setBottomPanel().accept(null);
|
||||
}
|
||||
@@ -82,7 +85,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
|
||||
// ── Panel ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private VBox buildPanel(ItemGrip g) {
|
||||
private VBox buildPanel(ItemGrip g, boolean isShield) {
|
||||
posXSpin = doubleSpin(g.getPosX(), -2.0, 2.0, 0.01, "m");
|
||||
posYSpin = doubleSpin(g.getPosY(), -2.0, 2.0, 0.01, "m");
|
||||
posZSpin = doubleSpin(g.getPosZ(), -2.0, 2.0, 0.01, "m");
|
||||
@@ -123,7 +126,9 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
row("Y:", rotYSpin),
|
||||
row("Z:", rotZSpin),
|
||||
new Separator(),
|
||||
styledHint("Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."),
|
||||
styledHint(isShield
|
||||
? "Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge."
|
||||
: "Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."),
|
||||
new Separator(),
|
||||
saveBtn,
|
||||
cancelBtn
|
||||
@@ -156,7 +161,9 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
grip.setRotY(rotYSpin.getValue().floatValue());
|
||||
grip.setRotZ(rotZSpin.getValue().floatValue());
|
||||
|
||||
ctx.input().gripEditorActive = false;
|
||||
SharedInput input = ctx.input();
|
||||
input.gripEditorActive = false;
|
||||
input.gripEditorIsShield = false;
|
||||
|
||||
Consumer<ItemGrip> save = pendingOnSave;
|
||||
Runnable ret = onReturn;
|
||||
@@ -168,7 +175,8 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher {
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
ctx.input().gripEditorActive = false;
|
||||
ctx.input().gripEditorActive = false;
|
||||
ctx.input().gripEditorIsShield = false;
|
||||
|
||||
Runnable ret = onReturn;
|
||||
pendingOnSave = null;
|
||||
|
||||
@@ -386,7 +386,8 @@ public class ViewportController {
|
||||
private boolean isObjectMode() {
|
||||
return input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT
|
||||
|| input.activeLayer == SharedInput.LAYER_AUSWAHL;
|
||||
|| input.activeLayer == SharedInput.LAYER_AUSWAHL
|
||||
|| input.activeLayer == SharedInput.LAYER_ITEMS_EDIT;
|
||||
}
|
||||
|
||||
private void startEditTimer() {
|
||||
|
||||
@@ -5,8 +5,10 @@ import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.ToolContext;
|
||||
import de.blight.editor.toolset.EditorUiHelper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
/**
|
||||
* Owns the world-zone side panels:
|
||||
@@ -41,15 +43,96 @@ public class WorldPanelController {
|
||||
auswahlDynamicContent.getChildren().add(noSel);
|
||||
return;
|
||||
}
|
||||
// Format: "idx|itemId|x|y|z|rotX|rotY|rotZ"
|
||||
String[] parts = info.split("\\|");
|
||||
String itemId = parts.length > 1 ? parts[1] : "?";
|
||||
float cx = parts.length > 2 ? parseF(parts[2]) : 0f;
|
||||
float cy = parts.length > 3 ? parseF(parts[3]) : 0f;
|
||||
float cz = parts.length > 4 ? parseF(parts[4]) : 0f;
|
||||
float cRotY = parts.length > 6 ? parseF(parts[6]) : 0f;
|
||||
|
||||
Label idLabel = new Label("Item: " + itemId);
|
||||
idLabel.setStyle("-fx-font-weight: bold;");
|
||||
|
||||
Spinner<Double> xSpin = posSpin(cx);
|
||||
Spinner<Double> ySpin = posSpin(cy);
|
||||
Spinner<Double> zSpin = posSpin(cz);
|
||||
Spinner<Double> rotYSpin = rotSpin(cRotY);
|
||||
|
||||
SharedInput input = ctx.input();
|
||||
Runnable sendEdit = () -> {
|
||||
input.itemEditX = xSpin.getValue().floatValue();
|
||||
input.itemEditY = ySpin.getValue().floatValue();
|
||||
input.itemEditZ = zSpin.getValue().floatValue();
|
||||
input.itemEditRotY = rotYSpin.getValue().floatValue();
|
||||
input.itemEditRequest = true;
|
||||
};
|
||||
xSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
ySpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
zSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
rotYSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
|
||||
Button deleteBtn = new Button("Löschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white;");
|
||||
deleteBtn.setOnAction(e -> ctx.input().deleteSelectedItemRequested = true);
|
||||
auswahlDynamicContent.getChildren().addAll(idLabel, deleteBtn);
|
||||
deleteBtn.setOnAction(e -> input.deleteSelectedItemRequested = true);
|
||||
|
||||
auswahlDynamicContent.getChildren().addAll(
|
||||
idLabel,
|
||||
new Separator(),
|
||||
EditorUiHelper.sectionTitle("Position"),
|
||||
spinRow("X:", xSpin), spinRow("Y:", ySpin), spinRow("Z:", zSpin),
|
||||
new Separator(),
|
||||
EditorUiHelper.sectionTitle("Rotation"),
|
||||
spinRow("Y°:", rotYSpin),
|
||||
new Separator(),
|
||||
deleteBtn
|
||||
);
|
||||
}
|
||||
|
||||
private static float parseF(String s) {
|
||||
try { return Float.parseFloat(s); } catch (NumberFormatException e) { return 0f; }
|
||||
}
|
||||
|
||||
private static Spinner<Double> posSpin(float initial) {
|
||||
SpinnerValueFactory.DoubleSpinnerValueFactory vf =
|
||||
new SpinnerValueFactory.DoubleSpinnerValueFactory(-10000, 10000, initial, 0.5);
|
||||
vf.setConverter(new StringConverter<>() {
|
||||
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.2f", v); }
|
||||
@Override public Double fromString(String s) {
|
||||
try { return Double.parseDouble(s.replace(",", ".")); }
|
||||
catch (NumberFormatException e) { return vf.getValue(); }
|
||||
}
|
||||
});
|
||||
Spinner<Double> sp = new Spinner<>(vf);
|
||||
sp.setEditable(true);
|
||||
sp.setMaxWidth(Double.MAX_VALUE);
|
||||
return sp;
|
||||
}
|
||||
|
||||
private static Spinner<Double> rotSpin(float initial) {
|
||||
SpinnerValueFactory.DoubleSpinnerValueFactory vf =
|
||||
new SpinnerValueFactory.DoubleSpinnerValueFactory(-360, 360, initial, 5.0);
|
||||
vf.setConverter(new StringConverter<>() {
|
||||
@Override public String toString(Double v) { return v == null ? "0" : String.format("%.1f°", v); }
|
||||
@Override public Double fromString(String s) {
|
||||
try { return Double.parseDouble(s.replace("°", "").replace(",", ".")); }
|
||||
catch (NumberFormatException e) { return vf.getValue(); }
|
||||
}
|
||||
});
|
||||
Spinner<Double> sp = new Spinner<>(vf);
|
||||
sp.setEditable(true);
|
||||
sp.setMaxWidth(Double.MAX_VALUE);
|
||||
return sp;
|
||||
}
|
||||
|
||||
private static HBox spinRow(String labelText, Spinner<Double> spinner) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(28);
|
||||
HBox.setHgrow(spinner, Priority.ALWAYS);
|
||||
HBox row = new HBox(6, lbl, spinner);
|
||||
row.setAlignment(Pos.CENTER_LEFT);
|
||||
return row;
|
||||
}
|
||||
|
||||
public void updateZonenPanel(String kind) {
|
||||
@@ -321,6 +404,108 @@ public class WorldPanelController {
|
||||
return panel;
|
||||
}
|
||||
|
||||
public VBox buildAuswahlItemEditPanel(String info) {
|
||||
String[] parts = info != null ? info.split("\\|") : new String[0];
|
||||
String itemId = parts.length > 1 ? parts[1] : "?";
|
||||
float cx = parts.length > 2 ? parseF(parts[2]) : 0f;
|
||||
float cy = parts.length > 3 ? parseF(parts[3]) : 0f;
|
||||
float cz = parts.length > 4 ? parseF(parts[4]) : 0f;
|
||||
// Format: idx|itemId|x|y|z|rotX|rotY|rotZ (rotX/rotZ in radians → convert to degrees)
|
||||
float cRotX = parts.length > 5 ? (float) Math.toDegrees(parseF(parts[5])) : 0f;
|
||||
float cRotY = parts.length > 6 ? (float) Math.toDegrees(parseF(parts[6])) : 0f;
|
||||
float cRotZ = parts.length > 7 ? (float) Math.toDegrees(parseF(parts[7])) : 0f;
|
||||
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
|
||||
Label titleLbl = new Label("Item-Pickup");
|
||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
Label idLbl = new Label(itemId);
|
||||
idLbl.setStyle("-fx-text-fill: #555; -fx-font-size: 11;");
|
||||
|
||||
// Werkzeug-Umschalter Verschieben / Rotieren
|
||||
SharedInput input = ctx.input();
|
||||
Button moveBtn = new Button("Verschieben");
|
||||
Button rotateBtn = new Button("Rotieren");
|
||||
moveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
rotateBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
final String TOOL_ON = "-fx-background-color: #2980b9; -fx-text-fill: white; -fx-font-weight: bold;";
|
||||
final String TOOL_OFF = "-fx-background-color: #bdc3c7; -fx-text-fill: #555;";
|
||||
Runnable updateToolStyle = () -> {
|
||||
boolean mv = input.objectEditTool == SharedInput.EDIT_TOOL_MOVE;
|
||||
moveBtn.setStyle(mv ? TOOL_ON : TOOL_OFF);
|
||||
rotateBtn.setStyle(mv ? TOOL_OFF : TOOL_ON);
|
||||
};
|
||||
updateToolStyle.run();
|
||||
moveBtn.setOnAction(e -> {
|
||||
input.objectEditTool = SharedInput.EDIT_TOOL_MOVE;
|
||||
updateToolStyle.run();
|
||||
});
|
||||
rotateBtn.setOnAction(e -> {
|
||||
input.objectEditTool = SharedInput.EDIT_TOOL_ROTATE;
|
||||
updateToolStyle.run();
|
||||
});
|
||||
HBox toolRow = new HBox(6, moveBtn, rotateBtn);
|
||||
toolRow.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox.setHgrow(moveBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(rotateBtn, Priority.ALWAYS);
|
||||
|
||||
Spinner<Double> xSpin = posSpin(cx);
|
||||
Spinner<Double> ySpin = posSpin(cy);
|
||||
Spinner<Double> zSpin = posSpin(cz);
|
||||
Spinner<Double> rotXSpin = rotSpin(cRotX);
|
||||
Spinner<Double> rotYSpin = rotSpin(cRotY);
|
||||
Spinner<Double> rotZSpin = rotSpin(cRotZ);
|
||||
|
||||
Runnable sendEdit = () -> {
|
||||
input.itemEditX = xSpin.getValue().floatValue();
|
||||
input.itemEditY = ySpin.getValue().floatValue();
|
||||
input.itemEditZ = zSpin.getValue().floatValue();
|
||||
input.itemEditRotX = (float) Math.toRadians(rotXSpin.getValue());
|
||||
input.itemEditRotY = (float) Math.toRadians(rotYSpin.getValue());
|
||||
input.itemEditRotZ = (float) Math.toRadians(rotZSpin.getValue());
|
||||
input.itemEditRequest = true;
|
||||
};
|
||||
xSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
ySpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
zSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
rotXSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
rotYSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
rotZSpin.valueProperty().addListener((o, a, b) -> sendEdit.run());
|
||||
|
||||
Button deleteBtn = new Button("Loeschen");
|
||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
deleteBtn.setStyle("-fx-background-color: #c0392b; -fx-text-fill: white; -fx-font-weight: bold;");
|
||||
deleteBtn.setOnAction(e -> input.deleteSelectedItemRequested = true);
|
||||
|
||||
inner.getChildren().addAll(
|
||||
titleLbl, idLbl, new Separator(),
|
||||
toolRow,
|
||||
new Separator(),
|
||||
EditorUiHelper.sectionTitle("Position"),
|
||||
spinRow("X:", xSpin), spinRow("Y:", ySpin), spinRow("Z:", zSpin),
|
||||
new Separator(),
|
||||
EditorUiHelper.sectionTitle("Rotation"),
|
||||
spinRow("X°:", rotXSpin), spinRow("Y°:", rotYSpin), spinRow("Z°:", rotZSpin),
|
||||
new Separator(),
|
||||
deleteBtn,
|
||||
new Separator(),
|
||||
EditorUiHelper.styledHint("Klick auf leere Flaeche"),
|
||||
EditorUiHelper.styledHint(" → Abwaehlen")
|
||||
);
|
||||
|
||||
ScrollPane scroll = new ScrollPane(inner);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scroll.setStyle("-fx-background-color: transparent; -fx-background: transparent;");
|
||||
|
||||
VBox panel = new VBox(scroll);
|
||||
VBox.setVgrow(scroll, Priority.ALWAYS);
|
||||
panel.setPrefWidth(260);
|
||||
panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;");
|
||||
return panel;
|
||||
}
|
||||
|
||||
public VBox buildWegePanel() {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
|
||||
@@ -14,7 +14,6 @@ import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class AreaEditorView extends BorderPane {
|
||||
@@ -234,50 +233,11 @@ public class AreaEditorView extends BorderPane {
|
||||
|
||||
// ── Localisation defaults ──────────────────────────────────────────────────
|
||||
|
||||
private void writeDefaultLocalisation(String fullId) {
|
||||
private static void writeDefaultLocalisation(String fullId) {
|
||||
if (fullId == null || fullId.isBlank()) return;
|
||||
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
|
||||
if (shortId.isBlank()) return;
|
||||
String key = fullId + ".name";
|
||||
String defaultName = EditorNaming.toDisplayName(shortId);
|
||||
|
||||
Path langSrc = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang");
|
||||
Path[] langMirrors = {
|
||||
ProjectRoot.resolve("blight-lang", "bin", "main", "lang"),
|
||||
ProjectRoot.resolve("blight-lang", "build", "resources", "main", "lang"),
|
||||
};
|
||||
for (String locale : new String[]{"de", "en"}) {
|
||||
Path src = langSrc.resolve("messages_" + locale + ".properties");
|
||||
try {
|
||||
writeOrUpdateKey(src, key, defaultName);
|
||||
for (Path mirrorDir : langMirrors) {
|
||||
Path mirror = mirrorDir.resolve("messages_" + locale + ".properties");
|
||||
if (Files.exists(mirror)) {
|
||||
Files.copy(src, mirror, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// non-critical
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeOrUpdateKey(Path file, String key, String value) throws IOException {
|
||||
if (!Files.exists(file)) return;
|
||||
String needle = key + "=";
|
||||
String needleSpace = key + " =";
|
||||
java.util.List<String> lines = new java.util.ArrayList<>(Files.readAllLines(file, StandardCharsets.UTF_8));
|
||||
boolean found = false;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (lines.get(i).stripLeading().startsWith(needle) || lines.get(i).stripLeading().startsWith(needleSpace)) {
|
||||
lines.set(i, key + "=" + value);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) lines.add(key + "=" + value);
|
||||
Files.writeString(file, String.join("\n", lines) + "\n",
|
||||
StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
LangDefaults.writeOrUpdate(fullId + ".name", EditorNaming.toDisplayName(shortId));
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -48,6 +48,7 @@ public class ItemEditorView extends BorderPane {
|
||||
private Spinner<Integer> goldSpinner;
|
||||
private Spinner<Integer> damageSpinner;
|
||||
private VBox weaponSection;
|
||||
private VBox shieldSection;
|
||||
private TextField modelRefField;
|
||||
private VBox formContainer;
|
||||
private CheckBox consumableCheck;
|
||||
@@ -55,7 +56,7 @@ public class ItemEditorView extends BorderPane {
|
||||
private VBox effectsRows;
|
||||
|
||||
public interface GripEditorLauncher {
|
||||
void open(String modelPath, ItemGrip currentGrip, java.util.function.Consumer<ItemGrip> onSave);
|
||||
void open(String modelPath, ItemGrip currentGrip, boolean isShield, java.util.function.Consumer<ItemGrip> onSave);
|
||||
}
|
||||
|
||||
private Runnable onIdChanged;
|
||||
@@ -156,7 +157,6 @@ public class ItemEditorView extends BorderPane {
|
||||
if (wasFocused && !isFocused) onIdCommitted();
|
||||
});
|
||||
idField.setOnAction(e -> onIdCommitted());
|
||||
idField.textProperty().addListener((obs, o, n) -> scheduleSave());
|
||||
|
||||
catCombo = new ComboBox<>();
|
||||
catCombo.getItems().addAll(ItemCategory.values());
|
||||
@@ -246,6 +246,20 @@ public class ItemEditorView extends BorderPane {
|
||||
weaponSection.setVisible(false);
|
||||
weaponSection.setManaged(false);
|
||||
|
||||
// ── Schild-Sektion ────────────────────────────────────────────────────
|
||||
Button shieldGripBtn = new Button("Unterarm-Griff definieren...");
|
||||
shieldGripBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
shieldGripBtn.setStyle("-fx-background-color: #2a4a5a; -fx-text-fill: #aaddff; -fx-cursor: hand;");
|
||||
shieldGripBtn.setOnAction(e -> openShieldGripEditor());
|
||||
|
||||
shieldSection = new VBox(6,
|
||||
new Separator(),
|
||||
sectionTitle("Schild-Griff"),
|
||||
shieldGripBtn
|
||||
);
|
||||
shieldSection.setVisible(false);
|
||||
shieldSection.setManaged(false);
|
||||
|
||||
modelRefField = new TextField();
|
||||
modelRefField.setPromptText("Modell-Pfad (z.B. Models/Items/sword.j3o)");
|
||||
modelRefField.setEditable(false);
|
||||
@@ -300,6 +314,7 @@ public class ItemEditorView extends BorderPane {
|
||||
row("Wert (Gold):", goldSpinner),
|
||||
row("Modell:", modelFullRow),
|
||||
weaponSection,
|
||||
shieldSection,
|
||||
new Separator(),
|
||||
consumableCheck,
|
||||
consumablesSection
|
||||
@@ -310,7 +325,16 @@ public class ItemEditorView extends BorderPane {
|
||||
private void openGripEditor() {
|
||||
if (current == null || gripEditorLauncher == null) return;
|
||||
String modelPath = modelRefField.getText().trim();
|
||||
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), grip -> {
|
||||
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), false, grip -> {
|
||||
current.setGrip(grip);
|
||||
persistCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
private void openShieldGripEditor() {
|
||||
if (current == null || gripEditorLauncher == null) return;
|
||||
String modelPath = modelRefField.getText().trim();
|
||||
gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), true, grip -> {
|
||||
current.setGrip(grip);
|
||||
persistCurrent();
|
||||
});
|
||||
@@ -319,10 +343,12 @@ public class ItemEditorView extends BorderPane {
|
||||
private void updateWeaponSectionVisibility() {
|
||||
ItemCategory cat = catCombo.getValue();
|
||||
ItemSubCategory sub = subCatCombo.getValue();
|
||||
boolean isWeapon = cat == ItemCategory.WEAPON
|
||||
&& sub != ItemSubCategory.SHIELD;
|
||||
boolean isShield = cat == ItemCategory.WEAPON && sub == ItemSubCategory.SHIELD;
|
||||
boolean isWeapon = cat == ItemCategory.WEAPON && !isShield;
|
||||
weaponSection.setVisible(isWeapon);
|
||||
weaponSection.setManaged(isWeapon);
|
||||
shieldSection.setVisible(isShield);
|
||||
shieldSection.setManaged(isShield);
|
||||
}
|
||||
|
||||
// ── Modell-Auswahl ────────────────────────────────────────────────────────
|
||||
@@ -333,14 +359,29 @@ public class ItemEditorView extends BorderPane {
|
||||
}
|
||||
|
||||
private void onIdCommitted() {
|
||||
String id = idField.getText().trim();
|
||||
if (id.isBlank()) return;
|
||||
nameLabel.setText(id + ".name");
|
||||
descLabel.setText(id + ".description");
|
||||
scheduleSave();
|
||||
String newId = idField.getText().trim();
|
||||
if (newId.isBlank()) return;
|
||||
nameLabel.setText(newId + ".name");
|
||||
descLabel.setText(newId + ".description");
|
||||
// Alte Datei löschen, wenn die ID geändert wurde
|
||||
if (current != null) {
|
||||
String oldId = current.getItemId();
|
||||
if (oldId != null && !oldId.isBlank() && !oldId.equals(newId)) {
|
||||
try { ItemIO.delete(oldId, itemDir); } catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
persistCurrent();
|
||||
writeDefaultLocalisation(newId);
|
||||
if (onIdChanged != null) onIdChanged.run();
|
||||
}
|
||||
|
||||
// ── Localisation defaults ─────────────────────────────────────────────────
|
||||
|
||||
private static void writeDefaultLocalisation(String itemId) {
|
||||
if (itemId == null || itemId.isBlank()) return;
|
||||
LangDefaults.writeIfAbsent(itemId + ".name", EditorNaming.toDisplayName(itemId));
|
||||
}
|
||||
|
||||
// ── Auto-save ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void scheduleSave() {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.editor.ProjectRoot;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Schreibt Default-Einträge in die Sprachdateien (messages_de/en.properties)
|
||||
* inklusive der Build-Mirror-Verzeichnisse.
|
||||
*/
|
||||
public final class LangDefaults {
|
||||
|
||||
private static final Path LANG_SRC = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang");
|
||||
private static final Path[] MIRRORS = {
|
||||
ProjectRoot.resolve("blight-lang", "bin", "main", "lang"),
|
||||
ProjectRoot.resolve("blight-lang", "build", "resources", "main", "lang"),
|
||||
};
|
||||
private static final String[] LOCALES = {"de", "en"};
|
||||
|
||||
private LangDefaults() {}
|
||||
|
||||
/**
|
||||
* Fügt {@code key=value} in alle Locale-Dateien ein, sofern der Schlüssel
|
||||
* dort noch nicht vorhanden ist. Bestehende Übersetzungen werden nicht
|
||||
* überschrieben.
|
||||
*/
|
||||
public static void writeIfAbsent(String key, String value) {
|
||||
forEach(key, value, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt {@code key=value} in alle Locale-Dateien; überschreibt einen
|
||||
* vorhandenen Eintrag oder hängt ihn ans Ende, falls er fehlt.
|
||||
*/
|
||||
public static void writeOrUpdate(String key, String value) {
|
||||
forEach(key, value, true);
|
||||
}
|
||||
|
||||
// ── intern ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static void forEach(String key, String value, boolean overwrite) {
|
||||
for (String locale : LOCALES) {
|
||||
Path src = LANG_SRC.resolve("messages_" + locale + ".properties");
|
||||
try {
|
||||
writeToFile(src, key, value, overwrite);
|
||||
for (Path mirrorDir : MIRRORS) {
|
||||
Path mirror = mirrorDir.resolve("messages_" + locale + ".properties");
|
||||
if (Files.exists(mirror)) {
|
||||
Files.copy(src, mirror, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeToFile(Path file, String key, String value, boolean overwrite)
|
||||
throws IOException {
|
||||
if (!Files.exists(file)) return;
|
||||
String needle = key + "=";
|
||||
String needleSpace = key + " =";
|
||||
List<String> lines = new ArrayList<>(Files.readAllLines(file, StandardCharsets.UTF_8));
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String stripped = lines.get(i).stripLeading();
|
||||
if (stripped.startsWith(needle) || stripped.startsWith(needleSpace)) {
|
||||
if (!overwrite) return;
|
||||
lines.set(i, key + "=" + value);
|
||||
Files.writeString(file, String.join("\n", lines) + "\n",
|
||||
StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
return;
|
||||
}
|
||||
}
|
||||
lines.add(key + "=" + value);
|
||||
Files.writeString(file, String.join("\n", lines) + "\n",
|
||||
StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package de.blight.editor.ui;
|
||||
import de.blight.common.LocationIO;
|
||||
import de.blight.common.model.Location;
|
||||
import de.blight.common.model.TextReference;
|
||||
import de.blight.editor.ProjectRoot;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -15,8 +14,6 @@ import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.List;
|
||||
|
||||
public class LocationEditorView extends BorderPane {
|
||||
@@ -194,50 +191,11 @@ public class LocationEditorView extends BorderPane {
|
||||
|
||||
// ── Localisation defaults ──────────────────────────────────────────────────
|
||||
|
||||
private void writeDefaultLocalisation(String fullId) {
|
||||
private static void writeDefaultLocalisation(String fullId) {
|
||||
if (fullId == null || fullId.isBlank()) return;
|
||||
String shortId = fullId.startsWith(PREFIX) ? fullId.substring(PREFIX.length()) : fullId;
|
||||
if (shortId.isBlank()) return;
|
||||
String key = fullId + ".name";
|
||||
String defaultName = EditorNaming.toDisplayName(shortId);
|
||||
|
||||
Path langSrc = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang");
|
||||
Path[] langMirrors = {
|
||||
ProjectRoot.resolve("blight-lang", "bin", "main", "lang"),
|
||||
ProjectRoot.resolve("blight-lang", "build", "resources", "main", "lang"),
|
||||
};
|
||||
for (String locale : new String[]{"de", "en"}) {
|
||||
Path src = langSrc.resolve("messages_" + locale + ".properties");
|
||||
try {
|
||||
writeOrUpdateKey(src, key, defaultName);
|
||||
for (Path mirrorDir : langMirrors) {
|
||||
Path mirror = mirrorDir.resolve("messages_" + locale + ".properties");
|
||||
if (Files.exists(mirror)) {
|
||||
Files.copy(src, mirror, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// non-critical
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeOrUpdateKey(Path file, String key, String value) throws IOException {
|
||||
if (!Files.exists(file)) return;
|
||||
String needle = key + "=";
|
||||
String needleSpace = key + " =";
|
||||
java.util.List<String> lines = new java.util.ArrayList<>(Files.readAllLines(file, StandardCharsets.UTF_8));
|
||||
boolean found = false;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (lines.get(i).stripLeading().startsWith(needle) || lines.get(i).stripLeading().startsWith(needleSpace)) {
|
||||
lines.set(i, key + "=" + value);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) lines.add(key + "=" + value);
|
||||
Files.writeString(file, String.join("\n", lines) + "\n",
|
||||
StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
LangDefaults.writeOrUpdate(fullId + ".name", EditorNaming.toDisplayName(shortId));
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -38,12 +38,36 @@ public class AnimPlayer {
|
||||
private LinearBlendSpace blendSpace;
|
||||
private String pendingClip;
|
||||
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) {
|
||||
this.composer = composer;
|
||||
this.skinning = skinning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt einen Clip rückwärts ab (speed=-1, startet am Ende).
|
||||
* Geeignet für IDLE_TO_BLOCK rückwärts beim Loslassen der Blocktaste.
|
||||
*/
|
||||
public boolean playReversed(String clipName, float transition) {
|
||||
this.pendingReversed = true;
|
||||
return play(clipName, transition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spielt einen Clip ab, optional ab einem Zeitversatz (z.B. Ausholen überspringen).
|
||||
*
|
||||
* @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.
|
||||
* Bei laufendem Clip und {@code transition > 0}: nahtloser Übergang
|
||||
@@ -82,6 +106,7 @@ public class AnimPlayer {
|
||||
blendSpace = space;
|
||||
pendingClip = clipName;
|
||||
blendElapsed = 0f;
|
||||
blendDuration = Math.max(0.01f, transition);
|
||||
currentAction = blend;
|
||||
currentClip = null;
|
||||
log.debug("[AnimPlayer] Blend → '{}'", clipName);
|
||||
@@ -101,7 +126,7 @@ public class AnimPlayer {
|
||||
public void update(float tpf) {
|
||||
if (pendingClip == null || blendSpace == null) return;
|
||||
blendElapsed += tpf;
|
||||
float bt = Math.min(1f, blendElapsed / BLEND_DURATION);
|
||||
float bt = Math.min(1f, blendElapsed / blendDuration);
|
||||
blendSpace.setValue(bt);
|
||||
if (bt >= 1f) finishBlend();
|
||||
}
|
||||
@@ -129,13 +154,40 @@ public class AnimPlayer {
|
||||
|
||||
private boolean directPlay(String clipName) {
|
||||
try {
|
||||
Action act = composer.action(clipName);
|
||||
if (act instanceof BlendableAction ba) ba.setTransitionLength(0.0);
|
||||
// Diagnose: zeigt warum length=0 entsteht
|
||||
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);
|
||||
currentClip = clipName;
|
||||
pendingClip = 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;
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimPlayer] direktPlay('{}') fehlgeschlagen: {}", clipName, e.getMessage());
|
||||
@@ -153,16 +205,20 @@ public class AnimPlayer {
|
||||
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
Action next = composer.action(clip);
|
||||
if (next instanceof BlendableAction ba) ba.setTransitionLength(0.0);
|
||||
currentAction = composer.setCurrentAction(clip);
|
||||
currentClip = clip;
|
||||
double len = currentAction != null ? currentAction.getLength() : 0.0;
|
||||
// AnimLayer.setTime: kein extra controlUpdate, kein loop=false-Seiteneffekt.
|
||||
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
|
||||
if (len > 0.01 && elapsed > 0) {
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(elapsed % len);
|
||||
if (pendingReversed && currentAction != null && len > 0.01) {
|
||||
currentAction.setSpeed(-1f);
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime((float) len);
|
||||
pendingReversed = false;
|
||||
} else if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) {
|
||||
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
|
||||
float t = (pendingStartOffset + elapsed) % (float) len;
|
||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t);
|
||||
pendingStartOffset = 0f;
|
||||
}
|
||||
pendingReversed = false;
|
||||
log.debug("[AnimPlayer] finishBlend → '{}'", clip);
|
||||
} catch (Exception e) {
|
||||
log.warn("[AnimPlayer] finishBlend('{}') fehlgeschlagen: {}", clip, e.getMessage());
|
||||
|
||||
@@ -7,8 +7,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 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<>();
|
||||
/** Ablauf-Definitionen: Name → geordnete Liste von Aktions-Namen (aus actionMap). */
|
||||
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. */
|
||||
public static class SubClipDef {
|
||||
@@ -60,6 +64,10 @@ public class AnimSet {
|
||||
return sequences != null ? sequences : new LinkedHashMap<>();
|
||||
}
|
||||
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}. */
|
||||
public void save(Path setDir, String setName) throws IOException {
|
||||
|
||||
@@ -34,18 +34,21 @@ public enum AnimationAction {
|
||||
|
||||
// ── Schwert ───────────────────────────────────────────────────────────────
|
||||
SWORD_ATTACK_1,
|
||||
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN,
|
||||
SWORD_ATTACK_2, SWORD_HEAVY_ATTACK,
|
||||
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK,
|
||||
SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,
|
||||
SWORD_IDLE_TO_BLOCK, SWORD_COUNTERATTACK, SWORD_BLOCK_IMPACT, SWORD_IDLE_IMPACT,
|
||||
SWORD_IDLE, SWORD_WALK, SWORD_RUN,
|
||||
|
||||
// ── Axt ───────────────────────────────────────────────────────────────────
|
||||
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_HIT_TAKEN,
|
||||
AXE_ATTACK_1, AXE_ATTACK_2, AXE_HEAVY_ATTACK,
|
||||
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK,
|
||||
AXE_ATTACK_1, AXE_ATTACK_2, AXE_ATTACK_3, AXE_HEAVY_ATTACK,
|
||||
AXE_IDLE_TO_BLOCK, AXE_COUNTERATTACK, AXE_BLOCK_IMPACT, AXE_IDLE_IMPACT,
|
||||
AXE_IDLE, AXE_WALK, AXE_RUN,
|
||||
|
||||
// ── Zweihänder ────────────────────────────────────────────────────────────
|
||||
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_HIT_TAKEN,
|
||||
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_HEAVY_ATTACK,
|
||||
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK,
|
||||
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK,
|
||||
TWO_HAND_IDLE_TO_BLOCK, TWO_HAND_COUNTERATTACK, TWO_HAND_BLOCK_IMPACT, TWO_HAND_IDLE_IMPACT,
|
||||
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
|
||||
|
||||
// ── Bogen ─────────────────────────────────────────────────────────────────
|
||||
@@ -96,32 +99,44 @@ public enum AnimationAction {
|
||||
case SWORD_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Schwert)");
|
||||
case SWORD_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Schwert)");
|
||||
case SWORD_BLOCK -> TextRegistry.resolve(null, key, "Block (Schwert)");
|
||||
case SWORD_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
|
||||
case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)");
|
||||
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
|
||||
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
||||
case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
|
||||
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
|
||||
case SWORD_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Schwert)");
|
||||
case SWORD_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Schwert)");
|
||||
case SWORD_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Schwert)");
|
||||
case SWORD_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
|
||||
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
||||
case SWORD_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)");
|
||||
case SWORD_RUN -> TextRegistry.resolve(null, key, "Laufen (Schwert)");
|
||||
|
||||
case AXE_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Axt)");
|
||||
case AXE_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Axt)");
|
||||
case AXE_BLOCK -> TextRegistry.resolve(null, key, "Block (Axt)");
|
||||
case AXE_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Axt)");
|
||||
case AXE_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Axt)");
|
||||
case AXE_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Axt)");
|
||||
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
|
||||
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
|
||||
case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)");
|
||||
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
|
||||
case AXE_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Axt)");
|
||||
case AXE_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Axt)");
|
||||
case AXE_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Axt)");
|
||||
case AXE_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Axt)");
|
||||
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
|
||||
case AXE_WALK -> TextRegistry.resolve(null, key, "Gehen (Axt)");
|
||||
case AXE_RUN -> TextRegistry.resolve(null, key, "Laufen (Axt)");
|
||||
|
||||
case TWO_HAND_DRAW -> TextRegistry.resolve(null, key, "Ziehen (Zweihänder)");
|
||||
case TWO_HAND_SHEATHE -> TextRegistry.resolve(null, key, "Wegstecken (Zweihänder)");
|
||||
case TWO_HAND_BLOCK -> TextRegistry.resolve(null, key, "Block (Zweihänder)");
|
||||
case TWO_HAND_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
|
||||
case TWO_HAND_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Zweihänder)");
|
||||
case TWO_HAND_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Zweihänder)");
|
||||
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
|
||||
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
|
||||
case TWO_HAND_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Zweihänder)");
|
||||
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
|
||||
case TWO_HAND_IDLE_TO_BLOCK -> TextRegistry.resolve(null, key, "Idle → Block (Zweihänder)");
|
||||
case TWO_HAND_COUNTERATTACK -> TextRegistry.resolve(null, key, "Konter (Zweihänder)");
|
||||
case TWO_HAND_BLOCK_IMPACT -> TextRegistry.resolve(null, key, "Block-Treffer (Zweihänder)");
|
||||
case TWO_HAND_IDLE_IMPACT -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
|
||||
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
|
||||
case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)");
|
||||
case TWO_HAND_RUN -> TextRegistry.resolve(null, key, "Laufen (Zweihänder)");
|
||||
|
||||
|
||||
@@ -43,10 +43,6 @@ public class AnimationLibrary extends BaseAppState {
|
||||
/** Clips die snapRootBoneXZ erhalten (Locomotion: Walk, Run, Sprint, …). */
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -103,6 +99,14 @@ public class AnimationLibrary extends BaseAppState {
|
||||
|
||||
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;
|
||||
if (srcArm != null && srcArm != sc.getArmature()) {
|
||||
// Immer retarget() aufrufen – auch bei gleichen Knochen-Namen.
|
||||
@@ -131,8 +135,10 @@ public class AnimationLibrary extends BaseAppState {
|
||||
}
|
||||
|
||||
ac.addAnimClip(target);
|
||||
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
||||
if (clipName.equals("sit_down")) {
|
||||
int trackCount = target.getTracks() != null ? target.getTracks().length : 0;
|
||||
log.info("[AnimLib] Clip '{}' → AnimComposer '{}': length={}s tracks={}",
|
||||
clipName, model.getName(), target.getLength(), trackCount);
|
||||
if (clipName.equals("sit_down") || target.getLength() == 0) {
|
||||
dumpClipTracks(target);
|
||||
}
|
||||
return true;
|
||||
@@ -221,10 +227,8 @@ public class AnimationLibrary extends BaseAppState {
|
||||
snapClipNames.clear();
|
||||
try {
|
||||
AnimSet set = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName);
|
||||
set.getActionMap().forEach((action, clip) -> {
|
||||
if (LOCOMOTION_ACTIONS.contains(action)) snapClipNames.add(clip);
|
||||
});
|
||||
log.info("[AnimLib] snapRootBoneXZ für Locomotion-Clips: {}", snapClipNames);
|
||||
snapClipNames.addAll(set.getSnapClips());
|
||||
log.info("[AnimLib] snapRootBoneXZ für Clips: {}", snapClipNames);
|
||||
} catch (Exception e) {
|
||||
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) {
|
||||
String fileName = file.getFileName().toString();
|
||||
String clipName = fileName.replaceFirst("\\.j3o$", "");
|
||||
String assetKey = "Characters/clips/" + fileName;
|
||||
String fileName = file.getFileName().toString();
|
||||
String fileClipKey = fileName.replaceFirst("\\.j3o$", "");
|
||||
String assetKey = "Characters/clips/" + fileName;
|
||||
|
||||
try {
|
||||
Spatial loaded = assetManager.loadModel(assetKey);
|
||||
@@ -334,11 +338,37 @@ public class AnimationLibrary extends BaseAppState {
|
||||
}
|
||||
Armature armature = sc != null ? sc.getArmature() : null;
|
||||
|
||||
AnimClip bestClip = null;
|
||||
for (String name : ac.getAnimClipsNames()) {
|
||||
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
|
||||
clips.put(name, animClip);
|
||||
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) {
|
||||
log.warn("[AnimLib] Fehler beim Laden von {}: {}", assetKey, e.getMessage());
|
||||
|
||||
@@ -81,11 +81,15 @@ public final class RetargetingSystem {
|
||||
if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint j)
|
||||
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;
|
||||
for (String name : nameMap.keySet()) {
|
||||
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) {
|
||||
log.warn("[Retarget] Keine Keyframe-Zeiten für '{}'", sourceClip.getName());
|
||||
@@ -398,18 +402,10 @@ public final class RetargetingSystem {
|
||||
|
||||
// ── 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) {
|
||||
for (var e : nameMap.entrySet()) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -34,10 +34,20 @@ public class PlayerInputControl {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
||||
|
||||
private static final float MOVE_SPEED = 0.07f;
|
||||
private static final float SPRINT_MULT = 1.5f;
|
||||
private static final float WALK_MULT = 0.5f;
|
||||
private static final float ROTATE_SPEED = 10f;
|
||||
private static final float MOVE_SPEED = 0.07f;
|
||||
private static final float SPRINT_MULT = 1.5f;
|
||||
private static final float WALK_MULT = 0.5f;
|
||||
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_RANGED = "DrawRangedWeapon";
|
||||
@@ -59,16 +69,27 @@ public class PlayerInputControl {
|
||||
private boolean forward, backward, left, right, sprint, walk;
|
||||
private boolean paused = false;
|
||||
|
||||
private enum BlockPhase { NONE, WAITING, ENTERING, HOLDING, EXITING }
|
||||
|
||||
// ── Kampf-Zustand ─────────────────────────────────────────────────────────
|
||||
private MainCharacter mainCharacter = null;
|
||||
private boolean weaponDrawn = false;
|
||||
private ItemSubCategory drawnWeaponType = null;
|
||||
private boolean isAiming = false;
|
||||
private boolean meleeAttackToggle = false;
|
||||
private boolean rightMouseHeld = false;
|
||||
private boolean combatAnimActive = false;
|
||||
private float combatAnimRemaining = 0f;
|
||||
private Runnable combatAnimCallback = null;
|
||||
private BlockPhase blockPhase = BlockPhase.NONE;
|
||||
private float blockPhaseTimer = 0f;
|
||||
private boolean combatAnimActive = false;
|
||||
private float combatAnimRemaining = 0f;
|
||||
private Runnable combatAnimCallback = null;
|
||||
private boolean combatAnimSkipComboWindow = false;
|
||||
/** Aktueller Schritt in der Kombo-Sequenz (0–3): 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK3-kritisch. */
|
||||
private int comboStep = 0;
|
||||
/** Zeitfenster nach einem Angriff, in dem der nächste Kombo-Schlag akzeptiert wird. */
|
||||
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 String animSetName;
|
||||
@@ -121,6 +142,8 @@ public class PlayerInputControl {
|
||||
private int jumpFrames = 0;
|
||||
private int groundGraceFrames = 0;
|
||||
private double nextTransitionLength = 0.0;
|
||||
private float nextStartOffset = 0f;
|
||||
private boolean nextPlayReversed = false;
|
||||
|
||||
private boolean pickupActive = false;
|
||||
private float pickupRemaining = 0f;
|
||||
@@ -241,6 +264,7 @@ public class PlayerInputControl {
|
||||
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
|
||||
public boolean isWeaponDrawn() { return weaponDrawn; }
|
||||
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
||||
public boolean isLastAttackCritical() { return lastAttackCritical; }
|
||||
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
||||
|
||||
public boolean isPaused() { return paused; }
|
||||
@@ -253,8 +277,11 @@ public class PlayerInputControl {
|
||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
stopWading();
|
||||
rightMouseHeld = false;
|
||||
blockPhase = BlockPhase.NONE;
|
||||
combatAnimActive = false;
|
||||
combatAnimCallback = null;
|
||||
comboStep = 0;
|
||||
comboWindowActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,20 +684,45 @@ public class PlayerInputControl {
|
||||
// Kampf-Anim-Timer
|
||||
if (combatAnimActive) {
|
||||
combatAnimRemaining -= tpf;
|
||||
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||
if (combatAnimRemaining <= 0f) {
|
||||
combatAnimActive = false;
|
||||
currentAnim = null;
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
Runnable cb = combatAnimCallback;
|
||||
combatAnimCallback = null;
|
||||
boolean skipWindow = combatAnimSkipComboWindow;
|
||||
combatAnimSkipComboWindow = false;
|
||||
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
|
||||
if (jumpFrames > 0) jumpFrames--;
|
||||
if (groundGraceFrames > 0) groundGraceFrames--;
|
||||
|
||||
if (!combatAnimActive) {
|
||||
// Block-Phase verwaltet ihre eigenen Animationen.
|
||||
if (!combatAnimActive && !isBlocking()) {
|
||||
AnimationAction target;
|
||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||
@@ -684,10 +736,6 @@ public class PlayerInputControl {
|
||||
|
||||
target = resolveWeaponAction(target);
|
||||
|
||||
if (weaponDrawn && rightMouseHeld && isMeleeType(drawnWeaponType)) {
|
||||
target = getMeleeBlockAction(drawnWeaponType);
|
||||
}
|
||||
|
||||
if (target != currentAnim) {
|
||||
playAction(target);
|
||||
currentAnim = target;
|
||||
@@ -707,7 +755,8 @@ public class PlayerInputControl {
|
||||
}
|
||||
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
||||
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 (action != AnimationAction.DEFAULT) {
|
||||
String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT);
|
||||
@@ -755,6 +804,7 @@ public class PlayerInputControl {
|
||||
if (drawnWeaponType == null) { weaponDrawn = false; return; }
|
||||
isAiming = false;
|
||||
rightMouseHeld = false;
|
||||
blockPhase = BlockPhase.NONE;
|
||||
AnimationAction sheathAnim = getSheathAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(sheathAnim, 0.5f);
|
||||
requestAnimation(sheathAnim, dur, () -> {
|
||||
@@ -765,16 +815,25 @@ public class PlayerInputControl {
|
||||
}
|
||||
|
||||
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)) {
|
||||
AnimationAction atk = meleeAttackToggle
|
||||
? getMeleeAttack2Action(drawnWeaponType)
|
||||
: getMeleeAttack1Action(drawnWeaponType);
|
||||
meleeAttackToggle = !meleeAttackToggle;
|
||||
int step = comboStep;
|
||||
boolean isComboFollow = comboWindowActive; // war Kombo-Fenster offen?
|
||||
AnimationAction atk = getMeleeComboAction(drawnWeaponType, step);
|
||||
lastAttackCritical = (step == 3);
|
||||
comboStep = (step + 1) % 4;
|
||||
comboWindowActive = false;
|
||||
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;
|
||||
// Blend-out zu Idle startet BLEND_DURATION früher → kein sichtbares Einfrieren am letzten Frame
|
||||
combatAnimRemaining = Math.max(0.1f, dur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
||||
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
|
||||
combatAnimCallback = null;
|
||||
playAction(atk);
|
||||
currentAnim = atk;
|
||||
@@ -785,14 +844,16 @@ public class PlayerInputControl {
|
||||
float fireDur = resolveClipLength(fireAnim, 0.5f);
|
||||
float reloadDur = resolveClipLength(reloadAnim, 0.8f);
|
||||
combatAnimActive = true;
|
||||
combatAnimRemaining = Math.max(0.1f, fireDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
||||
combatAnimRemaining = Math.max(0.1f, fireDur - COMBAT_BLEND);
|
||||
combatAnimCallback = () -> {
|
||||
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;
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
playAction(reloadAnim);
|
||||
currentAnim = reloadAnim;
|
||||
};
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
playAction(fireAnim);
|
||||
currentAnim = fireAnim;
|
||||
}
|
||||
@@ -801,12 +862,103 @@ public class PlayerInputControl {
|
||||
private void handleCombatAlt(boolean isPressed) {
|
||||
if (!weaponDrawn || blockingAnimActive) return;
|
||||
if (isMeleeType(drawnWeaponType)) {
|
||||
rightMouseHeld = isPressed;
|
||||
if (isPressed) {
|
||||
if (blockPhase == BlockPhase.NONE) {
|
||||
blockPhase = BlockPhase.WAITING;
|
||||
blockPhaseTimer = 0f;
|
||||
}
|
||||
} else {
|
||||
switch (blockPhase) {
|
||||
case WAITING -> {
|
||||
blockPhase = BlockPhase.NONE;
|
||||
playCounterAttack();
|
||||
}
|
||||
case ENTERING, HOLDING -> startBlockExit();
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
} else if (isRangedType(drawnWeaponType) && isPressed) {
|
||||
isAiming = !isAiming;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBlockPhase(float tpf) {
|
||||
switch (blockPhase) {
|
||||
case WAITING -> {
|
||||
blockPhaseTimer += tpf;
|
||||
if (blockPhaseTimer >= BLOCK_TAP_MAX) {
|
||||
if (hasShieldEquipped()) {
|
||||
startBlockEnter();
|
||||
} else {
|
||||
blockPhase = BlockPhase.NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
case ENTERING -> {
|
||||
blockPhaseTimer -= tpf;
|
||||
if (blockPhaseTimer <= 0f) {
|
||||
blockPhase = BlockPhase.HOLDING;
|
||||
AnimationAction blockAct = getMeleeBlockAction(drawnWeaponType);
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
playAction(blockAct);
|
||||
currentAnim = blockAct;
|
||||
}
|
||||
}
|
||||
case EXITING -> {
|
||||
blockPhaseTimer -= tpf;
|
||||
if (blockPhaseTimer <= 0f) {
|
||||
blockPhase = BlockPhase.NONE;
|
||||
currentAnim = null;
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private void startBlockEnter() {
|
||||
blockPhase = BlockPhase.ENTERING;
|
||||
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(idleToBlock, 0.5f);
|
||||
blockPhaseTimer = dur;
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
playAction(idleToBlock);
|
||||
currentAnim = idleToBlock;
|
||||
}
|
||||
|
||||
private void startBlockExit() {
|
||||
blockPhase = BlockPhase.EXITING;
|
||||
AnimationAction idleToBlock = getMeleeIdleToBlockAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(idleToBlock, 0.5f);
|
||||
blockPhaseTimer = dur;
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
nextPlayReversed = true;
|
||||
playAction(idleToBlock);
|
||||
currentAnim = idleToBlock;
|
||||
}
|
||||
|
||||
private void playCounterAttack() {
|
||||
if (combatAnimActive) return;
|
||||
AnimationAction counterAct = getMeleeCounterAttackAction(drawnWeaponType);
|
||||
float dur = resolveClipLength(counterAct, 0.7f);
|
||||
nextTransitionLength = COMBAT_BLEND;
|
||||
combatAnimActive = true;
|
||||
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
|
||||
combatAnimCallback = null;
|
||||
combatAnimSkipComboWindow = true;
|
||||
playAction(counterAct);
|
||||
currentAnim = counterAct;
|
||||
}
|
||||
|
||||
public boolean isBlocking() {
|
||||
return blockPhase == BlockPhase.ENTERING
|
||||
|| blockPhase == BlockPhase.HOLDING
|
||||
|| blockPhase == BlockPhase.EXITING;
|
||||
}
|
||||
|
||||
private boolean hasShieldEquipped() {
|
||||
return mainCharacter != null && mainCharacter.getEquippedShield() != null;
|
||||
}
|
||||
|
||||
private AnimationAction resolveWeaponAction(AnimationAction base) {
|
||||
if (!weaponDrawn || drawnWeaponType == null) return base;
|
||||
return switch (base) {
|
||||
@@ -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) {
|
||||
return switch (s) {
|
||||
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) {
|
||||
return switch (s) {
|
||||
case CROSSBOW -> AnimationAction.CROSSBOW_DRAW;
|
||||
@@ -1185,10 +1386,17 @@ public class PlayerInputControl {
|
||||
if (animPlayer == null) {
|
||||
animPlayer = new AnimPlayer(animComposer, skinningControl);
|
||||
}
|
||||
float transLen = (float) nextTransitionLength;
|
||||
float transLen = (float) nextTransitionLength;
|
||||
float startOff = nextStartOffset;
|
||||
boolean reversed = nextPlayReversed;
|
||||
nextTransitionLength = 0.0;
|
||||
boolean ok = animPlayer.play(clip, transLen);
|
||||
log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED");
|
||||
nextStartOffset = 0f;
|
||||
nextPlayReversed = false;
|
||||
boolean ok = reversed
|
||||
? animPlayer.playReversed(clip, transLen)
|
||||
: animPlayer.play(clip, transLen, startOff);
|
||||
log.info("[Anim] AnimPlayer.{}('{}') transLen={} → {}", reversed ? "playReversed" : "play",
|
||||
clip, transLen, ok ? "OK" : "FAILED");
|
||||
if (ok) {
|
||||
runningClip = animPlayer.getCurrentClip();
|
||||
applyAnimOffset(clip);
|
||||
|
||||
@@ -27,30 +27,54 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WeaponAttachmentControl.class);
|
||||
|
||||
private static final String BONE_SPINE = "mixamorig:Spine2";
|
||||
private static final String BONE_HAND = "mixamorig:RightHand";
|
||||
private static final String BONE_SPINE = "mixamorig:Spine2";
|
||||
private static final String BONE_HAND = "mixamorig:RightHand";
|
||||
private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
|
||||
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
|
||||
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
|
||||
private static final float HALF_PI = (float) Math.PI * 0.5f;
|
||||
|
||||
// Grip-Zielposition im Spine2-lokalen Raum (Meter): Griff ~40cm über dem Brustbein
|
||||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.05f, 0.40f, -0.08f);
|
||||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
|
||||
/** Zusatz-Versatz beim gezückten Schwert in lokalem Waffen-Space (X=entlang Klinge, Y=entlang Griff, Z=senkrecht zur Schneide). */
|
||||
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.0f, 0.05f);
|
||||
|
||||
/** Schild am Rücken: Griff unterhalb der Schultern, parallel zum Boden, Schild über dem Schwert. */
|
||||
private static final Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f);
|
||||
|
||||
// Schild am Arm (gezückt): Rotation relativ zur Bind-Pose des Unterarm-Knochens.
|
||||
// Werte: 0f = keine Drehung, HALF_PI = +90°, -HALF_PI = -90°
|
||||
private static final float SHIELD_ARM_ROT_X = -HALF_PI;
|
||||
private static final float SHIELD_ARM_ROT_Y = 0f;
|
||||
private static final float SHIELD_ARM_ROT_Z = 0f;
|
||||
|
||||
// Verschiebung entlang des Unterarms Richtung Ellbogen (lokale −Y des LeftHand-Bones).
|
||||
private static final float SHIELD_ARM_ELBOW_OFFSET = 0.25f;
|
||||
|
||||
private final AssetManager assetManager;
|
||||
private final PlayerInputControl playerInput;
|
||||
private final MainCharacter mc;
|
||||
|
||||
private com.jme3.anim.SkinningControl skinCtrl;
|
||||
private Node backNode; // Spine2-Attachment-Node (im skalierten Char-Modell)
|
||||
private Node handNode; // RightHand-Attachment-Node
|
||||
private Node backNode;
|
||||
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;
|
||||
// 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 boolean lastDrawnState = false;
|
||||
private Spatial weaponSpatial;
|
||||
private boolean gripDiagLogged = false;
|
||||
|
||||
private Item lastEquippedShield;
|
||||
private Spatial shieldSpatial;
|
||||
|
||||
private Node leftForeArmNode;
|
||||
private Quaternion leftForeArmBindRotInChar;
|
||||
|
||||
public WeaponAttachmentControl(AssetManager assetManager,
|
||||
PlayerInputControl playerInput,
|
||||
@@ -62,44 +86,68 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
|
||||
@Override
|
||||
protected void controlUpdate(float tpf) {
|
||||
// Lazy init: SkinningControl erst nach setAnimationContext() verfügbar
|
||||
if (skinCtrl == null) {
|
||||
skinCtrl = playerInput.getSkinningControl();
|
||||
if (skinCtrl == null) return;
|
||||
try {
|
||||
backNode = skinCtrl.getAttachmentsNode(BONE_SPINE);
|
||||
log.info("[WeaponAttach] backNode ({}): {}", BONE_SPINE, backNode);
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_SPINE, e.getMessage());
|
||||
}
|
||||
try {
|
||||
handNode = skinCtrl.getAttachmentsNode(BONE_HAND);
|
||||
log.info("[WeaponAttach] handNode ({}): {}", BONE_HAND, handNode);
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_HAND, e.getMessage());
|
||||
}
|
||||
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 shield = mc.getEquippedShield();
|
||||
boolean drawn = playerInput.isWeaponDrawn();
|
||||
|
||||
if (current != lastEquippedWeapon) {
|
||||
log.info("[WeaponAttach] Waffe geändert: {} → {}",
|
||||
lastEquippedWeapon != null ? lastEquippedWeapon.getItemId() : "null",
|
||||
current != null ? current.getItemId() : "null");
|
||||
detachWeapon();
|
||||
lastEquippedWeapon = current;
|
||||
if (current != null) {
|
||||
loadWeapon(current);
|
||||
}
|
||||
if (current != null) loadWeapon(current);
|
||||
}
|
||||
|
||||
if (weaponSpatial != null) {
|
||||
lastDrawnState = drawn;
|
||||
updateWeaponTransform(drawn, current != null ? current.getGrip() : null);
|
||||
}
|
||||
|
||||
if (shield != lastEquippedShield) {
|
||||
detachShield();
|
||||
lastEquippedShield = shield;
|
||||
if (shield != null) loadShield(shield);
|
||||
}
|
||||
|
||||
if (shieldSpatial != null) {
|
||||
updateShieldTransform(drawn, shield);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Laden ────────────────────────────────────────────────────────────────
|
||||
@@ -111,12 +159,8 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
}
|
||||
try {
|
||||
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);
|
||||
log.info("[WeaponAttach] Waffe geladen und an character-Node gehängt: {}",
|
||||
item.getModelRef().getPath());
|
||||
log.info("[WeaponAttach] Waffe geladen: {}", item.getModelRef().getPath());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeaponAttach] Waffenmodell nicht ladbar ({}): {}",
|
||||
item.getModelRef().getPath(), e.getMessage());
|
||||
@@ -130,31 +174,23 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
Node boneNode = drawn ? handNode : backNode;
|
||||
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();
|
||||
Vector3f boneInChar = charWorldRotInv.mult(
|
||||
boneWorldPos.subtract(spatial.getWorldTranslation()));
|
||||
Quaternion boneRotInChar = charWorldRotInv.mult(boneWorldRot);
|
||||
Vector3f charWorldPos = spatial.getWorldTranslation();
|
||||
|
||||
Vector3f boneInChar = charWorldRotInv.mult(
|
||||
boneNode.getWorldTranslation().subtract(charWorldPos));
|
||||
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
|
||||
|
||||
if (drawn) {
|
||||
applyHandTransform(boneInChar, boneRotInChar, grip);
|
||||
Vector3f attachInChar = (palmOffsetLocal != null)
|
||||
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
|
||||
: boneInChar;
|
||||
applyHandTransform(attachInChar, boneRotInChar, grip);
|
||||
} else {
|
||||
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) {
|
||||
Quaternion gripRot = (grip != null)
|
||||
? 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())
|
||||
: 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)
|
||||
? boneRotInChar.mult(handBindRotInChar.inverse())
|
||||
: 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.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) {
|
||||
com.jme3.anim.Armature arm = sc.getArmature();
|
||||
if (arm == null) return null;
|
||||
@@ -190,24 +270,82 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
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) {
|
||||
// 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);
|
||||
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
|
||||
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
|
||||
// ── Schild ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void loadShield(Item item) {
|
||||
if (item.getModelRef() == null || item.getModelRef().getPath() == null) {
|
||||
log.warn("[WeaponAttach] Kein modelRef an Schild '{}'.", item.getItemId());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
shieldSpatial = assetManager.loadModel(item.getModelRef().getPath());
|
||||
((Node) spatial).attachChild(shieldSpatial);
|
||||
log.info("[WeaponAttach] Schild geladen: {}", item.getModelRef().getPath());
|
||||
} catch (Exception e) {
|
||||
log.warn("[WeaponAttach] Schildmodell nicht ladbar ({}): {}",
|
||||
item.getModelRef().getPath(), e.getMessage());
|
||||
shieldSpatial = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateShieldTransform(boolean drawn, Item shield) {
|
||||
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
|
||||
Vector3f charWorldPos = spatial.getWorldTranslation();
|
||||
ItemGrip grip = shield != null ? shield.getGrip() : null;
|
||||
|
||||
if (drawn && leftForeArmNode != null) {
|
||||
Vector3f boneInChar = charWorldRotInv.mult(
|
||||
leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
|
||||
Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation());
|
||||
applyShieldArmTransform(boneInChar, boneRotInChar, grip);
|
||||
shieldSpatial.setCullHint(Spatial.CullHint.Inherit);
|
||||
} else if (!drawn && backNode != null) {
|
||||
Vector3f boneInChar = charWorldRotInv.mult(
|
||||
backNode.getWorldTranslation().subtract(charWorldPos));
|
||||
Quaternion boneRotInChar = charWorldRotInv.mult(backNode.getWorldRotation());
|
||||
applyShieldBackTransform(boneInChar, boneRotInChar, grip);
|
||||
shieldSpatial.setCullHint(Spatial.CullHint.Inherit);
|
||||
} else {
|
||||
shieldSpatial.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyShieldArmTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
||||
Quaternion animDelta = (leftForeArmBindRotInChar != null)
|
||||
? boneRotInChar.mult(leftForeArmBindRotInChar.inverse())
|
||||
: boneRotInChar;
|
||||
Quaternion armRot = new Quaternion().fromAngles(SHIELD_ARM_ROT_X, SHIELD_ARM_ROT_Y, SHIELD_ARM_ROT_Z);
|
||||
Quaternion shieldRot = animDelta.mult(armRot);
|
||||
Vector3f gripPos = (grip != null)
|
||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||
: Vector3f.ZERO;
|
||||
// Ankerpunkt Richtung Ellbogen verschieben (lokale −Y des LeftHand-Bones)
|
||||
Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -SHIELD_ARM_ELBOW_OFFSET, 0f)));
|
||||
shieldSpatial.setLocalRotation(shieldRot);
|
||||
shieldSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
|
||||
}
|
||||
|
||||
// weaponPos + weaponRot * gripPos = gripTarget
|
||||
weaponSpatial.setLocalRotation(weaponRot);
|
||||
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
|
||||
private void applyShieldBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
||||
// 180° um Y: Modell-+X → Spine-X (Griff nach links), Modell-+Z → Spine-−Z (flach am Rücken)
|
||||
Quaternion backRot = new Quaternion().fromAngles(0f, (float) Math.PI, 0f);
|
||||
Quaternion shieldRot = boneRotInChar.mult(backRot);
|
||||
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(SHIELD_BACK_OFFSET));
|
||||
Vector3f gripPos = (grip != null)
|
||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||
: Vector3f.ZERO;
|
||||
shieldSpatial.setLocalRotation(shieldRot);
|
||||
shieldSpatial.setLocalTranslation(gripTarget.subtract(shieldRot.mult(gripPos)));
|
||||
}
|
||||
|
||||
// ── Detach ────────────────────────────────────────────────────────────────
|
||||
@@ -220,6 +358,13 @@ public class WeaponAttachmentControl extends AbstractControl {
|
||||
lastDrawnState = false;
|
||||
}
|
||||
|
||||
private void detachShield() {
|
||||
if (shieldSpatial != null) {
|
||||
shieldSpatial.removeFromParent();
|
||||
shieldSpatial = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||
}
|
||||
|
||||
@@ -186,8 +186,7 @@ public class WorldInteractableState extends BaseAppState {
|
||||
|
||||
private final ActionListener interactListener = (name, isPressed, tpf) -> {
|
||||
if (!isPressed || phase != Phase.IDLE) return;
|
||||
int idx = findNearestInRange();
|
||||
if (idx >= 0) startApproach(idx);
|
||||
if (hoveredIdx >= 0) startApproach(hoveredIdx);
|
||||
};
|
||||
|
||||
private final ActionListener getUpListener = (name, isPressed, tpf) -> {
|
||||
@@ -247,22 +246,6 @@ public class WorldInteractableState extends BaseAppState {
|
||||
return new Vector3f(e.worldX(), e.worldY(), e.worldZ());
|
||||
}
|
||||
|
||||
// ── Suche nächstes Interactable ───────────────────────────────────────────
|
||||
|
||||
private int findNearestInRange() {
|
||||
Vector3f pos = physicsChar.getPhysicsLocation();
|
||||
int bestIdx = -1;
|
||||
float bestDist = Float.MAX_VALUE;
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
InteractableEntry e = entries.get(i);
|
||||
float dx = e.worldX() - pos.x;
|
||||
float dz = e.worldZ() - pos.z;
|
||||
float d = (float) Math.sqrt(dx * dx + dz * dz);
|
||||
float range = (e.type() == InteractableType.BENCH) ? BENCH_RANGE : BED_RANGE;
|
||||
if (d < range && d < bestDist) { bestDist = d; bestIdx = i; }
|
||||
}
|
||||
return bestIdx;
|
||||
}
|
||||
|
||||
// ── Annäherung ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -153,6 +153,9 @@ public class WorldItemsState extends BaseAppState {
|
||||
for (PlacedItem pi : items) {
|
||||
Spatial s = buildVisual(pi);
|
||||
s.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
|
||||
com.jme3.math.Quaternion q = new com.jme3.math.Quaternion();
|
||||
q.fromAngles(pi.rotX(), pi.rotY(), pi.rotZ());
|
||||
s.setLocalRotation(q);
|
||||
s.setUserData("itemId", pi.itemId());
|
||||
itemsRoot.attachChild(s);
|
||||
visuals.add(s);
|
||||
|
||||
@@ -243,3 +243,7 @@ location.silas.name=Silas
|
||||
location.silas_.name=Silas
|
||||
location.silas_haus.name=Silas Haus
|
||||
location.silas_heim.name=Silas Heim
|
||||
einfacher_schild.name=Einfacher Schild
|
||||
rostiges_kurzschwert.name=Rostiges Kurzschwert
|
||||
blutagave.name=Blutagave
|
||||
erzmoss.name=Erzmoss
|
||||
|
||||
@@ -98,3 +98,7 @@ location.silas.name=Silas
|
||||
location.silas_.name=Silas
|
||||
location.silas_haus.name=Silas Haus
|
||||
location.silas_heim.name=Silas Heim
|
||||
einfacher_schild.name=Simple Shield
|
||||
rostiges_kurzschwert.name=Rusty Short Sword
|
||||
blutagave.name=Blood Agave
|
||||
erzmoss.name=Ore Moss
|
||||
|
||||
@@ -126,6 +126,8 @@ dialog.silas.wer_bist_du.textnpc
|
||||
dialog.silas.wer_seid_ihr.description
|
||||
dialog.silas.wer_seid_ihr.textmainchar
|
||||
dialog.silas.wer_seid_ihr.textnpc
|
||||
einfacher_schild.description
|
||||
einfacher_schild.name
|
||||
erzmoss.description
|
||||
erzmoss.name
|
||||
hero.name
|
||||
@@ -138,6 +140,10 @@ location.silas_
|
||||
location.silas_haus
|
||||
location.silas_heim
|
||||
location.test
|
||||
neues_item_1789920920760.description
|
||||
neues_item_1789920920760.name
|
||||
neues_item_1789920asdf920760.description
|
||||
neues_item_1789920asdf920760.name
|
||||
quest.blight_wuehler.description
|
||||
quest.blight_wuehler.name
|
||||
quest.blight_wuehler.success
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# uuid itemId x y z
|
||||
fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693
|
||||
# uuid itemId x y z rotX rotY rotZ
|
||||
fd832b8d-021a-4136-be0b-4ec1cb4f863c rostiges_kurzschwert -2.46281 2.00000 -2.27693 0.00000 0.00000 0.00000
|
||||
feebed68-92b8-4458-9c7c-b1b781c1eba5 einfacher_schild -4.45856 1.72895 -3.67467 -0.14178 0.64812 0.00000
|
||||
|
||||
Reference in New Issue
Block a user