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