2026-01-25 20:39:01 -05:00
|
|
|
package kamkeel.npcs.entity;
|
|
|
|
|
|
|
|
|
|
import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
|
|
|
|
|
import cpw.mods.fml.relauncher.Side;
|
|
|
|
|
import cpw.mods.fml.relauncher.SideOnly;
|
|
|
|
|
import io.netty.buffer.ByteBuf;
|
2026-02-04 17:45:02 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.Ability;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
2026-01-29 14:19:57 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
|
2026-02-03 17:20:36 -03:00
|
|
|
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
2026-02-14 21:06:12 -05:00
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
import kamkeel.npcs.controllers.data.ability.data.*;
|
2026-02-14 15:21:56 -05:00
|
|
|
import kamkeel.npcs.util.AnchorPointHelper;
|
2026-01-25 20:39:01 -05:00
|
|
|
import net.minecraft.entity.Entity;
|
|
|
|
|
import net.minecraft.entity.EntityLivingBase;
|
2026-01-30 05:01:54 -05:00
|
|
|
import net.minecraft.entity.player.EntityPlayer;
|
2026-01-25 20:39:01 -05:00
|
|
|
import net.minecraft.nbt.NBTTagCompound;
|
2026-01-29 14:19:57 -05:00
|
|
|
import net.minecraft.nbt.NBTTagList;
|
2026-01-25 20:39:01 -05:00
|
|
|
import net.minecraft.util.AxisAlignedBB;
|
2026-01-30 05:01:54 -05:00
|
|
|
import net.minecraft.util.DamageSource;
|
2026-02-13 15:01:28 -05:00
|
|
|
import net.minecraft.util.Vec3;
|
2026-01-25 20:39:01 -05:00
|
|
|
import net.minecraft.world.World;
|
|
|
|
|
import noppes.npcs.NpcDamageSource;
|
2026-02-03 17:20:36 -03:00
|
|
|
import noppes.npcs.api.entity.IEnergyProjectile;
|
|
|
|
|
import noppes.npcs.api.entity.IEntity;
|
2026-01-25 20:39:01 -05:00
|
|
|
import noppes.npcs.entity.EntityNPCInterface;
|
2026-02-03 17:20:36 -03:00
|
|
|
import noppes.npcs.scripted.NpcAPI;
|
2026-01-25 20:39:01 -05:00
|
|
|
|
2026-01-29 14:19:57 -05:00
|
|
|
import java.util.ArrayList;
|
2026-01-25 20:39:01 -05:00
|
|
|
import java.util.List;
|
2026-02-03 21:26:27 -03:00
|
|
|
import java.util.Map;
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Base class for all ability projectiles (Orb, Disc, Beam, Laser).
|
|
|
|
|
* Provides common functionality for visuals, combat, effects, and interpolation.
|
|
|
|
|
*
|
|
|
|
|
* Design inspired by LouisXIV's energy attack system.
|
|
|
|
|
*/
|
2026-02-03 17:20:36 -03:00
|
|
|
public abstract class EntityAbilityProjectile extends Entity implements IEnergyProjectile, IEntityAdditionalSpawnData {
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
// ==================== VISUAL PROPERTIES ====================
|
|
|
|
|
protected float size = 1.0f;
|
|
|
|
|
|
|
|
|
|
// ==================== COMBAT PROPERTIES ====================
|
2026-02-03 17:20:36 -03:00
|
|
|
protected EnergyCombatData combatData = new EnergyCombatData();
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
// ==================== EFFECT PROPERTIES ====================
|
2026-01-29 14:19:57 -05:00
|
|
|
protected List<AbilityEffect> effects = new ArrayList<>();
|
2026-01-25 20:39:01 -05:00
|
|
|
|
2026-01-26 18:38:43 -05:00
|
|
|
// ==================== LIGHTNING EFFECT PROPERTIES ====================
|
2026-02-03 17:20:36 -03:00
|
|
|
protected EnergyLightningData lightningData = new EnergyLightningData();
|
2026-01-26 18:38:43 -05:00
|
|
|
|
|
|
|
|
// Client-side lightning state (not saved to NBT)
|
|
|
|
|
@cpw.mods.fml.relauncher.SideOnly(cpw.mods.fml.relauncher.Side.CLIENT)
|
|
|
|
|
public transient Object lightningState; // Actually AttachedLightningRenderer.LightningState
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
// ==================== DISPLAY PROPERTIES ====================
|
|
|
|
|
protected EnergyDisplayData displayData = new EnergyDisplayData();
|
|
|
|
|
|
|
|
|
|
// ==================== ANCHOR PROPERTIES ====================
|
|
|
|
|
protected EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
|
|
|
|
|
|
|
|
|
|
// ==================== LIFESPAN PROPERTIES ====================
|
|
|
|
|
protected EnergyLifespanData lifespanData = new EnergyLifespanData();
|
2026-01-25 20:39:01 -05:00
|
|
|
protected long deathWorldTime = -1; // World time when entity should die (-1 = not set)
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
// ==================== HOMING PROPERTIES ====================
|
|
|
|
|
protected EnergyHomingData homingData = new EnergyHomingData();
|
|
|
|
|
|
2026-02-03 18:00:52 -03:00
|
|
|
// ==================== TRAJECTORY PROPERTIES ====================
|
|
|
|
|
protected EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
|
2026-02-03 21:26:27 -03:00
|
|
|
protected Map<Integer, Integer> pathDelays;
|
2026-02-03 18:00:52 -03:00
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
// ==================== TRACKING ====================
|
|
|
|
|
protected double startX, startY, startZ;
|
|
|
|
|
protected int ownerEntityId = -1;
|
|
|
|
|
protected int targetEntityId = -1;
|
|
|
|
|
|
|
|
|
|
// ==================== STATE ====================
|
|
|
|
|
protected boolean hasHit = false;
|
2026-01-28 20:43:32 -05:00
|
|
|
protected boolean previewMode = false; // Client-side preview mode (no damage/effects)
|
2026-01-30 05:01:54 -05:00
|
|
|
protected EntityLivingBase previewOwner = null; // Direct reference for GUI preview (no world lookup)
|
2026-01-25 20:39:01 -05:00
|
|
|
|
2026-02-04 17:45:02 -05:00
|
|
|
/** The ability that spawned this projectile. Transient, not saved to NBT. */
|
|
|
|
|
protected transient Ability sourceAbility = null;
|
|
|
|
|
|
2026-02-14 15:21:56 -05:00
|
|
|
// ==================== CHARGING STATE ====================
|
|
|
|
|
protected boolean charging = false;
|
|
|
|
|
protected int chargeDuration = 40;
|
|
|
|
|
protected int chargeTick = 0;
|
|
|
|
|
protected float targetSize = 1.0f;
|
|
|
|
|
protected static final int DW_CHARGING = 20;
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
// ==================== ROTATION INTERPOLATION ====================
|
|
|
|
|
public float prevRotationValX, prevRotationValY, prevRotationValZ;
|
|
|
|
|
public float rotationValX, rotationValY, rotationValZ;
|
|
|
|
|
|
|
|
|
|
// ==================== SIZE INTERPOLATION ====================
|
|
|
|
|
protected float renderCurrentSize;
|
|
|
|
|
protected float prevRenderSize;
|
|
|
|
|
|
|
|
|
|
// ==================== POSITION INTERPOLATION ====================
|
|
|
|
|
protected double interpTargetX, interpTargetY, interpTargetZ;
|
|
|
|
|
protected double interpTargetMotionX, interpTargetMotionY, interpTargetMotionZ;
|
|
|
|
|
protected int interpSteps;
|
|
|
|
|
|
|
|
|
|
public EntityAbilityProjectile(World world) {
|
|
|
|
|
super(world);
|
|
|
|
|
this.setSize(0.5f, 0.5f);
|
|
|
|
|
this.noClip = false;
|
|
|
|
|
this.isImmuneToFire = true;
|
|
|
|
|
this.ignoreFrustumCheck = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-29 14:19:57 -05:00
|
|
|
* Initialize common properties using data classes. Call from subclass constructors.
|
2026-01-25 20:39:01 -05:00
|
|
|
*/
|
2026-02-14 15:21:56 -05:00
|
|
|
protected void initProjectile(EntityLivingBase owner, EntityLivingBase target,
|
2026-02-03 15:45:45 -03:00
|
|
|
double x, double y, double z, float size,
|
2026-02-03 17:20:36 -03:00
|
|
|
EnergyDisplayData display, EnergyCombatData combat,
|
2026-02-03 21:26:27 -03:00
|
|
|
EnergyLightningData lightning, EnergyLifespanData lifespan,
|
|
|
|
|
EnergyTrajectoryData trajectory) {
|
2026-01-25 20:39:01 -05:00
|
|
|
this.setPosition(x, y, z);
|
|
|
|
|
this.startX = x;
|
|
|
|
|
this.startY = y;
|
|
|
|
|
this.startZ = z;
|
|
|
|
|
|
|
|
|
|
this.ownerEntityId = owner.getEntityId();
|
|
|
|
|
this.targetEntityId = target != null ? target.getEntityId() : -1;
|
|
|
|
|
|
|
|
|
|
// Visual
|
|
|
|
|
this.size = size;
|
2026-02-03 17:20:36 -03:00
|
|
|
|
|
|
|
|
this.displayData = display;
|
|
|
|
|
this.combatData = combat;
|
|
|
|
|
this.lifespanData = lifespan; // deathWorldTime will be set on first tick when world is available
|
|
|
|
|
this.lightningData = lightning;
|
2026-02-03 21:26:27 -03:00
|
|
|
this.trajectoryData = trajectory;
|
2026-01-26 18:38:43 -05:00
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
// Initialize render size
|
|
|
|
|
this.renderCurrentSize = size;
|
|
|
|
|
this.prevRenderSize = size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void entityInit() {
|
2026-02-14 15:21:56 -05:00
|
|
|
this.dataWatcher.addObject(DW_CHARGING, (byte) 0);
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
@SideOnly(Side.CLIENT)
|
|
|
|
|
public boolean isInRangeToRenderDist(double distance) {
|
|
|
|
|
double d1 = this.boundingBox.getAverageEdgeLength() * 4.0D;
|
|
|
|
|
d1 *= 128.0D;
|
|
|
|
|
return distance < d1 * d1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean shouldRenderInPass(int pass) {
|
|
|
|
|
return pass == 1; // Translucent pass
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onUpdate() {
|
|
|
|
|
// Store previous values for interpolation
|
|
|
|
|
this.prevPosX = this.posX;
|
|
|
|
|
this.prevPosY = this.posY;
|
|
|
|
|
this.prevPosZ = this.posZ;
|
|
|
|
|
this.prevRotationValX = this.rotationValX;
|
|
|
|
|
this.prevRotationValY = this.rotationValY;
|
|
|
|
|
this.prevRotationValZ = this.rotationValZ;
|
|
|
|
|
this.prevRenderSize = this.renderCurrentSize;
|
|
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
// Skip super.onUpdate() in preview mode to avoid world checks
|
|
|
|
|
if (!previewMode) {
|
|
|
|
|
super.onUpdate();
|
|
|
|
|
}
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
// Update rotation
|
|
|
|
|
updateRotation();
|
|
|
|
|
|
|
|
|
|
// Lerp render size toward actual size
|
|
|
|
|
this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f;
|
|
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
// Skip lifetime/distance checks in preview mode
|
|
|
|
|
if (!previewMode) {
|
2026-02-03 04:27:54 -05:00
|
|
|
// Failsafe: if owner entity is gone, dead, or NPC was killed/reset, self-destruct
|
|
|
|
|
if (ownerEntityId >= 0 && ticksExisted > 5) {
|
|
|
|
|
Entity owner = worldObj.getEntityByID(ownerEntityId);
|
|
|
|
|
if (owner == null || owner.isDead) {
|
|
|
|
|
this.setDead();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) {
|
|
|
|
|
this.setDead();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
// Set death time on first tick if not already set (handles chunk load/unload)
|
|
|
|
|
if (deathWorldTime < 0 && worldObj != null) {
|
2026-02-03 17:20:36 -03:00
|
|
|
deathWorldTime = worldObj.getTotalWorldTime() + getMaxLifetime();
|
2026-01-28 20:43:32 -05:00
|
|
|
}
|
2026-01-25 20:39:01 -05:00
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
// Check lifespan using world time (survives chunk unload/reload)
|
|
|
|
|
if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) {
|
|
|
|
|
this.setDead();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-25 20:39:01 -05:00
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
// Check max distance (subclass can override if needed)
|
|
|
|
|
if (checkMaxDistance()) {
|
|
|
|
|
this.setDead();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-25 20:39:01 -05:00
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
if (hasHit) {
|
|
|
|
|
this.setDead();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subclass-specific update
|
|
|
|
|
updateProjectile();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update rotation values. Override for custom rotation behavior.
|
|
|
|
|
*/
|
|
|
|
|
protected void updateRotation() {
|
2026-02-03 17:20:36 -03:00
|
|
|
this.rotationValX += getRotationSpeed() * 0.7f;
|
|
|
|
|
this.rotationValY += getRotationSpeed();
|
|
|
|
|
this.rotationValZ += getRotationSpeed() * 0.5f;
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
if (this.rotationValX > 360.0f) this.rotationValX -= 360.0f;
|
|
|
|
|
if (this.rotationValY > 360.0f) this.rotationValY -= 360.0f;
|
|
|
|
|
if (this.rotationValZ > 360.0f) this.rotationValZ -= 360.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if projectile has exceeded max distance.
|
|
|
|
|
* @return true if should die
|
|
|
|
|
*/
|
|
|
|
|
protected boolean checkMaxDistance() {
|
|
|
|
|
double distTraveled = Math.sqrt(
|
|
|
|
|
(posX - startX) * (posX - startX) +
|
|
|
|
|
(posY - startY) * (posY - startY) +
|
|
|
|
|
(posZ - startZ) * (posZ - startZ)
|
|
|
|
|
);
|
2026-02-03 17:20:36 -03:00
|
|
|
return distTraveled >= getMaxDistance();
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Subclass-specific update logic. Called every tick.
|
|
|
|
|
*/
|
|
|
|
|
protected abstract void updateProjectile();
|
|
|
|
|
|
|
|
|
|
// ==================== POSITION INTERPOLATION ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setPositionAndRotation2(double x, double y, double z, float yaw, float pitch, int posRotationIncrements) {
|
|
|
|
|
this.interpTargetX = x;
|
|
|
|
|
this.interpTargetY = y;
|
|
|
|
|
this.interpTargetZ = z;
|
|
|
|
|
this.interpSteps = posRotationIncrements;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setVelocity(double motionX, double motionY, double motionZ) {
|
|
|
|
|
this.interpTargetMotionX = motionX;
|
|
|
|
|
this.interpTargetMotionY = motionY;
|
|
|
|
|
this.interpTargetMotionZ = motionZ;
|
|
|
|
|
this.motionX = motionX;
|
|
|
|
|
this.motionY = motionY;
|
|
|
|
|
this.motionZ = motionZ;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle client-side position interpolation.
|
|
|
|
|
*/
|
|
|
|
|
protected void handleClientInterpolation() {
|
|
|
|
|
if (this.interpSteps > 0) {
|
|
|
|
|
double newX = this.posX + (this.interpTargetX - this.posX) / this.interpSteps;
|
|
|
|
|
double newY = this.posY + (this.interpTargetY - this.posY) / this.interpSteps;
|
|
|
|
|
double newZ = this.posZ + (this.interpTargetZ - this.posZ) / this.interpSteps;
|
|
|
|
|
|
|
|
|
|
this.motionX = this.motionX + (this.interpTargetMotionX - this.motionX) / this.interpSteps;
|
|
|
|
|
this.motionY = this.motionY + (this.interpTargetMotionY - this.motionY) / this.interpSteps;
|
|
|
|
|
this.motionZ = this.motionZ + (this.interpTargetMotionZ - this.motionZ) / this.interpSteps;
|
|
|
|
|
|
|
|
|
|
this.setPosition(newX, newY, newZ);
|
|
|
|
|
this.interpSteps--;
|
|
|
|
|
} else {
|
|
|
|
|
this.posX += this.motionX;
|
|
|
|
|
this.posY += this.motionY;
|
|
|
|
|
this.posZ += this.motionZ;
|
|
|
|
|
this.setPosition(this.posX, this.posY, this.posZ);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
// ==================== PREVIEW MODE ====================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set preview mode for GUI display.
|
|
|
|
|
* In preview mode, no damage or world effects are applied.
|
|
|
|
|
*/
|
|
|
|
|
public void setPreviewMode(boolean preview) {
|
|
|
|
|
this.previewMode = preview;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if entity is in preview mode.
|
|
|
|
|
*/
|
|
|
|
|
public boolean isPreviewMode() {
|
|
|
|
|
return previewMode;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
// ==================== DAMAGE & EFFECTS ====================
|
|
|
|
|
|
|
|
|
|
protected void applyDamage(EntityLivingBase target) {
|
2026-01-28 20:43:32 -05:00
|
|
|
if (previewMode) return; // Skip damage in preview mode
|
2026-02-03 17:20:36 -03:00
|
|
|
applyDamage(target, this.getDamage(), this.getKnockback());
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void applyDamage(EntityLivingBase target, float dmg, float kb) {
|
2026-01-28 20:43:32 -05:00
|
|
|
if (previewMode) return; // Skip damage in preview mode
|
2026-02-03 17:20:36 -03:00
|
|
|
Entity owner = getOwnerEntity();
|
2026-01-25 20:39:01 -05:00
|
|
|
|
2026-02-14 21:06:12 -05:00
|
|
|
// Check for ability extenders (e.g., DBC Addon damage routing)
|
2026-02-04 17:45:02 -05:00
|
|
|
boolean handled = false;
|
|
|
|
|
if (sourceAbility != null && owner instanceof EntityLivingBase) {
|
2026-02-14 21:06:12 -05:00
|
|
|
double dx = target.posX - posX;
|
|
|
|
|
double dz = target.posZ - posZ;
|
|
|
|
|
float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f;
|
|
|
|
|
handled = AbilityController.Instance.fireOnAbilityDamage(
|
|
|
|
|
sourceAbility, (EntityLivingBase) owner, target,
|
|
|
|
|
dmg, kb, kbUp, dx, dz);
|
2026-02-04 17:45:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!handled) {
|
|
|
|
|
// Default damage path
|
|
|
|
|
if (owner instanceof EntityNPCInterface) {
|
|
|
|
|
target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg);
|
|
|
|
|
} else if (owner instanceof EntityPlayer) {
|
|
|
|
|
target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg);
|
|
|
|
|
} else if (owner instanceof EntityLivingBase) {
|
|
|
|
|
target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg);
|
|
|
|
|
} else {
|
|
|
|
|
target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg);
|
|
|
|
|
}
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (kb > 0) {
|
|
|
|
|
double dx = target.posX - posX;
|
|
|
|
|
double dz = target.posZ - posZ;
|
|
|
|
|
double len = Math.sqrt(dx * dx + dz * dz);
|
|
|
|
|
if (len > 0) {
|
|
|
|
|
double kbX = (dx / len) * kb * 0.5;
|
|
|
|
|
double kbZ = (dz / len) * kb * 0.5;
|
2026-02-03 17:20:36 -03:00
|
|
|
double kbY = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1;
|
2026-01-25 20:39:01 -05:00
|
|
|
target.addVelocity(kbX, kbY, kbZ);
|
|
|
|
|
target.velocityChanged = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applyEffects(target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void applyEffects(EntityLivingBase target) {
|
2026-01-29 14:19:57 -05:00
|
|
|
for (AbilityEffect effect : effects) {
|
|
|
|
|
effect.apply(target);
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-29 14:19:57 -05:00
|
|
|
/**
|
|
|
|
|
* Set the effects list from the ability's configured effects.
|
|
|
|
|
*/
|
|
|
|
|
public void setEffects(List<AbilityEffect> effects) {
|
|
|
|
|
this.effects = effects != null ? effects : new ArrayList<>();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 17:45:02 -05:00
|
|
|
/**
|
|
|
|
|
* Set the source ability that spawned this projectile.
|
|
|
|
|
* Used by external damage handlers to access ability-specific data.
|
|
|
|
|
*/
|
|
|
|
|
public void setSourceAbility(Ability ability) {
|
|
|
|
|
this.sourceAbility = ability;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the source ability that spawned this projectile.
|
|
|
|
|
*/
|
|
|
|
|
public Ability getSourceAbility() {
|
|
|
|
|
return sourceAbility;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
protected void doExplosion() {
|
2026-01-28 20:43:32 -05:00
|
|
|
if (previewMode) return; // Skip explosion in preview mode
|
2026-01-25 20:39:01 -05:00
|
|
|
worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f);
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
Entity owner = getOwnerEntity();
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox(
|
2026-02-03 17:20:36 -03:00
|
|
|
posX - getExplosionRadius(), posY - getExplosionRadius(), posZ - getExplosionRadius(),
|
|
|
|
|
posX + getExplosionRadius(), posY + getExplosionRadius(), posZ + getExplosionRadius()
|
2026-01-25 20:39:01 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
|
|
List<EntityLivingBase> targets = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox);
|
|
|
|
|
|
|
|
|
|
for (EntityLivingBase target : targets) {
|
|
|
|
|
if (target == owner) continue;
|
|
|
|
|
|
|
|
|
|
double dist = Math.sqrt(
|
|
|
|
|
Math.pow(target.posX - posX, 2) +
|
|
|
|
|
Math.pow(target.posY - posY, 2) +
|
|
|
|
|
Math.pow(target.posZ - posZ, 2)
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
if (dist <= getExplosionRadius()) {
|
|
|
|
|
float falloff = 1.0f - (float) (dist / getExplosionRadius()) * getExplosionDamageFalloff();
|
|
|
|
|
applyDamage(target, getDamage() * falloff, getKnockback() * falloff);
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-13 15:01:28 -05:00
|
|
|
// ==================== LAUNCH HELPERS ====================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* For player casters, snap the projectile to a position along the player's look vector.
|
|
|
|
|
* This ensures projectiles launch aligned with the crosshair rather than from the hand anchor.
|
|
|
|
|
* Only affects player casters; NPC projectiles launch from their anchor position as configured.
|
|
|
|
|
* Should be called in startMoving/startFiring BEFORE setting startX/Y/Z.
|
|
|
|
|
*/
|
|
|
|
|
protected void snapToPlayerLookVector() {
|
|
|
|
|
Entity owner = getOwnerEntity();
|
|
|
|
|
if (!(owner instanceof EntityPlayer)) return;
|
|
|
|
|
|
|
|
|
|
Vec3 look = owner.getLookVec();
|
|
|
|
|
double eyeY = owner.posY + owner.getEyeHeight();
|
|
|
|
|
float frontDist = Math.max(0.5f, size * 0.5f);
|
|
|
|
|
|
|
|
|
|
double newX = owner.posX + look.xCoord * frontDist;
|
|
|
|
|
double newY = eyeY + look.yCoord * frontDist;
|
|
|
|
|
double newZ = owner.posZ + look.zCoord * frontDist;
|
|
|
|
|
|
|
|
|
|
setPosition(newX, newY, newZ);
|
|
|
|
|
prevPosX = newX;
|
|
|
|
|
prevPosY = newY;
|
|
|
|
|
prevPosZ = newZ;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 15:21:56 -05:00
|
|
|
// ==================== CHARGING METHODS ====================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if entity is in charging state (synced via data watcher).
|
|
|
|
|
* In preview mode, uses local field since data watcher isn't synced.
|
|
|
|
|
*/
|
|
|
|
|
public boolean isCharging() {
|
|
|
|
|
if (previewMode) return this.charging;
|
|
|
|
|
return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set charging state (server only, synced to clients via data watcher).
|
|
|
|
|
*/
|
|
|
|
|
protected void setCharging(boolean value) {
|
|
|
|
|
this.charging = value;
|
|
|
|
|
if (!worldObj.isRemote) {
|
|
|
|
|
this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public float getChargeProgress() {
|
|
|
|
|
if (chargeDuration <= 0) return 1.0f;
|
|
|
|
|
return Math.min(1.0f, (float) chargeTick / chargeDuration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Setup this entity in charging mode (for windup phase).
|
|
|
|
|
* Grows from 0 to current size over chargeDuration ticks.
|
|
|
|
|
* Subclasses with different charging behavior should override or use their own method.
|
|
|
|
|
*/
|
|
|
|
|
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
|
|
|
|
|
setCharging(true);
|
|
|
|
|
this.chargeDuration = chargeDuration;
|
|
|
|
|
this.chargeTick = 0;
|
|
|
|
|
this.anchorData = anchor;
|
|
|
|
|
this.targetSize = this.size;
|
|
|
|
|
this.size = 0.01f;
|
|
|
|
|
this.renderCurrentSize = 0.01f;
|
|
|
|
|
this.prevRenderSize = 0.01f;
|
|
|
|
|
this.motionX = 0;
|
|
|
|
|
this.motionY = 0;
|
|
|
|
|
this.motionZ = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update during charging state - grow size and follow anchor.
|
|
|
|
|
* Default implementation grows {@code size} from 0 to {@code targetSize}.
|
|
|
|
|
* Override in subclasses for type-specific charging (Disc, Beam).
|
|
|
|
|
*/
|
|
|
|
|
protected void updateCharging() {
|
|
|
|
|
chargeTick++;
|
|
|
|
|
Entity owner = getOwnerEntity();
|
|
|
|
|
if (owner == null || owner.isDead) {
|
|
|
|
|
setDead();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
float progress = getChargeProgress();
|
|
|
|
|
this.size = targetSize * progress;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
// ==================== ENTITY HELPERS ====================
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
public Entity getOwnerEntity() {
|
2026-01-28 20:43:32 -05:00
|
|
|
// In preview mode, use direct reference (no world lookup)
|
|
|
|
|
if (previewMode && previewOwner != null) {
|
|
|
|
|
return previewOwner;
|
|
|
|
|
}
|
2026-01-25 20:39:01 -05:00
|
|
|
if (ownerEntityId == -1) return null;
|
|
|
|
|
return worldObj.getEntityByID(ownerEntityId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
|
|
|
|
public int getOwnerEntityId() {
|
|
|
|
|
return ownerEntityId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public IEntity getOwner() {
|
|
|
|
|
if (previewMode) return null;
|
|
|
|
|
if (getOwnerEntity() == null) return null;
|
|
|
|
|
return NpcAPI.Instance().getIEntity(getOwnerEntity());
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
/**
|
|
|
|
|
* Set the preview owner for GUI preview mode.
|
|
|
|
|
* This allows anchor point calculations without world entity lookup.
|
|
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
public void setPreviewOwner(EntityLivingBase owner) {
|
2026-01-28 20:43:32 -05:00
|
|
|
this.previewOwner = owner;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
|
|
|
|
public int getTargetEntityId() {
|
|
|
|
|
return targetEntityId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Entity getTargetEntity() {
|
2026-01-25 20:39:01 -05:00
|
|
|
if (targetEntityId == -1) return null;
|
|
|
|
|
return worldObj.getEntityByID(targetEntityId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
|
|
|
|
public IEntity getTarget() {
|
|
|
|
|
if (getTargetEntity() == null) return null;
|
|
|
|
|
return NpcAPI.Instance().getIEntity(getTargetEntity());
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
/**
|
|
|
|
|
* Check if an entity should be ignored for collision.
|
2026-02-14 15:21:56 -05:00
|
|
|
* Ignores: the owner, same-faction NPCs, and other projectiles from the same caster.
|
2026-01-25 20:39:01 -05:00
|
|
|
*/
|
|
|
|
|
protected boolean shouldIgnoreEntity(Entity entity) {
|
2026-02-03 17:20:36 -03:00
|
|
|
Entity owner = getOwnerEntity();
|
2026-01-25 20:39:01 -05:00
|
|
|
if (entity == owner) return true;
|
|
|
|
|
|
|
|
|
|
if (owner instanceof EntityNPCInterface && entity instanceof EntityNPCInterface) {
|
|
|
|
|
EntityNPCInterface ownerNpc = (EntityNPCInterface) owner;
|
|
|
|
|
EntityNPCInterface targetNpc = (EntityNPCInterface) entity;
|
|
|
|
|
if (ownerNpc.faction.id == targetNpc.faction.id) return true;
|
|
|
|
|
}
|
2026-02-03 17:20:36 -03:00
|
|
|
|
2026-02-14 15:21:56 -05:00
|
|
|
// Ignore other projectiles from the same caster (e.g. multi-projectile abilities)
|
|
|
|
|
if (entity instanceof EntityAbilityProjectile) {
|
|
|
|
|
EntityAbilityProjectile other = (EntityAbilityProjectile) entity;
|
|
|
|
|
if (other.ownerEntityId == this.ownerEntityId) return true;
|
|
|
|
|
}
|
2026-02-03 17:20:36 -03:00
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== VISUAL GETTERS ====================
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-25 20:39:01 -05:00
|
|
|
public float getSize() {
|
|
|
|
|
return size;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-25 20:39:01 -05:00
|
|
|
public int getInnerColor() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return displayData.getInnerColor();
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-25 20:39:01 -05:00
|
|
|
public int getOuterColor() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return displayData.getOuterColor();
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-26 04:42:23 -05:00
|
|
|
public boolean isOuterColorEnabled() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return displayData.isOuterColorEnabled();
|
2026-01-26 04:42:23 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-26 04:42:23 -05:00
|
|
|
public float getOuterColorWidth() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return displayData.getOuterColorWidth();
|
2026-01-26 04:42:23 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-28 20:49:16 -03:00
|
|
|
public float getOuterColorAlpha() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return displayData.getOuterColorAlpha();
|
2026-01-28 20:49:16 -03:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-25 20:39:01 -05:00
|
|
|
public float getRotationSpeed() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return displayData.getRotationSpeed();
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
// ==================== ROTATION GETTERS ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
2026-01-25 20:39:01 -05:00
|
|
|
public float getInterpolatedRotationX(float partialTicks) {
|
|
|
|
|
return this.prevRotationValX + (this.rotationValX - this.prevRotationValX) * partialTicks;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-25 20:39:01 -05:00
|
|
|
public float getInterpolatedRotationY(float partialTicks) {
|
|
|
|
|
return this.prevRotationValY + (this.rotationValY - this.prevRotationValY) * partialTicks;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-25 20:39:01 -05:00
|
|
|
public float getInterpolatedRotationZ(float partialTicks) {
|
|
|
|
|
return this.prevRotationValZ + (this.rotationValZ - this.prevRotationValZ) * partialTicks;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-25 20:39:01 -05:00
|
|
|
public float getInterpolatedSize(float partialTicks) {
|
|
|
|
|
return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 18:38:43 -05:00
|
|
|
// ==================== LIGHTNING GETTERS ====================
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-26 18:38:43 -05:00
|
|
|
public boolean hasLightningEffect() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return lightningData.isLightningEffect();
|
2026-01-26 18:38:43 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-26 18:38:43 -05:00
|
|
|
public float getLightningDensity() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return lightningData.getLightningDensity();
|
2026-01-26 18:38:43 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-26 18:38:43 -05:00
|
|
|
public float getLightningRadius() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return lightningData.getLightningRadius();
|
2026-01-26 18:38:43 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
@Override
|
2026-01-26 18:38:43 -05:00
|
|
|
public int getLightningFadeTime() {
|
2026-02-03 17:20:36 -03:00
|
|
|
return lightningData.getLightningFadeTime();
|
2026-01-26 18:38:43 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
// ==================== LIFESPAN GETTERS ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getMaxDistance() { return lifespanData.getMaxDistance(); }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getMaxLifetime() { return lifespanData.getMaxLifetime(); }
|
|
|
|
|
|
|
|
|
|
// ==================== COMBAT GETTERS ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getDamage() { return combatData.getDamage(); }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getKnockback() { return combatData.knockback; }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getKnockbackUp() { return combatData.knockbackUp; }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean isExplosive() { return combatData.isExplosive(); }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getExplosionRadius() { return combatData.explosionRadius; }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
|
|
|
|
|
|
2026-02-03 18:00:52 -03:00
|
|
|
// ==================== HOMING GETTERS ====================
|
2026-02-03 17:20:36 -03:00
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean isHoming() { return homingData.isHoming(); }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getHomingStrength() { return homingData.getHomingStrength(); }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getHomingRange() { return homingData.getHomingRange(); }
|
|
|
|
|
|
2026-02-03 18:00:52 -03:00
|
|
|
// ==================== TRAJECTORY GETTERS ====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
// ==================== ANCHOR GETTERS ====================
|
|
|
|
|
|
2026-02-03 18:00:52 -03:00
|
|
|
@Override
|
|
|
|
|
public float getSpeed() { return homingData.getSpeed(); }
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
public AnchorPoint getAnchorPoint() { return anchorData.getAnchorPoint(); }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getAnchor() { return anchorData.getAnchor(); }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getAnchorOffsetX() { return anchorData.getAnchorOffsetX(); }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getAnchorOffsetY() { return anchorData.getAnchorOffsetY(); }
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getAnchorOffsetZ() { return anchorData.getAnchorOffsetZ(); }
|
|
|
|
|
|
|
|
|
|
// ==================== MOVEMENT GETTERS ====================
|
|
|
|
|
|
|
|
|
|
@Override public double getStartX() { return startX; }
|
|
|
|
|
|
|
|
|
|
@Override public double getStartY() { return startY; }
|
|
|
|
|
|
|
|
|
|
@Override public double getStartZ() { return startZ; }
|
|
|
|
|
|
|
|
|
|
@Override public double getMotionX() { return motionX; }
|
|
|
|
|
|
|
|
|
|
@Override public double getMotionY() { return motionY; }
|
|
|
|
|
|
|
|
|
|
@Override public double getMotionZ() { return motionZ; }
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
// ==================== BRIGHTNESS ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public float getBrightness(float partialTicks) {
|
|
|
|
|
return 1.0f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
@SideOnly(Side.CLIENT)
|
|
|
|
|
public int getBrightnessForRender(float partialTicks) {
|
|
|
|
|
return 0xF000F0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== 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) {
|
|
|
|
|
this.size = nbt.getFloat("Size");
|
|
|
|
|
|
2026-01-29 14:19:57 -05:00
|
|
|
// 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(AbilityEffect.fromNBT(effectsList.getCompoundTagAt(i)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
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.ownerEntityId = nbt.getInteger("OwnerId");
|
|
|
|
|
this.targetEntityId = nbt.getInteger("TargetId");
|
|
|
|
|
|
|
|
|
|
this.motionX = nbt.getDouble("MotionX");
|
|
|
|
|
this.motionY = nbt.getDouble("MotionY");
|
|
|
|
|
this.motionZ = nbt.getDouble("MotionZ");
|
|
|
|
|
|
|
|
|
|
if (this.worldObj != null && this.worldObj.isRemote) {
|
|
|
|
|
this.renderCurrentSize = this.size;
|
|
|
|
|
this.prevRenderSize = this.size;
|
|
|
|
|
}
|
2026-02-03 17:20:36 -03:00
|
|
|
|
|
|
|
|
anchorData.readNBT(nbt);
|
|
|
|
|
combatData.readNBT(nbt);
|
|
|
|
|
displayData.readNBT(nbt);
|
|
|
|
|
homingData.readNBT(nbt);
|
|
|
|
|
lifespanData.readNBT(nbt);
|
|
|
|
|
lightningData.readNBT(nbt);
|
2026-02-03 21:26:27 -03:00
|
|
|
trajectoryData.readNBT(nbt);
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Write base projectile properties to NBT.
|
|
|
|
|
*/
|
|
|
|
|
protected void writeBaseNBT(NBTTagCompound nbt) {
|
|
|
|
|
nbt.setFloat("Size", size);
|
|
|
|
|
|
2026-01-29 14:19:57 -05:00
|
|
|
// Write effects list
|
|
|
|
|
if (!effects.isEmpty()) {
|
|
|
|
|
NBTTagList effectsList = new NBTTagList();
|
|
|
|
|
for (AbilityEffect effect : effects) {
|
|
|
|
|
effectsList.appendTag(effect.writeNBT());
|
|
|
|
|
}
|
|
|
|
|
nbt.setTag("Effects", effectsList);
|
|
|
|
|
}
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
nbt.setLong("DeathWorldTime", deathWorldTime);
|
|
|
|
|
|
|
|
|
|
nbt.setDouble("StartX", startX);
|
|
|
|
|
nbt.setDouble("StartY", startY);
|
|
|
|
|
nbt.setDouble("StartZ", startZ);
|
|
|
|
|
|
|
|
|
|
nbt.setInteger("OwnerId", ownerEntityId);
|
|
|
|
|
nbt.setInteger("TargetId", targetEntityId);
|
|
|
|
|
|
|
|
|
|
nbt.setDouble("MotionX", motionX);
|
|
|
|
|
nbt.setDouble("MotionY", motionY);
|
|
|
|
|
nbt.setDouble("MotionZ", motionZ);
|
2026-01-26 18:38:43 -05:00
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
anchorData.writeNBT(nbt);
|
|
|
|
|
combatData.writeNBT(nbt);
|
|
|
|
|
displayData.writeNBT(nbt);
|
|
|
|
|
homingData.writeNBT(nbt);
|
|
|
|
|
lifespanData.writeNBT(nbt);
|
|
|
|
|
lightningData.writeNBT(nbt);
|
2026-02-03 21:26:27 -03:00
|
|
|
trajectoryData.writeNBT(nbt);
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Subclass-specific NBT reading.
|
|
|
|
|
*/
|
|
|
|
|
protected abstract void readProjectileNBT(NBTTagCompound nbt);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Subclass-specific NBT writing.
|
|
|
|
|
*/
|
|
|
|
|
protected abstract void writeProjectileNBT(NBTTagCompound nbt);
|
|
|
|
|
|
|
|
|
|
// ==================== SPAWN DATA ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void writeSpawnData(ByteBuf buffer) {
|
|
|
|
|
try {
|
|
|
|
|
NBTTagCompound compound = new NBTTagCompound();
|
|
|
|
|
this.writeEntityToNBT(compound);
|
|
|
|
|
cpw.mods.fml.common.network.ByteBufUtils.writeTag(buffer, compound);
|
|
|
|
|
} catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void readSpawnData(ByteBuf buffer) {
|
|
|
|
|
try {
|
|
|
|
|
NBTTagCompound compound = cpw.mods.fml.common.network.ByteBufUtils.readTag(buffer);
|
|
|
|
|
if (compound != null) {
|
|
|
|
|
this.readEntityFromNBT(compound);
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ignored) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== COLLISION SETTINGS ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean canBeCollidedWith() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean canBePushed() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected boolean canTriggerWalking() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean isBurning() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SideOnly(Side.CLIENT)
|
|
|
|
|
public float getShadowSize() {
|
|
|
|
|
return 0.0f;
|
|
|
|
|
}
|
|
|
|
|
}
|