diff --git a/assets/imported/models/rostiger+zweihaender.glb b/assets/imported/models/rostiger+zweihaender.glb new file mode 100644 index 0000000..7a91dce Binary files /dev/null and b/assets/imported/models/rostiger+zweihaender.glb differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/imported/rostiger+zweihaender.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/imported/rostiger+zweihaender.j3o.thumb.png new file mode 100644 index 0000000..f3abe86 Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/imported/rostiger+zweihaender.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/waffen/schwerter/rostiger_zweihaender.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/waffen/schwerter/rostiger_zweihaender.j3o.thumb.png new file mode 100644 index 0000000..854b1e0 Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/waffen/schwerter/rostiger_zweihaender.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Characters/sets/human.animset.json b/blight-assets/src/main/resources/Characters/sets/human.animset.json index e1c0c2f..b241aca 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -194,6 +194,14 @@ "Roll", "Sneaking Forward", "Sword And Shield Jump Running", - "Sword And Shield Jump" + "Sword And Shield Jump", + "Great Sword Walk backwards", + "Great Sword Walk", + "Great Sword Strafe left", + "Great Sword Strafe Right", + "Great Sword Running Jump", + "Great Sword Run", + "Great Sword Heavy Attack", + "Great Sword Attack 3" ] } \ No newline at end of file diff --git a/blight-assets/src/main/resources/Models/imported/rostiger+zweihaender.j3o.meta b/blight-assets/src/main/resources/Models/imported/rostiger+zweihaender.j3o.meta new file mode 100644 index 0000000..57ad244 --- /dev/null +++ b/blight-assets/src/main/resources/Models/imported/rostiger+zweihaender.j3o.meta @@ -0,0 +1,30 @@ +#Tue Oct 06 21:54:42 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=rostiger+zweihaender +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 diff --git a/blight-assets/src/main/resources/Models/waffen/schwerter/rostiger_zweihaender.j3o b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiger_zweihaender.j3o new file mode 100644 index 0000000..af6a29b Binary files /dev/null and b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiger_zweihaender.j3o differ diff --git a/blight-assets/src/main/resources/Models/waffen/schwerter/rostiger_zweihaender.j3o.meta b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiger_zweihaender.j3o.meta new file mode 100644 index 0000000..f838199 --- /dev/null +++ b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiger_zweihaender.j3o.meta @@ -0,0 +1,30 @@ +#Mon Oct 12 14:17:44 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=rostiger_zweihaender +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=false +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o index 3a32c9b..1c8a29f 100644 Binary files a/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o and b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o differ diff --git a/blight-assets/src/main/resources/items/weapon/einfacher_schild.item b/blight-assets/src/main/resources/items/weapon/einfacher_schild.item index e215526..585fc45 100644 --- a/blight-assets/src/main/resources/items/weapon/einfacher_schild.item +++ b/blight-assets/src/main/resources/items/weapon/einfacher_schild.item @@ -16,7 +16,13 @@ "posZ": -0.13, "rotX": 90.0, "rotY": 0.0, - "rotZ": 90.0 + "rotZ": 90.0, + "grip2PosX": 0.0, + "grip2PosY": 0.0, + "grip2PosZ": 0.0, + "handOffsetX": 0.0, + "handOffsetY": 0.0, + "handOffsetZ": 0.0 }, "modelRef": { "path": "Models/waffen/schilder/einfacher schild.j3o" diff --git a/blight-assets/src/main/resources/items/weapon/rostiger_zweihaender.item b/blight-assets/src/main/resources/items/weapon/rostiger_zweihaender.item new file mode 100644 index 0000000..87a93a8 --- /dev/null +++ b/blight-assets/src/main/resources/items/weapon/rostiger_zweihaender.item @@ -0,0 +1,31 @@ +{ + "itemId": "rostiger_zweihaender", + "category": "WEAPON", + "subCategory": "TWO_HANDED_SWORD", + "name": { + "id": "rostiger_zweihaender.name" + }, + "description": { + "id": "rostiger_zweihaender.description" + }, + "worthGold": 0, + "damage": 50, + "grip": { + "posX": 0.10248406, + "posY": 0.104730606, + "posZ": 0.5130257, + "rotX": 0.0, + "rotY": 0.0, + "rotZ": 0.0, + "grip2PosX": 0.0024840608, + "grip2PosY": 0.104730606, + "grip2PosZ": 0.5630257, + "handOffsetX": 0.0, + "handOffsetY": 0.0, + "handOffsetZ": 0.0 + }, + "modelRef": { + "path": "Models/waffen/schwerter/rostiger_zweihaender.j3o" + }, + "consumable": false +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item b/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item index 51c394a..35376ac 100644 --- a/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item +++ b/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item @@ -16,7 +16,13 @@ "posZ": 0.0, "rotX": 0.0, "rotY": -90.0, - "rotZ": 90.0 + "rotZ": 90.0, + "grip2PosX": 0.0, + "grip2PosY": 0.0, + "grip2PosZ": 0.0, + "handOffsetX": 0.0, + "handOffsetY": 0.0, + "handOffsetZ": 0.0 }, "modelRef": { "path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o" diff --git a/blight-common/src/main/java/de/blight/common/model/ItemGrip.java b/blight-common/src/main/java/de/blight/common/model/ItemGrip.java index 1b65baa..79158dc 100644 --- a/blight-common/src/main/java/de/blight/common/model/ItemGrip.java +++ b/blight-common/src/main/java/de/blight/common/model/ItemGrip.java @@ -7,6 +7,15 @@ import lombok.Setter; @Getter @Setter public class ItemGrip { + /** Offset (Modell-local), den das Runtime-System beim Anbringen der Waffe addiert. */ + public static final float HAND_GRIP_OFFSET_X = 0.1f; + public static final float HAND_GRIP_OFFSET_Y = 0.0f; + public static final float HAND_GRIP_OFFSET_Z = 0.05f; + private float posX, posY, posZ; private float rotX, rotY, rotZ; + /** Position des linken Hand-Griffs im Modell-Space (nur Zweihänder). */ + private float grip2PosX, grip2PosY, grip2PosZ; + /** Zusätzlicher Offset in Character-Space (X=rechts, Y=oben, Z=vorwärts). */ + private float handOffsetX, handOffsetY, handOffsetZ; } diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index c1bf148..8551a70 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -197,7 +197,7 @@ public class EditorApp extends Application { stoneController = new de.blight.editor.toolset.stone.StoneController(toolCtx); contentController = new de.blight.editor.toolset.content.ContentController(toolCtx); contentController.setOnEnterContentMode(() -> { - if (assetController.isOverlayOpen()) assetController.closeOverlay(); + assetController.closeOverlayImmediate(); assetController.hideTabBtn(); }); tripoController = new de.blight.editor.toolset.tripo.TripoController(toolCtx); @@ -281,10 +281,13 @@ public class EditorApp extends Application { return; } } - // Tab: Sub-Modus innerhalb des aktiven Tools cyclen + // Tab: Sub-Modus innerhalb des aktiven Tools cyclen. + // Nicht auslösen wenn Fokus auf einem Eingabefeld liegt (TextInputControl oder Spinner – + // Spinner.getFocusOwner() gibt den Spinner selbst zurück, nicht das interne TextField). if (e.getCode() == KeyCode.TAB && "world".equals(currentTool) && !e.isAltDown() && !e.isControlDown() - && !(scene.getFocusOwner() instanceof javafx.scene.control.TextInputControl)) { + && !(scene.getFocusOwner() instanceof javafx.scene.control.TextInputControl) + && !(scene.getFocusOwner() instanceof javafx.scene.control.Spinner)) { e.consume(); cycleToolSubMode(); return; @@ -618,13 +621,14 @@ public class EditorApp extends Application { private void switchToWorldEditor() { boolean wasObjEditor = "objecteditor".equals(registry.activeToolId()) || "objecteditor".equals(currentTool); boolean wasModelEditor = "modeleditor".equals(registry.activeToolId()) || "modeleditor".equals(currentTool); + boolean wasGripEditor = "gripEditor".equals(currentTool); boolean wasThumbnailManager = "thumbnailmanager".equals(currentTool); registry.deactivateAll(); currentTool = "world"; if (jmeApp != null) jmeApp.setRenderThrottled(false); root.setLeft(null); // ggf. gesetzten Content-Tree entfernen assetController.showTabBtn(); - if (wasThumbnailManager) assetController.openOverlay(); + assetController.openOverlay(); topBar.getChildren().set(1, topBarController.getWorldToolBar()); if (contentController.hasRoutineJmeSlot()) { contentController.detachRoutineJmeSlot(); @@ -647,9 +651,22 @@ public class EditorApp extends Application { input.activeLayer = 0; input.modelEditorOpenPath = null; topBarController.getBaseBtn().setSelected(true); + } else if (wasGripEditor) { + input.activeLayer = 0; + input.gripEditorActive = false; + input.gripEditorIsShield = false; + Platform.runLater(() -> topBarController.getAuswahlBtn().fire()); } } + /** Wechselt ggf. zum Welteditor und feuert dann den gewünschten Toolbar-Button. */ + private void fireWorldToolBtn(Runnable btn) { + Platform.runLater(() -> { + if (!"world".equals(currentTool)) switchToWorldEditor(); + btn.run(); + }); + } + private void ensureNotContentEditor() { if (contentController.isSingleWindowContentActive(currentTool)) { switchToWorldEditor(); @@ -665,7 +682,7 @@ public class EditorApp extends Application { thumbnailManagerView.refresh(); } // Asset-Baum-Overlay ausblenden – wird in switchToWorldEditor() wiederhergestellt - if (assetController.isOverlayOpen()) assetController.closeOverlay(); + assetController.closeOverlayImmediate(); assetController.hideTabBtn(); topBar.getChildren().set(1, buildThumbnailManagerToolBar()); root.setLeft(null); @@ -1067,16 +1084,18 @@ public class EditorApp extends Application { input.topologyRequest = sel ? 1 : 2; } } - case F1 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getAuswahlBtn().fire()); } - case F2 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getBaseBtn().fire()); } - case F3 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getVoxelMergedBtn().fire()); } - case F4 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> topBarController.getTextureBtn().fire()); } + case F1 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getAuswahlBtn().fire()); } + case F2 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getBaseBtn().fire()); } + case F3 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getVoxelMergedBtn().fire()); } + case F4 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getTextureBtn().fire()); } case F5 -> { if (pressed) { if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getGrassBtn().fire()); } else { ToolController active = registry.getActive(); - if (active != null) { active.onPreview(); } else if (onF5 != null) { onF5.run(); } + if (active != null) { active.onPreview(); } + else if (onF5 != null) { onF5.run(); } + else { fireWorldToolBtn(() -> topBarController.getGrassBtn().fire()); } } } } @@ -1085,28 +1104,29 @@ public class EditorApp extends Application { if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getStoneBtn().fire()); } else { ToolController active = registry.getActive(); - if (active != null) { active.onRandomSeed(); } else if (onF6 != null) { onF6.run(); } + if (active != null) { active.onRandomSeed(); } + else if (onF6 != null) { onF6.run(); } + else { fireWorldToolBtn(() -> topBarController.getStoneBtn().fire()); } } } } case F7 -> { - if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getObjPlaceBtn().fire()); } - else if (pressed && "objecteditor".equals(registry.activeToolId())) { Platform.runLater(objectController::activatePlaceMode); } + if (pressed && "objecteditor".equals(registry.activeToolId())) { Platform.runLater(objectController::activatePlaceMode); } + else if (pressed) { fireWorldToolBtn(() -> topBarController.getObjPlaceBtn().fire()); } } case F8 -> { - if (pressed) { - if ("world".equals(currentTool)) { Platform.runLater(() -> topBarController.getAtmosphaereBtn().fire()); } - else if (input.ctrlHeld) { input.debugNoLightToggle = true; } - } else if (input.ctrlHeld) { input.debugNoLightToggle = true; } + if (pressed && input.ctrlHeld) { input.debugNoLightToggle = true; } + else if (!pressed && input.ctrlHeld) { input.debugNoLightToggle = true; } + else if (pressed) { fireWorldToolBtn(() -> topBarController.getAtmosphaereBtn().fire()); } } case F9 -> { if (pressed && input.ctrlHeld) { Platform.runLater(() -> contentController.activateContentSection(8, currentTool)); } - else if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getWasserMergedBtn().fire()); } + else if (pressed) { fireWorldToolBtn(() -> topBarController.getWasserMergedBtn().fire()); } } - case F10 -> { if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getWegeBtn().fire()); } } - case F11 -> { if (pressed && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getZonenBtn().fire()); } } - case F12 -> { if (pressed && !input.ctrlHeld && "world".equals(currentTool)) { Platform.runLater(() -> topBarController.getPlayToolBtn().fire()); } } + case F10 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getWegeBtn().fire()); } + case F11 -> { if (pressed) fireWorldToolBtn(() -> topBarController.getZonenBtn().fire()); } + case F12 -> { if (pressed && !input.ctrlHeld) fireWorldToolBtn(() -> topBarController.getPlayToolBtn().fire()); } } } diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 997963d..464558c 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -878,6 +878,12 @@ public class SharedInput { public volatile boolean modelEditorBakeScaleRequest = false; public volatile java.nio.file.Path modelEditorBakeScalePath = null; + /** JFX → JME: Waffe an Z-Achse ausrichten und Mitte in Ursprung setzen. */ + public volatile boolean modelEditorAlignWeaponRequest = false; + public volatile java.nio.file.Path modelEditorAlignWeaponPath = null; + /** JME → JFX: Ergebnis der letzten Ausrichte-Operation (null = kein Ergebnis). */ + public volatile String modelEditorAlignWeaponResult = null; + /** JME → JFX: j3o hatte eine gebakte Scale, die von der Meta abwich – Spinner aktualisieren. */ public volatile boolean modelEditorBakedScaleDetected = false; @@ -934,6 +940,8 @@ public class SharedInput { public volatile String bakeStatusMsg = null; /** JME → JFX: Anzahl dirty Chunks in der Session (0 = nichts zu backen). */ public volatile int sessionDirtyVoxelCount = 0; + /** JFX → JME: Pfad zum aktiven Session-Verzeichnis (gesetzt von GameController beim Start). */ + public volatile java.nio.file.Path activeSessionDir = null; /** JFX → JME: Nur dirty Chunks backen (Bake-on-Exit). */ public volatile boolean bakeOnExitRequested = false; /** JFX → JME: Laufenden Bake-on-Exit abbrechen. */ @@ -1209,10 +1217,25 @@ public class SharedInput { public volatile float gripEditorRotX = 0f; public volatile float gripEditorRotY = 0f; public volatile float gripEditorRotZ = 0f; + public volatile float gripEditorGrip2PosX = 0f; + public volatile float gripEditorGrip2PosY = 0f; + public volatile float gripEditorGrip2PosZ = 0f; + public volatile float gripEditorHandOffX = 0f; + public volatile float gripEditorHandOffY = 0f; + public volatile float gripEditorHandOffZ = 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; + /** JFX → JME: Griff-Position aus Modell-Geometrie schätzen (Parierstange erkennen). */ + public volatile boolean gripEditorGuessRequest = false; + /** JME → JFX: Ergebnis der Griff-Schätzung — X/Y = Querschnittsmitte, Z = Griff-Z mit Puffer. + * gripEditorGuessPosZ = Float.NaN bedeutet: kein Ergebnis vorhanden. */ + public volatile float gripEditorGuessPosX = 0f; + public volatile float gripEditorGuessPosY = 0f; + public volatile float gripEditorGuessPosZ = Float.NaN; + /** Klingenrichtung im Modellraum: -1 = Klinge bei -Z, +1 = Klinge bei +Z. Wird von guessGripPosition gesetzt. */ + public volatile float gripEditorBladeDirZ = -1f; /** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */ public record SculptEdit(float screenX, float screenY, int action) {} diff --git a/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java b/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java index bba6eb4..6daad57 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ItemPlacementState.java @@ -279,6 +279,7 @@ public class ItemPlacementState extends BaseAppState { try { PlacedItemIO.save(items); + syncBpiToSession(); log.info("[ItemPlacement] Item '{}' platziert bei ({}, {}, {})", pi.itemId(), pt.x, pt.y, pt.z); } catch (IOException e) { log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage()); @@ -664,6 +665,7 @@ public class ItemPlacementState extends BaseAppState { nodes.remove(idx); try { PlacedItemIO.save(items); + syncBpiToSession(); } catch (IOException e) { log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage()); } @@ -681,6 +683,7 @@ public class ItemPlacementState extends BaseAppState { try { PlacedItemIO.save(items); + syncBpiToSession(); } catch (IOException e) { log.warn("[ItemPlacement] Speichern nach Edit fehlgeschlagen: {}", e.getMessage()); } @@ -689,6 +692,7 @@ public class ItemPlacementState extends BaseAppState { private void saveItems() { try { PlacedItemIO.save(items); + syncBpiToSession(); } catch (IOException e) { log.warn("[ItemPlacement] Speichern fehlgeschlagen: {}", e.getMessage()); } @@ -696,6 +700,17 @@ public class ItemPlacementState extends BaseAppState { // ── Hilfsmethoden ──────────────────────────────────────────────────────── + private void syncBpiToSession() { + Path sd = input.activeSessionDir; + if (sd == null) return; + try { + java.nio.file.Files.copy(PlacedItemIO.getPath(), sd.resolve("blight_placed_items.bpi"), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + log.warn("[ItemPlacement] Session-Sync fehlgeschlagen: {}", ex.getMessage()); + } + } + private static void applyRot(Node n, float rotX, float rotY, float rotZ) { Quaternion q = new Quaternion(); q.fromAngles(rotX, rotY, rotZ); diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java index 7fc4b9d..712dfa7 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java @@ -106,6 +106,7 @@ public class ModelEditorState extends BaseAppState { private Node gripGizmoNode = null; private float gripPosX = 0f, gripPosY = 0f, gripPosZ = 0f; private float gripRotX = 0f, gripRotY = 0f, gripRotZ = 0f; + private float grip2PosX = 0f, grip2PosY = 0f, grip2PosZ = 0f; private float interactableOffsetZ = 0f; private float interactableRotY = 0f; @@ -197,6 +198,10 @@ public class ModelEditorState extends BaseAppState { app.getStateManager().detach(previewEnvCam); previewEnvCam = null; } + // Probe jetzt zur Szene hinzufügen – erst hier ist sie korrekt initialisiert + if (previewRoot != null) { + previewRoot.addLight(previewProbe); + } previewProbeReady = true; // Emissive-Fallback von bereits geladenen Materialien entfernen for (Material m : pbrMaterials) { @@ -223,6 +228,22 @@ public class ModelEditorState extends BaseAppState { } } + if (input.modelEditorAlignWeaponRequest) { + input.modelEditorAlignWeaponRequest = false; + Path alignPath = input.modelEditorAlignWeaponPath; + if (alignPath != null) { + alignWeaponToZ(alignPath); + } else { + log.warn("[ModelEditor] AlignWeapon: kein Pfad gesetzt"); + input.modelEditorAlignWeaponResult = "FEHLER: kein Pfad"; + } + } + + if (input.gripEditorGuessRequest) { + input.gripEditorGuessRequest = false; + guessGripPosition(); + } + if (input.modelEditorEmbedLodsRequest) { input.modelEditorEmbedLodsRequest = false; embedLodsAndSave(input.modelEditorEmbedLodsPath); @@ -242,12 +263,15 @@ public class ModelEditorState extends BaseAppState { // Grip-Gizmo aktualisieren if (input.gripEditorActive && input.gripEditorGizmoDirty) { input.gripEditorGizmoDirty = false; - gripPosX = input.gripEditorPosX; - gripPosY = input.gripEditorPosY; - gripPosZ = input.gripEditorPosZ; - gripRotX = input.gripEditorRotX; - gripRotY = input.gripEditorRotY; - gripRotZ = input.gripEditorRotZ; + gripPosX = input.gripEditorPosX; + gripPosY = input.gripEditorPosY; + gripPosZ = input.gripEditorPosZ; + gripRotX = input.gripEditorRotX; + gripRotY = input.gripEditorRotY; + gripRotZ = input.gripEditorRotZ; + grip2PosX = input.gripEditorGrip2PosX; + grip2PosY = input.gripEditorGrip2PosY; + grip2PosZ = input.gripEditorGrip2PosZ; rebuildGripGizmo(); } @@ -472,7 +496,22 @@ public class ModelEditorState extends BaseAppState { // Rendering schwarz erscheinen lassen. try { app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(assetPath)); } catch (Exception ignored) {} - Spatial model = app.getAssetManager().loadModel(assetPath); + // Direkt aus ASSET_ROOT (src/main/resources) laden – umgeht den ClasspathLocator, + // der bin/main bevorzugt und nach einem Bake möglicherweise die veraltete Kopie liefert. + java.nio.file.Path absFile = ASSET_ROOT.resolve( + java.nio.file.Paths.get(assetPath.replace('/', java.io.File.separatorChar))); + Spatial model; + if (absFile.toFile().exists()) { + BinaryImporter directImp = BinaryImporter.getInstance(); + directImp.setAssetManager(app.getAssetManager()); + Savable directLoaded = directImp.load(absFile.toFile()); + log.info("[ModelEditor] Direkt geladen: {} ({}B)", absFile.getFileName(), absFile.toFile().length()); + model = (directLoaded instanceof Spatial) ? (Spatial) directLoaded + : app.getAssetManager().loadModel(assetPath); + } else { + log.warn("[ModelEditor] Nicht in ASSET_ROOT, lade via AM: {}", assetPath); + model = app.getAssetManager().loadModel(assetPath); + } stripControls(model); initImpostorControls(model); recalcNormalsIfMissing(model); @@ -696,12 +735,12 @@ public class ModelEditorState extends BaseAppState { new ColorRGBA(0.4f, 0.4f, 0.5f, 1f))); previewRoot.addLight(new AmbientLight(new ColorRGBA(0.65f, 0.65f, 0.7f, 1f))); - // LightProbe: JME3-PBR braucht Probe damit g_AmbientLightColor verwendet wird. - // EnvironmentCamera backt die Probe aus dem hellen Übergangshintergrund heraus; - // danach wird der dunkle Display-Hintergrund wiederhergestellt. + // LightProbe: erst nach dem Backen zur Szene hinzufügen – eine leere (uninitialisierte) + // Probe stört den PBR-Shader und macht das Modell dunkel, bevor der Bake fertig ist. + // Bis dahin sorgen DirectionalLights + AmbientLight für sichtbare Beleuchtung. previewProbe = new LightProbe(); previewProbe.getArea().setRadius(1000f); - previewRoot.addLight(previewProbe); + // previewRoot.addLight(previewProbe) → wird in done()-Callback aufgerufen previewEnvCam = new EnvironmentCamera(256, Vector3f.ZERO.clone()); app.getStateManager().attach(previewEnvCam); previewProbeFrame = 0; @@ -728,14 +767,17 @@ public class ModelEditorState extends BaseAppState { gripGizmoNode.setCullHint(Spatial.CullHint.Always); previewRoot.attachChild(gripGizmoNode); - gripPosX = input.gripEditorPosX; - gripPosY = input.gripEditorPosY; - gripPosZ = input.gripEditorPosZ; - gripRotX = input.gripEditorRotX; - gripRotY = input.gripEditorRotY; - gripRotZ = input.gripEditorRotZ; + gripPosX = input.gripEditorPosX; + gripPosY = input.gripEditorPosY; + gripPosZ = input.gripEditorPosZ; + gripRotX = input.gripEditorRotX; + gripRotY = input.gripEditorRotY; + gripRotZ = input.gripEditorRotZ; + grip2PosX = input.gripEditorGrip2PosX; + grip2PosY = input.gripEditorGrip2PosY; + grip2PosZ = input.gripEditorGrip2PosZ; - orbitYaw = 30f; + orbitYaw = input.gripEditorActive ? 90f : 30f; orbitPitch = 25f; } @@ -1218,15 +1260,20 @@ public class ModelEditorState extends BaseAppState { projectRoot.resolve("blight-assets").resolve("bin").resolve("main").resolve(rel), projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main").resolve(rel), }; - for (java.nio.file.Path mirror : mirrors) { - if (java.nio.file.Files.exists(mirror)) { - try { - java.nio.file.Files.copy(savedFile, mirror, - java.nio.file.StandardCopyOption.REPLACE_EXISTING); - log.info("[ModelEditor] Spiegel aktualisiert: {}", mirror); - } catch (Exception e) { - log.warn("[ModelEditor] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage()); - } + java.nio.file.Path[] mirrorRoots = { + projectRoot.resolve("blight-assets").resolve("bin").resolve("main"), + projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main"), + }; + for (int i = 0; i < mirrors.length; i++) { + java.nio.file.Path mirror = mirrors[i]; + if (!java.nio.file.Files.isDirectory(mirrorRoots[i])) continue; + try { + java.nio.file.Files.createDirectories(mirror.getParent()); + java.nio.file.Files.copy(savedFile, mirror, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + log.info("[ModelEditor] Spiegel aktualisiert: {}", mirror); + } catch (Exception e) { + log.warn("[ModelEditor] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage()); } } } @@ -1424,6 +1471,296 @@ public class ModelEditorState extends BaseAppState { } } + private void alignWeaponToZ(Path j3oPath) { + log.info("[ModelEditor] alignWeaponToZ GESTARTET: {}", j3oPath); + try { + BinaryImporter importer = BinaryImporter.getInstance(); + importer.setAssetManager(app.getAssetManager()); + Savable savable = importer.load(j3oPath.toFile()); + if (!(savable instanceof Spatial root)) { + log.warn("[ModelEditor] AlignWeapon: kein Spatial in {}", j3oPath.getFileName()); + input.modelEditorAlignWeaponResult = "FEHLER: kein Spatial"; + return; + } + root.setLocalTranslation(Vector3f.ZERO); + root.setLocalRotation(Quaternion.IDENTITY); + root.setLocalScale(1f); + root.updateGeometricState(); + + // Schritt 1: alle Vertex-Positionen sammeln + java.util.List verts = new java.util.ArrayList<>(); + collectAllVertices(root, new Matrix4f(), verts); + if (verts.size() < 2) { + log.warn("[ModelEditor] AlignWeapon: zu wenige Vertices ({})", verts.size()); + return; + } + + // Schritt 2: Die zwei am weitesten entfernten Punkte finden (Durchmesser-Approx.) + // Startet von beliebigem Punkt → A = Farthest(v0) → B = Farthest(A) + Vector3f A = awFarthestFrom3D(verts, verts.get(0)); + Vector3f B = awFarthestFrom3D(verts, A); + log.info("[ModelEditor] AlignWeapon: Hauptachse {} → {} Länge={}", A, B, A.distance(B)); + + // Schritt 3: Richtung A→B auf +Z ausrichten + Vector3f bladeDir = B.subtract(A).normalizeLocal(); + if (bladeDir.z < 0) bladeDir.negateLocal(); // +Z-Richtung bevorzugen + Quaternion rot1 = awRotateToZ(bladeDir); + + // Mitte der Hauptachse → Ursprung + Vector3f midpoint = A.add(B).multLocal(0.5f); + + // Schritt 4: Vertices in neues Koordinatensystem transformieren (rot1 + Translation) + // v' = rot1 * (v - midpoint) + java.util.List verts2 = new java.util.ArrayList<>(verts.size()); + for (Vector3f v : verts) { + verts2.add(rot1.mult(v.subtract(midpoint))); + } + + // Schritt 5: In der XY-Ebene die zwei weitesten Punkte finden (Z ignoriert) + Vector3f C = awFarthestFromXY(verts2, verts2.get(0)); + Vector3f D = awFarthestFromXY(verts2, C); + log.info("[ModelEditor] AlignWeapon: Breitenachse ({},{}) → ({},{})", C.x, C.y, D.x, D.y); + + // Schritt 6: Rotation um Z, damit die Breitenrichtung auf X liegt + Quaternion rot2 = new Quaternion(); + float wx = D.x - C.x, wy = D.y - C.y; + if (wx*wx + wy*wy > 1e-8f) { + float angle = FastMath.atan2(wy, wx); + rot2.fromAngles(0f, 0f, -angle); + } + + // Schritt 7: Gesamt-Rotation = rot2 ∘ rot1 (erst rot1, dann rot2) + Quaternion combined = rot2.mult(rot1); + + // Schritt 7b: Bestimme Klingen-Richtung anhand der Parierstangen-Position. + // Die Parierstange (breitester Querschnitt) liegt immer an der Klingenbasis. + // normZ >0.5 → Parierstange in +Z-Hälfte → Pommel bei +Z → flip nötig. + // normZ <0.5 → Parierstange in -Z-Hälfte → Klinge bereits bei +Z → kein flip. + { + final int BKTS = 40; + float zMin2 = Float.MAX_VALUE, zMax2 = -Float.MAX_VALUE; + for (Vector3f v : verts2) { if (v.z < zMin2) zMin2 = v.z; if (v.z > zMax2) zMax2 = v.z; } + float zR2 = zMax2 - zMin2; + if (zR2 > 0.01f) { + float[] xmx = new float[BKTS], xmn = new float[BKTS]; + float[] ymx = new float[BKTS], ymn = new float[BKTS]; + java.util.Arrays.fill(xmx, -Float.MAX_VALUE); java.util.Arrays.fill(xmn, Float.MAX_VALUE); + java.util.Arrays.fill(ymx, -Float.MAX_VALUE); java.util.Arrays.fill(ymn, Float.MAX_VALUE); + for (Vector3f v : verts2) { + int b = Math.max(0, Math.min(BKTS - 1, (int) ((v.z - zMin2) / zR2 * (BKTS - 1)))); + if (v.x > xmx[b]) xmx[b] = v.x; if (v.x < xmn[b]) xmn[b] = v.x; + if (v.y > ymx[b]) ymx[b] = v.y; if (v.y < ymn[b]) ymn[b] = v.y; + } + float maxScore2 = 0f; + int guardBucket = 0; + for (int i = 0; i < BKTS; i++) { + float s = Math.max(xmx[i] > xmn[i] ? xmx[i] - xmn[i] : 0f, + ymx[i] > ymn[i] ? ymx[i] - ymn[i] : 0f); + if (s > maxScore2) { maxScore2 = s; guardBucket = i; } + } + float guardNorm = (float) guardBucket / (BKTS - 1); + log.info("[ModelEditor] AlignWeapon: Parierstange bei normZ={} (Bucket {}/{})", + String.format("%.2f", guardNorm), guardBucket, BKTS); + if (guardNorm > 0.5f) { + // Parierstange in +Z-Hälfte → Pommel bei +Z, Klinge bei -Z → 180° um X flippen + Quaternion flip = new Quaternion().fromAngles(FastMath.PI, 0f, 0f); + combined = flip.mult(combined); + log.info("[ModelEditor] AlignWeapon: Pommel bei +Z detektiert → Schwert geflippt"); + } + } + } + + Vector3f rotatedMid = combined.mult(midpoint); + root.setLocalRotation(combined); + root.setLocalTranslation(rotatedMid.negate()); + root.setLocalScale(1f); + + ModelExportUtil.stripControls(root); + ModelExportUtil.bakeTransform(root, new Matrix4f()); + + // Vertex-Bounds nach Baking prüfen (Debug) + java.util.List bakedVerts = new java.util.ArrayList<>(); + collectAllVertices(root, new Matrix4f(), bakedVerts); + float xMin = Float.MAX_VALUE, xMax = -Float.MAX_VALUE; + float yMin = Float.MAX_VALUE, yMax = -Float.MAX_VALUE; + float zMin = Float.MAX_VALUE, zMax = -Float.MAX_VALUE; + for (Vector3f v : bakedVerts) { + if (v.x < xMin) xMin = v.x; if (v.x > xMax) xMax = v.x; + if (v.y < yMin) yMin = v.y; if (v.y > yMax) yMax = v.y; + if (v.z < zMin) zMin = v.z; if (v.z > zMax) zMax = v.z; + } + log.info("[ModelEditor] AlignWeapon nach Baking: X[{},{}] Y[{},{}] Z[{},{}]", + String.format("%.3f",xMin), String.format("%.3f",xMax), + String.format("%.3f",yMin), String.format("%.3f",yMax), + String.format("%.3f",zMin), String.format("%.3f",zMax)); + + BinaryExporter.getInstance().save(root, j3oPath.toFile()); + syncToMirrors(j3oPath); + + input.modelEditorScaleX = 1.0f; + input.modelEditorScaleY = 1.0f; + input.modelEditorScaleZ = 1.0f; + input.modelEditorBakedScaleDetected = true; + input.modelEditorOpenPath = currentPath; + input.modelEditorAlignWeaponResult = String.format( + "OK: Z=%.2fm X=%.2fm Y=%.2fm", + zMax - zMin, xMax - xMin, yMax - yMin); + + log.info("[ModelEditor] Waffe ausgerichtet und gespeichert: {}", j3oPath.getFileName()); + } catch (Exception e) { + log.error("[ModelEditor] AlignWeapon fehlgeschlagen: {}", e.getMessage(), e); + input.modelEditorAlignWeaponResult = "FEHLER: " + e.getMessage(); + } + } + + /** Sammelt alle Vertex-Positionen aus dem Spatial-Baum, transformiert durch den akkumulierten Matrix. */ + private static void collectAllVertices(Spatial s, Matrix4f parentMat, java.util.List out) { + Matrix4f localMat = new Matrix4f(); + s.getLocalTransform().toTransformMatrix(localMat); + Matrix4f worldMat = parentMat.mult(localMat); + if (s instanceof Geometry g) { + FloatBuffer pos = g.getMesh().getFloatBuffer(VertexBuffer.Type.Position); + if (pos != null) { + pos.rewind(); + while (pos.remaining() >= 3) { + Vector3f v = new Vector3f(pos.get(), pos.get(), pos.get()); + worldMat.mult(v, v); + out.add(v); + } + pos.rewind(); + } + } else if (s instanceof Node n) { + for (Spatial child : n.getChildren()) collectAllVertices(child, worldMat, out); + } + } + + /** Farthest-Point in 3D von einem Ausgangspunkt. */ + private static Vector3f awFarthestFrom3D(java.util.List verts, Vector3f from) { + Vector3f best = verts.get(0); + float bestDist = from.distanceSquared(best); + for (Vector3f v : verts) { + float d = from.distanceSquared(v); + if (d > bestDist) { bestDist = d; best = v; } + } + return best; + } + + /** Farthest-Point nur in XY-Ebene (Z wird ignoriert). */ + private static Vector3f awFarthestFromXY(java.util.List verts, Vector3f from) { + Vector3f best = verts.get(0); + float bx = from.x - best.x, by = from.y - best.y; + float bestDist = bx*bx + by*by; + for (Vector3f v : verts) { + float dx = from.x - v.x, dy = from.y - v.y; + float d = dx*dx + dy*dy; + if (d > bestDist) { bestDist = d; best = v; } + } + return best; + } + + /** Quaternion, der den Vektor dir auf +Z dreht. */ + private static Quaternion awRotateToZ(Vector3f dir) { + Quaternion q = new Quaternion(); + float dot = FastMath.clamp(dir.dot(Vector3f.UNIT_Z), -1f, 1f); + if (dot >= 1f - 1e-6f) return q; // bereits +Z + if (dot <= -1f + 1e-6f) { + q.fromAngles(FastMath.PI, 0f, 0f); // genau gegenüber: 180° um X + return q; + } + Vector3f axis = dir.cross(Vector3f.UNIT_Z).normalizeLocal(); + q.fromAngleNormalAxis(FastMath.acos(dot), axis); + return q; + } + + /** + * Schätzt Position und Richtung des Griffes anhand der Modell-Geometrie. + * Erkennt die Parierstange (breitester Querschnitt in X oder Y) und verschiebt + * den Griff-Kontaktpunkt 8 cm in Richtung Pommel (weg von der Klingenspitze). + * Voraussetzung: Klinge liegt auf Z-Achse (nach Ausrichten). + * Ergebnis wird in input.gripEditorGuessPosX/Y/Z geschrieben. + */ + private void guessGripPosition() { + input.gripEditorGuessPosZ = Float.NaN; // Sentinel zurücksetzen + if (originalSpatial == null) return; + + java.util.List verts = new java.util.ArrayList<>(); + collectAllVertices(originalSpatial, new Matrix4f(), verts); + if (verts.size() < 10) return; + + float zMin = Float.MAX_VALUE, zMax = -Float.MAX_VALUE; + for (Vector3f v : verts) { + if (v.z < zMin) zMin = v.z; + if (v.z > zMax) zMax = v.z; + } + float zRange = zMax - zMin; + if (zRange < 0.1f) return; + + final int BUCKETS = 80; + float[] xMaxB = new float[BUCKETS]; + float[] xMinB = new float[BUCKETS]; + float[] yMaxB = new float[BUCKETS]; + float[] yMinB = new float[BUCKETS]; + java.util.Arrays.fill(xMaxB, -Float.MAX_VALUE); + java.util.Arrays.fill(xMinB, Float.MAX_VALUE); + java.util.Arrays.fill(yMaxB, -Float.MAX_VALUE); + java.util.Arrays.fill(yMinB, Float.MAX_VALUE); + + for (Vector3f v : verts) { + int b = (int) ((v.z - zMin) / zRange * (BUCKETS - 1)); + b = Math.max(0, Math.min(BUCKETS - 1, b)); + if (v.x > xMaxB[b]) xMaxB[b] = v.x; + if (v.x < xMinB[b]) xMinB[b] = v.x; + if (v.y > yMaxB[b]) yMaxB[b] = v.y; + if (v.y < yMinB[b]) yMinB[b] = v.y; + } + + // Score = max(X-Spanne, Y-Spanne) — robust wenn Parierstange in X oder Y breiter ist + float[] score = new float[BUCKETS]; + for (int i = 0; i < BUCKETS; i++) { + float xSpan = (xMaxB[i] > xMinB[i]) ? xMaxB[i] - xMinB[i] : 0f; + float ySpan = (yMaxB[i] > yMinB[i]) ? yMaxB[i] - yMinB[i] : 0f; + score[i] = Math.max(xSpan, ySpan); + } + + // Bucket mit höchstem Score = Parierstange + int bestBucket = 0; + for (int i = 1; i < BUCKETS; i++) { + if (score[i] > score[bestBucket]) bestBucket = i; + } + float guardZ = zMin + (bestBucket + 0.5f) / BUCKETS * zRange; + // X: globaler Midpoint — Klinge läuft entlang Z, X-Mitte ist über alle Buckets konstant + float xGlobalMin = Float.MAX_VALUE, xGlobalMax = -Float.MAX_VALUE; + for (int i = 0; i < BUCKETS; i++) { + if (xMinB[i] < xGlobalMin) xGlobalMin = xMinB[i]; + if (xMaxB[i] > xGlobalMax) xGlobalMax = xMaxB[i]; + } + float guardX = (xGlobalMax + xGlobalMin) * 0.5f; + // Y: Midpoint im Crossguard-Bucket (Höhe der Parierstange) + float guardY = (yMaxB[bestBucket] > yMinB[bestBucket]) ? (yMaxB[bestBucket] + yMinB[bestBucket]) * 0.5f : 0f; + + // Pommel-Richtung: Klingenspitze = dünnstes Ende → Pommel = dickeres Ende + int endB = Math.max(1, BUCKETS / 12); // ~7 Buckets je Seite + float scoreLow = 0f, scoreHigh = 0f; + for (int i = 0; i < endB; i++) scoreLow += score[i]; + for (int i = BUCKETS - endB; i < BUCKETS; i++) scoreHigh += score[i]; + float pommelDirZ = (scoreHigh > scoreLow) ? +1f : -1f; + input.gripEditorBladeDirZ = -pommelDirZ; // Klinge = entgegengesetzt zum Pommel + + // 17 cm Puffer in Richtung Pommel: Hand liegt knapp unterhalb der Parierstange im Griffbereich + final float BUFFER = 0.17f; + float gripZ = guardZ + pommelDirZ * BUFFER; + + log.info("[ModelEditor] GripGuess: Parierstange Z={}, pommelDir={}, Griff Z={} X={} Y={}", + String.format("%.3f", guardZ), pommelDirZ > 0 ? "+Z" : "-Z", + String.format("%.3f", gripZ), + String.format("%.3f", guardX), + String.format("%.3f", guardY)); + + input.gripEditorGuessPosX = guardX; + input.gripEditorGuessPosY = guardY; + input.gripEditorGuessPosZ = gripZ; + } + private static boolean hasSkinningControl(Spatial s) { if (s.getControl(SkinningControl.class) != null) return true; if (s instanceof Node n) { @@ -1533,66 +1870,77 @@ public class ModelEditorState extends BaseAppState { input.modelInteractableActive ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); } - /** Zeichnet / aktualisiert den Grip-Gizmo: 15cm orange (Waffe) oder 28cm cyan (Schild/Unterarm). */ + /** Zeichnet / aktualisiert den Grip-Gizmo: 15cm orange (Waffe/Grip1), blau (Grip2), cyan (Schild). */ public void rebuildGripGizmo() { if (gripGizmoNode == null) return; gripGizmoNode.detachAllChildren(); boolean shieldMode = input.gripEditorIsShield; + float shaftLen = shieldMode ? 0.24f : 0.12f; + float shaftRad = shieldMode ? 0.010f : 0.008f; + float headRad = shieldMode ? 0.026f : 0.022f; + float axisLen = shieldMode ? 0.08f : 0.06f; + float markerSz = shieldMode ? 0.018f : 0.015f; + float bladeDirZ = input.gripEditorBladeDirZ; - // 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; + // Grip 1: orange (Waffe) oder cyan (Schild) + ColorRGBA c1s = shieldMode ? new ColorRGBA(0f, 0.75f, 0.9f, 1f) : new ColorRGBA(1f, 0.55f, 0f, 1f); + ColorRGBA c1h = shieldMode ? new ColorRGBA(0f, 0.5f, 0.75f, 1f) : new ColorRGBA(1f, 0.3f, 0f, 1f); + ColorRGBA c1a = shieldMode ? new ColorRGBA(0.6f, 1f, 0.6f, 1f) : new ColorRGBA(1f, 0.8f, 0.1f, 1f); + gripGizmoNode.attachChild(buildGripGroup("gripGroup", + gripPosX, gripPosY, gripPosZ, gripRotX, gripRotY, gripRotZ, + bladeDirZ, c1s, c1h, c1a, shaftLen, shaftRad, headRad, axisLen, markerSz)); - 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); + // Grip 2: blau — nur für Waffen, nur wenn Position gesetzt + if (!shieldMode && (grip2PosX != 0f || grip2PosY != 0f || grip2PosZ != 0f)) { + gripGizmoNode.attachChild(buildGripGroup("grip2Group", + grip2PosX, grip2PosY, grip2PosZ, 0, 0, 0, + bladeDirZ, + new ColorRGBA(0.2f, 0.5f, 1f, 1f), + new ColorRGBA(0.1f, 0.3f, 0.9f, 1f), + new ColorRGBA(0.5f, 0.75f, 1f, 1f), + shaftLen, shaftRad, headRad, axisLen, markerSz)); + } - Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + gripGizmoNode.setCullHint(Spatial.CullHint.Inherit); + } + + private Node buildGripGroup(String name, + float posX, float posY, float posZ, float rotXDeg, float rotYDeg, float rotZDeg, + float bladeDirZ, ColorRGBA shaftColor, ColorRGBA headColor, ColorRGBA axisColor, + float shaftLen, float shaftRad, float headRad, float axisLen, float markerSz) { + + Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", shaftColor); Material matHead = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); matHead.setColor("Color", headColor); - - Node group = new Node("gripGroup"); - group.setLocalTranslation(gripPosX, gripPosY, gripPosZ); - - Quaternion rot = new Quaternion(); - rot.fromAngles( - com.jme3.math.FastMath.DEG_TO_RAD * gripRotX, - com.jme3.math.FastMath.DEG_TO_RAD * gripRotY, - com.jme3.math.FastMath.DEG_TO_RAD * gripRotZ); - group.setLocalRotation(rot); - - float markerSize = shieldMode ? 0.018f : 0.015f; - Geometry marker = new Geometry("gMarker", new Box(markerSize, markerSize, markerSize)); - marker.setMaterial(mat); - - // Schaft entlang +Z - Geometry shaft = new Geometry("gShaft", new Cylinder(4, 8, shaftRad, shaftLen, true)); - shaft.setMaterial(mat); - shaft.setLocalTranslation(0f, 0f, shaftLen * 0.5f); - - // Kegelspitze: Dome zeigt per Default nach +Y → 90° um X → zeigt nach +Z - Geometry head = new Geometry("gHead", new Dome(Vector3f.ZERO, 2, 8, headRad, false)); - head.setMaterial(matHead); - head.setLocalTranslation(0f, 0f, shaftLen); - Quaternion headRot = new Quaternion(); - headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X); - head.setLocalRotation(headRot); - - // 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)); + + Node group = new Node(name); + group.setLocalTranslation(posX, posY, posZ); + if (rotXDeg != 0 || rotYDeg != 0 || rotZDeg != 0) { + Quaternion rot = new Quaternion(); + rot.fromAngles(FastMath.DEG_TO_RAD * rotXDeg, FastMath.DEG_TO_RAD * rotYDeg, FastMath.DEG_TO_RAD * rotZDeg); + group.setLocalRotation(rot); + } + + Geometry marker = new Geometry(name + "M", new Box(markerSz, markerSz, markerSz)); + marker.setMaterial(mat); + + Geometry shaft = new Geometry(name + "S", new Cylinder(4, 8, shaftRad, shaftLen, true)); + shaft.setMaterial(mat); + shaft.setLocalTranslation(0f, 0f, bladeDirZ * shaftLen * 0.5f); + + Geometry head = new Geometry(name + "H", new Dome(Vector3f.ZERO, 2, 8, headRad, false)); + head.setMaterial(matHead); + head.setLocalTranslation(0f, 0f, bladeDirZ * shaftLen); + Quaternion headRot = new Quaternion(); + headRot.fromAngleAxis(bladeDirZ * FastMath.HALF_PI, Vector3f.UNIT_X); + head.setLocalRotation(headRot); + + // Orientierungsstab +Y (oben): JME-Cylinder liegt auf Z, Rx(-90°) → +Y + Geometry gripAxis = new Geometry(name + "A", new Cylinder(4, 8, 0.006f, axisLen, true)); gripAxis.setMaterial(matAxis); Quaternion axisRot = new Quaternion(); axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X); @@ -1603,8 +1951,7 @@ public class ModelEditorState extends BaseAppState { group.attachChild(shaft); group.attachChild(gripAxis); group.attachChild(head); - gripGizmoNode.attachChild(group); - gripGizmoNode.setCullHint(Spatial.CullHint.Inherit); + return group; } /** Blendet den Grip-Gizmo ein oder aus. */ diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/asset/AssetTreeController.java b/blight-editor/src/main/java/de/blight/editor/toolset/asset/AssetTreeController.java index 5aa5b6e..e29e63b 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/asset/AssetTreeController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/asset/AssetTreeController.java @@ -646,6 +646,12 @@ public class AssetTreeController { tt.play(); } + public void closeOverlayImmediate() { + assetOverlayOpen = false; + assetTabBtn.setText("›"); + assetPanel.setTranslateX(-420); + } + // ── Asset-Tree-Cell (DnD + Kontextmenü) ────────────────────────────────── private TreeCell buildAssetCell() { diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java b/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java index ea48ab5..b538b5f 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java @@ -113,6 +113,14 @@ public class ContentController { /** Per-Frame-Polling: animEmbedStatus + animStripStatus → charEditorStatusLabel. */ public void onFrame() { + float guessZ = ctx.input().gripEditorGuessPosZ; + if (!Float.isNaN(guessZ)) { + float guessX = ctx.input().gripEditorGuessPosX; + float guessY = ctx.input().gripEditorGuessPosY; + ctx.input().gripEditorGuessPosZ = Float.NaN; + gripEditorController.applyGripGuess(guessX, guessY, guessZ); + } + String embedStatus = ctx.input().animEmbedStatus; if (embedStatus != null) { ctx.input().animEmbedStatus = null; @@ -349,24 +357,22 @@ public class ContentController { showContentEditor(); contentLastSection = "item"; if (contentItemBtn != null) contentItemBtn.setSelected(true); - de.blight.editor.ui.ItemEditorView view = - new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); - wireItemView(view); - setContentCenter(view); + activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); + wireItemView(activeItemView); + setContentCenter(activeItemView); setContentRight(null); - if (selectItemId != null) view.selectItem(selectItemId); + if (selectItemId != null) activeItemView.selectItem(selectItemId); return; } 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); - ctx.setCenterView().accept(view); + activeItemView = new de.blight.editor.ui.ItemEditorView(ctx.assetRoot().resolve("items")); + wireItemView(activeItemView); + ctx.setCenterView().accept(activeItemView); ctx.setRightPanel().accept(null); - if (selectItemId != null) view.selectItem(selectItemId); + if (selectItemId != null) activeItemView.selectItem(selectItemId); } /** Prefs-Restore: stellt Dual-Window-Position und Content-Section wieder her. */ diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/content/GripEditorController.java b/blight-editor/src/main/java/de/blight/editor/toolset/content/GripEditorController.java index 08664be..910324d 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/content/GripEditorController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/content/GripEditorController.java @@ -23,6 +23,8 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { private Spinner posXSpin, posYSpin, posZSpin; private Spinner rotXSpin, rotYSpin, rotZSpin; + private Spinner grip2XSpin, grip2YSpin, grip2ZSpin; + private Spinner handOffXSpin, handOffYSpin, handOffZSpin; private Consumer pendingOnSave; private Runnable onReturn; @@ -31,6 +33,28 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { public void setOnReturn(Runnable r) { this.onReturn = r; } + /** Wird aus ContentController.onFrame() aufgerufen wenn JME Position und Richtung des Griffes ermittelt hat. */ + public void applyGripGuess(float gX, float gY, float gZ) { + if (posXSpin == null) return; + suppressSpinnerEvents = true; + posXSpin.getValueFactory().setValue((double) gX); + posYSpin.getValueFactory().setValue((double) gY); + posZSpin.getValueFactory().setValue((double) gZ); + // Rotation zurücksetzen: Schaft zeigt zur Klinge, gelber Stab nach oben + rotXSpin.getValueFactory().setValue(0.0); + rotYSpin.getValueFactory().setValue(0.0); + rotZSpin.getValueFactory().setValue(0.0); + // 2. Griff (linke Hand): direkt angrenzend an den 1. Griff, 5cm weiter Richtung Pommel. + // Grip2-Spinner = Modell-Koordinaten direkt (kein HAND_GRIP_OFFSET), deshalb JSON-Pos von Grip1 verwenden. + float bladeDirZ = ctx.input().gripEditorBladeDirZ; // -1 = Klinge bei -Z, +1 = Klinge bei +Z + float grip1JsonZ = gZ + ItemGrip.HAND_GRIP_OFFSET_Z; + grip2XSpin.getValueFactory().setValue((double) gX); + grip2YSpin.getValueFactory().setValue((double) gY); + grip2ZSpin.getValueFactory().setValue((double)(grip1JsonZ - bladeDirZ * 0.05f)); + suppressSpinnerEvents = false; + onSpinnerChanged(); + } + public GripEditorController(ToolContext ctx) { this.ctx = ctx; } @@ -47,13 +71,20 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { input.gripEditorIsShield = isShield; // Grip-Werte aus Item übernehmen (oder Defaults) + // Spinner/Gizmo zeigen den Handflächen-Kontaktpunkt (= JSON-pos minus HAND_GRIP_OFFSET) ItemGrip g = currentGrip != null ? currentGrip : new ItemGrip(); - input.gripEditorPosX = g.getPosX(); - input.gripEditorPosY = g.getPosY(); - input.gripEditorPosZ = g.getPosZ(); - input.gripEditorRotX = g.getRotX(); - input.gripEditorRotY = g.getRotY(); - input.gripEditorRotZ = g.getRotZ(); + input.gripEditorPosX = g.getPosX() - ItemGrip.HAND_GRIP_OFFSET_X; + input.gripEditorPosY = g.getPosY() - ItemGrip.HAND_GRIP_OFFSET_Y; + input.gripEditorPosZ = g.getPosZ() - ItemGrip.HAND_GRIP_OFFSET_Z; + input.gripEditorRotX = g.getRotX(); + input.gripEditorRotY = g.getRotY(); + input.gripEditorRotZ = g.getRotZ(); + input.gripEditorGrip2PosX = g.getGrip2PosX(); + input.gripEditorGrip2PosY = g.getGrip2PosY(); + input.gripEditorGrip2PosZ = g.getGrip2PosZ(); + input.gripEditorHandOffX = g.getHandOffsetX(); + input.gripEditorHandOffY = g.getHandOffsetY(); + input.gripEditorHandOffZ = g.getHandOffsetZ(); input.gripEditorGizmoDirty = true; // Waffenmodell im Modell-Editor laden @@ -61,12 +92,30 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { input.modelEditorOpenPath = modelPath; } input.activeLayer = SharedInput.LAYER_MODEL_EDITOR; + // JME vollständig aktivieren, damit LightProbe-Bake (8 Frames) nicht gedrosselt läuft + if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false); // Hauptfenster: JME-Viewport + Grip-Panel ctx.setCenterView().accept(ctx.viewportSupplier().get()); ctx.setCurrentTool().accept("gripEditor"); ctx.setMainToolbar().accept(buildToolbar()); - ctx.setRightPanel().accept(buildPanel(g, isShield)); + + VBox innerPanel = buildPanel(g, isShield); + ScrollPane scroll = new ScrollPane(innerPanel); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;"); + scroll.skinProperty().addListener((obs, o, n) -> { + javafx.scene.Node vp = scroll.lookup(".viewport"); + if (vp != null) vp.setStyle("-fx-background-color: #252535;"); + }); + VBox outerPanel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + outerPanel.setPrefWidth(220); + outerPanel.setMaxWidth(220); + outerPanel.setStyle("-fx-background-color: #252535;"); + ctx.setRightPanel().accept(outerPanel); + ctx.setLeftPanel().accept(null); ctx.setBottomPanel().accept(null); } @@ -86,12 +135,18 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { // ── Panel ───────────────────────────────────────────────────────────────── 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"); - rotXSpin = doubleSpin(g.getRotX(), -180.0, 180.0, 1.0, "°"); - rotYSpin = doubleSpin(g.getRotY(), -180.0, 180.0, 1.0, "°"); - rotZSpin = doubleSpin(g.getRotZ(), -180.0, 180.0, 1.0, "°"); + posXSpin = doubleSpin(g.getPosX() - ItemGrip.HAND_GRIP_OFFSET_X, -2.0, 2.0, 0.01, "m"); + posYSpin = doubleSpin(g.getPosY() - ItemGrip.HAND_GRIP_OFFSET_Y, -2.0, 2.0, 0.01, "m"); + posZSpin = doubleSpin(g.getPosZ() - ItemGrip.HAND_GRIP_OFFSET_Z, -2.0, 2.0, 0.01, "m"); + rotXSpin = doubleSpin(g.getRotX(), -180.0, 180.0, 1.0, "°"); + rotYSpin = doubleSpin(g.getRotY(), -180.0, 180.0, 1.0, "°"); + rotZSpin = doubleSpin(g.getRotZ(), -180.0, 180.0, 1.0, "°"); + grip2XSpin = doubleSpin(g.getGrip2PosX(), -2.0, 2.0, 0.01, "m"); + grip2YSpin = doubleSpin(g.getGrip2PosY(), -2.0, 2.0, 0.01, "m"); + grip2ZSpin = doubleSpin(g.getGrip2PosZ(), -2.0, 2.0, 0.01, "m"); + handOffXSpin = doubleSpin(g.getHandOffsetX(), -1.0, 1.0, 0.01, "m"); + handOffYSpin = doubleSpin(g.getHandOffsetY(), -1.0, 1.0, 0.01, "m"); + handOffZSpin = doubleSpin(g.getHandOffsetZ(), -1.0, 1.0, 0.01, "m"); posXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); posYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); @@ -99,12 +154,48 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); rotZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + grip2XSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + grip2YSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + grip2ZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + handOffXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + handOffYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + handOffZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); Button saveBtn = new Button("Speichern"); saveBtn.setMaxWidth(Double.MAX_VALUE); saveBtn.setStyle("-fx-background-color: #2a5a2a; -fx-text-fill: #aaffaa; -fx-cursor: hand; -fx-font-weight: bold;"); saveBtn.setOnAction(e -> onSave()); + Button guessBtn = new Button("Griff raten"); + guessBtn.setMaxWidth(Double.MAX_VALUE); + guessBtn.setStyle("-fx-background-color: #1a3a5a; -fx-text-fill: #88ccff; -fx-cursor: hand;"); + guessBtn.setTooltip(new javafx.scene.control.Tooltip( + "Erkennt die Parierstange im Modell und setzt\n" + + "den Griff-Kontaktpunkt an diese Position.\n" + + "Voraussetzung: Klinge liegt auf der Z-Achse.")); + guessBtn.setOnAction(e -> ctx.input().gripEditorGuessRequest = true); + + Button resetBtn = new Button("Griff zurücksetzen"); + resetBtn.setMaxWidth(Double.MAX_VALUE); + resetBtn.setStyle("-fx-background-color: #5a3a1a; -fx-text-fill: #ffcc88; -fx-cursor: hand;"); + resetBtn.setOnAction(e -> { + suppressSpinnerEvents = true; + posXSpin.getValueFactory().setValue(0.0); + posYSpin.getValueFactory().setValue(0.0); + posZSpin.getValueFactory().setValue(0.0); + rotXSpin.getValueFactory().setValue(0.0); + rotYSpin.getValueFactory().setValue(0.0); + rotZSpin.getValueFactory().setValue(0.0); + grip2XSpin.getValueFactory().setValue(0.0); + grip2YSpin.getValueFactory().setValue(0.0); + grip2ZSpin.getValueFactory().setValue(0.0); + handOffXSpin.getValueFactory().setValue(0.0); + handOffYSpin.getValueFactory().setValue(0.0); + handOffZSpin.getValueFactory().setValue(0.0); + suppressSpinnerEvents = false; + onSpinnerChanged(); + }); + Button cancelBtn = new Button("Abbrechen"); cancelBtn.setMaxWidth(Double.MAX_VALUE); cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;"); @@ -116,21 +207,37 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { panel.setPrefWidth(220); panel.getChildren().addAll( - styledTitle("Position (m)"), + styledTitle("1. Griff – Position (m)"), + styledHint("Handflächen-Kontaktpunkt im Modellraum (oranges Gizmo)."), row("X:", posXSpin), row("Y:", posYSpin), row("Z:", posZSpin), new Separator(), - styledTitle("Rotation (°)"), + styledTitle("Rotation Hand (°)"), + styledHint("Dreht den gesamten Griff-Gizmo. 0° = Hand oben (+Y), Klinge in -Z."), row("X:", rotXSpin), row("Y:", rotYSpin), row("Z:", rotZSpin), new Separator(), + styledTitle("2. Griff – Linke Hand (m)"), + styledHint("Zweiter Griffpunkt (blaues Gizmo) für Zweihänder.\nDirekt angrenzend an den 1. Griff, Richtung Knauf."), + row("X:", grip2XSpin), + row("Y:", grip2YSpin), + row("Z:", grip2ZSpin), + new Separator(), + styledTitle("Hand-Offset (Char-Space, m)"), + styledHint("Verschiebt die sichtbare Hand relativ zum Griff-Kontaktpunkt.\nX=rechts, Y=oben, Z=vorwärts."), + row("X:", handOffXSpin), + row("Y:", handOffYSpin), + row("Z:", handOffZSpin), + new Separator(), styledHint(isShield - ? "Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge." - : "Pfeil zeigt Klingenrichtung.\n15cm Länge = Maßstab."), + ? "Cyan-Pfeil zeigt vom Ellbogen zur Hand.\n28cm = Unterarmlänge." + : "Orange-Pfeil zeigt zur Klinge, gelber Stab nach oben (+Y).\nBlauer Pfeil = 2. Griff (linke Hand)."), new Separator(), saveBtn, + guessBtn, + resetBtn, cancelBtn ); return panel; @@ -141,12 +248,18 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { private void onSpinnerChanged() { if (suppressSpinnerEvents) return; SharedInput input = ctx.input(); - input.gripEditorPosX = posXSpin.getValue().floatValue(); - input.gripEditorPosY = posYSpin.getValue().floatValue(); - input.gripEditorPosZ = posZSpin.getValue().floatValue(); - input.gripEditorRotX = rotXSpin.getValue().floatValue(); - input.gripEditorRotY = rotYSpin.getValue().floatValue(); - input.gripEditorRotZ = rotZSpin.getValue().floatValue(); + input.gripEditorPosX = posXSpin.getValue().floatValue(); + input.gripEditorPosY = posYSpin.getValue().floatValue(); + input.gripEditorPosZ = posZSpin.getValue().floatValue(); + input.gripEditorRotX = rotXSpin.getValue().floatValue(); + input.gripEditorRotY = rotYSpin.getValue().floatValue(); + input.gripEditorRotZ = rotZSpin.getValue().floatValue(); + input.gripEditorGrip2PosX = grip2XSpin.getValue().floatValue(); + input.gripEditorGrip2PosY = grip2YSpin.getValue().floatValue(); + input.gripEditorGrip2PosZ = grip2ZSpin.getValue().floatValue(); + input.gripEditorHandOffX = handOffXSpin.getValue().floatValue(); + input.gripEditorHandOffY = handOffYSpin.getValue().floatValue(); + input.gripEditorHandOffZ = handOffZSpin.getValue().floatValue(); input.gripEditorGizmoDirty = true; } @@ -154,12 +267,19 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { private void onSave() { ItemGrip grip = new ItemGrip(); - grip.setPosX(posXSpin.getValue().floatValue()); - grip.setPosY(posYSpin.getValue().floatValue()); - grip.setPosZ(posZSpin.getValue().floatValue()); + // Spinner zeigen Handflächen-Position → HAND_GRIP_OFFSET wieder addieren für JSON + grip.setPosX(posXSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_X); + grip.setPosY(posYSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_Y); + grip.setPosZ(posZSpin.getValue().floatValue() + ItemGrip.HAND_GRIP_OFFSET_Z); grip.setRotX(rotXSpin.getValue().floatValue()); grip.setRotY(rotYSpin.getValue().floatValue()); grip.setRotZ(rotZSpin.getValue().floatValue()); + grip.setGrip2PosX(grip2XSpin.getValue().floatValue()); + grip.setGrip2PosY(grip2YSpin.getValue().floatValue()); + grip.setGrip2PosZ(grip2ZSpin.getValue().floatValue()); + grip.setHandOffsetX(handOffXSpin.getValue().floatValue()); + grip.setHandOffsetY(handOffYSpin.getValue().floatValue()); + grip.setHandOffsetZ(handOffZSpin.getValue().floatValue()); SharedInput input = ctx.input(); input.gripEditorActive = false; diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java b/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java index 70b72dc..fa4dc95 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java @@ -247,6 +247,7 @@ public class GameController { Path runDir = ProjectRoot.PATH.resolve("run"); Path sessionDir = runDir.resolve("session-" + System.currentTimeMillis()); copyDirectory(srcMapDir, sessionDir); + in.activeSessionDir = sessionDir; try { java.util.List sessions; @@ -310,6 +311,7 @@ public class GameController { } } consoleBuffer.offer("--- Spiel beendet ---"); + in.activeSessionDir = null; Platform.runLater(() -> { if (primary != null) primary.setIconified(false); if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); } diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java b/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java index fc7ed59..131fd4b 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/model/ModelEditorController.java @@ -98,6 +98,7 @@ public class ModelEditorController implements ToolController { modelEditorCurrentPath = relPath; ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR; + if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false); ctx.setCenterView().accept(ctx.viewportSupplier().get()); openAssetOverlay.run(); @@ -181,6 +182,13 @@ public class ModelEditorController implements ToolController { if (modelEditorSpinZ != null) modelEditorSpinZ.getValueFactory().setValue(bsZ); } + // Align-Weapon-Ergebnis anzeigen + String alignResult = ctx.input().modelEditorAlignWeaponResult; + if (alignResult != null) { + ctx.input().modelEditorAlignWeaponResult = null; + ctx.setStatus().accept("Ausgerichtet: " + alignResult); + } + // Bounds-Aktualisierung if (ctx.input().modelEditorBoundsReady) { ctx.input().modelEditorBoundsReady = false; @@ -811,7 +819,13 @@ public class ModelEditorController implements ToolController { closeBtn.setMaxWidth(Double.MAX_VALUE); closeBtn.setStyle("-fx-background-color:#555; -fx-text-fill:#ccc;"); - saveBtn.setOnAction(e -> saveModelMeta( + saveBtn.setOnAction(e -> { + if (modelEditorSpinX != null) { + modelEditorSpinX.commitValue(); + modelEditorSpinY.commitValue(); + modelEditorSpinZ.commitValue(); + } + saveModelMeta( relPath, absolutePath, nameTF.getText().trim(), catCB.getValue() != null ? catCB.getValue().trim() : "", @@ -843,7 +857,8 @@ public class ModelEditorController implements ToolController { : "", modelEditorCategoryCB != null ? modelEditorCategoryCB.getValue() - : de.blight.common.ModelCategory.NONE)); + : de.blight.common.ModelCategory.NONE); + }); placeBtn.setOnAction(e -> { ctx.input().modelEditorCloseRequest = true; @@ -858,6 +873,35 @@ public class ModelEditorController implements ToolController { ctx.switchToWorld().run(); }); + // Waffe an Z-Achse ausrichten + Label toolsTitle = new Label("Werkzeuge:"); + toolsTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); + Button alignWeaponBtn = new Button("Waffe an Z-Achse ausrichten"); + alignWeaponBtn.setMaxWidth(Double.MAX_VALUE); + alignWeaponBtn.setStyle("-fx-background-color:#3a3a5a; -fx-text-fill:#ddddff; -fx-cursor:hand;"); + alignWeaponBtn.setTooltip(new Tooltip( + "Dreht das Modell so, dass die längste Achse auf +Z zeigt.\n" + + "Verschiebt die Mitte der Waffe in den Ursprung (0,0,0).\n" + + "Bäckt das Ergebnis in die Vertex-Positionen ein.")); + alignWeaponBtn.setOnAction(e -> { + if (absolutePath == null) return; + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.initOwner(ctx.stageSupplier().get()); + confirm.setTitle("Waffe ausrichten"); + confirm.setHeaderText("Modell an Z-Achse ausrichten?"); + confirm.setContentText( + "Die längste Achse der Waffe wird auf +Z ausgerichtet\n" + + "und die Mitte in den Ursprung gesetzt.\n\n" + + "Die Transformation wird direkt in die Vertex-\n" + + "Positionen eingebrannt. Nicht rückgängig machbar."); + confirm.showAndWait().ifPresent(btn -> { + if (btn == ButtonType.OK) { + ctx.input().modelEditorAlignWeaponPath = absolutePath; + ctx.input().modelEditorAlignWeaponRequest = true; + } + }); + }); + panel.getChildren().addAll( title, new Separator(), @@ -889,6 +933,8 @@ public class ModelEditorController implements ToolController { new Separator(), footstepTitle, modelEditorFootstepSurfaceCB, new Separator(), + toolsTitle, alignWeaponBtn, + new Separator(), saveBtn, placeBtn, closeBtn ); return panel; @@ -972,6 +1018,7 @@ public class ModelEditorController implements ToolController { ctx.setMainToolbar().accept(buildModelImportToolBar()); ctx.input().activeLayer = SharedInput.LAYER_MODEL_EDITOR; + if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false); ctx.setCenterView().accept(ctx.viewportSupplier().get()); openAssetOverlay.run(); @@ -1291,6 +1338,14 @@ public class ModelEditorController implements ToolController { s.setEditable(true); s.setMaxWidth(Double.MAX_VALUE); s.setStyle("-fx-background-color:#3a3a4e;"); + // Tab-Filter: feuert in der Filter-Phase, bevor FocusTraversalInputMap den Fokus verschiebt. + // Damit ist getText() noch der getippte Wert, nicht der bereits zurückgesetzte. + s.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, ev -> { + if (ev.getCode() == javafx.scene.input.KeyCode.TAB) s.commitValue(); + }); + s.focusWithinProperty().addListener((obs, was, now) -> { + if (!now) s.commitValue(); + }); return s; } diff --git a/blight-game/src/main/java/de/blight/game/control/NpcWeaponAttachmentControl.java b/blight-game/src/main/java/de/blight/game/control/NpcWeaponAttachmentControl.java index bbed4f5..ad5db4b 100644 --- a/blight-game/src/main/java/de/blight/game/control/NpcWeaponAttachmentControl.java +++ b/blight-game/src/main/java/de/blight/game/control/NpcWeaponAttachmentControl.java @@ -11,6 +11,7 @@ import com.jme3.scene.Spatial; import com.jme3.scene.control.AbstractControl; import de.blight.common.model.Item; import de.blight.common.model.ItemGrip; +import de.blight.common.model.ItemSubCategory; import de.blight.game.animation.RetargetingSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,11 +29,13 @@ public class NpcWeaponAttachmentControl extends AbstractControl { 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; private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f); - private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.00f, 0.05f); + private static final Vector3f HAND_GRIP_OFFSET = new Vector3f( + ItemGrip.HAND_GRIP_OFFSET_X, ItemGrip.HAND_GRIP_OFFSET_Y, ItemGrip.HAND_GRIP_OFFSET_Z); private static final Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f); private static final float SHIELD_ARM_ROT_X = -HALF_PI; @@ -97,7 +100,6 @@ public class NpcWeaponAttachmentControl extends AbstractControl { } catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage()); } try { leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM); } catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage()); } - handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND); leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM); @@ -126,7 +128,6 @@ public class NpcWeaponAttachmentControl extends AbstractControl { palmOffsetLocal = computePalmOffset(handNode, palmNodeRef); } } - Quaternion charWorldRotInv = spatial.getWorldRotation().inverse(); Vector3f charWorldPos = spatial.getWorldTranslation(); @@ -137,11 +138,23 @@ public class NpcWeaponAttachmentControl extends AbstractControl { Vector3f boneInChar = charWorldRotInv.mult(boneNode.getWorldTranslation().subtract(charWorldPos)); Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation()); if (drawn) { + ItemGrip wGrip = weaponItem != null ? weaponItem.getGrip() : null; + boolean isTwoHand = weaponItem != null && weaponItem.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD; Vector3f attachInChar = (palmOffsetLocal != null) ? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f))) : boneInChar; - applyHandTransform(weaponSpatial, attachInChar, boneRotInChar, - weaponItem != null ? weaponItem.getGrip() : null); + boolean usesTwoHand = isTwoHand && leftForeArmNode != null + && wGrip != null + && (wGrip.getGrip2PosX() != 0f || wGrip.getGrip2PosY() != 0f || wGrip.getGrip2PosZ() != 0f); + if (usesTwoHand) { + Vector3f handballen = (palmOffsetLocal != null) + ? boneInChar.subtract(boneRotInChar.mult(palmOffsetLocal.mult(0.5f))) + : boneInChar; + applyTwoHandTransform(weaponSpatial, handballen, boneRotInChar, + wGrip, charWorldRotInv, charWorldPos); + } else { + applyHandTransform(weaponSpatial, attachInChar, boneRotInChar, wGrip); + } } else { applyBackTransform(weaponSpatial, boneInChar, boneRotInChar, weaponItem != null ? weaponItem.getGrip() : null); @@ -195,22 +208,92 @@ public class NpcWeaponAttachmentControl extends AbstractControl { Quaternion animDelta = (handBindRotInChar != null) ? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar; - Quaternion weaponRot = animDelta.mult(gripRot.inverse()); + boolean bladeAtMinusZ = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ(); + Quaternion bladeFlip = bladeAtMinusZ + ? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f) + : new Quaternion(); + Quaternion weaponRot = animDelta.mult(bladeFlip).mult(gripRot.inverse()); + Vector3f handOff = bladeAtMinusZ + ? new Vector3f(HAND_GRIP_OFFSET.x, HAND_GRIP_OFFSET.y, -HAND_GRIP_OFFSET.z) + : HAND_GRIP_OFFSET; Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos)); - weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET)); + weaponPos = weaponPos.add(weaponRot.mult(handOff)); s.setLocalRotation(weaponRot); s.setLocalTranslation(weaponPos); } + /** + * Zweihänder: Schwert dreht sich frei um Griff1 (rechte Handteller-Innenfläche) als Pivot; + * die Richtung wird so gewählt, dass Griff2 so nah wie möglich am linken Handteller liegt. + */ + private void applyTwoHandTransform(Spatial s, Vector3f bonePosInChar, Quaternion boneRotInChar, + ItemGrip grip, Quaternion charWorldRotInv, Vector3f charWorldPos) { + Quaternion gripRot = new Quaternion().fromAngles( + DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ()); + Quaternion animDelta = (handBindRotInChar != null) + ? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar; + boolean bladeAtMinusZ3 = grip.getGrip2PosZ() > grip.getPosZ(); + Quaternion bladeFlip3 = bladeAtMinusZ3 + ? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f) + : new Quaternion(); + Quaternion weaponRot = animDelta.mult(bladeFlip3).mult(gripRot.inverse()); + + // Griff-Achse im Modell-Space: Griff1 → Griff2 + Vector3f g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()); + Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ()); + Vector3f modelGripAxis = g2.subtract(g1).normalizeLocal(); + Vector3f currentGripAxis = weaponRot.mult(modelGripAxis); + + // Zielachse: rechtes Handgelenk → linkes Handgelenk im Char-Space + Vector3f rightWristInChar = charWorldRotInv.mult(handNode.getWorldTranslation().subtract(charWorldPos)); + Vector3f leftWristInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); + Vector3f targetGripAxis = leftWristInChar.subtract(rightWristInChar).normalizeLocal(); + + Quaternion swingCorr = swingFromTo(currentGripAxis, targetGripAxis); + weaponRot = swingCorr.mult(weaponRot); + + // Grip1 direkt am Handteller platzieren (bonePosInChar enthält palmOffset bereits). + // Kein zusätzlicher HAND_GRIP_OFFSET – wir haben die exakten Greifpositionen. + Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1)); + s.setLocalRotation(weaponRot); + s.setLocalTranslation(weaponPos); + } + + private static Quaternion swingFromTo(Vector3f from, Vector3f to) { + float dot = com.jme3.math.FastMath.clamp(from.dot(to), -1f, 1f); + Vector3f axis = from.cross(to); + float axisLen = axis.length(); + if (axisLen < 0.001f) { + if (dot > 0f) return new Quaternion(); + Vector3f perp = Math.abs(from.x) <= Math.abs(from.y) + ? from.cross(Vector3f.UNIT_X).normalizeLocal() + : from.cross(Vector3f.UNIT_Y).normalizeLocal(); + return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.PI, perp); + } + return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.acos(dot), axis.divideLocal(axisLen)); + } + + private static final Quaternion BACK_ROT_NORM = new Quaternion(0f, -(float) Math.sqrt(0.5), (float) Math.sqrt(0.5), 0f); + private void applyBackTransform(Spatial s, Vector3f boneInChar, Quaternion boneRotInChar, ItemGrip grip) { - Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI); - Quaternion weaponRot = boneRotInChar.mult(backRot); + Quaternion gripRot = (grip != null) + ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), + DEG_TO_RAD * grip.getRotY(), + DEG_TO_RAD * grip.getRotZ()) + : new Quaternion(); + boolean bladeAtMinusZ2 = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ(); + Quaternion bladeFlip2 = bladeAtMinusZ2 + ? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f) + : new Quaternion(); + Quaternion weaponRot = boneRotInChar.mult(BACK_ROT_NORM).mult(bladeFlip2).mult(gripRot.inverse()); Vector3f target = boneInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET)); Vector3f gripPos = grip != null ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; + Vector3f backPos = target.subtract(weaponRot.mult(gripPos)); + if (bladeAtMinusZ2) backPos = backPos.add(0.1f, 0f, 0f); s.setLocalRotation(weaponRot); - s.setLocalTranslation(target.subtract(weaponRot.mult(gripPos))); + s.setLocalTranslation(backPos); } // ── Schild-Transforms (1:1 von WeaponAttachmentControl) ────────────────────── diff --git a/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java b/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java index 56c13ba..4bcf44a 100644 --- a/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java +++ b/blight-game/src/main/java/de/blight/game/control/WeaponAttachmentControl.java @@ -11,6 +11,7 @@ import com.jme3.scene.Spatial; import com.jme3.scene.control.AbstractControl; import de.blight.common.model.Item; import de.blight.common.model.ItemGrip; +import de.blight.common.model.ItemSubCategory; import de.blight.common.model.MainCharacter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,12 +32,13 @@ public class WeaponAttachmentControl extends AbstractControl { 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; private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f); - /** Zusatz-Versatz beim gezückten Schwert in lokalem Waffen-Space (X=entlang Klinge, Y=entlang Griff, Z=senkrecht zur Schneide). */ - private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.0f, 0.05f); + private static final Vector3f HAND_GRIP_OFFSET = new Vector3f( + ItemGrip.HAND_GRIP_OFFSET_X, ItemGrip.HAND_GRIP_OFFSET_Y, ItemGrip.HAND_GRIP_OFFSET_Z); /** 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); @@ -126,7 +128,6 @@ public class WeaponAttachmentControl extends AbstractControl { log.info("[WeaponAttach] palmOffsetLocal (from nodes)={}", palmOffsetLocal); } } - Item current = mc.getEquippedMeleeWeapon(); Item shield = mc.getEquippedShield(); boolean drawn = playerInput.isWeaponDrawn(); @@ -139,7 +140,7 @@ public class WeaponAttachmentControl extends AbstractControl { if (weaponSpatial != null) { lastDrawnState = drawn; - updateWeaponTransform(drawn, current != null ? current.getGrip() : null); + updateWeaponTransform(drawn, current); } if (shield != lastEquippedShield) { @@ -173,7 +174,8 @@ public class WeaponAttachmentControl extends AbstractControl { // ── Transform-Update (jedes Frame) ──────────────────────────────────────── - private void updateWeaponTransform(boolean drawn, ItemGrip grip) { + private void updateWeaponTransform(boolean drawn, Item item) { + ItemGrip grip = item != null ? item.getGrip() : null; Node boneNode = drawn ? handNode : backNode; if (boneNode == null) return; @@ -185,10 +187,22 @@ public class WeaponAttachmentControl extends AbstractControl { Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation()); if (drawn) { + boolean isTwoHand = item != null && item.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD; Vector3f attachInChar = (palmOffsetLocal != null) ? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f))) : boneInChar; - applyHandTransform(attachInChar, boneRotInChar, grip); + boolean usesTwoHand = isTwoHand && leftForeArmNode != null + && grip != null + && (grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f); + if (usesTwoHand) { + // Handballen: entgegengesetzte Richtung von palmOffset (der Richtung Finger zeigt) + Vector3f handballen = (palmOffsetLocal != null) + ? boneInChar.subtract(boneRotInChar.mult(palmOffsetLocal.mult(0.5f))) + : boneInChar; + applyTwoHandTransform(handballen, boneRotInChar, grip, charWorldRotInv, charWorldPos); + } else { + applyHandTransform(attachInChar, boneRotInChar, grip); + } } else { applyBackTransform(boneInChar, boneRotInChar, grip); } @@ -205,7 +219,11 @@ public class WeaponAttachmentControl extends AbstractControl { Quaternion animDelta = (handBindRotInChar != null) ? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar; - Quaternion weaponRot = animDelta.mult(gripRot.inverse()); + boolean bladeAtMinusZ2 = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ(); + Quaternion bladeFlip2 = bladeAtMinusZ2 + ? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f) + : new Quaternion(); + Quaternion weaponRot = animDelta.mult(bladeFlip2).mult(gripRot.inverse()); if (!gripDiagLogged && weaponSpatial != null) { gripDiagLogged = true; @@ -240,12 +258,74 @@ public class WeaponAttachmentControl extends AbstractControl { } } + Vector3f handOff2 = bladeAtMinusZ2 + ? new Vector3f(HAND_GRIP_OFFSET.x, HAND_GRIP_OFFSET.y, -HAND_GRIP_OFFSET.z) + : HAND_GRIP_OFFSET; Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos)); - weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET)); + weaponPos = weaponPos.add(weaponRot.mult(handOff2)); + if (grip != null && (grip.getHandOffsetX() != 0f || grip.getHandOffsetY() != 0f || grip.getHandOffsetZ() != 0f)) { + weaponPos = weaponPos.add(grip.getHandOffsetX(), grip.getHandOffsetY(), grip.getHandOffsetZ()); + } weaponSpatial.setLocalRotation(weaponRot); weaponSpatial.setLocalTranslation(weaponPos); } + /** + * Zweihänder-Constraint: orientiert die Waffe so, dass die Linie grip1→grip2 (Modell-Space) + * auf die Linie RightHand→LeftHand (Char-Space) zeigt. Roll bleibt aus gripRot erhalten. + */ + private void applyTwoHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, + ItemGrip grip, + Quaternion charWorldRotInv, Vector3f charWorldPos) { + // Erst Standard-Einhand-Rotation berechnen (gibt Roll-Basis aus gripRot) + Quaternion gripRot = new Quaternion().fromAngles( + DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ()); + Quaternion animDelta = (handBindRotInChar != null) + ? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar; + boolean bladeAtMinusZ3 = grip.getGrip2PosZ() > grip.getPosZ(); + Quaternion bladeFlip3 = bladeAtMinusZ3 + ? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f) + : new Quaternion(); + Quaternion weaponRot = animDelta.mult(bladeFlip3).mult(gripRot.inverse()); + + // Griff-Achse im Modell-Space: grip1 → grip2 + Vector3f g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()); + Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ()); + Vector3f modelGripAxis = g2.subtract(g1).normalizeLocal(); + + // Aktuelle Griff-Achse im Char-Space (nach Einhand-Rotation) + Vector3f currentGripAxis = weaponRot.mult(modelGripAxis); + + // Ziel-Achse: rechtes Handgelenk → linkes Handgelenk im Char-Space + Vector3f rightWristInChar = charWorldRotInv.mult(handNode.getWorldTranslation().subtract(charWorldPos)); + Vector3f leftWristInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); + Vector3f targetGripAxis = leftWristInChar.subtract(rightWristInChar).normalizeLocal(); + + // Minimale Swing-Rotation: dreht currentGripAxis → targetGripAxis, kein Roll-Einfluss + Quaternion swingCorr = swingFromTo(currentGripAxis, targetGripAxis); + weaponRot = swingCorr.mult(weaponRot); + + // Grip1 direkt am Handteller platzieren (bonePosInChar enthält palmOffset bereits). + // Kein zusätzlicher HAND_GRIP_OFFSET – wir haben die exakten Greifpositionen. + Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1)); + weaponSpatial.setLocalRotation(weaponRot); + weaponSpatial.setLocalTranslation(weaponPos); + } + + private static Quaternion swingFromTo(Vector3f from, Vector3f to) { + float dot = com.jme3.math.FastMath.clamp(from.dot(to), -1f, 1f); + Vector3f axis = from.cross(to); + float axisLen = axis.length(); + if (axisLen < 0.001f) { + if (dot > 0f) return new Quaternion(); + Vector3f perp = Math.abs(from.x) <= Math.abs(from.y) + ? from.cross(Vector3f.UNIT_X).normalizeLocal() + : from.cross(Vector3f.UNIT_Y).normalizeLocal(); + return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.PI, perp); + } + return new Quaternion().fromAngleNormalAxis(com.jme3.math.FastMath.acos(dot), axis.divideLocal(axisLen)); + } + private static float round1(float v) { return Math.round(v * 10f) / 10f; } /** @@ -273,15 +353,32 @@ public class WeaponAttachmentControl extends AbstractControl { return bindMat.toRotationQuat().normalizeLocal(); } + // backRot normalisiert: fromAngles(0,0,PI/2) * kurzschwert_gripRot + // → Waffe wird zuerst per gripRot^-1 normalisiert, dann per backRot am Rücken ausgerichtet. + // Ergebnis für Kurzschwert (gripRot=fromAngles(0,-90°,90°)) identisch zur alten Formel. + private static final Quaternion BACK_ROT_NORM = new Quaternion(0f, -(float) Math.sqrt(0.5), (float) Math.sqrt(0.5), 0f); + private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { - Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI); - Quaternion weaponRot = boneRotInChar.mult(backRot); + Quaternion gripRot = (grip != null) + ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), + DEG_TO_RAD * grip.getRotY(), + DEG_TO_RAD * grip.getRotZ()) + : new Quaternion(); + // Klinge bei -Z erkannt (grip2PosZ > posZ) → 180° um X vorschalten, damit BACK_ROT_NORM + // dieselbe Wirkung hat wie bei korrekt ausgerichtetem Modell (Klinge bei +Z). + boolean bladeAtMinusZ = grip != null && grip.getGrip2PosZ() != 0f && grip.getGrip2PosZ() > grip.getPosZ(); + Quaternion bladeFlip = bladeAtMinusZ + ? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f) + : new Quaternion(); + Quaternion weaponRot = boneRotInChar.mult(BACK_ROT_NORM).mult(bladeFlip).mult(gripRot.inverse()); Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET)); - Vector3f gripPos = (grip != null) + Vector3f gripPos = (grip != null) ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; + Vector3f backPos = gripTarget.subtract(weaponRot.mult(gripPos)); + if (bladeAtMinusZ) backPos = backPos.add(0.1f, 0f, 0f); weaponSpatial.setLocalRotation(weaponRot); - weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos))); + weaponSpatial.setLocalTranslation(backPos); } // ── Schild ──────────────────────────────────────────────────────────────── diff --git a/blight-lang/src/main/resources/lang/messages_de.properties b/blight-lang/src/main/resources/lang/messages_de.properties index 46b3c7d..958913a 100644 --- a/blight-lang/src/main/resources/lang/messages_de.properties +++ b/blight-lang/src/main/resources/lang/messages_de.properties @@ -285,3 +285,4 @@ npc.aggro.death.5=Das nächste Mal bleib, wo du hingehörst. npc.bandit1.name=Bandit npc.bjoernson.name=Bjoernson npc.silas.name=Silas +rostiger_zweihaender.name=Rostiger Zweihaender diff --git a/blight-lang/src/main/resources/lang/messages_en.properties b/blight-lang/src/main/resources/lang/messages_en.properties index 5b242ec..92b90fd 100644 --- a/blight-lang/src/main/resources/lang/messages_en.properties +++ b/blight-lang/src/main/resources/lang/messages_en.properties @@ -140,3 +140,4 @@ npc.aggro.death.5=Next time, know your place. npc.bandit1.name=Bandit npc.bjoernson.name=Bjoernson npc.silas.name=Silas +rostiger_zweihaender.name=Rostiger Zweihaender diff --git a/blight-lang/src/main/resources/lang/text_keys.txt b/blight-lang/src/main/resources/lang/text_keys.txt index be4e092..02ac7b7 100644 --- a/blight-lang/src/main/resources/lang/text_keys.txt +++ b/blight-lang/src/main/resources/lang/text_keys.txt @@ -144,6 +144,8 @@ neues_item_1789920920760.description neues_item_1789920920760.name neues_item_1789920asdf920760.description neues_item_1789920asdf920760.name +neues_item_1791316630215.description +neues_item_1791316630215.name quest.blight_wuehler.description quest.blight_wuehler.name quest.blight_wuehler.success @@ -153,6 +155,8 @@ quest.kuehles_bier.success quest.magnetiterz.description quest.magnetiterz.name quest.magnetiterz.success +rostiger_zweihaender.description +rostiger_zweihaender.name rostiges_kurzschwert.description rostiges_kurzschwert.name silas.name diff --git a/blight-map/src/main/map/blight_placed_items.bpi b/blight-map/src/main/map/blight_placed_items.bpi index 6209e86..e9c2b41 100644 --- a/blight-map/src/main/map/blight_placed_items.bpi +++ b/blight-map/src/main/map/blight_placed_items.bpi @@ -1,3 +1,4 @@ # 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 +caa6cd73-4a2d-4112-9e22-b4d250ea41b0 rostiges_kurzschwert -2.46281 2.00000 -2.27693 0.00000 0.00000 0.00000 +4fa26305-9741-4c8f-856d-20242b613486 einfacher_schild -4.45856 1.72895 -3.67467 -0.14178 0.64812 0.00000 +89bd439c-b4d9-4fa9-8886-4ba51e19d244 rostiger_zweihaender 0.02061 2.00000 -10.13625 -1.58565 0.00000 0.00000