package kamkeel.npcs.entity; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData; import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData; import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.EventHooks; import noppes.npcs.NpcDamageSource; import noppes.npcs.api.entity.IEntity; import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * Base class for all ability projectiles (Orb, Disc, Beam, Laser, Slicer). * Provides common functionality for combat, effects, homing, and interpolation. * Extends EntityEnergyAbility for shared visual/owner/charging state. *
* Design inspired by LouisXIV's energy attack system.
*/
public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// ==================== ACTIVE PROJECTILE TRACKING ====================
private static final Map
* Note: Owner death checks are handled by onUpdate() before this is called.
*/
protected void updateCharging() {
chargeTick++;
float progress = getChargeProgress();
this.size = targetSize * progress;
Entity owner = getOwnerEntity();
if (owner instanceof EntityLivingBase) {
Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData);
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
}
}
// ==================== HOMING ====================
/**
* Update homing toward target. Uses position-based homing with distance-scaled strength.
* Override in subclasses for type-specific homing (e.g. Beam uses head offset).
*/
protected void updateHoming() {
if (!isHoming()) return;
Entity target = getTargetEntity();
if (target == null || !target.isEntityAlive()) return;
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
double dz = target.posZ - posZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist <= getHomingRange() && dist > 0) {
float effectiveStrength = getHomingStrength();
if (dist < getHomingRange() * 0.3) {
effectiveStrength = Math.min(1.0f, getHomingStrength() * 2.5f);
} else if (dist < getHomingRange() * 0.6) {
effectiveStrength = Math.min(0.8f, getHomingStrength() * 1.5f);
}
double desiredVX = (dx / dist) * getSpeed();
double desiredVY = (dy / dist) * getSpeed();
double desiredVZ = (dz / dist) * getSpeed();
motionX += (desiredVX - motionX) * effectiveStrength;
motionY += (desiredVY - motionY) * effectiveStrength;
motionZ += (desiredVZ - motionZ) * effectiveStrength;
double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ);
if (vLen > 0) {
motionX = (motionX / vLen) * getSpeed();
motionY = (motionY / vLen) * getSpeed();
motionZ = (motionZ / vLen) * getSpeed();
}
}
}
// ==================== VELOCITY HELPERS ====================
/**
* Calculate initial velocity toward target or forward based on owner facing.
* Call from subclass constructors after initProjectile().
*/
protected void calculateInitialVelocity(EntityLivingBase owner, EntityLivingBase target,
double x, double y, double z) {
if (target != null) {
double dx = target.posX - x;
double dy = (target.posY + target.getEyeHeight()) - y;
double dz = target.posZ - z;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
this.motionX = (dx / len) * getSpeed();
this.motionY = (dy / len) * getSpeed();
this.motionZ = (dz / len) * getSpeed();
}
} else {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
this.motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
this.motionY = -Math.sin(pitch) * getSpeed();
this.motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
}
// ==================== CHARGING NBT ====================
/**
* Read common charging state from NBT. Call from subclass readProjectileNBT().
*/
protected void readChargingNBT(NBTTagCompound nbt) {
boolean isChargingVal = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
this.charging = isChargingVal;
this.dataWatcher.updateObject(DW_CHARGING, (byte) (isChargingVal ? 1 : 0));
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size;
}
/**
* Write common charging state to NBT. Call from subclass writeProjectileNBT().
*/
protected void writeChargingNBT(NBTTagCompound nbt) {
nbt.setBoolean("Charging", isCharging());
nbt.setInteger("ChargeDuration", chargeDuration);
nbt.setInteger("ChargeTick", chargeTick);
nbt.setFloat("TargetSize", targetSize);
}
// ==================== ENTITY HELPERS ====================
public IEntity getOwner() {
if (previewMode) return null;
if (getOwnerEntity() == null) return null;
return NpcAPI.Instance().getIEntity(getOwnerEntity());
}
public int getTargetEntityId() {
return targetEntityId;
}
public void setTargetEntityId(int id) {
this.targetEntityId = id;
}
public Entity getTargetEntity() {
if (targetEntityId == -1) return null;
return worldObj.getEntityByID(targetEntityId);
}
public IEntity getTarget() {
if (getTargetEntity() == null) return null;
return NpcAPI.Instance().getIEntity(getTargetEntity());
}
/**
* Check if an entity should be ignored for collision.
* Ignores: the owner, same-faction NPCs, passive NPCs, NPCs friendly to player owner,
* party members (if friendly fire is off), and other projectiles from the same caster.
*/
protected boolean shouldIgnoreEntity(Entity entity) {
Entity owner = getOwnerEntity();
if (entity == owner) return true;
// Ignore other projectiles from the same caster (e.g. multi-projectile abilities)
if (entity instanceof EntityEnergyProjectile) {
EntityEnergyProjectile other = (EntityEnergyProjectile) entity;
if (other.ownerEntityId == this.ownerEntityId) return true;
}
// NPC target checks
if (entity instanceof EntityNPCInterface) {
EntityNPCInterface targetNpc = (EntityNPCInterface) entity;
// Ignore passive NPCs
if (targetNpc.faction.isPassive) return true;
// NPC owner: ignore same-faction NPCs
if (owner instanceof EntityNPCInterface) {
EntityNPCInterface ownerNpc = (EntityNPCInterface) owner;
if (ownerNpc.faction.id == targetNpc.faction.id) return true;
}
// Player owner: ignore NPCs whose faction is friendly to the player
if (owner instanceof EntityPlayer) {
if (targetNpc.faction.isFriendlyToPlayer((EntityPlayer) owner)) return true;
}
}
// Party friendly fire check: player owner hitting another player
if (owner instanceof EntityPlayer && entity instanceof EntityPlayer) {
EntityPlayer ownerPlayer = (EntityPlayer) owner;
EntityPlayer targetPlayer = (EntityPlayer) entity;
PlayerData ownerData = PlayerData.get(ownerPlayer);
PlayerData targetData = PlayerData.get(targetPlayer);
if (ownerData.partyUUID != null && ownerData.partyUUID.equals(targetData.partyUUID)) {
Party party = PartyController.Instance().getParty(ownerData.partyUUID);
if (party != null && !party.friendlyFire()) return true;
}
}
return false;
}
// ==================== VISUAL GETTERS & SETTERS ====================
public float getSize() {
return size;
}
public void setProjectileSize(float size) {
this.size = size;
this.renderCurrentSize = size;
this.prevRenderSize = size;
}
/**
* Set the start position for distance calculations.
* Used by factory methods when creating projectiles without the full constructor.
*/
public void setStartPosition(double x, double y, double z) {
this.startX = x;
this.startY = y;
this.startZ = z;
}
// ==================== ROTATION GETTERS ====================
public float getInterpolatedRotationX(float partialTicks) {
return this.prevRotationValX + (this.rotationValX - this.prevRotationValX) * partialTicks;
}
public float getInterpolatedRotationY(float partialTicks) {
return this.prevRotationValY + (this.rotationValY - this.prevRotationValY) * partialTicks;
}
public float getInterpolatedRotationZ(float partialTicks) {
return this.prevRotationValZ + (this.rotationValZ - this.prevRotationValZ) * partialTicks;
}
public float getInterpolatedSize(float partialTicks) {
return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks;
}
// ==================== LIFESPAN GETTERS & SETTERS ====================
public float getMaxDistance() {
return lifespanData.getMaxDistance();
}
public void setMaxDistance(float distance) {
lifespanData.setMaxDistance(distance);
}
public int getMaxLifetime() {
return lifespanData.getMaxLifetime();
}
public void setMaxLifetime(int ticks) {
lifespanData.setMaxLifetime(ticks);
}
// ==================== COMBAT GETTERS & SETTERS ====================
public float getDamage() {
return combatData.getDamage();
}
public void setCombatDamage(float damage) {
combatData.setDamage(damage);
}
public float getKnockback() {
return combatData.knockback;
}
public void setCombatKnockback(float knockback) {
combatData.setKnockback(knockback);
}
public float getKnockbackUp() {
return combatData.knockbackUp;
}
public void setCombatKnockbackUp(float knockbackUp) {
combatData.setKnockbackUp(knockbackUp);
}
public boolean isExplosive() {
return combatData.isExplosive();
}
public void setExplosive(boolean explosive) {
combatData.setExplosive(explosive);
}
public float getExplosionRadius() {
return combatData.explosionRadius;
}
public void setExplosionRadius(float radius) {
combatData.setExplosionRadius(radius);
}
public float getExplosionDamageFalloff() {
return combatData.explosionDamageFalloff;
}
public void setExplosionDamageFalloff(float falloff) {
combatData.setExplosionDamageFalloff(falloff);
}
// ==================== HOMING GETTERS & SETTERS ====================
public boolean isHoming() {
return homingData.isHoming();
}
public void setHomingEnabled(boolean homing) {
homingData.setHoming(homing);
}
public float getHomingStrength() {
return homingData.getHomingStrength();
}
public void setHomingStrength(float strength) {
homingData.setHomingStrength(strength);
}
public float getHomingRange() {
return homingData.getHomingRange();
}
public void setHomingRange(float range) {
homingData.setHomingRange(range);
}
// ==================== SPEED & ANCHOR GETTERS & SETTERS ====================
public float getSpeed() {
return homingData.getSpeed();
}
public void setSpeed(float speed) {
homingData.setSpeed(speed);
}
public AnchorPoint getAnchorPoint() {
return anchorData.getAnchorPoint();
}
public int getAnchor() {
return anchorData.getAnchor();
}
public float getAnchorOffsetX() {
return anchorData.getAnchorOffsetX();
}
public float getAnchorOffsetY() {
return anchorData.getAnchorOffsetY();
}
public float getAnchorOffsetZ() {
return anchorData.getAnchorOffsetZ();
}
// ==================== MOVEMENT GETTERS ====================
public double getStartX() {
return startX;
}
public double getStartY() {
return startY;
}
public double getStartZ() {
return startZ;
}
// ==================== STATE GETTERS ====================
public boolean getHasHit() {
return hasHit;
}
// ==================== NBT ====================
@Override
protected void readEntityFromNBT(NBTTagCompound nbt) {
readBaseNBT(nbt);
readProjectileNBT(nbt);
}
@Override
protected void writeEntityToNBT(NBTTagCompound nbt) {
writeBaseNBT(nbt);
writeProjectileNBT(nbt);
}
/**
* Read base projectile properties from NBT.
*/
protected void readBaseNBT(NBTTagCompound nbt) {
readEnergyBaseNBT(nbt);
this.size = sanitize(nbt.getFloat("Size"), 1.0f, MAX_ENTITY_SIZE);
// Read effects list
this.effects.clear();
if (nbt.hasKey("Effects")) {
NBTTagList effectsList = nbt.getTagList("Effects", 10); // 10 = compound tag
for (int i = 0; i < effectsList.tagCount(); i++) {
effects.add(AbilityPotionEffect.fromNBT(effectsList.getCompoundTagAt(i)));
}
}
this.deathWorldTime = nbt.hasKey("DeathWorldTime") ? nbt.getLong("DeathWorldTime") : -1;
this.startX = nbt.getDouble("StartX");
this.startY = nbt.getDouble("StartY");
this.startZ = nbt.getDouble("StartZ");
this.targetEntityId = nbt.getInteger("TargetId");
this.motionX = nbt.getDouble("MotionX");
this.motionY = nbt.getDouble("MotionY");
this.motionZ = nbt.getDouble("MotionZ");
// Always initialize render size from NBT size so entities
// loading in from outside render distance render correctly
this.renderCurrentSize = this.size;
this.prevRenderSize = this.size;
anchorData.readNBT(nbt);
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
trajectoryData.readNBT(nbt);
}
/**
* Write base projectile properties to NBT.
*/
protected void writeBaseNBT(NBTTagCompound nbt) {
writeEnergyBaseNBT(nbt);
nbt.setFloat("Size", size);
// Write effects list (always write, even if empty, so readBaseNBT finds the key)
NBTTagList effectsList = new NBTTagList();
for (AbilityPotionEffect effect : effects) {
effectsList.appendTag(effect.writeNBT());
}
nbt.setTag("Effects", effectsList);
nbt.setLong("DeathWorldTime", deathWorldTime);
nbt.setDouble("StartX", startX);
nbt.setDouble("StartY", startY);
nbt.setDouble("StartZ", startZ);
nbt.setInteger("TargetId", targetEntityId);
nbt.setDouble("MotionX", motionX);
nbt.setDouble("MotionY", motionY);
nbt.setDouble("MotionZ", motionZ);
anchorData.writeNBT(nbt);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
trajectoryData.writeNBT(nbt);
}
/**
* Subclass-specific NBT reading.
*/
protected abstract void readProjectileNBT(NBTTagCompound nbt);
/**
* Subclass-specific NBT writing.
*/
protected abstract void writeProjectileNBT(NBTTagCompound nbt);
// ==================== COLLISION SETTINGS ====================
@Override
public boolean canBeCollidedWith() {
return false;
}
}