Ability Anchor Points + Orb / Disc Summoning

This commit is contained in:
Kam committed 2026-01-26 23:55:07 -05:00
1 parent 0484596a91
commit 0f44a3c6c6
12 files changed
+984 -95

No files matched your search

@@ -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;
}
}
@@ -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; }
}
@@ -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; }
}
@@ -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;
}
}
@@ -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);
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}