From eda8931b5fa21737fc86a459837269f3399f607d Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 21:29:26 -0500 Subject: [PATCH] Refactor entity owner checks and enhance FULL_MODEL rotation support in ability entities --- .../npcs/entity/EntityAbilityBeam.java | 45 +++--- .../npcs/entity/EntityAbilityDisc.java | 8 +- .../npcs/entity/EntityAbilityLaser.java | 3 +- .../npcs/entity/EntityAbilityProjectile.java | 28 ++-- .../npcs/entity/EntityAbilitySweeper.java | 12 +- .../kamkeel/npcs/util/AnchorPointHelper.java | 136 +++++++++++++++++- 6 files changed, 175 insertions(+), 57 deletions(-) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 6f5b3a64d..bfdca5346 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -386,33 +386,26 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { chargeTick++; Entity owner = getOwnerEntity(); - if (owner == null || owner.isDead) { - setDead(); - return; + if (owner != null) { + Vec3 pos; + if (owner instanceof EntityLivingBase) { + pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance); + } else { + float yaw = (float) Math.toRadians(owner.rotationYaw); + double offsetX = -Math.sin(yaw) * chargeOffsetDistance; + double offsetZ = Math.cos(yaw) * chargeOffsetDistance; + pos = Vec3.createVectorHelper( + owner.posX + offsetX, + owner.posY + owner.getEyeHeight() * 0.7, + owner.posZ + offsetZ + ); + } + + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + startX = pos.xCoord; + startY = pos.yCoord; + startZ = pos.zCoord; } - - // Calculate position based on anchor point - Vec3 pos; - if (owner instanceof EntityLivingBase) { - pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance); - } else { - // Fallback for non-living entities (shouldn't happen normally) - float yaw = (float) Math.toRadians(owner.rotationYaw); - double offsetX = -Math.sin(yaw) * chargeOffsetDistance; - double offsetZ = Math.cos(yaw) * chargeOffsetDistance; - pos = Vec3.createVectorHelper( - owner.posX + offsetX, - owner.posY + owner.getEyeHeight() * 0.7, - owner.posZ + offsetZ - ); - } - - setPosition(pos.xCoord, pos.yCoord, pos.zCoord); - - // Also update origin for when firing starts - startX = pos.xCoord; - startY = pos.yCoord; - startZ = pos.zCoord; } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 43fa459fc..c0f3cbe8d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -391,19 +391,13 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { protected void updateCharging() { chargeTick++; - Entity owner = getOwnerEntity(); - if (owner == null || owner.isDead) { - setDead(); - return; - } - // Grow size based on charge progress float progress = getChargeProgress(); this.discRadius = targetDiscRadius * progress; this.discThickness = targetDiscThickness * progress; this.size = this.discRadius; // Base size for interpolation - // Calculate position based on anchor point, offset downward by half radius to center + Entity owner = getOwnerEntity(); if (owner instanceof EntityLivingBase) { Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index a10dae0b8..d17024680 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -156,8 +156,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { */ private void updateLaserOriginAndDirection() { Entity owner = getOwnerEntity(); - if (owner == null || owner.isDead) return; - if (!(owner instanceof EntityLivingBase)) return; + if (owner == null || !(owner instanceof EntityLivingBase)) return; EntityLivingBase livingOwner = (EntityLivingBase) owner; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 9368c5269..ac6add74d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -184,16 +184,20 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP // Skip lifetime/distance checks in preview mode if (!previewMode) { - // Failsafe: if owner entity is gone, dead, or NPC was killed/reset, self-destruct + // Only self-destruct if the owner is confirmed dead/killed. + // A null owner (unloaded/out of chunk) is NOT treated as dead — + // the projectile keeps acting normally and deathWorldTime handles lifetime. if (ownerEntityId >= 0 && ticksExisted > 5) { Entity owner = worldObj.getEntityByID(ownerEntityId); - if (owner == null || owner.isDead) { - this.setDead(); - return; - } - if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) { - this.setDead(); - return; + if (owner != null) { + if (owner.isDead) { + this.setDead(); + return; + } + if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) { + this.setDead(); + return; + } } } @@ -506,16 +510,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP * Update during charging state - grow size and follow anchor. * Default implementation grows {@code size} from 0 to {@code targetSize}. * Override in subclasses for type-specific charging (Disc, Beam). + * + * Note: Owner death checks are handled by onUpdate() before this is called. */ protected void updateCharging() { chargeTick++; - Entity owner = getOwnerEntity(); - if (owner == null || owner.isDead) { - setDead(); - return; - } float progress = getChargeProgress(); this.size = targetSize * progress; + Entity owner = getOwnerEntity(); if (owner instanceof EntityLivingBase) { Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index 4d072090e..6f0068d26 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -144,15 +144,19 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa return; } - // Follow owner Entity owner = getOwner(); - if (owner == null) { + if (owner != null && owner.isDead) { + this.setDead(); + return; + } + if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) { this.setDead(); return; } - // Update position to follow owner - this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + if (owner != null) { + this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + } // Check if sweep is done if (completedRotations >= numberOfRotations) { diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 802841c2a..3c9b10f1d 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -119,6 +119,20 @@ public class AnchorPointHelper { y += rotatedOffset.yCoord; z += rotatedOffset.zCoord; + // Apply FULL_MODEL rotation if active + FramePart fullModel = getFullModelPart(entity); + if (fullModel != null) { + double dx = x - entity.posX; + double dy = y - entity.posY; + double dz = z - entity.posZ; + Vec3 rotated = applyFullModelToWorldOffset(fullModel, entity, dx, dy, dz, bodyYaw, scale); + return Vec3.createVectorHelper( + entity.posX + rotated.xCoord, + entity.posY + rotated.yCoord, + entity.posZ + rotated.zCoord + ); + } + return Vec3.createVectorHelper(x, y, z); } @@ -162,16 +176,32 @@ public class AnchorPointHelper { /** * Calculate hand position using animation data if available. + * Applies FULL_MODEL rotation if an active animation has it. */ private static Vec3 calculateHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { // Try to get animated position first - Vec3 animatedPos = getAnimatedHandPosition(entity, anchor, rightHand, scale); - if (animatedPos != null) { - return animatedPos; + Vec3 pos = getAnimatedHandPosition(entity, anchor, rightHand, scale); + if (pos == null) { + // Fallback to static position + pos = calculateFallbackHandPosition(entity, anchor, rightHand, scale); } - // Fallback to static position - return calculateFallbackHandPosition(entity, anchor, rightHand, scale); + // Apply FULL_MODEL rotation if active + FramePart fullModel = getFullModelPart(entity); + if (fullModel != null) { + double dx = pos.xCoord - entity.posX; + double dy = pos.yCoord - entity.posY; + double dz = pos.zCoord - entity.posZ; + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + Vec3 rotated = applyFullModelToWorldOffset(fullModel, entity, dx, dy, dz, bodyYaw, scale); + return Vec3.createVectorHelper( + entity.posX + rotated.xCoord, + entity.posY + rotated.yCoord, + entity.posZ + rotated.zCoord + ); + } + + return pos; } /** @@ -390,4 +420,100 @@ public class AnchorPointHelper { double worldZ = -offsetX * sin + offsetZ * cos; return Vec3.createVectorHelper(worldX, offsetY, worldZ); } + + // ==================== FULL_MODEL ANIMATION SUPPORT ==================== + + /** + * Get the FULL_MODEL FramePart if the entity has an active animation with it. + * Returns null if no FULL_MODEL animation is active. + */ + private static FramePart getFullModelPart(EntityLivingBase entity) { + AnimationData animData = AnimationData.getData(entity); + if (animData == null || !animData.isActive() || animData.animation == null) { + return null; + } + Frame frame = (Frame) animData.animation.currentFrame(); + if (frame == null) { + return null; + } + FramePart part = frame.frameParts.get(EnumAnimationPart.FULL_MODEL); + return part; + } + + /** + * Apply FULL_MODEL rotation to a world-space offset from entity position. + * Converts the offset to model space, applies FULL_MODEL rotation and pivots, + * then converts back to world space. + * + * The rendering pipeline is: entity_pos * body_yaw * scale(-1,-1,1) * FULL_MODEL * parts + * Combined body_yaw + scale maps model space to world space: + * worldX = mx*cos(yaw) + mz*sin(yaw) + * worldY = -my (model Y-down → world Y-up) + * worldZ = mx*sin(yaw) - mz*cos(yaw) + * This transform is its own inverse for X,Z. + * + * FULL_MODEL rotation order matches renderer: vertex sees Z, then Y, then X. + */ + private static Vec3 applyFullModelToWorldOffset(FramePart fullModel, EntityLivingBase entity, + double worldDX, double worldDY, double worldDZ, + float bodyYaw, float scale) { + float[] rotations = getRotations(fullModel, entity); + float[] pivots = getPivots(fullModel, entity); + + // Quick exit if no transform needed + boolean hasRotation = rotations[0] != 0 || rotations[1] != 0 || rotations[2] != 0; + boolean hasPivot = pivots[0] != 0 || pivots[1] != 0 || pivots[2] != 0; + if (!hasRotation && !hasPivot) { + return Vec3.createVectorHelper(worldDX, worldDY, worldDZ); + } + + // Convert world offset to model space + // Body yaw + scale combined transform is an involution (self-inverse) for X,Z + double cosYaw = Math.cos(bodyYaw); + double sinYaw = Math.sin(bodyYaw); + + double mx = worldDX * cosYaw + worldDZ * sinYaw; + double mz = worldDX * sinYaw - worldDZ * cosYaw; + double my = -worldDY; // Invert Y (world Y-up → model Y-down) + + // Apply FULL_MODEL rotations (vertex order: Z first, then Y, then X) + // Matches renderer: glRotatef(X), glRotatef(Y), glRotatef(Z) → vertex sees Z,Y,X + + // Z rotation + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); + double nx = mx * cosZ - my * sinZ; + double ny = mx * sinZ + my * cosZ; + mx = nx; + my = ny; + + // Y rotation + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); + nx = mx * cosY + mz * sinY; + double nz = -mx * sinY + mz * cosY; + mx = nx; + mz = nz; + + // X rotation + double cosX = Math.cos(rotations[0]); + double sinX = Math.sin(rotations[0]); + ny = my * cosX - mz * sinX; + nz = my * sinX + mz * cosX; + my = ny; + mz = nz; + + // Apply pivots in model space (after rotation, matching GL vertex ordering) + // Renderer: glTranslatef(px, -py, pz) → pivot Y is negated for model Y-down space + mx += pivots[0] * MODEL_SCALE * scale; + my -= pivots[1] * MODEL_SCALE * scale; + mz += pivots[2] * MODEL_SCALE * scale; + + // Convert back to world space (same involution for X,Z; invert Y back) + double newDX = mx * cosYaw + mz * sinYaw; + double newDZ = mx * sinYaw - mz * cosYaw; + double newDY = -my; + + return Vec3.createVectorHelper(newDX, newDY, newDZ); + } }