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 35376ac..4a9168b 100644 --- a/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item +++ b/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item @@ -11,12 +11,12 @@ "worthGold": 0, "damage": 0, "grip": { - "posX": 0.37, - "posY": 0.08, - "posZ": 0.0, + "posX": 0.11149749, + "posY": 0.07795838, + "posZ": 0.36503196, "rotX": 0.0, - "rotY": -90.0, - "rotZ": 90.0, + "rotY": 0.0, + "rotZ": 0.0, "grip2PosX": 0.0, "grip2PosY": 0.0, "grip2PosZ": 0.0, diff --git a/blight-assets/src/main/resources/weapon_attachment.json b/blight-assets/src/main/resources/weapon_attachment.json new file mode 100644 index 0000000..a9bf810 --- /dev/null +++ b/blight-assets/src/main/resources/weapon_attachment.json @@ -0,0 +1,18 @@ +{ + "SWORD": { + "handOffX": 0.1, + "handOffY": 0.0, + "handOffZ": 0.05, + "hand2OffX": 0.0, + "hand2OffY": 0.0, + "hand2OffZ": 0.0 + }, + "TWO_HANDED_SWORD": { + "handOffX": 0.08, + "handOffY": 0.15, + "handOffZ": -0.09, + "hand2OffX": -0.005, + "hand2OffY": -0.04, + "hand2OffZ": 0.065 + } +} \ No newline at end of file diff --git a/blight-common/src/main/java/de/blight/common/model/WeaponAttachmentIO.java b/blight-common/src/main/java/de/blight/common/model/WeaponAttachmentIO.java new file mode 100644 index 0000000..85a24c4 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/model/WeaponAttachmentIO.java @@ -0,0 +1,99 @@ +package de.blight.common.model; + +import com.google.gson.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +/** + * Lädt und speichert per-Kategorie Waffen-Attachment-Konfiguration (weapon_attachment.json). + * Singleton-Map: wird beim ersten Zugriff automatisch vom Classpath geladen. + */ +public final class WeaponAttachmentIO { + + private static final Logger log = LoggerFactory.getLogger(WeaponAttachmentIO.class); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final String CLASSPATH_FILE = "weapon_attachment.json"; + + private static final Map CONFIGS = new HashMap<>(); + private static boolean loaded = false; + + private WeaponAttachmentIO() {} + + /** Gibt die Attachment-Konfiguration für die gegebene Kategorie zurück (lazy-load vom Classpath). */ + public static WeaponCategoryAttachment get(ItemSubCategory cat) { + ensureLoaded(); + if (cat == null) return new WeaponCategoryAttachment(); + return CONFIGS.getOrDefault(cat.name(), new WeaponCategoryAttachment()); + } + + public static void set(ItemSubCategory cat, WeaponCategoryAttachment cfg) { + if (cat != null) CONFIGS.put(cat.name(), cfg); + } + + /** Lädt weapon_attachment.json vom Classpath (für Runtime im Spiel). */ + public static synchronized void loadFromClasspath() { + try (InputStream is = WeaponAttachmentIO.class.getClassLoader() + .getResourceAsStream(CLASSPATH_FILE)) { + if (is == null) { + log.warn("[WeaponAttachmentIO] {} nicht auf Classpath gefunden – Defaults verwendet", + CLASSPATH_FILE); + loaded = true; + return; + } + parse(new String(is.readAllBytes(), StandardCharsets.UTF_8)); + loaded = true; + } catch (IOException e) { + log.warn("[WeaponAttachmentIO] Ladefehler: {}", e.getMessage()); + loaded = true; + } + } + + /** Lädt weapon_attachment.json vom Dateisystem (für Editor). */ + public static void load(Path file) { + if (!Files.exists(file)) return; + try { + parse(Files.readString(file, StandardCharsets.UTF_8)); + loaded = true; + } catch (IOException e) { + log.warn("[WeaponAttachmentIO] Ladefehler: {}", e.getMessage()); + } + } + + /** Speichert die aktuelle Konfiguration in eine JSON-Datei (nur im Editor). */ + public static void save(Path file) { + try { + Files.writeString(file, GSON.toJson(CONFIGS), StandardCharsets.UTF_8); + log.info("[WeaponAttachmentIO] Gespeichert: {}", file); + } catch (IOException e) { + log.warn("[WeaponAttachmentIO] Speicherfehler: {}", e.getMessage()); + } + } + + /** Gibt eine Kopie der aktuellen Config-Map zurück (für Editor-Anzeige). */ + public static Map getAll() { + ensureLoaded(); + return new HashMap<>(CONFIGS); + } + + private static synchronized void ensureLoaded() { + if (!loaded) loadFromClasspath(); + } + + private static void parse(String json) { + CONFIGS.clear(); + JsonObject obj = JsonParser.parseString(json).getAsJsonObject(); + for (var entry : obj.entrySet()) { + CONFIGS.put(entry.getKey(), + GSON.fromJson(entry.getValue(), WeaponCategoryAttachment.class)); + } + log.info("[WeaponAttachmentIO] {} Kategorien geladen", CONFIGS.size()); + } +} diff --git a/blight-common/src/main/java/de/blight/common/model/WeaponCategoryAttachment.java b/blight-common/src/main/java/de/blight/common/model/WeaponCategoryAttachment.java new file mode 100644 index 0000000..51c1632 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/model/WeaponCategoryAttachment.java @@ -0,0 +1,18 @@ +package de.blight.common.model; + +import lombok.Getter; +import lombok.Setter; + +/** Attachment-Parameter für eine Waffen-Kategorie (ersetzt den hardcodierten HAND_GRIP_OFFSET). */ +@Getter +@Setter +public class WeaponCategoryAttachment { + /** Rechte Hand: Offset in Waffen-Local-Space. Standardwert = bisheriger HAND_GRIP_OFFSET. */ + private float handOffX = ItemGrip.HAND_GRIP_OFFSET_X; + private float handOffY = ItemGrip.HAND_GRIP_OFFSET_Y; + private float handOffZ = ItemGrip.HAND_GRIP_OFFSET_Z; + /** Linke Hand (Zweihänder): Korrektur auf g2 in Waffen-Local-Space. */ + private float hand2OffX = 0f; + private float hand2OffY = 0f; + private float hand2OffZ = 0f; +} 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 464558c..ca91484 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -1237,6 +1237,27 @@ public class SharedInput { /** Klingenrichtung im Modellraum: -1 = Klinge bei -Z, +1 = Klinge bei +Z. Wird von guessGripPosition gesetzt. */ public volatile float gripEditorBladeDirZ = -1f; + // ── Charakter-Preview im Grip-Editor ───────────────────────────────────── + /** JFX→JME: Charakter-Preview ein- (true) oder ausschalten (false). */ + public volatile boolean gripEditorCharPreview = false; + /** JFX→JME: ItemSubCategory-Name der aktuell editierten Waffe. */ + public volatile String gripEditorSubCategory = null; + /** JME→JFX: Charakter-Preview ist bereit / aktiv. */ + public volatile boolean gripEditorCharPreviewReady = false; + /** JFX→JME: Kategorie-Offsets (rechte Hand) für Live-Update im Preview. */ + public volatile float gripEditorCatHandOffX = 0.1f; + public volatile float gripEditorCatHandOffY = 0f; + public volatile float gripEditorCatHandOffZ = 0.05f; + /** JFX→JME: Kategorie-Offsets (linke Hand, nur Zweihänder). */ + public volatile float gripEditorCatHand2OffX = 0f; + public volatile float gripEditorCatHand2OffY = 0f; + public volatile float gripEditorCatHand2OffZ = 0f; + /** JFX→JME: true wenn Kategorie-Offsets geändert wurden. */ + public volatile boolean gripEditorCatOffsetsDirty = false; + + // ── Attachment-Editor (eigenständige Ansicht für Hand-Anker pro Kategorie) ─ + public volatile boolean attachmentEditorActive = false; + /** Klick/Drag im Viewport im Sculpt-Modus. action 0=links, 1=rechts. */ public record SculptEdit(float screenX, float screenY, int action) {} public final ConcurrentLinkedQueue sculptEditQueue = new ConcurrentLinkedQueue<>(); 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 712dfa7..c1f2e96 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 @@ -34,6 +34,12 @@ import com.jme3.effect.ParticleMesh; import com.jme3.material.RenderState; import com.jme3.texture.Texture; import com.jme3.util.BufferUtils; +import com.jme3.anim.AnimComposer; +import com.jme3.math.Matrix4f; +import de.blight.common.model.ItemGrip; +import de.blight.common.model.ItemSubCategory; +import de.blight.common.model.WeaponAttachmentIO; +import de.blight.common.model.WeaponCategoryAttachment; import de.blight.editor.SharedInput; import de.blight.editor.util.ImpostorUtil; import de.blight.editor.util.ModelExportUtil; @@ -107,6 +113,16 @@ public class ModelEditorState extends BaseAppState { private float gripPosX = 0f, gripPosY = 0f, gripPosZ = 0f; private float gripRotX = 0f, gripRotY = 0f, gripRotZ = 0f; private float grip2PosX = 0f, grip2PosY = 0f, grip2PosZ = 0f; + + // ── Charakter-Preview ──────────────────────────────────────────────────── + private Node charPreviewNode = null; + private Spatial charWeaponSpatial = null; + private Node charHandNode = null; // RightHand-Attachment + private Node charLeftHandNode = null; // LeftHand-Attachment + private Node charPalmNode = null; // RightHandMiddle1-Attachment + private Quaternion charHandBindRot = null; + private Vector3f charPalmOffset = null; + private int charPalmDelayFrames = 0; private float interactableOffsetZ = 0f; private float interactableRotY = 0f; @@ -244,6 +260,29 @@ public class ModelEditorState extends BaseAppState { guessGripPosition(); } + // Charakter-Preview ein-/ausschalten + boolean wantsPreview = (input.gripEditorCharPreview && input.gripEditorActive) + || input.attachmentEditorActive; + if (wantsPreview && charPreviewNode == null) { + enableCharPreview(); + } else if (!wantsPreview && charPreviewNode != null) { + disableCharPreview(); + } + + // Palm-Offset nach 2 Frames berechnen + if (charPalmDelayFrames > 0 && charPalmNode != null && charHandNode != null) { + if (--charPalmDelayFrames == 0) { + Vector3f delta = charPalmNode.getWorldTranslation() + .subtract(charHandNode.getWorldTranslation()); + charPalmOffset = charHandNode.getWorldRotation().inverse().mult(delta); + } + } + + // Waffe im Charakter-Preview jedes Frame neu positionieren + if (charPreviewNode != null && charWeaponSpatial != null && charHandNode != null) { + updateCharPreviewWeapon(); + } + if (input.modelEditorEmbedLodsRequest) { input.modelEditorEmbedLodsRequest = false; embedLodsAndSave(input.modelEditorEmbedLodsPath); @@ -260,8 +299,14 @@ public class ModelEditorState extends BaseAppState { loadModel(openPath); } - // Grip-Gizmo aktualisieren - if (input.gripEditorActive && input.gripEditorGizmoDirty) { + // Invariante: wenn Charakter-Preview aktiv, standalone-Modell immer ausblenden + if (charPreviewNode != null) { + if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Always); + if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always); + } + + // Grip-Gizmo aktualisieren (nicht im reinen Attachment-Editor-Modus) + if (input.gripEditorActive && !input.attachmentEditorActive && input.gripEditorGizmoDirty) { input.gripEditorGizmoDirty = false; gripPosX = input.gripEditorPosX; gripPosY = input.gripEditorPosY; @@ -799,6 +844,7 @@ public class ModelEditorState extends BaseAppState { originalSpatial = null; interactableArrowNode = null; gripGizmoNode = null; + disableCharPreview(); input.gripEditorActive = false; camLight = null; mainLight = null; @@ -2024,4 +2070,236 @@ public class ModelEditorState extends BaseAppState { float dz = -cosEle * (float) Math.cos(azi); return new Vector3f(dx, dy, dz).normalizeLocal(); } + + // ── Charakter-Preview ──────────────────────────────────────────────────── + + private void enableCharPreview() { + if (previewRoot == null || currentPath == null) return; + try { + Spatial hero = app.getAssetManager().loadModel("Characters/Models/hero.j3o"); + charPreviewNode = new Node("charPreview"); + charPreviewNode.attachChild(hero); + previewRoot.attachChild(charPreviewNode); + + SkinningControl skin = findSkinCtrl(hero); + if (skin != null) { + charHandBindRot = extractJointBindRot(skin, "mixamorig:RightHand"); + try { charHandNode = skin.getAttachmentsNode("mixamorig:RightHand"); } catch (Exception e) { log.warn("[CharPreview] RightHand: {}", e.getMessage()); } + try { charLeftHandNode = skin.getAttachmentsNode("mixamorig:LeftHand"); } catch (Exception e) { log.warn("[CharPreview] LeftHand: {}", e.getMessage()); } + try { charPalmNode = skin.getAttachmentsNode("mixamorig:RightHandMiddle1"); charPalmDelayFrames = 2; } catch (Exception e) { log.warn("[CharPreview] Palm: {}", e.getMessage()); } + + AnimComposer anim = findAnimComposer(hero); + if (anim != null) { + String clipName = previewAnimName(input.gripEditorSubCategory); + loadClipToComposer(anim, skin, clipName); + } + } + + charWeaponSpatial = app.getAssetManager().loadModel(currentPath); + charPreviewNode.attachChild(charWeaponSpatial); + + // Standalone-Waffe ausblenden + if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Always); + if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always); + + orbitDist = 2.5f; + orbitYaw = 135f; + orbitPitch = 15f; + input.gripEditorCharPreviewReady = true; + log.info("[CharPreview] Aktiviert"); + } catch (Exception e) { + log.warn("[CharPreview] Fehler beim Laden: {}", e.getMessage()); + disableCharPreview(); + } + } + + private void disableCharPreview() { + if (charPreviewNode != null) { + charPreviewNode.removeFromParent(); + charPreviewNode = null; + charWeaponSpatial = null; + charHandNode = null; + charLeftHandNode = null; + charPalmNode = null; + charHandBindRot = null; + charPalmOffset = null; + } + if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Inherit); + if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always); + input.gripEditorCharPreviewReady = false; + } + + private void updateCharPreviewWeapon() { + // Kategorie-Offsets aus SharedInput lesen (live-update) + WeaponCategoryAttachment cfg = new WeaponCategoryAttachment(); + cfg.setHandOffX(input.gripEditorCatHandOffX); + cfg.setHandOffY(input.gripEditorCatHandOffY); + cfg.setHandOffZ(input.gripEditorCatHandOffZ); + cfg.setHand2OffX(input.gripEditorCatHand2OffX); + cfg.setHand2OffY(input.gripEditorCatHand2OffY); + cfg.setHand2OffZ(input.gripEditorCatHand2OffZ); + + // Grip-Werte aus SharedInput (Spinner = display-Werte, JSON-Werte = + HAND_GRIP_OFFSET) + float g1x = input.gripEditorPosX + ItemGrip.HAND_GRIP_OFFSET_X; + float g1y = input.gripEditorPosY + ItemGrip.HAND_GRIP_OFFSET_Y; + float g1z = input.gripEditorPosZ + ItemGrip.HAND_GRIP_OFFSET_Z; + float g2x = input.gripEditorGrip2PosX; + float g2y = input.gripEditorGrip2PosY; + float g2z = input.gripEditorGrip2PosZ; + boolean bladeAtMinusZ = g2z > g1z; + boolean isTwoHand = bladeAtMinusZ && (g2x != 0f || g2y != 0f || g2z != 0f); + + Quaternion charWorldRot = charPreviewNode.getWorldRotation(); + Quaternion charWorldRotInv = charWorldRot.inverse(); + Vector3f charWorldPos = charPreviewNode.getWorldTranslation(); + + Vector3f boneInChar = charWorldRotInv.mult(charHandNode.getWorldTranslation().subtract(charWorldPos)); + Quaternion boneRotInChar = charWorldRotInv.mult(charHandNode.getWorldRotation()); + Vector3f palmOff = charPalmOffset; + Vector3f attachInChar = (palmOff != null) + ? boneInChar.add(boneRotInChar.mult(palmOff.mult(0.5f))) + : boneInChar; + + float DEG_TO_RAD = (float) Math.PI / 180f; + Quaternion gripRot = new Quaternion().fromAngles( + DEG_TO_RAD * input.gripEditorRotX, + DEG_TO_RAD * input.gripEditorRotY, + DEG_TO_RAD * input.gripEditorRotZ); + Quaternion animDelta = (charHandBindRot != null) + ? boneRotInChar.mult(charHandBindRot.inverse()) + : boneRotInChar; + 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(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ()) + : new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ()); + + Quaternion finalRot; + Vector3f finalPos; + + if (isTwoHand && charLeftHandNode != null) { + Vector3f hand2Off = bladeAtMinusZ + ? new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), -cfg.getHand2OffZ()) + : new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), cfg.getHand2OffZ()); + Vector3f g1 = new Vector3f(g1x, g1y, g1z); + Vector3f g2 = new Vector3f(g2x, g2y, g2z); + Vector3f g1eff = g1.subtract(handOff); + Vector3f g2eff = g2.add(hand2Off); + + Vector3f leftHandInChar = charWorldRotInv.mult( + charLeftHandNode.getWorldTranslation().subtract(charWorldPos)); + Vector3f currentGripDir = weaponRot.mult(g2eff.subtract(g1eff).normalizeLocal()); + Vector3f targetDir = leftHandInChar.subtract(attachInChar).normalizeLocal(); + finalRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot); + finalPos = attachInChar.subtract(finalRot.mult(g1eff)); + } else { + Vector3f gripPos = new Vector3f(g1x, g1y, g1z); + finalRot = weaponRot; + finalPos = attachInChar.subtract(finalRot.mult(gripPos)).addLocal(finalRot.mult(handOff)); + } + + charWeaponSpatial.setLocalRotation(finalRot); + charWeaponSpatial.setLocalTranslation(finalPos); + } + + private static String previewAnimName(String subCategory) { + if (subCategory == null) return "Idle"; + return switch (subCategory) { + case "TWO_HANDED_SWORD" -> "Great Sword Idle"; + case "SWORD" -> "Sword And Shield Idle"; + default -> "Idle"; + }; + } + + private void loadClipToComposer(AnimComposer ac, SkinningControl dstSC, String clipName) { + if (ac.getAnimClipsNames().contains(clipName)) { + ac.setCurrentAction(clipName); + return; + } + try { + String assetKey = "Characters/clips/" + clipName + ".j3o"; + Spatial clipModel = app.getAssetManager().loadModel(assetKey); + AnimComposer clipAC = findAnimComposer(clipModel); + SkinningControl clipSC = findSkinCtrl(clipModel); + if (clipAC == null || clipAC.getAnimClipsNames().isEmpty()) { + log.warn("[CharPreview] Clip-Datei leer oder kein AnimComposer: {}", assetKey); + return; + } + com.jme3.anim.Armature srcArm = clipSC != null ? clipSC.getArmature() : null; + com.jme3.anim.Armature dstArm = dstSC != null ? dstSC.getArmature() : null; + String firstName = clipAC.getAnimClipsNames().iterator().next(); + com.jme3.anim.AnimClip src = clipAC.getAnimClip(firstName); + com.jme3.anim.AnimClip target; + if (srcArm != null && dstArm != null) { + target = de.blight.game.animation.RetargetingSystem.retarget(src, srcArm, dstArm); + if (target == null) target = src; + } else { + target = src; + } + if (!clipName.equals(target.getName())) { + com.jme3.anim.AnimClip renamed = new com.jme3.anim.AnimClip(clipName); + renamed.setTracks(target.getTracks()); + target = renamed; + } + ac.addAnimClip(target); + ac.setCurrentAction(clipName); + log.info("[CharPreview] Clip '{}' geladen und gestartet", clipName); + } catch (Exception e) { + log.warn("[CharPreview] Clip '{}' konnte nicht geladen werden: {}", clipName, e.getMessage()); + } + } + + private static SkinningControl findSkinCtrl(Spatial s) { + SkinningControl sc = s.getControl(SkinningControl.class); + if (sc != null) return sc; + if (s instanceof Node n) { + for (Spatial c : n.getChildren()) { + sc = findSkinCtrl(c); + if (sc != null) return sc; + } + } + return null; + } + + private static AnimComposer findAnimComposer(Spatial s) { + AnimComposer ac = s.getControl(AnimComposer.class); + if (ac != null) return ac; + if (s instanceof Node n) { + for (Spatial c : n.getChildren()) { + ac = findAnimComposer(c); + if (ac != null) return ac; + } + } + return null; + } + + private static Quaternion extractJointBindRot(SkinningControl sc, String jointName) { + com.jme3.anim.Armature arm = sc.getArmature(); + if (arm == null) return null; + com.jme3.anim.Joint joint = arm.getJoint(jointName); + if (joint == null) return null; + Matrix4f invBind = joint.getInverseModelBindMatrix(); + if (invBind == null) return null; + Matrix4f bindMat = invBind.invert(); + if (bindMat == null) return null; + return bindMat.toRotationQuat().normalizeLocal(); + } + + 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 len = axis.length(); + if (len < 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(len)); + } } diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/content/AttachmentEditorController.java b/blight-editor/src/main/java/de/blight/editor/toolset/content/AttachmentEditorController.java new file mode 100644 index 0000000..ce654df --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/content/AttachmentEditorController.java @@ -0,0 +1,279 @@ +package de.blight.editor.toolset.content; + +import de.blight.common.model.ItemGrip; +import de.blight.common.model.ItemSubCategory; +import de.blight.common.model.WeaponAttachmentIO; +import de.blight.common.model.WeaponCategoryAttachment; +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; + +/** + * Eigenständige Ansicht zum Bearbeiten der per-Kategorie Hand-Anker-Offsets. + * Zeigt den Helden direkt mit Waffe in der Idle-Animation – keine Griff-Gizmos. + */ +public class AttachmentEditorController { + + private final ToolContext ctx; + + private Spinner handOffXSpin, handOffYSpin, handOffZSpin; + private Spinner hand2OffXSpin, hand2OffYSpin, hand2OffZSpin; + + private boolean suppressEvents = false; + private ItemSubCategory itemSubCategory = null; + private Runnable onReturn = null; + + public AttachmentEditorController(ToolContext ctx) { + this.ctx = ctx; + } + + public void setOnReturn(Runnable r) { this.onReturn = r; } + + // ── Öffnen ──────────────────────────────────────────────────────────────── + + public void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip) { + itemSubCategory = subCategory; + + SharedInput input = ctx.input(); + // Grip-Editor-Modus nicht aktiv – reiner Attachment-Preview + input.gripEditorActive = false; + input.gripEditorIsShield = false; + input.gripEditorCharPreview = false; + input.attachmentEditorActive = true; + + // Subkategorie + aktuelle Config in SharedInput laden + input.gripEditorSubCategory = subCategory != null ? subCategory.name() : null; + WeaponCategoryAttachment cfg = WeaponAttachmentIO.get(subCategory); + input.gripEditorCatHandOffX = cfg.getHandOffX(); + input.gripEditorCatHandOffY = cfg.getHandOffY(); + input.gripEditorCatHandOffZ = cfg.getHandOffZ(); + input.gripEditorCatHand2OffX = cfg.getHand2OffX(); + input.gripEditorCatHand2OffY = cfg.getHand2OffY(); + input.gripEditorCatHand2OffZ = cfg.getHand2OffZ(); + input.gripEditorCatOffsetsDirty = false; + + // Grip-Werte des Items setzen, damit updateCharPreviewWeapon g1/g2 kennt + ItemGrip g = grip != null ? grip : new ItemGrip(); + 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.gripEditorGizmoDirty = false; + + // Waffenmodell laden + if (modelPath != null && !modelPath.isBlank()) { + input.modelEditorOpenPath = modelPath; + } + input.activeLayer = SharedInput.LAYER_MODEL_EDITOR; + if (ctx.jmeApp() != null) ctx.jmeApp().setRenderThrottled(false); + + ctx.setCenterView().accept(ctx.viewportSupplier().get()); + ctx.setCurrentTool().accept("attachmentEditor"); + ctx.setMainToolbar().accept(buildToolbar()); + + VBox innerPanel = buildPanel(cfg, subCategory); + 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); + } + + // ── Toolbar ─────────────────────────────────────────────────────────────── + + private ToolBar buildToolbar() { + Button backBtn = new Button("<- Zurück"); + backBtn.setOnAction(e -> onCancel()); + Label title = new Label("Hand-Anker-Editor"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + ToolBar tb = new ToolBar(); + tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), title); + return tb; + } + + // ── Panel ───────────────────────────────────────────────────────────────── + + private VBox buildPanel(WeaponCategoryAttachment cfg, ItemSubCategory subCategory) { + handOffXSpin = doubleSpin(cfg.getHandOffX(), -0.5, 0.5, 0.005, "m"); + handOffYSpin = doubleSpin(cfg.getHandOffY(), -0.5, 0.5, 0.005, "m"); + handOffZSpin = doubleSpin(cfg.getHandOffZ(), -0.5, 0.5, 0.005, "m"); + hand2OffXSpin = doubleSpin(cfg.getHand2OffX(), -0.5, 0.5, 0.005, "m"); + hand2OffYSpin = doubleSpin(cfg.getHand2OffY(), -0.5, 0.5, 0.005, "m"); + hand2OffZSpin = doubleSpin(cfg.getHand2OffZ(), -0.5, 0.5, 0.005, "m"); + + handOffXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + handOffYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + handOffZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + hand2OffXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + hand2OffYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + hand2OffZSpin.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 -> saveAndReturn()); + + Button cancelBtn = new Button("Abbrechen"); + cancelBtn.setMaxWidth(Double.MAX_VALUE); + cancelBtn.setStyle("-fx-background-color: #3a3a3a; -fx-text-fill: #cccccc; -fx-cursor: hand;"); + cancelBtn.setOnAction(e -> onCancel()); + + String catLabel = subCategory != null ? subCategory.name() : "–"; + + VBox panel = new VBox(8); + panel.setPadding(new Insets(12)); + panel.setStyle("-fx-background-color: #252535;"); + panel.setPrefWidth(220); + panel.getChildren().addAll( + styledTitle("Kategorie: " + catLabel), + styledHint("Offsets gelten für alle Waffen dieser Kategorie.\nDer Held zeigt die Waffe in der Idle-Animation."), + new Separator(), + styledTitle("Rechte Hand – Anker-Offset (m)"), + styledHint("Verschiebt den Befestigungspunkt der rechten Hand\nim Knochenraum des Charakters."), + row("X:", handOffXSpin), + row("Y:", handOffYSpin), + row("Z:", handOffZSpin), + new Separator(), + styledTitle("Linke Hand – Anker-Offset (m)"), + styledHint("Zweiter Griffpunkt (linke Hand) – nur relevant bei\nZweihänder-Waffen."), + row("X:", hand2OffXSpin), + row("Y:", hand2OffYSpin), + row("Z:", hand2OffZSpin), + new Separator(), + saveBtn, + cancelBtn + ); + return panel; + } + + // ── Spinner-Events ──────────────────────────────────────────────────────── + + private void onSpinnerChanged() { + if (suppressEvents) return; + SharedInput input = ctx.input(); + input.gripEditorCatHandOffX = handOffXSpin.getValue().floatValue(); + input.gripEditorCatHandOffY = handOffYSpin.getValue().floatValue(); + input.gripEditorCatHandOffZ = handOffZSpin.getValue().floatValue(); + input.gripEditorCatHand2OffX = hand2OffXSpin.getValue().floatValue(); + input.gripEditorCatHand2OffY = hand2OffYSpin.getValue().floatValue(); + input.gripEditorCatHand2OffZ = hand2OffZSpin.getValue().floatValue(); + input.gripEditorCatOffsetsDirty = true; + } + + // ── Speichern / Abbrechen ───────────────────────────────────────────────── + + private void saveAndReturn() { + if (itemSubCategory != null) { + WeaponCategoryAttachment cfg = new WeaponCategoryAttachment(); + cfg.setHandOffX(handOffXSpin.getValue().floatValue()); + cfg.setHandOffY(handOffYSpin.getValue().floatValue()); + cfg.setHandOffZ(handOffZSpin.getValue().floatValue()); + cfg.setHand2OffX(hand2OffXSpin.getValue().floatValue()); + cfg.setHand2OffY(hand2OffYSpin.getValue().floatValue()); + cfg.setHand2OffZ(hand2OffZSpin.getValue().floatValue()); + WeaponAttachmentIO.set(itemSubCategory, cfg); + java.nio.file.Path dest = ctx.assetRoot().resolve("weapon_attachment.json"); + WeaponAttachmentIO.save(dest); + syncToMirrors(dest); + } + closeEditor(); + } + + private void syncToMirrors(java.nio.file.Path savedFile) { + java.nio.file.Path projectRoot = de.blight.editor.ProjectRoot.PATH; + String[] mirrorRoots = { + "blight-assets/bin/main", + "blight-assets/build/resources/main" + }; + for (String root : mirrorRoots) { + java.nio.file.Path mirrorRoot = projectRoot.resolve(root); + if (!java.nio.file.Files.isDirectory(mirrorRoot)) continue; + java.nio.file.Path mirror = mirrorRoot.resolve("weapon_attachment.json"); + try { + java.nio.file.Files.copy(savedFile, mirror, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + // Mirror nicht vorhanden oder nicht beschreibbar – ignorieren + } + } + } + + private void onCancel() { + closeEditor(); + } + + private void closeEditor() { + SharedInput input = ctx.input(); + input.attachmentEditorActive = false; + input.gripEditorCharPreview = false; + input.gripEditorCatOffsetsDirty = false; + + Runnable ret = onReturn; + onReturn = null; + itemSubCategory = null; + + if (ret != null) ret.run(); else ctx.switchToWorld().run(); + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + private static Spinner doubleSpin(float initial, double min, double max, double step, String unit) { + SpinnerValueFactory.DoubleSpinnerValueFactory vf = + new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initial, step); + vf.setConverter(new javafx.util.StringConverter<>() { + @Override public String toString(Double v) { return v == null ? "0" : String.format("%.3f", v) + " " + unit; } + @Override public Double fromString(String s) { + try { return Double.parseDouble(s.replace(unit, "").replace(",", ".").trim()); } + catch (NumberFormatException e) { return vf.getValue(); } + } + }); + Spinner sp = new Spinner<>(vf); + sp.setEditable(true); + sp.setMaxWidth(Double.MAX_VALUE); + return sp; + } + + private static Label styledTitle(String text) { + Label l = new Label(text); + l.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #88aacc;"); + l.setWrapText(true); + return l; + } + + private static Label styledHint(String text) { + Label l = new Label(text); + l.setWrapText(true); + l.setStyle("-fx-text-fill: #888; -fx-font-size: 10;"); + return l; + } + + private static HBox row(String labelText, Spinner spinner) { + Label lbl = new Label(labelText); + lbl.setMinWidth(30); + lbl.setStyle("-fx-text-fill: #aaa;"); + HBox.setHgrow(spinner, Priority.ALWAYS); + HBox box = new HBox(6, lbl, spinner); + box.setAlignment(Pos.CENTER_LEFT); + return box; + } +} 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 b538b5f..9b47ca3 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 @@ -88,11 +88,13 @@ public class ContentController { // ────────────────────────────────────────────────────────────────────────── private Runnable onEnterContentMode; - private GripEditorController gripEditorController; + private GripEditorController gripEditorController; + private AttachmentEditorController attachmentEditorController; public ContentController(ToolContext ctx) { this.ctx = ctx; - this.gripEditorController = new GripEditorController(ctx); + this.gripEditorController = new GripEditorController(ctx); + this.attachmentEditorController = new AttachmentEditorController(ctx); } // ── Öffentliche API ──────────────────────────────────────────────────────── @@ -372,7 +374,10 @@ public class ContentController { wireItemView(activeItemView); ctx.setCenterView().accept(activeItemView); ctx.setRightPanel().accept(null); - if (selectItemId != null) activeItemView.selectItem(selectItemId); + if (selectItemId != null) { + activeItemView.selectItem(selectItemId); + selectItemInTree(selectItemId); + } } /** Prefs-Restore: stellt Dual-Window-Position und Content-Section wieder her. */ @@ -995,7 +1000,7 @@ public class ContentController { private void wireItemView(de.blight.editor.ui.ItemEditorView view) { view.setOnIdChanged(this::refreshContentItemTree); - view.setGripEditorLauncher((modelPath, currentGrip, isShield, onSave) -> { + view.setGripEditorLauncher((modelPath, currentGrip, isShield, subCategory, onSave) -> { String itemId = view.getCurrentItemId(); gripEditorController.setOnReturn(() -> { if (dualWindowMode) { @@ -1005,7 +1010,19 @@ public class ContentController { switchToItemEditor(itemId); } }); - gripEditorController.open(modelPath, currentGrip, isShield, onSave); + gripEditorController.open(modelPath, currentGrip, isShield, subCategory, onSave); + }); + view.setAttachmentEditorLauncher((modelPath, subCategory, grip) -> { + String itemId = view.getCurrentItemId(); + attachmentEditorController.setOnReturn(() -> { + if (dualWindowMode) { + buildItemViewInContent(); + if (activeItemView != null) activeItemView.selectItem(itemId); + } else { + switchToItemEditor(itemId); + } + }); + attachmentEditorController.open(modelPath, subCategory, grip); }); } @@ -1693,6 +1710,25 @@ public class ContentController { } } + private void selectItemInTree(String itemId) { + if (contentItemNode == null || contentTreeView == null || itemId == null) return; + contentItemNode.setExpanded(true); + for (TreeItem leaf : contentItemNode.getChildren()) { + String val = leaf.getValue(); + String leafId = val != null && val.contains(":") ? val.substring(val.indexOf(':') + 1) : val; + if (itemId.equals(leafId)) { + contentTreeUpdating = true; + try { + contentTreeView.getSelectionModel().select(leaf); + contentTreeView.scrollTo(contentTreeView.getRow(leaf)); + } finally { + contentTreeUpdating = false; + } + return; + } + } + } + private void refreshContentFractionTree() { if (contentFractionNode == null) return; contentTreeUpdating = true; 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 b58844b..ebb19fb 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 @@ -1,6 +1,7 @@ package de.blight.editor.toolset.content; import de.blight.common.model.ItemGrip; +import de.blight.common.model.ItemSubCategory; import de.blight.editor.SharedInput; import de.blight.editor.ToolContext; import de.blight.editor.state.ModelEditorState; @@ -30,6 +31,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { private Runnable onReturn; private boolean suppressSpinnerEvents = false; private boolean shieldMode = false; + private ItemSubCategory itemSubCategory = null; public void setOnReturn(Runnable r) { this.onReturn = r; } @@ -47,12 +49,13 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { rotXSpin.getValueFactory().setValue(0.0); rotYSpin.getValueFactory().setValue(0.0); rotZSpin.getValueFactory().setValue(0.0); - // 2. Griff (linke Hand): 20cm Richtung Pommel vom 1. Griff (Zweihänder-Grifflänge). - // Grip2-Spinner = Modell-Koordinaten direkt (kein HAND_GRIP_OFFSET), deshalb JSON-Pos von Grip1 verwenden. - float grip1JsonZ = posZ + ItemGrip.HAND_GRIP_OFFSET_Z; - grip2XSpin.getValueFactory().setValue((double) gX); - grip2YSpin.getValueFactory().setValue((double) gY); - grip2ZSpin.getValueFactory().setValue((double)(grip1JsonZ - bladeDirZ * 0.20f)); + // 2. Griff (linke Hand) nur für Zweihänder setzen + if (itemSubCategory == ItemSubCategory.TWO_HANDED_SWORD) { + float grip1JsonZ = posZ + ItemGrip.HAND_GRIP_OFFSET_Z; + grip2XSpin.getValueFactory().setValue((double) gX); + grip2YSpin.getValueFactory().setValue((double) gY); + grip2ZSpin.getValueFactory().setValue((double)(grip1JsonZ - bladeDirZ * 0.20f)); + } suppressSpinnerEvents = false; onSpinnerChanged(); } @@ -64,23 +67,29 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { // ── GripEditorLauncher ──────────────────────────────────────────────────── @Override - public void open(String modelPath, ItemGrip currentGrip, boolean isShield, Consumer onSave) { - pendingOnSave = onSave; - shieldMode = isShield; + public void open(String modelPath, ItemGrip currentGrip, boolean isShield, + ItemSubCategory subCategory, Consumer onSave) { + pendingOnSave = onSave; + shieldMode = isShield; + itemSubCategory = subCategory; SharedInput input = ctx.input(); - input.gripEditorActive = true; - input.gripEditorIsShield = isShield; + input.gripEditorActive = true; + input.gripEditorIsShield = isShield; + input.gripEditorSubCategory = subCategory != null ? subCategory.name() : null; + input.gripEditorCharPreview = false; + input.gripEditorCharPreviewReady = false; + input.attachmentEditorActive = false; // 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() - 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.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(); @@ -125,7 +134,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { // ── Toolbar ─────────────────────────────────────────────────────────────── private ToolBar buildToolbar() { - Button cancelBtn = new Button("← Zurück"); + Button cancelBtn = new Button("<- Zurück"); cancelBtn.setOnAction(e -> onCancel()); Label title = new Label("Griff-Editor"); title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); @@ -284,8 +293,9 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { grip.setHandOffsetZ(handOffZSpin.getValue().floatValue()); SharedInput input = ctx.input(); - input.gripEditorActive = false; - input.gripEditorIsShield = false; + input.gripEditorActive = false; + input.gripEditorIsShield = false; + input.gripEditorCharPreview = false; Consumer save = pendingOnSave; Runnable ret = onReturn; @@ -297,8 +307,10 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { } private void onCancel() { - ctx.input().gripEditorActive = false; - ctx.input().gripEditorIsShield = false; + SharedInput input = ctx.input(); + input.gripEditorActive = false; + input.gripEditorIsShield = false; + input.gripEditorCharPreview = false; Runnable ret = onReturn; pendingOnSave = null; @@ -313,7 +325,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { SpinnerValueFactory.DoubleSpinnerValueFactory vf = new SpinnerValueFactory.DoubleSpinnerValueFactory(min, max, initial, step); vf.setConverter(new javafx.util.StringConverter<>() { - @Override public String toString(Double v) { return v == null ? "0" : String.format("%.2f", v) + " " + unit; } + @Override public String toString(Double v) { return v == null ? "0" : String.format("%.3f", v) + " " + unit; } @Override public Double fromString(String s) { try { return Double.parseDouble(s.replace(unit, "").replace(",", ".").trim()); } catch (NumberFormatException e) { return vf.getValue(); } @@ -340,7 +352,7 @@ public class GripEditorController implements ItemEditorView.GripEditorLauncher { private static HBox row(String labelText, Spinner spinner) { Label lbl = new Label(labelText); - lbl.setMinWidth(20); + lbl.setMinWidth(30); lbl.setStyle("-fx-text-fill: #aaa;"); HBox.setHgrow(spinner, Priority.ALWAYS); HBox box = new HBox(6, lbl, spinner); diff --git a/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java index 2ca9101..dd9d6bf 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java @@ -56,11 +56,16 @@ public class ItemEditorView extends BorderPane { private VBox effectsRows; public interface GripEditorLauncher { - void open(String modelPath, ItemGrip currentGrip, boolean isShield, java.util.function.Consumer onSave); + void open(String modelPath, ItemGrip currentGrip, boolean isShield, ItemSubCategory subCategory, java.util.function.Consumer onSave); + } + + public interface AttachmentEditorLauncher { + void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip); } private Runnable onIdChanged; - private GripEditorLauncher gripEditorLauncher; + private GripEditorLauncher gripEditorLauncher; + private AttachmentEditorLauncher attachmentEditorLauncher; private final PauseTransition savePause = new PauseTransition(Duration.millis(600)); @@ -90,6 +95,7 @@ public class ItemEditorView extends BorderPane { public void setOnIdChanged(Runnable r) { this.onIdChanged = r; } public void setGripEditorLauncher(GripEditorLauncher l) { this.gripEditorLauncher = l; } + public void setAttachmentEditorLauncher(AttachmentEditorLauncher l) { this.attachmentEditorLauncher = l; } public String getCurrentItemId() { return current != null ? current.getItemId() : null; } public void selectItem(String itemId) { @@ -237,11 +243,17 @@ public class ItemEditorView extends BorderPane { gripBtn.setStyle("-fx-background-color: #3a3a5a; -fx-text-fill: #ddddff; -fx-cursor: hand;"); gripBtn.setOnAction(e -> openGripEditor()); + Button attachmentBtn = new Button("Hand-Anker bearbeiten..."); + attachmentBtn.setMaxWidth(Double.MAX_VALUE); + attachmentBtn.setStyle("-fx-background-color: #1a3a3a; -fx-text-fill: #88ffcc; -fx-cursor: hand;"); + attachmentBtn.setOnAction(e -> openAttachmentEditor()); + weaponSection = new VBox(6, new Separator(), sectionTitle("Kampfwerte"), row("Schaden:", damageSpinner), - gripBtn + gripBtn, + attachmentBtn ); weaponSection.setVisible(false); weaponSection.setManaged(false); @@ -325,16 +337,22 @@ 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(), false, grip -> { + gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), false, current.getSubCategory(), grip -> { current.setGrip(grip); persistCurrent(); }); } + private void openAttachmentEditor() { + if (current == null || attachmentEditorLauncher == null) return; + String modelPath = modelRefField.getText().trim(); + attachmentEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getSubCategory(), current.getGrip()); + } + private void openShieldGripEditor() { if (current == null || gripEditorLauncher == null) return; String modelPath = modelRefField.getText().trim(); - gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), true, grip -> { + gripEditorLauncher.open(modelPath.isBlank() ? null : modelPath, current.getGrip(), true, current.getSubCategory(), grip -> { current.setGrip(grip); persistCurrent(); }); 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 00c00d1..5c7124a 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 @@ -12,6 +12,8 @@ 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.WeaponAttachmentIO; +import de.blight.common.model.WeaponCategoryAttachment; import de.blight.game.animation.RetargetingSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -146,14 +148,13 @@ public class NpcWeaponAttachmentControl extends AbstractControl { boolean usesTwoHand = isTwoHand && leftForeArmNode != null && wGrip != null && (wGrip.getGrip2PosX() != 0f || wGrip.getGrip2PosY() != 0f || wGrip.getGrip2PosZ() != 0f); + WeaponCategoryAttachment cfg = WeaponAttachmentIO.get( + weaponItem != null ? weaponItem.getSubCategory() : null); if (usesTwoHand) { - Vector3f handballen = (palmOffsetLocal != null) - ? boneInChar.subtract(boneRotInChar.mult(palmOffsetLocal.mult(0.5f))) - : boneInChar; - applyTwoHandTransform(weaponSpatial, handballen, boneRotInChar, - wGrip, charWorldRotInv, charWorldPos); + applyTwoHandTransform(weaponSpatial, attachInChar, boneRotInChar, + wGrip, charWorldRotInv, charWorldPos, cfg); } else { - applyHandTransform(weaponSpatial, attachInChar, boneRotInChar, wGrip); + applyHandTransform(weaponSpatial, attachInChar, boneRotInChar, wGrip, cfg); } } else { applyBackTransform(weaponSpatial, boneInChar, boneRotInChar, @@ -196,7 +197,8 @@ public class NpcWeaponAttachmentControl extends AbstractControl { // ── Hand-Transform (1:1 von WeaponAttachmentControl) ───────────────────────── private void applyHandTransform(Spatial s, Vector3f bonePosInChar, - Quaternion boneRotInChar, ItemGrip grip) { + Quaternion boneRotInChar, ItemGrip grip, + WeaponCategoryAttachment cfg) { Quaternion gripRot = (grip != null) ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), @@ -214,8 +216,8 @@ public class NpcWeaponAttachmentControl extends AbstractControl { : 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; + ? new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ()) + : new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ()); Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos)); weaponPos = weaponPos.add(weaponRot.mult(handOff)); s.setLocalRotation(weaponRot); @@ -223,7 +225,8 @@ public class NpcWeaponAttachmentControl extends AbstractControl { } private void applyTwoHandTransform(Spatial s, Vector3f bonePosInChar, Quaternion boneRotInChar, - ItemGrip grip, Quaternion charWorldRotInv, Vector3f charWorldPos) { + ItemGrip grip, Quaternion charWorldRotInv, Vector3f charWorldPos, + WeaponCategoryAttachment cfg) { Quaternion gripRot = new Quaternion().fromAngles( DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ()); Quaternion animDelta = (handBindRotInChar != null) @@ -236,28 +239,21 @@ public class NpcWeaponAttachmentControl extends AbstractControl { Vector3f g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()); Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ()); - // Griffspanne virtuell auf 22cm; g1 (rechte Hand) 10cm weiter Richtung Klinge - Vector3f gripAxis = g1.subtract(g2).normalizeLocal(); - Vector3f gripCenter = g1.add(g2).multLocal(0.5f); - g1 = gripCenter.add(gripAxis.mult(0.21f)); - g2 = gripCenter.subtract(gripAxis.mult(0.11f)); + Vector3f handOff = bladeAtMinusZ + ? new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ()) + : new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ()); + Vector3f hand2Off = bladeAtMinusZ + ? new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), -cfg.getHand2OffZ()) + : new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), cfg.getHand2OffZ()); + Vector3f g1eff = g1.subtract(handOff); + Vector3f g2eff = g2.add(hand2Off); + Vector3f leftHandInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); - Quaternion leftBoneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation()); - Vector3f leftHandballen = (palmOffsetLocal != null) - ? leftHandInChar.subtract(leftBoneRotInChar.mult(palmOffsetLocal.mult(0.5f))) - : leftHandInChar; - leftHandballen.addLocal(0.10f, 0f, 0f); + Vector3f currentGripDir = weaponRot.mult(g2eff.subtract(g1eff).normalizeLocal()); + Vector3f targetDir = leftHandInChar.subtract(bonePosInChar).normalizeLocal(); + weaponRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot); - // Swing: Griffachse auf linken Handballen ausrichten (inkl. X-Korrektur) - Vector3f currentGripDir = weaponRot.mult(g2.subtract(g1).normalizeLocal()); - Vector3f targetDir = leftHandballen.subtract(bonePosInChar).normalizeLocal(); - float dot = currentGripDir.dot(targetDir); - if (dot > 0f) { - weaponRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot); - } - - // g1 exakt an der rechten Hand anpinnen — linke-Hand-Korrekturen beeinflussen rechte nicht - Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1)); + Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1eff)); s.setLocalRotation(weaponRot); s.setLocalTranslation(weaponPos); } 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 9050c6e..461ac47 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 @@ -13,6 +13,8 @@ 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 de.blight.common.model.WeaponAttachmentIO; +import de.blight.common.model.WeaponCategoryAttachment; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -188,6 +190,7 @@ public class WeaponAttachmentControl extends AbstractControl { if (drawn) { boolean isTwoHand = item != null && item.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD; + WeaponCategoryAttachment cfg = WeaponAttachmentIO.get(item != null ? item.getSubCategory() : null); Vector3f attachInChar = (palmOffsetLocal != null) ? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f))) : boneInChar; @@ -195,20 +198,17 @@ public class WeaponAttachmentControl extends AbstractControl { && 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); + applyTwoHandTransform(attachInChar, boneRotInChar, grip, charWorldRotInv, charWorldPos, cfg); } else { - applyHandTransform(attachInChar, boneRotInChar, grip); + applyHandTransform(attachInChar, boneRotInChar, grip, cfg); } } else { applyBackTransform(boneInChar, boneRotInChar, grip); } } - private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { + private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip, + WeaponCategoryAttachment cfg) { Quaternion gripRot = (grip != null) ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ()) : new Quaternion(); @@ -259,8 +259,8 @@ 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; + ? new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ()) + : new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ()); Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos)); weaponPos = weaponPos.add(weaponRot.mult(handOff2)); if (grip != null && (grip.getHandOffsetX() != 0f || grip.getHandOffsetY() != 0f || grip.getHandOffsetZ() != 0f)) { @@ -272,7 +272,8 @@ public class WeaponAttachmentControl extends AbstractControl { private void applyTwoHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip, - Quaternion charWorldRotInv, Vector3f charWorldPos) { + Quaternion charWorldRotInv, Vector3f charWorldPos, + WeaponCategoryAttachment cfg) { // Basis-Rotation aus rechter Hand (gibt Roll/Handflächenausrichtung) Quaternion gripRot = new Quaternion().fromAngles( DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ()); @@ -286,29 +287,25 @@ public class WeaponAttachmentControl extends AbstractControl { Vector3f g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()); Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ()); - // Griffspanne virtuell auf 22cm; g1 (rechte Hand) 10cm weiter Richtung Klinge - Vector3f gripAxis = g1.subtract(g2).normalizeLocal(); - Vector3f gripCenter = g1.add(g2).multLocal(0.5f); - g1 = gripCenter.add(gripAxis.mult(0.21f)); - g2 = gripCenter.subtract(gripAxis.mult(0.11f)); - Vector3f leftHandInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); - // Linke Hand: Handballen-Korrektur (Palm-Offset entgegen Fingerrichtung) – nur für midTarget, nicht für targetDir - Quaternion leftBoneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation()); - Vector3f leftHandballen = (palmOffsetLocal != null) - ? leftHandInChar.subtract(leftBoneRotInChar.mult(palmOffsetLocal.mult(0.5f))) - : leftHandInChar; - leftHandballen.addLocal(0.10f, 0f, 0f); + // g1 hat handOff eingebaut (Kategorie-Config), g2 nicht → g1eff bereinigt den Unterschied + Vector3f handOff = bladeAtMinusZ + ? new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), -cfg.getHandOffZ()) + : new Vector3f(cfg.getHandOffX(), cfg.getHandOffY(), cfg.getHandOffZ()); + // Korrektur g2 durch hand2Offset der Kategorie + Vector3f hand2Off = bladeAtMinusZ + ? new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), -cfg.getHand2OffZ()) + : new Vector3f(cfg.getHand2OffX(), cfg.getHand2OffY(), cfg.getHand2OffZ()); + Vector3f g1eff = g1.subtract(handOff); + Vector3f g2eff = g2.add(hand2Off); - // Swing: Griffachse auf linken Handballen ausrichten (inkl. X-Korrektur) - Vector3f currentGripDir = weaponRot.mult(g2.subtract(g1).normalizeLocal()); - Vector3f targetDir = leftHandballen.subtract(bonePosInChar).normalizeLocal(); - float dot = currentGripDir.dot(targetDir); - if (dot > 0f) { - weaponRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot); - } + // Swing: g1eff→g2eff-Richtung auf rechteHand→linkeHand ausrichten + Vector3f leftHandInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); + Vector3f currentGripDir = weaponRot.mult(g2eff.subtract(g1eff).normalizeLocal()); + Vector3f targetDir = leftHandInChar.subtract(bonePosInChar).normalizeLocal(); + weaponRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot); - // g1 exakt an der rechten Hand anpinnen — linke-Hand-Korrekturen beeinflussen rechte nicht - Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1)); + // g1eff an bonePosInChar pinnen + Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1eff)); weaponSpatial.setLocalRotation(weaponRot); weaponSpatial.setLocalTranslation(weaponPos); }