From 93a3c266e1fc80abdb8d8b6b906a0a3a45e1e674 Mon Sep 17 00:00:00 2001 From: Mario Date: Fri, 16 Oct 2026 13:52:21 +0200 Subject: [PATCH] Zwischenstand --- .../rostiges_kurzschwert.j3o.thumb.png | Bin 4419 -> 4064 bytes .../Characters/sets/human.animset.json | 4 +- .../waffen/schwerter/rostiges_kurzschwert.j3o | Bin 37794474 -> 37794007 bytes .../schwerter/rostiges_kurzschwert.j3o.meta | 2 +- .../items/weapon/rostiges_kurzschwert.item | 6 +- .../src/main/resources/shield_attachment.json | 8 + .../src/main/resources/weapon_attachment.json | 6 +- .../common/model/ShieldAttachmentIO.java | 92 +++++++ .../model/ShieldCategoryAttachment.java | 14 + .../java/de/blight/editor/SharedInput.java | 12 + .../blight/editor/state/ModelEditorState.java | 84 ++++-- .../content/AttachmentEditorController.java | 12 +- .../toolset/content/ContentController.java | 22 +- .../ShieldAttachmentEditorController.java | 253 ++++++++++++++++++ .../de/blight/editor/ui/ItemEditorView.java | 25 +- .../control/NpcWeaponAttachmentControl.java | 26 +- .../game/control/PlayerInputControl.java | 51 +++- .../game/control/WeaponAttachmentControl.java | 30 ++- 18 files changed, 577 insertions(+), 70 deletions(-) create mode 100644 blight-assets/src/main/resources/shield_attachment.json create mode 100644 blight-common/src/main/java/de/blight/common/model/ShieldAttachmentIO.java create mode 100644 blight-common/src/main/java/de/blight/common/model/ShieldCategoryAttachment.java create mode 100644 blight-editor/src/main/java/de/blight/editor/toolset/content/ShieldAttachmentEditorController.java diff --git a/blight-assets/src/main/resources/.thumbnails/Models/waffen/schwerter/rostiges_kurzschwert.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/waffen/schwerter/rostiges_kurzschwert.j3o.thumb.png index 3db202a5e0cf25eb590a07d1cdcabea49dee40e0..b2f9e2da9d071d75509c57179bfc7d6118869af7 100644 GIT binary patch literal 4064 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 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 b241aca..bc77f6a 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -125,12 +125,12 @@ "TWO_HAND_BLOCK_STRAFE_LEFT": "Great Sword Strafe left", "TWO_HAND_BLOCK_STRAFE_RIGHT": "Great Sword Strafe Right", "TWO_HAND_JUMP": "Great Sword Jump", - "TWO_HAND_RUNNING_JUMP": "Great Sword Running Jump", "TWO_HAND_IDLE_BORED": "Great Sword Idle boring", "TWO_HAND_IDLE_BORED_2": "Great Sword Idle boring 2", "TWO_HAND_IDLE_BORED_3": "Great Sword Idle boring 3", "TWO_HAND_DEATH": "Sword And Shield Death", - "TWO_HAND_DEATH_2": "Sword And Shield Death 2" + "TWO_HAND_DEATH_2": "Sword And Shield Death 2", + "TWO_HAND_RUNNING_JUMP": "Great Sword Running Jump" }, "previewModelPath": "Characters/Models/hero.j3o", "animOffsets": { 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 1c8a29fa8676585883f7b7d1b1c5cb35e4009790..9e2933c59e52b202ab155bbc6b88393a4bf348b2 100644 GIT binary patch delta 22323 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 M;d>@j5-1J+2ib27YybcN diff --git a/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o.meta b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o.meta index 4afbe2f..b861f0b 100644 --- a/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o.meta +++ b/blight-assets/src/main/resources/Models/waffen/schwerter/rostiges_kurzschwert.j3o.meta @@ -1,4 +1,4 @@ -#Sun Sep 20 15:31:32 CEST 2026 +#Thu Oct 15 15:05:26 CEST 2026 attachedEmitters.count=0 attachedLights.count=0 attachedSounds.count=0 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 4a9168b..87e80f0 100644 --- a/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item +++ b/blight-assets/src/main/resources/items/weapon/rostiges_kurzschwert.item @@ -11,9 +11,9 @@ "worthGold": 0, "damage": 0, "grip": { - "posX": 0.11149749, - "posY": 0.07795838, - "posZ": 0.36503196, + "posX": 0.1114975, + "posY": 0.07676052, + "posZ": -0.27503356, "rotX": 0.0, "rotY": 0.0, "rotZ": 0.0, diff --git a/blight-assets/src/main/resources/shield_attachment.json b/blight-assets/src/main/resources/shield_attachment.json new file mode 100644 index 0000000..1807d74 --- /dev/null +++ b/blight-assets/src/main/resources/shield_attachment.json @@ -0,0 +1,8 @@ +{ + "SHIELD": { + "rotX": -90.0, + "rotY": 0.0, + "rotZ": 0.0, + "elbowOffset": 0.25 + } +} diff --git a/blight-assets/src/main/resources/weapon_attachment.json b/blight-assets/src/main/resources/weapon_attachment.json index a9bf810..77fba56 100644 --- a/blight-assets/src/main/resources/weapon_attachment.json +++ b/blight-assets/src/main/resources/weapon_attachment.json @@ -1,8 +1,8 @@ { "SWORD": { - "handOffX": 0.1, - "handOffY": 0.0, - "handOffZ": 0.05, + "handOffX": 0.105, + "handOffY": 0.1, + "handOffZ": 0.5, "hand2OffX": 0.0, "hand2OffY": 0.0, "hand2OffZ": 0.0 diff --git a/blight-common/src/main/java/de/blight/common/model/ShieldAttachmentIO.java b/blight-common/src/main/java/de/blight/common/model/ShieldAttachmentIO.java new file mode 100644 index 0000000..117fb77 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/model/ShieldAttachmentIO.java @@ -0,0 +1,92 @@ +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 Schild-Attachment-Konfiguration (shield_attachment.json). + */ +public final class ShieldAttachmentIO { + + private static final Logger log = LoggerFactory.getLogger(ShieldAttachmentIO.class); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final String CLASSPATH_FILE = "shield_attachment.json"; + + private static final Map CONFIGS = new HashMap<>(); + private static boolean loaded = false; + + private ShieldAttachmentIO() {} + + public static ShieldCategoryAttachment get(ItemSubCategory cat) { + ensureLoaded(); + if (cat == null) return new ShieldCategoryAttachment(); + return CONFIGS.getOrDefault(cat.name(), new ShieldCategoryAttachment()); + } + + public static void set(ItemSubCategory cat, ShieldCategoryAttachment cfg) { + if (cat != null) CONFIGS.put(cat.name(), cfg); + } + + public static synchronized void loadFromClasspath() { + try (InputStream is = ShieldAttachmentIO.class.getClassLoader() + .getResourceAsStream(CLASSPATH_FILE)) { + if (is == null) { + log.warn("[ShieldAttachmentIO] {} nicht auf Classpath – Defaults verwendet", CLASSPATH_FILE); + loaded = true; + return; + } + parse(new String(is.readAllBytes(), StandardCharsets.UTF_8)); + loaded = true; + } catch (IOException e) { + log.warn("[ShieldAttachmentIO] Ladefehler: {}", e.getMessage()); + loaded = true; + } + } + + 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("[ShieldAttachmentIO] Ladefehler: {}", e.getMessage()); + } + } + + public static void save(Path file) { + try { + Files.writeString(file, GSON.toJson(CONFIGS), StandardCharsets.UTF_8); + log.info("[ShieldAttachmentIO] Gespeichert: {}", file); + } catch (IOException e) { + log.warn("[ShieldAttachmentIO] Speicherfehler: {}", e.getMessage()); + } + } + + 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(), ShieldCategoryAttachment.class)); + } + log.info("[ShieldAttachmentIO] {} Kategorien geladen", CONFIGS.size()); + } +} diff --git a/blight-common/src/main/java/de/blight/common/model/ShieldCategoryAttachment.java b/blight-common/src/main/java/de/blight/common/model/ShieldCategoryAttachment.java new file mode 100644 index 0000000..a48c1a3 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/model/ShieldCategoryAttachment.java @@ -0,0 +1,14 @@ +package de.blight.common.model; + +import lombok.Getter; +import lombok.Setter; + +/** Attachment-Parameter für einen Schild-Typ (am linken Unterarm). */ +@Getter +@Setter +public class ShieldCategoryAttachment { + private float rotX = -90f; + private float rotY = 0f; + private float rotZ = 0f; + private float elbowOffset = 0.25f; +} 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 ca91484..6a887fc 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -1258,6 +1258,18 @@ public class SharedInput { // ── Attachment-Editor (eigenständige Ansicht für Hand-Anker pro Kategorie) ─ public volatile boolean attachmentEditorActive = false; + // ── Schild-Attachment-Editor ────────────────────────────────────────────── + public volatile boolean shieldEditorActive = false; + public volatile String shieldEditorSubCategory = null; + public volatile float shieldEditorRotX = -90f; + public volatile float shieldEditorRotY = 0f; + public volatile float shieldEditorRotZ = 0f; + public volatile float shieldEditorElbowOffset = 0.25f; + public volatile float shieldEditorGripX = 0f; + public volatile float shieldEditorGripY = 0f; + public volatile float shieldEditorGripZ = 0f; + public volatile boolean shieldEditorOffsetsDirty = 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 c1f2e96..820644d 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 @@ -121,6 +121,7 @@ public class ModelEditorState extends BaseAppState { private Node charLeftHandNode = null; // LeftHand-Attachment private Node charPalmNode = null; // RightHandMiddle1-Attachment private Quaternion charHandBindRot = null; + private Quaternion charLeftHandBindRot = null; private Vector3f charPalmOffset = null; private int charPalmDelayFrames = 0; private float interactableOffsetZ = 0f; @@ -260,9 +261,21 @@ public class ModelEditorState extends BaseAppState { guessGripPosition(); } + // Modell laden – MUSS vor enableCharPreview stehen, damit currentPath aktuell ist + String openPath = input.modelEditorOpenPath; + if (openPath != null) { + input.modelEditorOpenPath = null; + mainModelPath = openPath; + input.modelEditorLodPreview = 0; + input.modelImportLod1PreviewNode.set(null); + input.modelImportLod2PreviewNode.set(null); + loadModel(openPath); + } + // Charakter-Preview ein-/ausschalten boolean wantsPreview = (input.gripEditorCharPreview && input.gripEditorActive) - || input.attachmentEditorActive; + || input.attachmentEditorActive + || input.shieldEditorActive; if (wantsPreview && charPreviewNode == null) { enableCharPreview(); } else if (!wantsPreview && charPreviewNode != null) { @@ -278,7 +291,7 @@ public class ModelEditorState extends BaseAppState { } } - // Waffe im Charakter-Preview jedes Frame neu positionieren + // Waffe/Schild im Charakter-Preview jedes Frame neu positionieren if (charPreviewNode != null && charWeaponSpatial != null && charHandNode != null) { updateCharPreviewWeapon(); } @@ -288,17 +301,6 @@ public class ModelEditorState extends BaseAppState { embedLodsAndSave(input.modelEditorEmbedLodsPath); } - // Modell laden - String openPath = input.modelEditorOpenPath; - if (openPath != null) { - input.modelEditorOpenPath = null; - mainModelPath = openPath; - input.modelEditorLodPreview = 0; - input.modelImportLod1PreviewNode.set(null); - input.modelImportLod2PreviewNode.set(null); - loadModel(openPath); - } - // Invariante: wenn Charakter-Preview aktiv, standalone-Modell immer ausblenden if (charPreviewNode != null) { if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Always); @@ -2083,14 +2085,17 @@ public class ModelEditorState extends BaseAppState { SkinningControl skin = findSkinCtrl(hero); if (skin != null) { - charHandBindRot = extractJointBindRot(skin, "mixamorig:RightHand"); + charHandBindRot = extractJointBindRot(skin, "mixamorig:RightHand"); + charLeftHandBindRot = extractJointBindRot(skin, "mixamorig:LeftHand"); 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); + String subCat = input.shieldEditorActive + ? input.shieldEditorSubCategory : input.gripEditorSubCategory; + String clipName = previewAnimName(subCat); loadClipToComposer(anim, skin, clipName); } } @@ -2116,13 +2121,14 @@ public class ModelEditorState extends BaseAppState { private void disableCharPreview() { if (charPreviewNode != null) { charPreviewNode.removeFromParent(); - charPreviewNode = null; - charWeaponSpatial = null; - charHandNode = null; - charLeftHandNode = null; - charPalmNode = null; - charHandBindRot = null; - charPalmOffset = null; + charPreviewNode = null; + charWeaponSpatial = null; + charHandNode = null; + charLeftHandNode = null; + charPalmNode = null; + charHandBindRot = null; + charLeftHandBindRot = null; + charPalmOffset = null; } if (modelWrapper != null) modelWrapper.setCullHint(Spatial.CullHint.Inherit); if (gripGizmoNode != null) gripGizmoNode.setCullHint(Spatial.CullHint.Always); @@ -2130,6 +2136,10 @@ public class ModelEditorState extends BaseAppState { } private void updateCharPreviewWeapon() { + if (input.shieldEditorActive) { + updateCharPreviewShield(); + return; + } // Kategorie-Offsets aus SharedInput lesen (live-update) WeaponCategoryAttachment cfg = new WeaponCategoryAttachment(); cfg.setHandOffX(input.gripEditorCatHandOffX); @@ -2205,11 +2215,39 @@ public class ModelEditorState extends BaseAppState { charWeaponSpatial.setLocalTranslation(finalPos); } + private void updateCharPreviewShield() { + if (charLeftHandNode == null) return; + float DEG_TO_RAD = (float) Math.PI / 180f; + Quaternion charWorldRotInv = charPreviewNode.getWorldRotation().inverse(); + Vector3f charWorldPos = charPreviewNode.getWorldTranslation(); + + Vector3f leftBoneInChar = charWorldRotInv.mult( + charLeftHandNode.getWorldTranslation().subtract(charWorldPos)); + Quaternion leftBoneRotInChar = charWorldRotInv.mult(charLeftHandNode.getWorldRotation()); + + Quaternion animDelta = (charLeftHandBindRot != null) + ? leftBoneRotInChar.mult(charLeftHandBindRot.inverse()) + : leftBoneRotInChar; + Quaternion armRot = new Quaternion().fromAngles( + DEG_TO_RAD * input.shieldEditorRotX, + DEG_TO_RAD * input.shieldEditorRotY, + DEG_TO_RAD * input.shieldEditorRotZ); + Quaternion shieldRot = animDelta.mult(armRot); + + Vector3f anchor = leftBoneInChar.add( + leftBoneRotInChar.mult(new Vector3f(0f, -input.shieldEditorElbowOffset, 0f))); + Vector3f gripPos = new Vector3f( + input.shieldEditorGripX, input.shieldEditorGripY, input.shieldEditorGripZ); + + charWeaponSpatial.setLocalRotation(shieldRot); + charWeaponSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos))); + } + 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"; + case "SWORD", "SHIELD" -> "Sword And Shield Idle"; default -> "Idle"; }; } 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 index ce654df..635111c 100644 --- 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 @@ -114,12 +114,12 @@ public class AttachmentEditorController { // ── 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 = doubleSpin(cfg.getHandOffX(), -2.0, 2.0, 0.005, "m"); + handOffYSpin = doubleSpin(cfg.getHandOffY(), -2.0, 2.0, 0.005, "m"); + handOffZSpin = doubleSpin(cfg.getHandOffZ(), -2.0, 2.0, 0.005, "m"); + hand2OffXSpin = doubleSpin(cfg.getHand2OffX(), -2.0, 2.0, 0.005, "m"); + hand2OffYSpin = doubleSpin(cfg.getHand2OffY(), -2.0, 2.0, 0.005, "m"); + hand2OffZSpin = doubleSpin(cfg.getHand2OffZ(), -2.0, 2.0, 0.005, "m"); handOffXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); handOffYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); 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 9b47ca3..1abd25f 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,13 +88,15 @@ public class ContentController { // ────────────────────────────────────────────────────────────────────────── private Runnable onEnterContentMode; - private GripEditorController gripEditorController; - private AttachmentEditorController attachmentEditorController; + private GripEditorController gripEditorController; + private AttachmentEditorController attachmentEditorController; + private ShieldAttachmentEditorController shieldAttachmentEditorController; public ContentController(ToolContext ctx) { this.ctx = ctx; - this.gripEditorController = new GripEditorController(ctx); - this.attachmentEditorController = new AttachmentEditorController(ctx); + this.gripEditorController = new GripEditorController(ctx); + this.attachmentEditorController = new AttachmentEditorController(ctx); + this.shieldAttachmentEditorController = new ShieldAttachmentEditorController(ctx); } // ── Öffentliche API ──────────────────────────────────────────────────────── @@ -1024,6 +1026,18 @@ public class ContentController { }); attachmentEditorController.open(modelPath, subCategory, grip); }); + view.setShieldAttachmentEditorLauncher((modelPath, subCategory, grip) -> { + String itemId = view.getCurrentItemId(); + shieldAttachmentEditorController.setOnReturn(() -> { + if (dualWindowMode) { + buildItemViewInContent(); + if (activeItemView != null) activeItemView.selectItem(itemId); + } else { + switchToItemEditor(itemId); + } + }); + shieldAttachmentEditorController.open(modelPath, subCategory, grip); + }); } private void buildItemViewInContent() { diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/content/ShieldAttachmentEditorController.java b/blight-editor/src/main/java/de/blight/editor/toolset/content/ShieldAttachmentEditorController.java new file mode 100644 index 0000000..3d1c9dc --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/content/ShieldAttachmentEditorController.java @@ -0,0 +1,253 @@ +package de.blight.editor.toolset.content; + +import de.blight.common.model.ItemGrip; +import de.blight.common.model.ItemSubCategory; +import de.blight.common.model.ShieldAttachmentIO; +import de.blight.common.model.ShieldCategoryAttachment; +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 Schild-Arm-Attachment-Parameter pro Kategorie. + * Zeigt den Helden in Sword-And-Shield Idle mit Schild am linken Arm. + */ +public class ShieldAttachmentEditorController { + + private final ToolContext ctx; + + private Spinner rotXSpin, rotYSpin, rotZSpin, elbowOffsetSpin; + + private boolean suppressEvents = false; + private ItemSubCategory itemSubCategory = null; + private Runnable onReturn = null; + + public ShieldAttachmentEditorController(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(); + input.gripEditorActive = false; + input.gripEditorCharPreview = false; + input.attachmentEditorActive = false; + input.shieldEditorActive = true; + + input.shieldEditorSubCategory = subCategory != null ? subCategory.name() : null; + ShieldCategoryAttachment cfg = ShieldAttachmentIO.get(subCategory); + input.shieldEditorRotX = cfg.getRotX(); + input.shieldEditorRotY = cfg.getRotY(); + input.shieldEditorRotZ = cfg.getRotZ(); + input.shieldEditorElbowOffset = cfg.getElbowOffset(); + input.shieldEditorOffsetsDirty = false; + + // Grip des Schilds für Preview-Positionierung + ItemGrip g = grip != null ? grip : new ItemGrip(); + input.shieldEditorGripX = g.getPosX(); + input.shieldEditorGripY = g.getPosY(); + input.shieldEditorGripZ = g.getPosZ(); + + 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("shieldAttachmentEditor"); + 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("Schild-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(ShieldCategoryAttachment cfg, ItemSubCategory subCategory) { + rotXSpin = doubleSpin(cfg.getRotX(), -360.0, 360.0, 1.0, "°"); + rotYSpin = doubleSpin(cfg.getRotY(), -360.0, 360.0, 1.0, "°"); + rotZSpin = doubleSpin(cfg.getRotZ(), -360.0, 360.0, 1.0, "°"); + elbowOffsetSpin = doubleSpin(cfg.getElbowOffset(), 0.0, 1.0, 0.005, "m"); + + rotXSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + rotYSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + rotZSpin.valueProperty().addListener((obs, o, n) -> onSpinnerChanged()); + elbowOffsetSpin.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("Steuert wie das Schild am linken Unterarm angebracht ist.\nDer Held zeigt die Pose in der Sword-And-Shield Idle-Animation."), + new Separator(), + styledTitle("Rotation am Arm (Grad)"), + styledHint("Dreht das Schild relativ zur Bind-Pose des LeftHand-Knochens."), + row("X:", rotXSpin), + row("Y:", rotYSpin), + row("Z:", rotZSpin), + new Separator(), + styledTitle("Ellbogen-Offset (m)"), + styledHint("Verschiebung in Richtung Ellbogen (lokale -Y des LeftHand-Knochens)."), + row("Off:", elbowOffsetSpin), + new Separator(), + saveBtn, + cancelBtn + ); + return panel; + } + + // ── Spinner-Events ──────────────────────────────────────────────────────── + + private void onSpinnerChanged() { + if (suppressEvents) return; + SharedInput input = ctx.input(); + input.shieldEditorRotX = rotXSpin.getValue().floatValue(); + input.shieldEditorRotY = rotYSpin.getValue().floatValue(); + input.shieldEditorRotZ = rotZSpin.getValue().floatValue(); + input.shieldEditorElbowOffset = elbowOffsetSpin.getValue().floatValue(); + input.shieldEditorOffsetsDirty = true; + } + + // ── Speichern / Abbrechen ───────────────────────────────────────────────── + + private void saveAndReturn() { + if (itemSubCategory != null) { + ShieldCategoryAttachment cfg = new ShieldCategoryAttachment(); + cfg.setRotX(rotXSpin.getValue().floatValue()); + cfg.setRotY(rotYSpin.getValue().floatValue()); + cfg.setRotZ(rotZSpin.getValue().floatValue()); + cfg.setElbowOffset(elbowOffsetSpin.getValue().floatValue()); + ShieldAttachmentIO.set(itemSubCategory, cfg); + java.nio.file.Path dest = ctx.assetRoot().resolve("shield_attachment.json"); + ShieldAttachmentIO.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 mirror = projectRoot.resolve(root).resolve("shield_attachment.json"); + try { + java.nio.file.Files.copy(savedFile, mirror, + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + // Mirror nicht vorhanden – ignorieren + } + } + } + + private void onCancel() { + closeEditor(); + } + + private void closeEditor() { + SharedInput input = ctx.input(); + input.shieldEditorActive = false; + input.shieldEditorOffsetsDirty = 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/ui/ItemEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/ItemEditorView.java index dd9d6bf..0a65791 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 @@ -63,9 +63,14 @@ public class ItemEditorView extends BorderPane { void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip); } + public interface ShieldAttachmentEditorLauncher { + void open(String modelPath, ItemSubCategory subCategory, ItemGrip grip); + } + private Runnable onIdChanged; - private GripEditorLauncher gripEditorLauncher; - private AttachmentEditorLauncher attachmentEditorLauncher; + private GripEditorLauncher gripEditorLauncher; + private AttachmentEditorLauncher attachmentEditorLauncher; + private ShieldAttachmentEditorLauncher shieldAttachmentEditorLauncher; private final PauseTransition savePause = new PauseTransition(Duration.millis(600)); @@ -96,6 +101,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 void setShieldAttachmentEditorLauncher(ShieldAttachmentEditorLauncher l) { this.shieldAttachmentEditorLauncher = l; } public String getCurrentItemId() { return current != null ? current.getItemId() : null; } public void selectItem(String itemId) { @@ -264,10 +270,16 @@ public class ItemEditorView extends BorderPane { shieldGripBtn.setStyle("-fx-background-color: #2a4a5a; -fx-text-fill: #aaddff; -fx-cursor: hand;"); shieldGripBtn.setOnAction(e -> openShieldGripEditor()); + Button shieldAttachmentBtn = new Button("Arm-Anker bearbeiten..."); + shieldAttachmentBtn.setMaxWidth(Double.MAX_VALUE); + shieldAttachmentBtn.setStyle("-fx-background-color: #1a4a4a; -fx-text-fill: #aaffee; -fx-cursor: hand;"); + shieldAttachmentBtn.setOnAction(e -> openShieldAttachmentEditor()); + shieldSection = new VBox(6, new Separator(), sectionTitle("Schild-Griff"), - shieldGripBtn + shieldGripBtn, + shieldAttachmentBtn ); shieldSection.setVisible(false); shieldSection.setManaged(false); @@ -358,6 +370,13 @@ public class ItemEditorView extends BorderPane { }); } + private void openShieldAttachmentEditor() { + if (current == null || shieldAttachmentEditorLauncher == null) return; + String modelPath = modelRefField.getText().trim(); + shieldAttachmentEditorLauncher.open(modelPath.isBlank() ? null : modelPath, + current.getSubCategory(), current.getGrip()); + } + private void updateWeaponSectionVisibility() { ItemCategory cat = catCombo.getValue(); ItemSubCategory sub = subCatCombo.getValue(); 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 5c7124a..eddfdfa 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.ShieldAttachmentIO; +import de.blight.common.model.ShieldCategoryAttachment; import de.blight.common.model.WeaponAttachmentIO; import de.blight.common.model.WeaponCategoryAttachment; import de.blight.game.animation.RetargetingSystem; @@ -35,7 +37,8 @@ public class NpcWeaponAttachmentControl extends AbstractControl { 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 BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f); + private static final Vector3f TWO_HAND_BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.03f); 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); @@ -157,8 +160,10 @@ public class NpcWeaponAttachmentControl extends AbstractControl { applyHandTransform(weaponSpatial, attachInChar, boneRotInChar, wGrip, cfg); } } else { + boolean twoHanded = weaponItem != null + && weaponItem.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD; applyBackTransform(weaponSpatial, boneInChar, boneRotInChar, - weaponItem != null ? weaponItem.getGrip() : null); + weaponItem != null ? weaponItem.getGrip() : null, twoHanded); } } } @@ -169,7 +174,9 @@ public class NpcWeaponAttachmentControl extends AbstractControl { if (drawn && leftForeArmNode != null) { Vector3f boneInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation()); - applyShieldArmTransform(shieldSpatial, boneInChar, boneRotInChar, grip); + ShieldCategoryAttachment shieldCfg = ShieldAttachmentIO.get( + shieldItem != null ? shieldItem.getSubCategory() : null); + applyShieldArmTransform(shieldSpatial, boneInChar, boneRotInChar, grip, shieldCfg); shieldSpatial.setCullHint(Spatial.CullHint.Inherit); } else if (!drawn && backNode != null) { Vector3f boneInChar = charWorldRotInv.mult(backNode.getWorldTranslation().subtract(charWorldPos)); @@ -275,7 +282,7 @@ public class NpcWeaponAttachmentControl extends AbstractControl { 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 boneRotInChar, ItemGrip grip, boolean twoHanded) { Quaternion gripRot = (grip != null) ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), @@ -286,7 +293,8 @@ public class NpcWeaponAttachmentControl extends AbstractControl { ? 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 target = boneInChar.add(boneRotInChar.mult( + twoHanded ? TWO_HAND_BACK_GRIP_OFFSET : BACK_GRIP_OFFSET)); Vector3f gripPos = grip != null ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; Vector3f backPos = target.subtract(weaponRot.mult(gripPos)); @@ -298,15 +306,17 @@ public class NpcWeaponAttachmentControl extends AbstractControl { // ── Schild-Transforms (1:1 von WeaponAttachmentControl) ────────────────────── private void applyShieldArmTransform(Spatial s, Vector3f bonePosInChar, - Quaternion boneRotInChar, ItemGrip grip) { + Quaternion boneRotInChar, ItemGrip grip, + ShieldCategoryAttachment cfg) { Quaternion animDelta = (leftForeArmBindRotInChar != null) ? boneRotInChar.mult(leftForeArmBindRotInChar.inverse()) : boneRotInChar; - Quaternion armRot = new Quaternion().fromAngles(SHIELD_ARM_ROT_X, SHIELD_ARM_ROT_Y, SHIELD_ARM_ROT_Z); + Quaternion armRot = new Quaternion().fromAngles( + DEG_TO_RAD * cfg.getRotX(), DEG_TO_RAD * cfg.getRotY(), DEG_TO_RAD * cfg.getRotZ()); Quaternion shieldRot = animDelta.mult(armRot); Vector3f gripPos = (grip != null) ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; - Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -SHIELD_ARM_ELBOW_OFFSET, 0f))); + Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -cfg.getElbowOffset(), 0f))); s.setLocalRotation(shieldRot); s.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos))); } diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index 5c93da4..8e7969e 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -6,6 +6,7 @@ import com.jme3.anim.tween.Tween; import com.jme3.anim.tween.action.BaseAction; import com.jme3.bullet.control.CharacterControl; import com.jme3.input.InputManager; +import com.jme3.input.KeyInput; import com.jme3.input.MouseInput; import com.jme3.input.RawInputListener; import com.jme3.input.controls.ActionListener; @@ -110,11 +111,14 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver { private static final String ACT_COMBAT_ALT = "CombatAltAction"; private static final String ACT_ROLL = "Roll"; private static final String ACT_SNEAK = "Sneak"; + private static final String ACT_BORED_1 = "BoredAnim1"; + private static final String ACT_BORED_2 = "BoredAnim2"; + private static final String ACT_BORED_3 = "BoredAnim3"; private static final String[] ACTION_NAMES = { "Forward", "Backward", "Left", "Right", "Jump", "Sprint", "Walk", ACT_DRAW_MELEE, ACT_DRAW_RANGED, ACT_SHEATH, ACT_COMBAT_ATK, ACT_COMBAT_ALT, - ACT_ROLL, ACT_SNEAK + ACT_ROLL, ACT_SNEAK, ACT_BORED_1, ACT_BORED_2, ACT_BORED_3 }; private final InputManager inputManager; @@ -381,8 +385,11 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver { } } case ACT_COMBAT_ALT -> handleCombatAlt(isPressed); - case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); } - case ACT_SNEAK -> { if (!weaponDrawn) handleSneakKey(isPressed); } + case ACT_ROLL -> { if (isPressed) triggerRollFromInput(); } + case ACT_SNEAK -> { if (!weaponDrawn) handleSneakKey(isPressed); } + case ACT_BORED_1 -> { if (isPressed) triggerBoredAnim(0); } + case ACT_BORED_2 -> { if (isPressed) triggerBoredAnim(1); } + case ACT_BORED_3 -> { if (isPressed) triggerBoredAnim(2); } } }; @@ -468,6 +475,7 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver { public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; } public boolean isWeaponDrawn() { return weaponDrawn; } + public boolean isDrawAnimActive() { return drawAnimActive; } public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; } public boolean isLastAttackCritical() { return lastAttackCritical; } public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; } @@ -832,6 +840,9 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver { inputManager.addMapping(ACT_COMBAT_ALT, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); inputManager.addMapping(ACT_ROLL, new KeyTrigger(kb.roll)); inputManager.addMapping(ACT_SNEAK, new KeyTrigger(kb.sneak)); + inputManager.addMapping(ACT_BORED_1, new KeyTrigger(KeyInput.KEY_F1)); + inputManager.addMapping(ACT_BORED_2, new KeyTrigger(KeyInput.KEY_F2)); + inputManager.addMapping(ACT_BORED_3, new KeyTrigger(KeyInput.KEY_F3)); inputManager.addListener(actionListener, ACTION_NAMES); } @@ -1222,7 +1233,8 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver { } else { boolean swordShield = drawnWeaponType == de.blight.common.model.ItemSubCategory.SWORD && hasShieldEquipped(); - if (swordShield) { + boolean twoHand = drawnWeaponType == ItemSubCategory.TWO_HANDED_SWORD; + if (swordShield || twoHand) { if (boredAnimRemaining > 0f) { boredAnimRemaining -= tpf; target = boredVariant; @@ -1231,9 +1243,15 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver { if (boredIdleTimer >= BORED_IDLE_DELAY) { boredIdleTimer = 0f; int pick = playerRng.nextInt(3); - boredVariant = pick == 0 ? AnimationAction.SWORD_IDLE_BORED - : pick == 1 ? AnimationAction.SWORD_IDLE_BORED_2 - : AnimationAction.SWORD_IDLE_BORED_3; + if (swordShield) { + boredVariant = pick == 0 ? AnimationAction.SWORD_IDLE_BORED + : pick == 1 ? AnimationAction.SWORD_IDLE_BORED_2 + : AnimationAction.SWORD_IDLE_BORED_3; + } else { + boredVariant = pick == 0 ? AnimationAction.TWO_HAND_IDLE_BORED + : pick == 1 ? AnimationAction.TWO_HAND_IDLE_BORED_2 + : AnimationAction.TWO_HAND_IDLE_BORED_3; + } boredAnimRemaining = resolveClipLength(boredVariant, 3.0f); target = boredVariant; } else { @@ -1676,6 +1694,25 @@ public class PlayerInputControl implements CombatStyleBase.AnimReceiver { currentAnim = AnimationAction.CROUCH_TO_STAND; } + private void triggerBoredAnim(int variant) { + if (!weaponDrawn || forward || backward || left || right || combatAnimActive || drawAnimActive) return; + if (physicsChar != null && !physicsChar.onGround()) return; + boolean swordShield = drawnWeaponType == de.blight.common.model.ItemSubCategory.SWORD && hasShieldEquipped(); + boolean twoHand = drawnWeaponType == ItemSubCategory.TWO_HANDED_SWORD; + if (!swordShield && !twoHand) return; + if (swordShield) { + boredVariant = variant == 0 ? AnimationAction.SWORD_IDLE_BORED + : variant == 1 ? AnimationAction.SWORD_IDLE_BORED_2 + : AnimationAction.SWORD_IDLE_BORED_3; + } else { + boredVariant = variant == 0 ? AnimationAction.TWO_HAND_IDLE_BORED + : variant == 1 ? AnimationAction.TWO_HAND_IDLE_BORED_2 + : AnimationAction.TWO_HAND_IDLE_BORED_3; + } + boredAnimRemaining = resolveClipLength(boredVariant, 3.0f); + boredIdleTimer = 0f; + } + private void triggerRollFromInput() { if (!weaponDrawn) return; if (rollActive || pickupActive || blockingAnimActive || turnActive 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 461ac47..101b3e0 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.ShieldAttachmentIO; +import de.blight.common.model.ShieldCategoryAttachment; import de.blight.common.model.WeaponAttachmentIO; import de.blight.common.model.WeaponCategoryAttachment; import org.slf4j.Logger; @@ -38,7 +40,8 @@ public class WeaponAttachmentControl extends AbstractControl { 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 BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f); + private static final Vector3f TWO_HAND_BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.03f); private static final Vector3f HAND_GRIP_OFFSET = new Vector3f( ItemGrip.HAND_GRIP_OFFSET_X, ItemGrip.HAND_GRIP_OFFSET_Y, ItemGrip.HAND_GRIP_OFFSET_Z); @@ -196,14 +199,16 @@ public class WeaponAttachmentControl extends AbstractControl { : boneInChar; boolean usesTwoHand = isTwoHand && leftForeArmNode != null && grip != null - && (grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f); + && (grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f) + && !playerInput.isDrawAnimActive(); if (usesTwoHand) { applyTwoHandTransform(attachInChar, boneRotInChar, grip, charWorldRotInv, charWorldPos, cfg); } else { applyHandTransform(attachInChar, boneRotInChar, grip, cfg); } } else { - applyBackTransform(boneInChar, boneRotInChar, grip); + boolean twoHanded = item != null && item.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD; + applyBackTransform(boneInChar, boneRotInChar, grip, twoHanded); } } @@ -356,7 +361,8 @@ public class WeaponAttachmentControl extends AbstractControl { // 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) { + private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip, + boolean twoHanded) { Quaternion gripRot = (grip != null) ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), @@ -369,7 +375,8 @@ public class WeaponAttachmentControl extends AbstractControl { ? 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 gripTarget = bonePosInChar.add(boneRotInChar.mult( + twoHanded ? TWO_HAND_BACK_GRIP_OFFSET : BACK_GRIP_OFFSET)); Vector3f gripPos = (grip != null) ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; @@ -406,7 +413,9 @@ public class WeaponAttachmentControl extends AbstractControl { Vector3f boneInChar = charWorldRotInv.mult( leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation()); - applyShieldArmTransform(boneInChar, boneRotInChar, grip); + ShieldCategoryAttachment shieldCfg = ShieldAttachmentIO.get( + shield != null ? shield.getSubCategory() : null); + applyShieldArmTransform(boneInChar, boneRotInChar, grip, shieldCfg); shieldSpatial.setCullHint(Spatial.CullHint.Inherit); } else if (!drawn && backNode != null) { Vector3f boneInChar = charWorldRotInv.mult( @@ -419,17 +428,18 @@ public class WeaponAttachmentControl extends AbstractControl { } } - private void applyShieldArmTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) { + private void applyShieldArmTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, + ItemGrip grip, ShieldCategoryAttachment cfg) { Quaternion animDelta = (leftForeArmBindRotInChar != null) ? boneRotInChar.mult(leftForeArmBindRotInChar.inverse()) : boneRotInChar; - Quaternion armRot = new Quaternion().fromAngles(SHIELD_ARM_ROT_X, SHIELD_ARM_ROT_Y, SHIELD_ARM_ROT_Z); + Quaternion armRot = new Quaternion().fromAngles( + DEG_TO_RAD * cfg.getRotX(), DEG_TO_RAD * cfg.getRotY(), DEG_TO_RAD * cfg.getRotZ()); Quaternion shieldRot = animDelta.mult(armRot); Vector3f gripPos = (grip != null) ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; - // Ankerpunkt Richtung Ellbogen verschieben (lokale −Y des LeftHand-Bones) - Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -SHIELD_ARM_ELBOW_OFFSET, 0f))); + Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -cfg.getElbowOffset(), 0f))); shieldSpatial.setLocalRotation(shieldRot); shieldSpatial.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos))); }