From 0f44a3c6c64906608ec9945b9923d6df20fbf620 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 26 Jan 2026 23:55:07 -0500 Subject: [PATCH] Ability Anchor Points + Orb / Disc Summoning --- .../controllers/data/ability/AnchorPoint.java | 55 ++++ .../data/ability/type/AbilityDisc.java | 73 +++-- .../data/ability/type/AbilityEnergyBeam.java | 13 +- .../data/ability/type/AbilityOrb.java | 76 +++-- .../npcs/entity/EntityAbilityBeam.java | 54 ++-- .../npcs/entity/EntityAbilityDisc.java | 275 ++++++++++++++-- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 183 +++++++++++ .../kamkeel/npcs/util/AnchorPointHelper.java | 303 ++++++++++++++++++ .../advanced/ability/SubGuiAbilityDisc.java | 22 +- .../ability/SubGuiAbilityEnergyBeam.java | 12 +- .../advanced/ability/SubGuiAbilityOrb.java | 12 +- .../assets/customnpcs/lang/en_US.lang | 1 + 12 files changed, 984 insertions(+), 95 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java create mode 100644 src/main/java/kamkeel/npcs/util/AnchorPointHelper.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java new file mode 100644 index 000000000..9bf408c27 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java @@ -0,0 +1,55 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines anchor points for ability charging effects. + * Determines where the charging visual (orb, disc, etc.) appears relative to the entity. + */ +public enum AnchorPoint { + FRONT(0, "Front"), // In front of entity face (default) + CENTER(1, "Center"), // Entity center + RIGHT_HAND(2, "Right Hand"), // Right arm/hand position + LEFT_HAND(3, "Left Hand"), // Left arm/hand position + ABOVE_HEAD(4, "Above Head"), // Above the entity + CHEST(5, "Chest"); // Chest level, centered + + private final int id; + private final String displayName; + + AnchorPoint(int id, String displayName) { + this.id = id; + this.displayName = displayName; + } + + public int getId() { + return id; + } + + public String getDisplayName() { + return displayName; + } + + /** + * Get AnchorPoint from NBT id value. + * Returns FRONT if id is invalid. + */ + public static AnchorPoint fromId(int id) { + for (AnchorPoint anchor : values()) { + if (anchor.id == id) { + return anchor; + } + } + return FRONT; + } + + /** + * Get all display names for GUI dropdowns. + */ + public static String[] getDisplayNames() { + AnchorPoint[] values = values(); + String[] names = new String[values.length]; + for (int i = 0; i < values.length; i++) { + names[i] = values[i].displayName; + } + return names; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 5aa34a969..95dcd4afb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -3,11 +3,13 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityDisc; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -28,7 +30,7 @@ public class AbilityDisc extends Ability { private float discRadius = 1.0f; private float discThickness = 0.2f; private float maxDistance = 35.0f; - private int maxLifetime = 250; + private int maxLifetime = 200; // Combat properties private float damage = 8.0f; @@ -66,6 +68,12 @@ public class AbilityDisc extends Ability { private float lightningDensity = 0.15f; private float lightningRadius = 0.5f; + // Anchor point for charging position + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + + // Transient state for disc entity (used during windup charging) + private transient int discEntityId = -1; + public AbilityDisc() { this.typeId = "ability.cnpc.disc"; this.name = "Disc"; @@ -105,24 +113,41 @@ public class AbilityDisc extends Ability { public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { if (world.isRemote) return; - double spawnX = npc.posX; - double spawnY = npc.posY + npc.getEyeHeight() * 0.8; - double spawnZ = npc.posZ; + // Find and start moving the disc that was spawned during windup + if (discEntityId != -1) { + Entity entity = world.getEntityByID(discEntityId); + if (entity instanceof EntityAbilityDisc) { + EntityAbilityDisc disc = (EntityAbilityDisc) entity; + disc.startMoving(target); + } + discEntityId = -1; + } + } - EntityAbilityDisc disc = new EntityAbilityDisc( - world, npc, target, - spawnX, spawnY, spawnZ, - discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, - damage, knockback, knockbackUp, - speed, homing, homingStrength, homingRange, - boomerang, boomerangDelay, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius - ); + @Override + public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; - world.spawnEntityInWorld(disc); + // Spawn disc in charging mode on first tick of windup + if (tick == 1) { + // Create disc in charging mode - follows NPC based on anchor point during windup + EntityAbilityDisc disc = EntityAbilityDisc.createCharging( + world, npc, target, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + boomerang, boomerangDelay, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, + anchorPoint, // Anchor point for charging position + windUpTicks // Charge duration = windup duration + ); + + world.spawnEntityInWorld(disc); + discEntityId = disc.getEntityId(); + } } @Override @@ -135,6 +160,14 @@ public class AbilityDisc extends Ability { @Override public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + // Despawn charging disc if interrupted during windup + if (discEntityId != -1) { + Entity entity = npc.worldObj.getEntityByID(discEntityId); + if (entity != null) { + entity.setDead(); + } + discEntityId = -1; + } } @Override @@ -193,6 +226,7 @@ public class AbilityDisc extends Ability { nbt.setBoolean("lightningEffect", lightningEffect); nbt.setFloat("lightningDensity", lightningDensity); nbt.setFloat("lightningRadius", lightningRadius); + nbt.setInteger("anchorPoint", anchorPoint.getId()); } @Override @@ -201,7 +235,7 @@ public class AbilityDisc extends Ability { this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 35.0f; - this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 250; + this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.2f; this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.15f; @@ -224,6 +258,7 @@ public class AbilityDisc extends Ability { this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; } // Getters & Setters @@ -281,4 +316,6 @@ public class AbilityDisc extends Ability { public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } + public AnchorPoint getAnchorPoint() { return anchorPoint; } + public void setAnchorPoint(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 9a5345435..18969b43c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -66,6 +67,9 @@ public class AbilityEnergyBeam extends Ability { private float lightningDensity = 0.15f; private float lightningRadius = 0.5f; + // Anchor point for charging position + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + // Transient state for beam entity (used during windup charging) private transient int beamEntityId = -1; @@ -127,7 +131,7 @@ public class AbilityEnergyBeam extends Ability { if (tick == 1) { float offsetDist = 1.0f; - // Create beam in charging mode - follows NPC's look direction during windup + // Create beam in charging mode - follows NPC based on anchor point during windup EntityAbilityBeam beam = EntityAbilityBeam.createCharging( world, npc, target, beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, @@ -139,7 +143,8 @@ public class AbilityEnergyBeam extends Ability { lightningEffect, lightningDensity, lightningRadius, lockMovement, // Anchored mode windUpTicks, // Charge duration = windup duration - offsetDist // Offset distance in front of NPC + offsetDist, // Offset distance (used by FRONT anchor) + anchorPoint // Anchor point for charging position ); world.spawnEntityInWorld(beam); @@ -221,6 +226,7 @@ public class AbilityEnergyBeam extends Ability { nbt.setBoolean("lightningEffect", lightningEffect); nbt.setFloat("lightningDensity", lightningDensity); nbt.setFloat("lightningRadius", lightningRadius); + nbt.setInteger("anchorPoint", anchorPoint.getId()); } @Override @@ -250,6 +256,7 @@ public class AbilityEnergyBeam extends Ability { this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; } // Getters & Setters @@ -303,4 +310,6 @@ public class AbilityEnergyBeam extends Ability { public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } + public AnchorPoint getAnchorPoint() { return anchorPoint; } + public void setAnchorPoint(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 358b82896..38ffd8db4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -3,11 +3,13 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityOrb; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -63,6 +65,12 @@ public class AbilityOrb extends Ability { private float lightningRadius = 0.5f; // Max distance lightning arcs - scales with orbSize private int lightningFadeTime = 6; // Ticks before lightning fades out + // Anchor point for charging position + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + + // Transient state for orb entity (used during windup charging) + private transient int orbEntityId = -1; + public AbilityOrb() { this.typeId = "ability.cnpc.orb"; this.name = "Orb"; @@ -102,25 +110,40 @@ public class AbilityOrb extends Ability { public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { if (world.isRemote) return; - // Spawn position at NPC's eye level - double spawnX = npc.posX; - double spawnY = npc.posY + npc.getEyeHeight() * 0.8; - double spawnZ = npc.posZ; + // Find and start moving the orb that was spawned during windup + if (orbEntityId != -1) { + Entity entity = world.getEntityByID(orbEntityId); + if (entity instanceof EntityAbilityOrb) { + EntityAbilityOrb orb = (EntityAbilityOrb) entity; + orb.startMoving(target); + } + orbEntityId = -1; + } + } - // Create and spawn the self-managing orb entity - EntityAbilityOrb orb = new EntityAbilityOrb( - world, npc, target, - spawnX, spawnY, spawnZ, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, - damage, knockback, knockbackUp, - orbSpeed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, lightningFadeTime - ); + @Override + public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; - world.spawnEntityInWorld(orb); + // Spawn orb in charging mode on first tick of windup + if (tick == 1) { + // Create orb in charging mode - follows NPC based on anchor point during windup + EntityAbilityOrb orb = EntityAbilityOrb.createCharging( + world, npc, target, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + orbSpeed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime, + anchorPoint, // Anchor point for charging position + windUpTicks // Charge duration = windup duration + ); + + world.spawnEntityInWorld(orb); + orbEntityId = orb.getEntityId(); + } } @Override @@ -135,7 +158,14 @@ public class AbilityOrb extends Ability { @Override public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - // Nothing to clean up - entity manages itself + // Despawn charging orb if interrupted during windup + if (orbEntityId != -1) { + Entity entity = npc.worldObj.getEntityByID(orbEntityId); + if (entity != null) { + entity.setDead(); + } + orbEntityId = -1; + } } @Override @@ -192,6 +222,7 @@ public class AbilityOrb extends Ability { nbt.setFloat("lightningDensity", lightningDensity); nbt.setFloat("lightningRadius", lightningRadius); nbt.setInteger("lightningFadeTime", lightningFadeTime); + nbt.setInteger("anchorPoint", anchorPoint.getId()); } @Override @@ -221,6 +252,7 @@ public class AbilityOrb extends Ability { this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; this.lightningFadeTime = nbt.hasKey("lightningFadeTime") ? nbt.getInteger("lightningFadeTime") : 6; + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; } // Getters & Setters @@ -423,4 +455,12 @@ public class AbilityOrb extends Ability { public void setLightningFadeTime(int lightningFadeTime) { this.lightningFadeTime = lightningFadeTime; } + + public AnchorPoint getAnchorPoint() { + return anchorPoint; + } + + public void setAnchorPoint(AnchorPoint anchorPoint) { + this.anchorPoint = anchorPoint; + } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 34030ab3f..70aaf5254 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -1,5 +1,7 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -55,6 +57,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { private int chargeDuration = 40; private int chargeTick = 0; private float chargeOffsetDistance = 1.0f; + private AnchorPoint anchorPoint = AnchorPoint.FRONT; // Trail fading for non-anchored beams (comet effect) private boolean fadeTrail = false; @@ -214,7 +217,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { /** * Create a beam in charging mode (for windup phase). * The beam will grow from 0 to headSize over chargeDuration ticks. - * Position follows the owner's look direction. + * Position follows the owner based on anchor point. */ public static EntityAbilityBeam createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, float beamWidth, float headSize, int innerColor, int outerColor, @@ -225,12 +228,13 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime, boolean lightningEffect, float lightningDensity, float lightningRadius, - boolean anchoredMode, int chargeDuration, float chargeOffsetDistance) { - // Calculate initial position in front of owner's look direction - float yaw = (float) Math.toRadians(owner.rotationYawHead); - double spawnX = owner.posX - Math.sin(yaw) * chargeOffsetDistance; - double spawnY = owner.posY + owner.getEyeHeight() * 0.7; - double spawnZ = owner.posZ + Math.cos(yaw) * chargeOffsetDistance; + boolean anchoredMode, int chargeDuration, float chargeOffsetDistance, + AnchorPoint anchorPoint) { + // Calculate initial position based on anchor point + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint, chargeOffsetDistance); + double spawnX = spawnPos.xCoord; + double spawnY = spawnPos.yCoord; + double spawnZ = spawnPos.zCoord; EntityAbilityBeam beam = new EntityAbilityBeam( world, owner, target, @@ -247,6 +251,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { beam.chargeDuration = chargeDuration; beam.chargeTick = 0; beam.chargeOffsetDistance = chargeOffsetDistance; + beam.anchorPoint = anchorPoint; // Non-anchored beams have fading trail (comet effect) beam.fadeTrail = !anchoredMode; @@ -420,7 +425,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } /** - * Update during charging state - follow owner's look direction. + * Update during charging state - follow owner based on anchor point. */ private void updateCharging() { chargeTick++; @@ -431,27 +436,28 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { return; } - // Follow owner's head/look direction (rotationYawHead for NPCs/players) - float yaw; + // Calculate position based on anchor point + Vec3 pos; if (owner instanceof EntityLivingBase) { - yaw = (float) Math.toRadians(((EntityLivingBase) owner).rotationYawHead); + pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint, chargeOffsetDistance); } else { - yaw = (float) Math.toRadians(owner.rotationYaw); + // 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 + ); } - double offsetX = -Math.sin(yaw) * chargeOffsetDistance; - double offsetZ = Math.cos(yaw) * chargeOffsetDistance; - - double newX = owner.posX + offsetX; - double newY = owner.posY + owner.getEyeHeight() * 0.7; - double newZ = owner.posZ + offsetZ; - - setPosition(newX, newY, newZ); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); // Also update origin for when firing starts - startX = newX; - startY = newY; - startZ = newZ; + startX = pos.xCoord; + startY = pos.yCoord; + startZ = pos.zCoord; } /** @@ -726,6 +732,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; + this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail"); this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20; @@ -761,6 +768,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { nbt.setInteger("ChargeDuration", chargeDuration); nbt.setInteger("ChargeTick", chargeTick); nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); + nbt.setInteger("AnchorPoint", anchorPoint.getId()); nbt.setBoolean("FadeTrail", fadeTrail); nbt.setInteger("TrailFadeTime", trailFadeTime); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index c0127286f..b1f57f9c9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -1,5 +1,7 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -35,10 +37,45 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { private float discRadius = 1.0f; // Width of disc private float discThickness = 0.2f; // Height of disc + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private float targetDiscRadius = 1.0f; // Full radius to grow to during charging + private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + public EntityAbilityDisc(World world) { super(world); } + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * Check if disc is in charging state (synced via data watcher). + */ + public boolean isCharging() { + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + /** * Full constructor with all parameters (no lightning). */ @@ -117,6 +154,97 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { } } + /** + * Create a disc in charging mode (for windup phase). + * The disc will grow from 0 to discRadius over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public static EntityAbilityDisc createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, + float discRadius, float discThickness, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean boomerang, int boomerangDelay, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, + AnchorPoint anchorPoint, int chargeDuration) { + // Calculate initial position based on anchor point + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + double spawnX = spawnPos.xCoord; + double spawnY = spawnPos.yCoord; + double spawnZ = spawnPos.zCoord; + + EntityAbilityDisc disc = new EntityAbilityDisc( + world, owner, target, + spawnX, spawnY, spawnZ, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + boomerang, boomerangDelay, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius); + + // Set charging state (uses data watcher for client sync) + disc.setCharging(true); + disc.chargeDuration = chargeDuration; + disc.chargeTick = 0; + disc.anchorPoint = anchorPoint; + + // Store target size and start at 0 for grow effect + disc.targetDiscRadius = disc.discRadius; + disc.targetDiscThickness = disc.discThickness; + disc.discRadius = 0.01f; // Start very small + disc.discThickness = 0.01f; + disc.size = 0.01f; // Base size used for rendering + disc.renderCurrentSize = 0.01f; + disc.prevRenderSize = 0.01f; + + // Clear motion while charging + disc.motionX = 0; + disc.motionY = 0; + disc.motionZ = 0; + + return disc; + } + + /** + * Start the disc moving (exit charging mode). + * Called by ability when windup ends. + */ + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - posX; + double dy = (target.posY + target.height * 0.5) - posY; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + @Override protected void updateRotation() { // Disc spins ONLY on Y axis (flat spin like a saw blade) @@ -127,6 +255,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { @Override protected void updateProjectile() { + // Handle charging state (windup phase) + if (isCharging()) { + updateCharging(); + return; + } + if (worldObj.isRemote) { handleClientInterpolation(); } else { @@ -141,17 +275,8 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { checkEntityCollision(); this.moveEntity(motionX, motionY, motionZ); - // Boomerang: if we've traveled far without hitting, start returning + // Track ticks for boomerang delay (checkMaxDistance handles the actual return trigger) if (boomerang && !returning && !hasHit) { - double distTraveled = Math.sqrt( - (posX - startX) * (posX - startX) + - (posY - startY) * (posY - startY) + - (posZ - startZ) * (posZ - startZ) - ); - // Start returning at 80% of max distance or after delay - if (distTraveled >= maxDistance * 0.8 || ticksSinceMiss >= boomerangDelay) { - returning = true; - } ticksSinceMiss++; } } @@ -170,20 +295,41 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { @Override protected boolean checkMaxDistance() { - // If boomerang and returning, don't die from max distance - if (boomerang && returning) { - // Die if we get close to owner - Entity owner = getOwner(); - if (owner != null) { - double distToOwner = Math.sqrt( - (posX - owner.posX) * (posX - owner.posX) + - (posY - owner.posY) * (posY - owner.posY) + - (posZ - owner.posZ) * (posZ - owner.posZ) - ); - return distToOwner < 1.0; + double distTraveled = Math.sqrt( + (posX - startX) * (posX - startX) + + (posY - startY) * (posY - startY) + + (posZ - startZ) * (posZ - startZ) + ); + + if (boomerang) { + // If not returning yet, check if we should start returning + if (!returning && !hasHit) { + if (distTraveled >= maxDistance * 0.8 || ticksSinceMiss >= boomerangDelay) { + returning = true; + } } + + // If returning, only die when close to owner + if (returning) { + Entity owner = getOwner(); + if (owner != null) { + double distToOwner = Math.sqrt( + (posX - owner.posX) * (posX - owner.posX) + + (posY - owner.posY) * (posY - owner.posY) + + (posZ - owner.posZ) * (posZ - owner.posZ) + ); + return distToOwner < 1.5; + } + // Owner gone but returning - keep going toward last known position + return false; + } + + // Not returning yet - don't die from distance (we'll trigger return above) + return false; } - return super.checkMaxDistance(); + + // Non-boomerang: standard max distance check + return distTraveled >= maxDistance; } private void updateHoming() { @@ -225,19 +371,28 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { private void updateReturnToOwner() { Entity owner = getOwner(); - if (owner == null) { - this.setDead(); - return; + + // If owner is gone (unloaded/dead), head back toward start position + double targetX, targetY, targetZ; + if (owner != null) { + targetX = owner.posX; + targetY = owner.posY + owner.height * 0.5; + targetZ = owner.posZ; + } else { + // Return to start position if owner not available + targetX = startX; + targetY = startY; + targetZ = startZ; } - double dx = owner.posX - posX; - double dy = (owner.posY + owner.height * 0.5) - posY; - double dz = owner.posZ - posZ; + double dx = targetX - posX; + double dy = targetY - posY; + double dz = targetZ - posZ; double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); if (dist > 0) { // Stronger homing when returning - double returnStrength = homingStrength * 2.0; + double returnStrength = Math.min(1.0, homingStrength * 2.5); double desiredVX = (dx / dist) * speed; double desiredVY = (dy / dist) * speed; double desiredVZ = (dz / dist) * speed; @@ -297,6 +452,52 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { } } + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + 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 + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + + // Update rotation during charging (disc still spins) + updateRotation(); + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + // ==================== GETTERS ==================== public float getSpeed() { @@ -336,6 +537,15 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f; this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f; this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning"); + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; + this.targetDiscRadius = nbt.hasKey("TargetDiscRadius") ? nbt.getFloat("TargetDiscRadius") : this.discRadius; + this.targetDiscThickness = nbt.hasKey("TargetDiscThickness") ? nbt.getFloat("TargetDiscThickness") : this.discThickness; } @Override @@ -349,5 +559,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { nbt.setFloat("DiscRadius", discRadius); nbt.setFloat("DiscThickness", discThickness); nbt.setBoolean("Returning", returning); + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setInteger("AnchorPoint", anchorPoint.getId()); + nbt.setFloat("TargetDiscRadius", targetDiscRadius); + nbt.setFloat("TargetDiscThickness", targetDiscThickness); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 38e3741f9..18a0c8503 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -1,5 +1,7 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -25,10 +27,44 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { private float homingStrength = 0.35f; // Increased from 0.15 for better tracking private float homingRange = 20.0f; + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private float targetSize = 1.0f; // Full size to grow to during charging + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + public EntityAbilityOrb(World world) { super(world); } + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * Check if orb is in charging state (synced via data watcher). + */ + public boolean isCharging() { + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + /** * Full constructor with all parameters. */ @@ -102,8 +138,100 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { } } + /** + * Create an orb in charging mode (for windup phase). + * The orb will grow from 0 to orbSize over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public static EntityAbilityOrb createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, + float orbSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime, + AnchorPoint anchorPoint, int chargeDuration) { + // Calculate initial position based on anchor point + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + double spawnX = spawnPos.xCoord; + double spawnY = spawnPos.yCoord; + double spawnZ = spawnPos.zCoord; + + EntityAbilityOrb orb = new EntityAbilityOrb( + world, owner, target, + spawnX, spawnY, spawnZ, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime); + + // Set charging state (uses data watcher for client sync) + orb.setCharging(true); + orb.chargeDuration = chargeDuration; + orb.chargeTick = 0; + orb.anchorPoint = anchorPoint; + + // Store target size and start at 0 for grow effect + orb.targetSize = orb.size; + orb.size = 0.01f; // Start very small + orb.renderCurrentSize = 0.01f; + orb.prevRenderSize = 0.01f; + + // Clear motion while charging + orb.motionX = 0; + orb.motionY = 0; + orb.motionZ = 0; + + return orb; + } + + /** + * Start the orb moving (exit charging mode). + * Called by ability when windup ends. + */ + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - posX; + double dy = (target.posY + target.height * 0.5) - posY; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + @Override protected void updateProjectile() { + // Handle charging state (windup phase) + if (isCharging()) { + updateCharging(); + return; + } + if (worldObj.isRemote) { handleClientInterpolation(); } else { @@ -207,6 +335,47 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { } } + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.size = targetSize * progress; + + // Calculate position based on anchor point + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + // ==================== GETTERS ==================== public float getSpeed() { @@ -238,6 +407,14 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; + this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; } @Override @@ -246,5 +423,11 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { nbt.setBoolean("Homing", homing); nbt.setFloat("HomingStrength", homingStrength); nbt.setFloat("HomingRange", homingRange); + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setInteger("AnchorPoint", anchorPoint.getId()); + nbt.setFloat("TargetSize", targetSize); } } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java new file mode 100644 index 000000000..7ad4dc581 --- /dev/null +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -0,0 +1,303 @@ +package kamkeel.npcs.util; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.util.Vec3; +import noppes.npcs.constants.EnumAnimationPart; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.controllers.data.Frame; +import noppes.npcs.controllers.data.FramePart; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Utility class for calculating world positions based on anchor points. + * Used for positioning charging effects on different body parts. + * Animation-aware: reads arm positions from NPC animation data when available. + */ +public class AnchorPointHelper { + + // Model coordinate scale factor (model coords to blocks) + private static final float MODEL_SCALE = 0.0625f; // 1/16 + + // Default offsets (in blocks) + private static final float ARM_LATERAL_OFFSET = 0.35f; // ~5.5 pixels lateral from center + private static final float ARM_FORWARD_OFFSET = 0.25f; // Arm extended slightly forward + private static final float FRONT_OFFSET = 1.0f; // Distance in front for FRONT anchor + + // Height multipliers relative to eye height + private static final float FRONT_HEIGHT_MULT = 0.7f; + private static final float CENTER_HEIGHT_MULT = 0.5f; + private static final float ARM_HEIGHT_MULT = 0.75f; + private static final float ABOVE_HEAD_HEIGHT_MULT = 1.3f; + private static final float CHEST_HEIGHT_MULT = 0.5f; + + /** + * Calculate the world position for a given anchor point on an entity. + * + * @param entity The entity to calculate the anchor position for + * @param anchor The anchor point type + * @return Vec3 containing the world coordinates for the anchor point + */ + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor) { + return calculateAnchorPosition(entity, anchor, FRONT_OFFSET); + } + + /** + * Calculate the world position for a given anchor point on an entity. + * For NPCs with active animations, this will read the actual arm positions. + * + * @param entity The entity to calculate the anchor position for + * @param anchor The anchor point type + * @param frontOffset Custom offset distance for FRONT anchor point + * @return Vec3 containing the world coordinates for the anchor point + */ + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor, float frontOffset) { + double x = entity.posX; + double y = entity.posY; + double z = entity.posZ; + float eyeHeight = entity.getEyeHeight(); + + // Get yaw in radians (use body yaw for arm positioning, head yaw for facing) + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + float headYaw = (float) Math.toRadians(entity.rotationYawHead); + + switch (anchor) { + case FRONT: + // In front of entity face (use head yaw) + x -= Math.sin(headYaw) * frontOffset; + y += eyeHeight * FRONT_HEIGHT_MULT; + z += Math.cos(headYaw) * frontOffset; + break; + + case CENTER: + // Entity center + y += eyeHeight * CENTER_HEIGHT_MULT; + break; + + case RIGHT_HAND: + // Try to get animated arm position + Vec3 rightArmPos = getAnimatedArmPosition(entity, true); + if (rightArmPos != null) { + return rightArmPos; + } + // Fallback: calculated position using body yaw + x += calculateHandOffsetX(bodyYaw, true); + y += eyeHeight * ARM_HEIGHT_MULT; + z += calculateHandOffsetZ(bodyYaw, true); + break; + + case LEFT_HAND: + // Try to get animated arm position + Vec3 leftArmPos = getAnimatedArmPosition(entity, false); + if (leftArmPos != null) { + return leftArmPos; + } + // Fallback: calculated position using body yaw + x += calculateHandOffsetX(bodyYaw, false); + y += eyeHeight * ARM_HEIGHT_MULT; + z += calculateHandOffsetZ(bodyYaw, false); + break; + + case ABOVE_HEAD: + // Above the entity + y += eyeHeight * ABOVE_HEAD_HEIGHT_MULT; + break; + + case CHEST: + // Chest level, centered + y += eyeHeight * CHEST_HEIGHT_MULT; + break; + } + + return Vec3.createVectorHelper(x, y, z); + } + + /** + * Get the animated arm position from NPC animation data. + * Uses Minecraft's hand positioning logic from RenderNPCHumanMale. + * Hand offset values: (-0.0625, 0.4375, 0.0625) for right hand after arm postRender. + * + * @param entity The entity (must be EntityNPCInterface for animation support) + * @param rightArm True for right arm, false for left arm + * @return Vec3 world position if animation data available, null otherwise + */ + private static Vec3 getAnimatedArmPosition(EntityLivingBase entity, boolean rightArm) { + if (!(entity instanceof EntityNPCInterface)) { + return null; + } + + EntityNPCInterface npc = (EntityNPCInterface) entity; + AnimationData animData = npc.display.animationData; + + if (animData == null || !animData.isActive() || animData.animation == null) { + return null; + } + + Frame frame = (Frame) animData.animation.currentFrame(); + if (frame == null) { + return null; + } + + EnumAnimationPart armPart = rightArm ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM; + if (!frame.frameParts.containsKey(armPart)) { + return null; + } + + FramePart part = frame.frameParts.get(armPart); + if (part == null) { + return null; + } + + // Ensure interpolation is up to date + part.interpolateOffset(); + part.interpolateAngles(); + + // Get body yaw for world rotation + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + + // === SHOULDER POSITION (model coordinates, in model units) === + // Right arm pivot: X = -5 (right side), Y = -2 (below head), Z = 0 + // Left arm pivot: X = 5 (left side), Y = -2, Z = 0 + // Note: In Minecraft model coords, Y=0 is at head level, shoulder is at Y=-2 + float shoulderModelX = rightArm ? -5.0f : 5.0f; + float shoulderModelY = -2.0f; + float shoulderModelZ = 0.0f; + + // Add animation pivot offsets (if any) + if (part.prevPivots != null) { + shoulderModelX += part.prevPivots[0]; + shoulderModelY += part.prevPivots[1]; + shoulderModelZ += part.prevPivots[2]; + } + + // === HAND OFFSET IN ARM'S LOCAL SPACE (from RenderNPCHumanMale) === + // These values are in blocks, applied AFTER postRender transforms + // Right: (-0.0625, 0.4375, 0.0625), Left: (0.0625, 0.4375, 0.0625) + float handLocalX = rightArm ? -0.0625f : 0.0625f; + float handLocalY = 0.4375f; // 7 model units down the arm + float handLocalZ = 0.0625f; // Slightly forward + + // === ARM ROTATION ANGLES === + float rotX = part.prevRotations != null ? part.prevRotations[0] : 0; + float rotY = part.prevRotations != null ? part.prevRotations[1] : 0; + float rotZ = part.prevRotations != null ? part.prevRotations[2] : 0; + + // === APPLY ARM ROTATIONS TO HAND OFFSET === + // Rotation order matches ModelRenderer.postRender: Z, Y, X + double hx = handLocalX; + double hy = handLocalY; + double hz = handLocalZ; + + // Rotate around Z axis + double cosZ = Math.cos(rotZ); + double sinZ = Math.sin(rotZ); + double tempX = hx * cosZ - hy * sinZ; + double tempY = hx * sinZ + hy * cosZ; + hx = tempX; + hy = tempY; + + // Rotate around Y axis + double cosY = Math.cos(rotY); + double sinY = Math.sin(rotY); + tempX = hx * cosY + hz * sinY; + double tempZ = -hx * sinY + hz * cosY; + hx = tempX; + hz = tempZ; + + // Rotate around X axis + double cosX = Math.cos(rotX); + double sinX = Math.sin(rotX); + tempY = hy * cosX - hz * sinX; + tempZ = hy * sinX + hz * cosX; + hy = tempY; + hz = tempZ; + + // === COMBINE SHOULDER + ROTATED HAND OFFSET === + // Convert shoulder from model units to blocks + double worldOffsetX = shoulderModelX * MODEL_SCALE + hx; + double worldOffsetY = shoulderModelY * MODEL_SCALE + hy; + double worldOffsetZ = shoulderModelZ * MODEL_SCALE + hz; + + // === ROTATE BY BODY YAW === + double cosYaw = Math.cos(bodyYaw); + double sinYaw = Math.sin(bodyYaw); + double rotatedX = worldOffsetX * cosYaw + worldOffsetZ * sinYaw; + double rotatedZ = -worldOffsetX * sinYaw + worldOffsetZ * cosYaw; + + // === FINAL WORLD POSITION === + // Entity posY is at feet, shoulder height is approximately eyeHeight * 0.9 + double shoulderWorldY = entity.posY + entity.getEyeHeight() * 0.9; + + double worldX = entity.posX + rotatedX; + double worldY = shoulderWorldY + worldOffsetY; + double worldZ = entity.posZ + rotatedZ; + + return Vec3.createVectorHelper(worldX, worldY, worldZ); + } + + /** + * Calculate the X offset for hand positions (fallback when no animation). + * + * @param yaw Entity's body yaw in radians + * @param rightHand True for right hand, false for left hand + * @return X offset in world coordinates + */ + private static double calculateHandOffsetX(float yaw, boolean rightHand) { + // Perpendicular direction (90 degrees from look direction) + // Right hand: +90 degrees, Left hand: -90 degrees + float perpYaw = yaw + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2); + + // Lateral offset (perpendicular to look direction) + double lateralX = Math.sin(perpYaw) * ARM_LATERAL_OFFSET; + + // Forward offset (in look direction) + double forwardX = -Math.sin(yaw) * ARM_FORWARD_OFFSET; + + return lateralX + forwardX; + } + + /** + * Calculate the Z offset for hand positions (fallback when no animation). + * + * @param yaw Entity's body yaw in radians + * @param rightHand True for right hand, false for left hand + * @return Z offset in world coordinates + */ + private static double calculateHandOffsetZ(float yaw, boolean rightHand) { + // Perpendicular direction (90 degrees from look direction) + float perpYaw = yaw + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2); + + // Lateral offset (perpendicular to look direction) + double lateralZ = -Math.cos(perpYaw) * ARM_LATERAL_OFFSET; + + // Forward offset (in look direction) + double forwardZ = Math.cos(yaw) * ARM_FORWARD_OFFSET; + + return lateralZ + forwardZ; + } + + /** + * Get the height multiplier for a given anchor point. + * Useful for external code that needs to know the Y position offset. + * + * @param anchor The anchor point type + * @return Height multiplier relative to eye height + */ + public static float getHeightMultiplier(AnchorPoint anchor) { + switch (anchor) { + case FRONT: + return FRONT_HEIGHT_MULT; + case CENTER: + return CENTER_HEIGHT_MULT; + case RIGHT_HAND: + case LEFT_HAND: + return ARM_HEIGHT_MULT; + case ABOVE_HEAD: + return ABOVE_HEAD_HEIGHT_MULT; + case CHEST: + return CHEST_HEIGHT_MULT; + default: + return FRONT_HEIGHT_MULT; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index 2a3439af1..33c5372fd 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -92,8 +93,11 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { y += 24; - // Row 7: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); + // Row 7: Max Lifetime + Effects button + addLabel(new GuiNpcLabel(112, "ability.lifetime", labelX, y + 5)); + addTextField(createIntField(112, fieldX, y, 50, disc.getMaxLifetime())); + + addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); } @Override @@ -104,7 +108,13 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 260; - // Row 1: Inner Color + Outer Color + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + addButton(new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPoint().getId())); + + y += 24; + + // Row 2: Inner Color + Outer Color addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", disc.getInnerColor() & 0xFFFFFF); GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); @@ -197,6 +207,9 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { disc.setLightningEffect(value == 1); initGui(); break; + case 210: + disc.setAnchorPoint(AnchorPoint.fromId(value)); + break; } } @@ -253,6 +266,9 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { case 111: disc.setMaxDistance(parseFloat(field, disc.getMaxDistance())); break; + case 112: + disc.setMaxLifetime(field.getInteger()); + break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index 1c7179deb..626fac6b9 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.type.AbilityEnergyBeam; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -105,7 +106,13 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 260; - // Row 1: Inner Color + Outer Color + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + addButton(new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPoint().getId())); + + y += 24; + + // Row 2: Inner Color + Outer Color addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", beam.getInnerColor() & 0xFFFFFF); GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); @@ -195,6 +202,9 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { beam.setLightningEffect(value == 1); initGui(); break; + case 210: + beam.setAnchorPoint(AnchorPoint.fromId(value)); + break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 6c5c598ba..c43569c46 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -95,7 +96,13 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 260; - // Row 1: Inner Color + Rotation Speed + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + addButton(new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPoint().getId())); + + y += 24; + + // Row 2: Inner Color + Rotation Speed addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", orb.getInnerColor() & 0xFFFFFF); GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); @@ -185,6 +192,9 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { orb.setLightningEffect(value == 1); initGui(); break; + case 210: + orb.setAnchorPoint(AnchorPoint.fromId(value)); + break; } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index d5dc945ce..01e3ae541 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1683,6 +1683,7 @@ ability.outerWidth=Outer Width ability.lightning=Enable Lightning ability.lightningDensity=Density ability.lightningRadius=Radius +ability.anchorPoint=Anchor Point ability.noVisualSettings=No visual settings for this ability type # Laser Shot Settings