375 lines
20 KiB
Java
375 lines
20 KiB
Java
package de.blight.game.control;
|
||
|
||
import com.jme3.asset.AssetManager;
|
||
import com.jme3.math.Matrix4f;
|
||
import com.jme3.math.Quaternion;
|
||
import com.jme3.math.Vector3f;
|
||
import com.jme3.renderer.RenderManager;
|
||
import com.jme3.renderer.ViewPort;
|
||
import com.jme3.scene.Node;
|
||
import com.jme3.scene.Spatial;
|
||
import com.jme3.scene.control.AbstractControl;
|
||
import de.blight.common.model.Item;
|
||
import de.blight.common.model.ItemGrip;
|
||
import de.blight.common.model.ItemSubCategory;
|
||
import de.blight.common.model.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;
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
|
||
/**
|
||
* Hängt Waffe und Schild am NPC – 1:1 dieselbe Logik wie WeaponAttachmentControl (Held).
|
||
* - weggesteckt (drawn=false) → Spine2-Knochen (Rücken)
|
||
* - gezückt (drawn=true) → RightHand (Waffe) / LeftHand (Schild am Arm)
|
||
*/
|
||
public class NpcWeaponAttachmentControl extends AbstractControl {
|
||
|
||
private static final Logger log = LoggerFactory.getLogger(NpcWeaponAttachmentControl.class);
|
||
|
||
private static final String BONE_SPINE = "mixamorig:Spine2";
|
||
private static final String BONE_HAND = "mixamorig:RightHand";
|
||
private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
|
||
private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand";
|
||
|
||
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
|
||
private static final float HALF_PI = (float) Math.PI * 0.5f;
|
||
|
||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
|
||
private static final Vector3f 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);
|
||
|
||
private static final float SHIELD_ARM_ROT_X = -HALF_PI;
|
||
private static final float SHIELD_ARM_ROT_Y = 0f;
|
||
private static final float SHIELD_ARM_ROT_Z = 0f;
|
||
private static final float SHIELD_ARM_ELBOW_OFFSET = 0.25f;
|
||
|
||
private final AssetManager assetManager;
|
||
private final Item weaponItem;
|
||
private final Item shieldItem;
|
||
|
||
private com.jme3.anim.SkinningControl skinCtrl;
|
||
private Node backNode;
|
||
private Node handNode;
|
||
private Node palmNodeRef;
|
||
private Node leftForeArmNode;
|
||
|
||
private Quaternion handBindRotInChar;
|
||
private Quaternion leftForeArmBindRotInChar;
|
||
private Vector3f palmOffsetLocal;
|
||
private int palmOffsetFrameDelay = 0;
|
||
|
||
private Spatial weaponSpatial;
|
||
private Spatial shieldSpatial;
|
||
private boolean drawn = false;
|
||
private boolean weaponHidden = false;
|
||
private boolean shieldHidden = false;
|
||
|
||
public NpcWeaponAttachmentControl(AssetManager assetManager, Item weaponItem, Item shieldItem) {
|
||
this.assetManager = assetManager;
|
||
this.weaponItem = weaponItem;
|
||
this.shieldItem = shieldItem;
|
||
}
|
||
|
||
public void setDrawn(boolean drawn) {
|
||
this.drawn = drawn;
|
||
}
|
||
|
||
public void hideWeapon() {
|
||
weaponHidden = true;
|
||
if (weaponSpatial != null) { weaponSpatial.setCullHint(Spatial.CullHint.Always); }
|
||
}
|
||
|
||
public void hideShield() {
|
||
shieldHidden = true;
|
||
if (shieldSpatial != null) { shieldSpatial.setCullHint(Spatial.CullHint.Always); }
|
||
}
|
||
|
||
@Override
|
||
protected void controlUpdate(float tpf) {
|
||
if (skinCtrl == null) {
|
||
skinCtrl = RetargetingSystem.findSkinningControl(spatial);
|
||
if (skinCtrl == null) { return; }
|
||
|
||
try { backNode = skinCtrl.getAttachmentsNode(BONE_SPINE); }
|
||
catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_SPINE, e.getMessage()); }
|
||
try { handNode = skinCtrl.getAttachmentsNode(BONE_HAND); }
|
||
catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_HAND, e.getMessage()); }
|
||
try {
|
||
palmNodeRef = skinCtrl.getAttachmentsNode(BONE_PALM);
|
||
palmOffsetFrameDelay = 2;
|
||
} catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage()); }
|
||
try { leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM); }
|
||
catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage()); }
|
||
handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND);
|
||
leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM);
|
||
|
||
log.info("[NpcWeaponAttach] Setup: backNode={} handNode={} palmNode={} forearmNode={} weapon={} shield={}",
|
||
backNode != null, handNode != null, palmNodeRef != null, leftForeArmNode != null,
|
||
weaponItem != null ? weaponItem.getItemId() : "null",
|
||
shieldItem != null ? shieldItem.getItemId() : "null");
|
||
|
||
if (weaponItem != null) {
|
||
weaponSpatial = loadAndAttach(weaponItem);
|
||
if (weaponHidden && weaponSpatial != null) { weaponSpatial.setCullHint(Spatial.CullHint.Always); }
|
||
}
|
||
if (shieldItem != null) {
|
||
shieldSpatial = loadAndAttach(shieldItem);
|
||
if (shieldHidden && shieldSpatial != null) { shieldSpatial.setCullHint(Spatial.CullHint.Always); }
|
||
}
|
||
|
||
log.info("[NpcWeaponAttach] Geladen: weaponSpatial={} shieldSpatial={}",
|
||
weaponSpatial != null, shieldSpatial != null);
|
||
}
|
||
|
||
// Palm-Offset nach frühestens 2 Frames berechnen (SkinningControl muss vorher gelaufen sein)
|
||
if (palmOffsetFrameDelay > 0 && palmNodeRef != null && handNode != null) {
|
||
palmOffsetFrameDelay--;
|
||
if (palmOffsetFrameDelay == 0) {
|
||
palmOffsetLocal = computePalmOffset(handNode, palmNodeRef);
|
||
}
|
||
}
|
||
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
|
||
Vector3f charWorldPos = spatial.getWorldTranslation();
|
||
|
||
// ── Waffe ────────────────────────────────────────────────────────────────
|
||
if (weaponSpatial != null && !weaponHidden) {
|
||
Node boneNode = drawn ? handNode : backNode;
|
||
if (boneNode != null) {
|
||
Vector3f boneInChar = charWorldRotInv.mult(boneNode.getWorldTranslation().subtract(charWorldPos));
|
||
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
|
||
if (drawn) {
|
||
ItemGrip wGrip = weaponItem != null ? weaponItem.getGrip() : null;
|
||
boolean isTwoHand = weaponItem != null && weaponItem.getSubCategory() == ItemSubCategory.TWO_HANDED_SWORD;
|
||
Vector3f attachInChar = (palmOffsetLocal != null)
|
||
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
|
||
: boneInChar;
|
||
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) {
|
||
applyTwoHandTransform(weaponSpatial, attachInChar, boneRotInChar,
|
||
wGrip, charWorldRotInv, charWorldPos, cfg);
|
||
} else {
|
||
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, twoHanded);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Schild ────────────────────────────────────────────────────────────────
|
||
if (shieldSpatial != null && !shieldHidden) {
|
||
ItemGrip grip = shieldItem != null ? shieldItem.getGrip() : null;
|
||
if (drawn && leftForeArmNode != null) {
|
||
Vector3f boneInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos));
|
||
Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation());
|
||
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));
|
||
Quaternion boneRotInChar = charWorldRotInv.mult(backNode.getWorldRotation());
|
||
applyShieldBackTransform(shieldSpatial, boneInChar, boneRotInChar, grip);
|
||
shieldSpatial.setCullHint(Spatial.CullHint.Inherit);
|
||
} else {
|
||
shieldSpatial.setCullHint(Spatial.CullHint.Always);
|
||
}
|
||
}
|
||
}
|
||
|
||
private Spatial loadAndAttach(Item item) {
|
||
if (item.getModelRef() == null || item.getModelRef().getPath() == null) { return null; }
|
||
try {
|
||
Spatial s = assetManager.loadModel(item.getModelRef().getPath());
|
||
((Node) spatial).attachChild(s);
|
||
return s;
|
||
} catch (Exception e) {
|
||
log.warn("[NpcWeaponAttach] Modell nicht ladbar: {} — {}", item.getModelRef().getPath(), e.getMessage());
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ── Hand-Transform (1:1 von WeaponAttachmentControl) ─────────────────────────
|
||
|
||
private void applyHandTransform(Spatial s, 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();
|
||
Vector3f gripPos = (grip != null)
|
||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||
: Vector3f.ZERO;
|
||
Quaternion animDelta = (handBindRotInChar != null)
|
||
? boneRotInChar.mult(handBindRotInChar.inverse())
|
||
: boneRotInChar;
|
||
boolean bladeAtMinusZ = grip != null && (
|
||
(grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
|
||
? grip.getGrip2PosZ() > grip.getPosZ()
|
||
: grip.getPosZ() > 0f);
|
||
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());
|
||
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos));
|
||
weaponPos = weaponPos.add(weaponRot.mult(handOff));
|
||
if (grip != null && (grip.getHandOffsetX() != 0f || grip.getHandOffsetY() != 0f || grip.getHandOffsetZ() != 0f)) {
|
||
weaponPos = weaponPos.add(grip.getHandOffsetX(), grip.getHandOffsetY(), grip.getHandOffsetZ());
|
||
}
|
||
s.setLocalRotation(weaponRot);
|
||
s.setLocalTranslation(weaponPos);
|
||
}
|
||
|
||
private void applyTwoHandTransform(Spatial s, Vector3f bonePosInChar, Quaternion boneRotInChar,
|
||
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)
|
||
? boneRotInChar.mult(handBindRotInChar.inverse()) : boneRotInChar;
|
||
boolean bladeAtMinusZ = grip.getGrip2PosZ() > grip.getPosZ();
|
||
Quaternion bladeFlip = bladeAtMinusZ
|
||
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
|
||
: new Quaternion();
|
||
Quaternion weaponRot = animDelta.mult(bladeFlip).mult(gripRot.inverse());
|
||
|
||
Vector3f g1 = new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ());
|
||
Vector3f g2 = new Vector3f(grip.getGrip2PosX(), grip.getGrip2PosY(), grip.getGrip2PosZ());
|
||
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));
|
||
Vector3f currentGripDir = weaponRot.mult(g2eff.subtract(g1eff).normalizeLocal());
|
||
Vector3f targetDir = leftHandInChar.subtract(bonePosInChar).normalizeLocal();
|
||
weaponRot = swingFromTo(currentGripDir, targetDir).mult(weaponRot);
|
||
|
||
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(g1eff));
|
||
s.setLocalRotation(weaponRot);
|
||
s.setLocalTranslation(weaponPos);
|
||
}
|
||
|
||
private static Quaternion swingFromTo(Vector3f from, Vector3f to) {
|
||
float dot = com.jme3.math.FastMath.clamp(from.dot(to), -1f, 1f);
|
||
Vector3f axis = from.cross(to);
|
||
float 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));
|
||
}
|
||
|
||
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, boolean twoHanded) {
|
||
Quaternion gripRot = (grip != null)
|
||
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(),
|
||
DEG_TO_RAD * grip.getRotY(),
|
||
DEG_TO_RAD * grip.getRotZ())
|
||
: new Quaternion();
|
||
boolean bladeAtMinusZ2 = grip != null && (
|
||
(grip.getGrip2PosX() != 0f || grip.getGrip2PosY() != 0f || grip.getGrip2PosZ() != 0f)
|
||
? grip.getGrip2PosZ() > grip.getPosZ()
|
||
: grip.getPosZ() > 0f);
|
||
Quaternion bladeFlip2 = bladeAtMinusZ2
|
||
? new Quaternion().fromAngles(com.jme3.math.FastMath.PI, 0f, 0f)
|
||
: new Quaternion();
|
||
Quaternion weaponRot = boneRotInChar.mult(BACK_ROT_NORM).mult(bladeFlip2).mult(gripRot.inverse());
|
||
Vector3f target = boneInChar.add(boneRotInChar.mult(
|
||
twoHanded ? TWO_HAND_BACK_GRIP_OFFSET : BACK_GRIP_OFFSET));
|
||
// X=0: Waffenmitte (nicht handspezifischer Griffpunkt) auf Wirbelsäule zentrieren
|
||
Vector3f gripPos = grip != null
|
||
? new Vector3f(0f, grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO;
|
||
Vector3f backPos = target.subtract(weaponRot.mult(gripPos));
|
||
s.setLocalRotation(weaponRot);
|
||
s.setLocalTranslation(backPos);
|
||
}
|
||
|
||
// ── Schild-Transforms (1:1 von WeaponAttachmentControl) ──────────────────────
|
||
|
||
private void applyShieldArmTransform(Spatial s, Vector3f bonePosInChar,
|
||
Quaternion boneRotInChar, ItemGrip grip,
|
||
ShieldCategoryAttachment cfg) {
|
||
Quaternion animDelta = (leftForeArmBindRotInChar != null)
|
||
? boneRotInChar.mult(leftForeArmBindRotInChar.inverse())
|
||
: boneRotInChar;
|
||
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, -cfg.getElbowOffset(), 0f)));
|
||
s.setLocalRotation(shieldRot);
|
||
s.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos)));
|
||
}
|
||
|
||
private void applyShieldBackTransform(Spatial s, Vector3f boneInChar,
|
||
Quaternion boneRotInChar, ItemGrip grip) {
|
||
Quaternion backRot = new Quaternion().fromAngles(0f, (float) Math.PI, 0f);
|
||
Quaternion shieldRot = boneRotInChar.mult(backRot);
|
||
Vector3f target = boneInChar.add(boneRotInChar.mult(SHIELD_BACK_OFFSET));
|
||
Vector3f gripPos = (grip != null)
|
||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO;
|
||
s.setLocalRotation(shieldRot);
|
||
s.setLocalTranslation(target.subtract(shieldRot.mult(gripPos)));
|
||
}
|
||
|
||
// ── Hilfsmethoden ────────────────────────────────────────────────────────────
|
||
|
||
private static Vector3f computePalmOffset(Node handNode, Node palmNode) {
|
||
Vector3f deltaWorld = palmNode.getWorldTranslation().subtract(handNode.getWorldTranslation());
|
||
return handNode.getWorldRotation().inverse().mult(deltaWorld);
|
||
}
|
||
|
||
private static Quaternion extractJointBindRot(com.jme3.anim.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();
|
||
}
|
||
|
||
@Override
|
||
public void setSpatial(Spatial spatial) {
|
||
super.setSpatial(spatial);
|
||
if (spatial == null) {
|
||
if (weaponSpatial != null) { weaponSpatial.removeFromParent(); weaponSpatial = null; }
|
||
if (shieldSpatial != null) { shieldSpatial.removeFromParent(); shieldSpatial = null; }
|
||
}
|
||
}
|
||
|
||
@Override
|
||
protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||
}
|