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