mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 19:34:09 +00:00
2449 lines
92 KiB
Java
2449 lines
92 KiB
Java
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.EnergyController;
|
|
import kamkeel.npcs.controllers.data.ability.Ability;
|
|
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityEnergyProjectile;
|
|
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
|
|
import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint;
|
|
import kamkeel.npcs.controllers.data.ability.enums.HitType;
|
|
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData;
|
|
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
|
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
|
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
|
|
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
|
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData;
|
|
import kamkeel.npcs.network.packets.data.energy.ProjectileReflectPacket;
|
|
import kamkeel.npcs.network.packets.data.energyexplosion.EnergyExplosionSpawnPacket;
|
|
import kamkeel.npcs.util.AnchorPointHelper;
|
|
import kamkeel.npcs.util.AttributeAttackUtil;
|
|
import kamkeel.npcs.util.CNPCDebug;
|
|
import net.minecraft.block.Block;
|
|
import net.minecraft.block.material.Material;
|
|
import net.minecraft.entity.Entity;
|
|
import net.minecraft.entity.EntityLivingBase;
|
|
import net.minecraft.entity.player.EntityPlayer;
|
|
import net.minecraft.init.Blocks;
|
|
import net.minecraft.nbt.NBTTagCompound;
|
|
import net.minecraft.nbt.NBTTagList;
|
|
import net.minecraft.util.AxisAlignedBB;
|
|
import net.minecraft.util.DamageSource;
|
|
import net.minecraft.util.MathHelper;
|
|
import net.minecraft.util.MovingObjectPosition;
|
|
import net.minecraft.util.Vec3;
|
|
import net.minecraft.world.Explosion;
|
|
import net.minecraft.world.World;
|
|
import net.minecraft.world.WorldServer;
|
|
import noppes.npcs.EventHooks;
|
|
import noppes.npcs.NpcDamageSource;
|
|
import noppes.npcs.api.entity.IEntity;
|
|
import noppes.npcs.config.ConfigEnergy;
|
|
import noppes.npcs.config.ConfigMain;
|
|
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.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
|
|
/**
|
|
* 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.
|
|
* <p>
|
|
* Design inspired by LouisXIV's energy attack system.
|
|
*/
|
|
public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
|
// ==================== ACTIVE PROJECTILE TRACKING ====================
|
|
private static final Map<Integer, List<WeakReference<EntityEnergyProjectile>>> activeProjectiles = new HashMap<>();
|
|
|
|
public static void trackProjectile(EntityEnergyProjectile projectile) {
|
|
int ownerId = projectile.getOwnerEntityId();
|
|
if (ownerId < 0) return;
|
|
List<WeakReference<EntityEnergyProjectile>> refs = activeProjectiles.get(ownerId);
|
|
if (refs == null) {
|
|
refs = new ArrayList<>();
|
|
activeProjectiles.put(ownerId, refs);
|
|
}
|
|
refs.add(new WeakReference<>(projectile));
|
|
}
|
|
|
|
public static void clearAllProjectiles() {
|
|
activeProjectiles.clear();
|
|
}
|
|
|
|
public static List<EntityEnergyProjectile> getActiveProjectiles(int ownerEntityId) {
|
|
List<WeakReference<EntityEnergyProjectile>> refs = activeProjectiles.get(ownerEntityId);
|
|
if (refs == null) return Collections.emptyList();
|
|
List<EntityEnergyProjectile> result = new ArrayList<>();
|
|
Iterator<WeakReference<EntityEnergyProjectile>> it = refs.iterator();
|
|
while (it.hasNext()) {
|
|
EntityEnergyProjectile p = it.next().get();
|
|
if (p == null || p.isDead) {
|
|
it.remove();
|
|
} else {
|
|
result.add(p);
|
|
}
|
|
}
|
|
if (refs.isEmpty()) activeProjectiles.remove(ownerEntityId);
|
|
return result;
|
|
}
|
|
|
|
// ==================== VISUAL PROPERTIES ====================
|
|
protected float size = 1.0f;
|
|
|
|
// ==================== COMBAT PROPERTIES ====================
|
|
protected EnergyCombatData combatData = new EnergyCombatData();
|
|
|
|
// ==================== EFFECT PROPERTIES ====================
|
|
protected List<AbilityPotionEffect> effects = new ArrayList<>();
|
|
|
|
// ==================== ANCHOR PROPERTIES ====================
|
|
protected EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
|
|
|
|
// ==================== LIFESPAN PROPERTIES ====================
|
|
protected EnergyLifespanData lifespanData = new EnergyLifespanData();
|
|
protected long deathWorldTime = -1; // World time when entity should die (-1 = not set)
|
|
|
|
// ==================== HOMING PROPERTIES ====================
|
|
protected EnergyHomingData homingData = new EnergyHomingData();
|
|
|
|
// ==================== TRACKING ====================
|
|
protected double startX, startY, startZ;
|
|
protected int targetEntityId = -1;
|
|
|
|
// ==================== STATE ====================
|
|
protected boolean reflected = false; // Set after barrier reflection; prevents owner-tracking on reflected projectiles
|
|
protected boolean hasHit = false;
|
|
protected int hitCount = 0;
|
|
protected final Set<Integer> hitOnceEntities = new HashSet<Integer>();
|
|
protected final Map<Integer, Integer> lastHitTickByEntity = new HashMap<Integer, Integer>();
|
|
protected static final int BARRIER_IMPACT_PAUSE_TICKS = 10;
|
|
protected static final int BARRIER_BREAK_SPARK_TICKS = 10;
|
|
protected static final int DW_BARRIER_SPARK_TICKS = 21;
|
|
protected static final int DW_SYNC_INNER_COLOR = 22;
|
|
protected static final int DW_SYNC_OUTER_COLOR = 23;
|
|
protected int barrierImpactPauseTicks = 0;
|
|
protected boolean barrierImpactDestroyOnResume = false;
|
|
protected double pausedMotionX, pausedMotionY, pausedMotionZ;
|
|
|
|
// ==================== CHARGING STATE (projectile-specific) ====================
|
|
protected float targetSize = 1.0f;
|
|
|
|
// ==================== 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 EntityEnergyProjectile(World world) {
|
|
super(world);
|
|
this.setSize(0.5f, 0.5f);
|
|
this.yOffset = this.height / 2.0f;
|
|
this.noClip = false;
|
|
}
|
|
|
|
@Override
|
|
protected void entityInit() {
|
|
super.entityInit(); // DW_CHARGING = 20
|
|
this.dataWatcher.addObject(DW_BARRIER_SPARK_TICKS, 0);
|
|
// displayData is not guaranteed to be initialized yet during Entity construction.
|
|
this.dataWatcher.addObject(DW_SYNC_INNER_COLOR, 0xFFFFFF);
|
|
this.dataWatcher.addObject(DW_SYNC_OUTER_COLOR, 0xFFFFFF);
|
|
}
|
|
|
|
/**
|
|
* Initialize common properties using data classes. Call from subclass constructors.
|
|
*/
|
|
protected void initProjectile(EntityLivingBase owner, EntityLivingBase target,
|
|
double x, double y, double z, float size,
|
|
EnergyDisplayData display, EnergyCombatData combat,
|
|
EnergyLightningData lightning, EnergyLifespanData lifespan) {
|
|
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;
|
|
|
|
// Defensive copy: entities must never share data objects with the source ability.
|
|
// Without copies, runtime mutations (e.g. barrier reflection changing colors/damage)
|
|
// bleed back into the ability template and corrupt future projectiles.
|
|
this.displayData = display != null ? display.copy() : new EnergyDisplayData();
|
|
this.combatData = combat != null ? combat.copy() : new EnergyCombatData();
|
|
this.lifespanData = lifespan != null ? lifespan.copy() : new EnergyLifespanData();
|
|
this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData();
|
|
syncProjectileColorWatchers();
|
|
|
|
// Initialize render size
|
|
this.renderCurrentSize = size;
|
|
this.prevRenderSize = size;
|
|
}
|
|
|
|
@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 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;
|
|
|
|
// Skip super.onUpdate() in preview mode to avoid world checks
|
|
if (!previewMode) {
|
|
super.onUpdate();
|
|
|
|
// Track on first server tick
|
|
if (ticksExisted == 1 && !worldObj.isRemote) {
|
|
trackProjectile(this);
|
|
}
|
|
} else {
|
|
// Preview entities skip super.onUpdate() but still need ticksExisted
|
|
// for renderer pulsing effects (e.g., sin(ticksExisted + partialTicks)).
|
|
this.ticksExisted++;
|
|
|
|
// Preview-specific safety: self-destruct if charging has exceeded duration + grace
|
|
if (isCharging() && chargeDuration > 0 && chargeTick > chargeDuration + PREVIEW_CHARGE_GRACE) {
|
|
this.setDead();
|
|
return;
|
|
}
|
|
|
|
// Preview hard lifetime cap (mirrors real entity HARD_LIFETIME_CAP)
|
|
if (ticksExisted > HARD_LIFETIME_CAP) {
|
|
this.setDead();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!previewMode) {
|
|
syncDisplayColorsFromWatchers();
|
|
}
|
|
|
|
// Update rotation
|
|
updateRotation();
|
|
|
|
// Update render size: during charging, track directly (size already grows smoothly).
|
|
// Post-charging, lerp for smooth visual transitions.
|
|
if (isCharging()) {
|
|
this.renderCurrentSize = this.size;
|
|
} else {
|
|
this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f;
|
|
}
|
|
|
|
// Server-synced visual timer used for brief barrier impact lightning.
|
|
if (!previewMode && !worldObj.isRemote) {
|
|
tickBarrierSparkTimer();
|
|
}
|
|
|
|
// Skip lifetime/distance checks in preview mode
|
|
if (!previewMode) {
|
|
// Only self-destruct if the owner is confirmed dead/killed.
|
|
// A null owner (unloaded/out of chunk) is NOT treated as dead —
|
|
// the projectile keeps acting normally and deathWorldTime handles lifetime.
|
|
if (ownerEntityId >= 0 && ticksExisted > 5) {
|
|
Entity owner = worldObj.getEntityByID(ownerEntityId);
|
|
if (owner != null) {
|
|
if (owner.isDead) {
|
|
this.setDead();
|
|
return;
|
|
}
|
|
if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) {
|
|
this.setDead();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Charging timeout - if charge phase exceeded with grace period, force kill
|
|
if (isCharging() && chargeTick > chargeDuration + CHARGE_TIMEOUT_GRACE) {
|
|
this.setDead();
|
|
return;
|
|
}
|
|
|
|
// Absolute hard lifetime cap (safety net for any stuck state)
|
|
if (ticksExisted > HARD_LIFETIME_CAP) {
|
|
this.setDead();
|
|
return;
|
|
}
|
|
|
|
// Set death time on first tick if not already set (handles chunk load/unload)
|
|
// Skip during charging phase - lifetime starts when the entity actually fires
|
|
if (deathWorldTime < 0 && worldObj != null && !isCharging()) {
|
|
deathWorldTime = worldObj.getTotalWorldTime() + getMaxLifetime();
|
|
}
|
|
|
|
// Check lifespan using world time (survives chunk unload/reload)
|
|
if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) {
|
|
if (!worldObj.isRemote) {
|
|
EventHooks.onEnergyProjectileExpired(this);
|
|
}
|
|
this.setDead();
|
|
return;
|
|
}
|
|
|
|
// Check max distance (subclass can override if needed)
|
|
if (checkMaxDistance()) {
|
|
if (!worldObj.isRemote) {
|
|
EventHooks.onEnergyProjectileExpired(this);
|
|
}
|
|
this.setDead();
|
|
return;
|
|
}
|
|
|
|
if (hasHit) {
|
|
this.setDead();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!previewMode && !worldObj.isRemote && !isCharging()) {
|
|
if (tickBarrierImpactPause()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check barrier collisions (server-side, non-preview, non-charging)
|
|
if (!previewMode && !worldObj.isRemote && !isCharging()) {
|
|
if (checkBarrierCollision()) {
|
|
return; // Barrier interaction handled (pause + destroy/continue)
|
|
}
|
|
}
|
|
|
|
// Subclass-specific update
|
|
updateProjectile();
|
|
|
|
// Fire tick event (server-side, non-preview only)
|
|
if (!previewMode && !worldObj.isRemote) {
|
|
EventHooks.onEnergyProjectileTick(this);
|
|
}
|
|
|
|
debugLogTick();
|
|
}
|
|
|
|
/**
|
|
* Update rotation values. Override for custom rotation behavior.
|
|
*/
|
|
protected void updateRotation() {
|
|
this.rotationValX += getRotationSpeed() * 0.7f;
|
|
this.rotationValY += getRotationSpeed();
|
|
this.rotationValZ += getRotationSpeed() * 0.5f;
|
|
|
|
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;
|
|
}
|
|
|
|
// ==================== DEBUG LOGGING ====================
|
|
|
|
/**
|
|
* Debug log called every tick. Logs position/size/charging state.
|
|
* Subclasses override debugLogExtra() for type-specific data.
|
|
*/
|
|
protected void debugLogTick() {
|
|
boolean isClient = worldObj.isRemote;
|
|
if (isClient ? !CNPCDebug.isClientEnabled("energy") : !CNPCDebug.isServerEnabled("energy"))
|
|
return;
|
|
|
|
String className = getClass().getSimpleName();
|
|
boolean dwCharging = isCharging(); // DataWatcher value (what client sees)
|
|
boolean localCharging = this.charging; // Local field (server truth)
|
|
|
|
String base = String.format("[%s id=%d tick=%d] pos=(%.2f,%.2f,%.2f) prev=(%.2f,%.2f,%.2f) " +
|
|
"size=%.3f renderSize=%.3f prevRenderSize=%.3f " +
|
|
"charging(dw)=%b charging(local)=%b chargeTick=%d/%d chargeProgress=%.3f",
|
|
className, getEntityId(), ticksExisted,
|
|
posX, posY, posZ, prevPosX, prevPosY, prevPosZ,
|
|
size, renderCurrentSize, prevRenderSize,
|
|
dwCharging, localCharging, chargeTick, chargeDuration, getChargeProgress());
|
|
|
|
// Flag mismatch between DataWatcher and local field
|
|
if (dwCharging != localCharging) {
|
|
base += " !!CHARGE_MISMATCH!!";
|
|
}
|
|
|
|
// Client interpolation state (only meaningful on client)
|
|
if (isClient) {
|
|
base += String.format(" interp(steps=%d target=(%.2f,%.2f,%.2f))",
|
|
interpSteps, interpTargetX, interpTargetY, interpTargetZ);
|
|
}
|
|
|
|
String extra = debugLogExtra();
|
|
String full = extra.isEmpty() ? base : base + " " + extra;
|
|
CNPCDebug.log("energy", isClient, full);
|
|
}
|
|
|
|
/**
|
|
* Subclass hook for type-specific debug data.
|
|
* Return empty string if nothing extra to log.
|
|
*/
|
|
protected String debugLogExtra() {
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
);
|
|
return distTraveled >= getMaxDistance();
|
|
}
|
|
|
|
/**
|
|
* Subclass-specific update logic. Called every tick.
|
|
*/
|
|
protected abstract void updateProjectile();
|
|
|
|
// ==================== BARRIER COLLISION ====================
|
|
|
|
/**
|
|
* Check for collision with energy barrier entities.
|
|
* Barriers only block incoming projectiles from enemies.
|
|
*
|
|
* @return true if barrier interaction was handled (caller should stop processing this tick)
|
|
*/
|
|
protected boolean checkBarrierCollision() {
|
|
List<EntityEnergyBarrier> barriers = EntityEnergyBarrier.getActiveBarriers(worldObj);
|
|
for (EntityEnergyBarrier barrier : barriers) {
|
|
if (barrier.isDead) continue;
|
|
|
|
// Quick distance pre-filter
|
|
double dx = barrier.posX - this.posX;
|
|
double dy = barrier.posY - this.posY;
|
|
double dz = barrier.posZ - this.posZ;
|
|
double distSq = dx * dx + dy * dy + dz * dz;
|
|
double maxRange = barrier.getMaxExtent() + 5.0;
|
|
if (distSq > maxRange * maxRange) continue;
|
|
|
|
if (barrier.isIncomingProjectile(this)) {
|
|
float damage = getModifiedDamage();
|
|
EntityEnergyBarrier.ProjectileHitOutcome hitOutcome = barrier.onProjectileHitResolved(this, damage);
|
|
if (hitOutcome == null || hitOutcome.result == EntityEnergyBarrier.ProjectileHitResult.PASS) {
|
|
continue;
|
|
}
|
|
if (handleBarrierHitOutcome(barrier, hitOutcome, damage)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handle the result of a projectile/barrier interaction.
|
|
*
|
|
* @param fullDamage The DBC-scaled damage that was passed to the barrier (from getModifiedDamage).
|
|
* @return true if caller should stop processing this tick.
|
|
*/
|
|
protected boolean handleBarrierHitOutcome(EntityEnergyBarrier barrier, EntityEnergyBarrier.ProjectileHitOutcome hitOutcome, float fullDamage) {
|
|
if (hitOutcome == null || hitOutcome.result == EntityEnergyBarrier.ProjectileHitResult.PASS) {
|
|
return false;
|
|
}
|
|
|
|
if (hitOutcome.result == EntityEnergyBarrier.ProjectileHitResult.BROKEN) {
|
|
if (hitOutcome.remainingProjectileDamage <= 0.0f) {
|
|
damageMultiplier = 0.0f;
|
|
setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS));
|
|
hasHit = true;
|
|
return true;
|
|
}
|
|
|
|
// Accumulate barrier pass-through ratio for extender damage scaling.
|
|
// fullDamage is the DBC-scaled damage the barrier saw; remainingProjectileDamage
|
|
// is what survived. Multiply into the running ratio so multiple barriers stack.
|
|
if (fullDamage > 0.0f) {
|
|
damageMultiplier *= (hitOutcome.remainingProjectileDamage / fullDamage);
|
|
}
|
|
|
|
setCombatDamage(hitOutcome.remainingProjectileDamage);
|
|
setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS));
|
|
return false;
|
|
}
|
|
|
|
if (hitOutcome.shouldReflect()) {
|
|
if (!reflectFromBarrier(barrier, hitOutcome.reflectStrengthPct)) {
|
|
beginBarrierImpactPause(true, barrier);
|
|
} else {
|
|
sendReflectionSync();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
beginBarrierImpactPause(true, barrier);
|
|
return true;
|
|
}
|
|
|
|
protected boolean reflectFromBarrier(EntityEnergyBarrier barrier, float reflectStrengthPct) {
|
|
if (barrier == null) {
|
|
return false;
|
|
}
|
|
|
|
double vx = motionX;
|
|
double vy = motionY;
|
|
double vz = motionZ;
|
|
double vLenSq = vx * vx + vy * vy + vz * vz;
|
|
if (vLenSq < 1.0e-8) {
|
|
return false;
|
|
}
|
|
|
|
double[] normal = getBarrierImpactNormal(barrier, vx, vy, vz);
|
|
if (normal == null) {
|
|
return false;
|
|
}
|
|
|
|
double dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
|
|
if (dot > 0.0) {
|
|
normal[0] = -normal[0];
|
|
normal[1] = -normal[1];
|
|
normal[2] = -normal[2];
|
|
dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
|
|
}
|
|
|
|
double rx = vx - 2.0 * dot * normal[0];
|
|
double ry = vy - 2.0 * dot * normal[1];
|
|
double rz = vz - 2.0 * dot * normal[2];
|
|
double rLenSq = rx * rx + ry * ry + rz * rz;
|
|
if (rLenSq < 1.0e-8) {
|
|
rx = -vx;
|
|
ry = -vy;
|
|
rz = -vz;
|
|
rLenSq = rx * rx + ry * ry + rz * rz;
|
|
}
|
|
|
|
// Prefer rebounding out and away from ground instead of immediately diving downward.
|
|
if (ry < -0.06) {
|
|
double speed = Math.sqrt(Math.max(rLenSq, 1.0e-8));
|
|
ry = Math.max(0.08, Math.abs(ry) * 0.35);
|
|
double newLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
if (newLen > 1.0e-8) {
|
|
double scale = speed / newLen;
|
|
rx *= scale;
|
|
ry *= scale;
|
|
rz *= scale;
|
|
}
|
|
}
|
|
|
|
// Push slightly outside the collision boundary so the reflected motion
|
|
// starts cleanly from the barrier surface instead of re-colliding in place.
|
|
snapOutsideBarrierForPause(barrier);
|
|
|
|
motionX = rx;
|
|
motionY = ry;
|
|
motionZ = rz;
|
|
pausedMotionX = rx;
|
|
pausedMotionY = ry;
|
|
pausedMotionZ = rz;
|
|
barrierImpactPauseTicks = 0;
|
|
barrierImpactDestroyOnResume = false;
|
|
|
|
// Reset start position and lifetime so the reflected projectile gets a fresh
|
|
// max-distance budget and lifetime. Without this, re-reflection fails because
|
|
// the projectile expires from the original launch's distance/time limits.
|
|
startX = posX;
|
|
startY = posY;
|
|
startZ = posZ;
|
|
deathWorldTime = -1;
|
|
|
|
// Detach from source ability BEFORE changing ownership.
|
|
// This lets the ability see the projectile as gone and free the caster
|
|
// (e.g., release movement/rotation locks on the player).
|
|
if (sourceAbility instanceof AbilityEnergyProjectile) {
|
|
((AbilityEnergyProjectile<?>) sourceAbility).detachEntity(this);
|
|
}
|
|
sourceAbility = null;
|
|
|
|
// Save original owner before transfer for potential target retargeting
|
|
int originalOwnerId = ownerEntityId;
|
|
|
|
Entity barrierOwner = barrier.getOwnerEntity();
|
|
if (barrierOwner != null) {
|
|
setOwnerEntityId(barrierOwner.getEntityId());
|
|
// Ensure ownership-sensitive systems can query this projectile under the new owner.
|
|
trackProjectile(this);
|
|
}
|
|
|
|
// Target Owner: set the reflected projectile's target to the original caster
|
|
if (barrier.getBarrierData().isTargetOwner() && originalOwnerId != -1) {
|
|
setTargetEntityId(originalOwnerId);
|
|
} else {
|
|
setTargetEntityId(-1);
|
|
}
|
|
|
|
setInnerColor(barrier.getInnerColor());
|
|
setOuterColor(barrier.getOuterColor());
|
|
|
|
float clampedStrength = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
|
|
float reductionFactor = 1.0f - clampedStrength / 100.0f;
|
|
float reducedDamage = getDamage() * reductionFactor;
|
|
setCombatDamage(Math.max(0.0f, reducedDamage));
|
|
damageMultiplier *= reductionFactor;
|
|
|
|
hitOnceEntities.clear();
|
|
lastHitTickByEntity.clear();
|
|
hitCount = 0;
|
|
reflected = true;
|
|
return true;
|
|
}
|
|
|
|
// ==================== REFLECTION SYNC ====================
|
|
|
|
/**
|
|
* Write reflection state for client sync.
|
|
* Subclasses should override writeProjectileReflectionData for type-specific fields.
|
|
*/
|
|
public NBTTagCompound writeReflectionData() {
|
|
NBTTagCompound nbt = new NBTTagCompound();
|
|
nbt.setInteger("OwnerId", ownerEntityId);
|
|
nbt.setDouble("PosX", posX);
|
|
nbt.setDouble("PosY", posY);
|
|
nbt.setDouble("PosZ", posZ);
|
|
nbt.setDouble("MotionX", motionX);
|
|
nbt.setDouble("MotionY", motionY);
|
|
nbt.setDouble("MotionZ", motionZ);
|
|
nbt.setInteger("InnerColor", getInnerColor());
|
|
nbt.setInteger("OuterColor", getOuterColor());
|
|
nbt.setBoolean("Reflected", reflected);
|
|
writeProjectileReflectionData(nbt);
|
|
return nbt;
|
|
}
|
|
|
|
/**
|
|
* Apply reflection state from server sync.
|
|
* Subclasses should override applyProjectileReflectionData for type-specific fields.
|
|
*/
|
|
public void applyReflectionData(NBTTagCompound nbt) {
|
|
ownerEntityId = nbt.getInteger("OwnerId");
|
|
double px = nbt.getDouble("PosX");
|
|
double py = nbt.getDouble("PosY");
|
|
double pz = nbt.getDouble("PosZ");
|
|
setPosition(px, py, pz);
|
|
motionX = nbt.getDouble("MotionX");
|
|
motionY = nbt.getDouble("MotionY");
|
|
motionZ = nbt.getDouble("MotionZ");
|
|
setInnerColor(nbt.getInteger("InnerColor"));
|
|
setOuterColor(nbt.getInteger("OuterColor"));
|
|
reflected = nbt.getBoolean("Reflected");
|
|
syncPositionStateToCurrent(true);
|
|
hitOnceEntities.clear();
|
|
lastHitTickByEntity.clear();
|
|
hitCount = 0;
|
|
applyProjectileReflectionData(nbt);
|
|
}
|
|
|
|
/**
|
|
* Subclass hook: write type-specific reflection data.
|
|
*/
|
|
protected void writeProjectileReflectionData(NBTTagCompound nbt) {
|
|
}
|
|
|
|
/**
|
|
* Subclass hook: apply type-specific reflection data on client.
|
|
*/
|
|
protected void applyProjectileReflectionData(NBTTagCompound nbt) {
|
|
}
|
|
|
|
/**
|
|
* Send reflection state to all tracking clients.
|
|
*/
|
|
protected void sendReflectionSync() {
|
|
if (worldObj == null || worldObj.isRemote) return;
|
|
ProjectileReflectPacket.sendToTracking(this, writeReflectionData());
|
|
}
|
|
|
|
protected double[] getBarrierImpactNormal(EntityEnergyBarrier barrier, double velocityX, double velocityY, double velocityZ) {
|
|
return barrier.getSurfaceNormal(posX, posY, posZ, velocityX, velocityY, velocityZ);
|
|
}
|
|
|
|
protected void beginBarrierImpactPause(boolean destroyOnResume, EntityEnergyBarrier barrier) {
|
|
if (barrierImpactPauseTicks <= 0) {
|
|
pausedMotionX = motionX;
|
|
pausedMotionY = motionY;
|
|
pausedMotionZ = motionZ;
|
|
}
|
|
|
|
barrierImpactDestroyOnResume = destroyOnResume;
|
|
barrierImpactPauseTicks = BARRIER_IMPACT_PAUSE_TICKS;
|
|
motionX = 0;
|
|
motionY = 0;
|
|
motionZ = 0;
|
|
|
|
int sparkTicks = getBarrierSparkTicks();
|
|
if (sparkTicks < BARRIER_IMPACT_PAUSE_TICKS) {
|
|
setBarrierSparkTicks(BARRIER_IMPACT_PAUSE_TICKS);
|
|
}
|
|
}
|
|
|
|
protected float getBarrierPauseOutsideDistance() {
|
|
// Keep the paused projectile just outside the surface without a visible "fling".
|
|
return Math.max(0.06f, Math.min(0.22f, size * 0.18f + 0.03f));
|
|
}
|
|
|
|
protected void snapOutsideBarrierForPause(EntityEnergyBarrier barrier) {
|
|
if (barrier == null) return;
|
|
|
|
float bias = getBarrierPauseOutsideDistance();
|
|
double[] point = barrier.getOutsideSurfacePoint(posX, posY, posZ, motionX, motionY, motionZ, bias);
|
|
setPosition(point[0], point[1], point[2]);
|
|
}
|
|
|
|
protected boolean tickBarrierImpactPause() {
|
|
if (barrierImpactPauseTicks <= 0) {
|
|
return false;
|
|
}
|
|
|
|
motionX = 0;
|
|
motionY = 0;
|
|
motionZ = 0;
|
|
barrierImpactPauseTicks--;
|
|
|
|
if (barrierImpactPauseTicks <= 0) {
|
|
if (barrierImpactDestroyOnResume) {
|
|
hasHit = true;
|
|
this.setDead();
|
|
} else {
|
|
motionX = pausedMotionX;
|
|
motionY = pausedMotionY;
|
|
motionZ = pausedMotionZ;
|
|
}
|
|
barrierImpactDestroyOnResume = false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected void tickBarrierSparkTimer() {
|
|
int sparkTicks = getBarrierSparkTicks();
|
|
if (sparkTicks > 0) {
|
|
setBarrierSparkTicks(sparkTicks - 1);
|
|
}
|
|
}
|
|
|
|
protected void setBarrierSparkTicks(int ticks) {
|
|
if (!worldObj.isRemote) {
|
|
this.dataWatcher.updateObject(DW_BARRIER_SPARK_TICKS, Math.max(0, ticks));
|
|
}
|
|
}
|
|
|
|
public int getBarrierSparkTicks() {
|
|
if (this.dataWatcher == null) {
|
|
return 0;
|
|
}
|
|
return Math.max(0, this.dataWatcher.getWatchableObjectInt(DW_BARRIER_SPARK_TICKS));
|
|
}
|
|
|
|
protected void syncProjectileColorWatchers() {
|
|
if (this.dataWatcher == null) {
|
|
return;
|
|
}
|
|
this.dataWatcher.updateObject(DW_SYNC_INNER_COLOR, this.displayData.getInnerColor());
|
|
this.dataWatcher.updateObject(DW_SYNC_OUTER_COLOR, this.displayData.getOuterColor());
|
|
}
|
|
|
|
protected void syncDisplayColorsFromWatchers() {
|
|
if (this.dataWatcher == null) {
|
|
return;
|
|
}
|
|
this.displayData.setInnerColor(this.dataWatcher.getWatchableObjectInt(DW_SYNC_INNER_COLOR));
|
|
this.displayData.setOuterColor(this.dataWatcher.getWatchableObjectInt(DW_SYNC_OUTER_COLOR));
|
|
}
|
|
|
|
@Override
|
|
public void setInnerColor(int color) {
|
|
super.setInnerColor(color);
|
|
if (!previewMode) {
|
|
syncProjectileColorWatchers();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setOuterColor(int color) {
|
|
super.setOuterColor(color);
|
|
if (!previewMode) {
|
|
syncProjectileColorWatchers();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the projectile damage modified by extenders (e.g. DBC damage scaling).
|
|
* Falls back to base getDamage() when no sourceAbility or no extenders modify it.
|
|
*/
|
|
protected float getModifiedDamage() {
|
|
float damage = getDamage();
|
|
if (sourceAbility != null) {
|
|
Entity owner = getOwnerEntity();
|
|
if (owner instanceof EntityLivingBase) {
|
|
damage = AbilityController.Instance.fireModifyProjectileDamage(
|
|
sourceAbility, (EntityLivingBase) owner, damage);
|
|
}
|
|
}
|
|
return damage;
|
|
}
|
|
|
|
// ==================== POSITION INTERPOLATION ====================
|
|
|
|
@Override
|
|
public void setPositionAndRotation2(double x, double y, double z, float yaw, float pitch, int posRotationIncrements) {
|
|
int steps = Math.max(1, posRotationIncrements);
|
|
double dx = x - this.posX;
|
|
double dy = y - this.posY;
|
|
double dz = z - this.posZ;
|
|
double dotToCurrentMotion = dx * this.motionX + dy * this.motionY + dz * this.motionZ;
|
|
|
|
// Spawn-time and large correction snaps: avoid visible startup rubber-banding.
|
|
if (worldObj != null && worldObj.isRemote) {
|
|
double distSq = dx * dx + dy * dy + dz * dz;
|
|
// Launch-time stale tracker packets can arrive one update late and point behind
|
|
// an already-predicted projectile. Skipping these avoids visible backward "bounce".
|
|
if (ticksExisted <= 24 && distSq <= 4.0D && dotToCurrentMotion < -0.0025D) {
|
|
return;
|
|
}
|
|
if (ticksExisted <= 2 || distSq > 16.0D) {
|
|
this.setPosition(x, y, z);
|
|
syncPositionState(x, y, z, true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.interpTargetX = x;
|
|
this.interpTargetY = y;
|
|
this.interpTargetZ = z;
|
|
if (worldObj != null && worldObj.isRemote && ticksExisted <= 24 && dotToCurrentMotion < 0.0D) {
|
|
// Keep existing velocity when correction points opposite current travel
|
|
// to prevent launch-time reverse lerp.
|
|
this.interpTargetMotionX = this.motionX;
|
|
this.interpTargetMotionY = this.motionY;
|
|
this.interpTargetMotionZ = this.motionZ;
|
|
} else {
|
|
this.interpTargetMotionX = dx / steps;
|
|
this.interpTargetMotionY = dy / steps;
|
|
this.interpTargetMotionZ = dz / steps;
|
|
}
|
|
this.interpSteps = steps;
|
|
}
|
|
|
|
@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.
|
|
* Only moves position during active interpolation (interpSteps > 0).
|
|
* Does NOT predict/drift between server updates — the motion values are
|
|
* derived from correction deltas, not actual entity velocity, so applying
|
|
* them between updates causes entities to fly off in wrong directions.
|
|
* This matches vanilla Minecraft behavior for non-player entities.
|
|
*/
|
|
protected void handleClientInterpolation() {
|
|
if (this.interpSteps > 0) {
|
|
double dx = this.interpTargetX - this.posX;
|
|
double dy = this.interpTargetY - this.posY;
|
|
double dz = this.interpTargetZ - this.posZ;
|
|
double distSq = dx * dx + dy * dy + dz * dz;
|
|
|
|
if (distSq < 0.0001) {
|
|
this.setPosition(this.interpTargetX, this.interpTargetY, this.interpTargetZ);
|
|
this.interpSteps = 0;
|
|
return;
|
|
}
|
|
|
|
double newX = this.posX + dx / this.interpSteps;
|
|
double newY = this.posY + dy / this.interpSteps;
|
|
double newZ = this.posZ + dz / 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--;
|
|
}
|
|
}
|
|
|
|
// ==================== DAMAGE & EFFECTS ====================
|
|
|
|
protected boolean applyDamage(EntityLivingBase target) {
|
|
if (previewMode) return false; // Skip damage in preview mode
|
|
return applyDamage(target, this.getDamage(), this.getKnockback());
|
|
}
|
|
|
|
protected boolean applyDamage(EntityLivingBase target, float dmg, float kb) {
|
|
if (previewMode) return false; // Skip damage in preview mode
|
|
if (target == null || shouldIgnoreEntity(target)) return false;
|
|
|
|
// Fire entity impact event (may cancel or modify damage)
|
|
if (!worldObj.isRemote) {
|
|
float result = EventHooks.onEnergyProjectileEntityImpact(this, target, dmg);
|
|
if (result < 0) return false; // Event was cancelled
|
|
dmg = result;
|
|
}
|
|
|
|
Entity owner = getOwnerEntity();
|
|
boolean ignoreIFrames = isIgnoreIFrames();
|
|
|
|
// Check for ability extenders (e.g., DBC Addon damage routing)
|
|
boolean handled = false;
|
|
boolean defaultDamageApplied = false;
|
|
if (sourceAbility != null && owner instanceof EntityLivingBase) {
|
|
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, damageMultiplier);
|
|
}
|
|
// Fallback: route through EnergyController for script-created entities with custom damage data
|
|
if (!handled && customDamageData != null && owner instanceof EntityLivingBase) {
|
|
double dx = target.posX - posX;
|
|
double dz = target.posZ - posZ;
|
|
float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f;
|
|
handled = EnergyController.Instance.fireOnEnergyDamage(
|
|
this, (EntityLivingBase) owner, target,
|
|
dmg, kb, kbUp, dx, dz, damageMultiplier, customDamageData);
|
|
}
|
|
|
|
int previousHurtResistantTime = Ability.clearHurtResistanceIfNeeded(target, ignoreIFrames);
|
|
try {
|
|
if (!handled) {
|
|
float finalDmg = dmg;
|
|
|
|
// Apply magic pipeline for player and NPC casters
|
|
if (this.magicData != null && !this.magicData.isEmpty() && owner instanceof EntityLivingBase) {
|
|
finalDmg = AttributeAttackUtil.calculateAbilityDamage(
|
|
(EntityLivingBase) owner, target, dmg, this.magicData);
|
|
}
|
|
|
|
// Default damage path
|
|
if (owner instanceof EntityNPCInterface) {
|
|
defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), finalDmg);
|
|
} else if (owner instanceof EntityPlayer) {
|
|
defaultDamageApplied = target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), finalDmg);
|
|
} else if (owner instanceof EntityLivingBase) {
|
|
defaultDamageApplied = target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), finalDmg);
|
|
} else {
|
|
defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", null), finalDmg);
|
|
}
|
|
}
|
|
} finally {
|
|
Ability.restoreHurtResistanceIfNeeded(target, ignoreIFrames, previousHurtResistantTime);
|
|
}
|
|
|
|
boolean allowSecondaryEffects = handled || dmg <= 0 || defaultDamageApplied;
|
|
if (!allowSecondaryEffects) return false;
|
|
|
|
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;
|
|
double kbY = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1;
|
|
target.addVelocity(kbX, kbY, kbZ);
|
|
target.velocityChanged = true;
|
|
}
|
|
}
|
|
|
|
applyEffects(target);
|
|
return true;
|
|
}
|
|
|
|
protected void applyEffects(EntityLivingBase target) {
|
|
for (AbilityPotionEffect effect : effects) {
|
|
effect.apply(target);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the effects list from the ability's configured effects.
|
|
*/
|
|
public void setAnchorData(EnergyAnchorData anchor) {
|
|
this.anchorData = anchor != null ? anchor.copy() : new EnergyAnchorData(AnchorPoint.FRONT);
|
|
}
|
|
|
|
public void setEffects(List<AbilityPotionEffect> effects) {
|
|
if (effects == null || effects.isEmpty()) {
|
|
this.effects = new ArrayList<>();
|
|
} else {
|
|
// Deep copy: entity must not share the ability's effects list or its entries.
|
|
this.effects = new ArrayList<>(effects.size());
|
|
for (AbilityPotionEffect effect : effects) {
|
|
this.effects.add(effect.copy());
|
|
}
|
|
}
|
|
}
|
|
|
|
protected boolean doExplosion() {
|
|
if (previewMode) return false; // Skip explosion in preview mode
|
|
if (worldObj.isRemote) return false; // Explosions are server-side only
|
|
float explosionRad = getExplosionRadius();
|
|
if (Float.isNaN(explosionRad) || explosionRad <= 0) return false;
|
|
final double explosionRadSq = explosionRad * explosionRad;
|
|
final float baseDamage = getDamage();
|
|
final float baseKnockback = getKnockback();
|
|
final float damageFalloff = getExplosionDamageFalloff();
|
|
spawnExplosionRenderEntity(explosionRad);
|
|
spawnExplosionVisuals(explosionRad);
|
|
worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f);
|
|
|
|
Entity owner = getOwnerEntity();
|
|
|
|
AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox(
|
|
posX - explosionRad, posY - explosionRad, posZ - explosionRad,
|
|
posX + explosionRad, posY + explosionRad, posZ + explosionRad
|
|
);
|
|
|
|
@SuppressWarnings("unchecked")
|
|
List<EntityLivingBase> targets = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox);
|
|
boolean anyDamaged = false;
|
|
|
|
for (EntityLivingBase target : targets) {
|
|
if (target == owner) continue;
|
|
if (shouldIgnoreExplosionTarget(target)) continue;
|
|
|
|
// Measure distance to nearest point on entity's bounding box, not feet position.
|
|
// Using feet position (target.posY) causes explosions at impact height to miss
|
|
// entities standing below — e.g. a projectile hitting at eye height (~1.62 above feet)
|
|
// would measure dist=1.62, failing a radius<=1.6 check despite a direct hit.
|
|
if (target.boundingBox == null) continue;
|
|
double closestX = Math.max(target.boundingBox.minX, Math.min(posX, target.boundingBox.maxX));
|
|
double closestY = Math.max(target.boundingBox.minY, Math.min(posY, target.boundingBox.maxY));
|
|
double closestZ = Math.max(target.boundingBox.minZ, Math.min(posZ, target.boundingBox.maxZ));
|
|
double dx = closestX - posX;
|
|
double dy = closestY - posY;
|
|
double dz = closestZ - posZ;
|
|
double distSq = dx * dx + dy * dy + dz * dz;
|
|
if (distSq > explosionRadSq) continue;
|
|
|
|
float falloff = 1.0f;
|
|
if (damageFalloff != 0.0f && distSq > 0.0D) {
|
|
float dist = (float) Math.sqrt(distSq);
|
|
falloff = 1.0f - (dist / explosionRad) * damageFalloff;
|
|
}
|
|
if (applyDamage(target, baseDamage * falloff, baseKnockback * falloff)) {
|
|
anyDamaged = true;
|
|
}
|
|
}
|
|
|
|
tryDestroyTerrain(explosionRad);
|
|
return anyDamaged;
|
|
}
|
|
|
|
/**
|
|
* Explosion safety filter.
|
|
* Reuses standard projectile-friendly checks (owner, faction, party).
|
|
*/
|
|
protected boolean shouldIgnoreExplosionTarget(EntityLivingBase target) {
|
|
if (target == null) return true;
|
|
return shouldIgnoreEntity(target);
|
|
}
|
|
|
|
/**
|
|
* Clamp a point to the nearest point inside the provided AABB.
|
|
*/
|
|
protected Vec3 closestPointOnBoundingBox(AxisAlignedBB box, double x, double y, double z) {
|
|
if (box == null) return null;
|
|
double closestX = Math.max(box.minX, Math.min(x, box.maxX));
|
|
double closestY = Math.max(box.minY, Math.min(y, box.maxY));
|
|
double closestZ = Math.max(box.minZ, Math.min(z, box.maxZ));
|
|
return Vec3.createVectorHelper(closestX, closestY, closestZ);
|
|
}
|
|
|
|
/**
|
|
* Spawn a packet-driven client preview so explosions are visible as geometry,
|
|
* not only as particles.
|
|
*/
|
|
protected void spawnExplosionRenderEntity(float explosionRad) {
|
|
if (worldObj == null || worldObj.isRemote) return;
|
|
float renderRad = Math.max(0.75f, Math.min(explosionRad, EnergyCombatData.MAX_EXPLOSION_RADIUS));
|
|
EntityEnergyExplosion fx = new EntityEnergyExplosion(worldObj, this, renderRad);
|
|
EnergyExplosionSpawnPacket.sendToTracking(getExplosionVisualInstanceId(), fx, this);
|
|
}
|
|
|
|
protected String getExplosionVisualInstanceId() {
|
|
return "energy_explosion_" + UUID.randomUUID();
|
|
}
|
|
|
|
/**
|
|
* Shared voxel-style explosion VFX for all energy projectile abilities.
|
|
*/
|
|
protected void spawnExplosionVisuals(float explosionRad) {
|
|
if (worldObj == null) return;
|
|
|
|
float visualRad = Math.max(0.75f, Math.min(explosionRad, EnergyCombatData.MAX_EXPLOSION_RADIUS));
|
|
int coreBursts = Math.max(1, Math.min(4, Math.round(visualRad * 0.35f)));
|
|
int shellCount = Math.max(1, Math.min(3, Math.round(visualRad / 3.5f) + 1));
|
|
|
|
// Core flash.
|
|
emitRandomBurstParticle("largeexplode", coreBursts, visualRad * 0.10, visualRad * 0.08, visualRad * 0.10, 0.01);
|
|
|
|
// Expanding voxel shells.
|
|
for (int i = 1; i <= shellCount; i++) {
|
|
double frac = i / (double) shellCount;
|
|
double shellRadius = visualRad * (0.35D + 0.65D * frac);
|
|
spawnVoxelShell(shellRadius, i, visualRad);
|
|
}
|
|
|
|
// Residual smoke volume.
|
|
int smokeCount = Math.max(6, Math.min(40, Math.round(visualRad * 4.5f)));
|
|
emitRandomBurstParticle("smoke", smokeCount, visualRad * 0.45, visualRad * 0.30, visualRad * 0.45, 0.01);
|
|
|
|
// Pull block fragments for stronger voxel feel.
|
|
spawnVoxelDebris(visualRad);
|
|
}
|
|
|
|
/**
|
|
* Emit random burst particles centered around this explosion.
|
|
*/
|
|
protected void emitRandomBurstParticle(String particle, int count,
|
|
double spreadX, double spreadY, double spreadZ,
|
|
double speed) {
|
|
if (worldObj == null || count <= 0 || particle == null || particle.isEmpty()) return;
|
|
|
|
if (worldObj.isRemote) {
|
|
for (int i = 0; i < count; i++) {
|
|
double px = posX + (rand.nextDouble() - 0.5D) * 2.0D * spreadX;
|
|
double py = posY + (rand.nextDouble() - 0.5D) * 2.0D * spreadY;
|
|
double pz = posZ + (rand.nextDouble() - 0.5D) * 2.0D * spreadZ;
|
|
double mx = rand.nextGaussian() * speed;
|
|
double my = rand.nextGaussian() * speed;
|
|
double mz = rand.nextGaussian() * speed;
|
|
worldObj.spawnParticle(particle, px, py, pz, mx, my, mz);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (worldObj instanceof WorldServer) {
|
|
((WorldServer) worldObj).func_147487_a(particle, posX, posY, posZ, count, spreadX, spreadY, spreadZ, speed);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emits a single directed voxel particle.
|
|
* Uses count=0 packet mode on server so motion values are preserved.
|
|
*/
|
|
protected void emitVoxelParticle(String particle, double x, double y, double z,
|
|
double motionX, double motionY, double motionZ) {
|
|
if (worldObj == null || particle == null || particle.isEmpty()) return;
|
|
|
|
if (worldObj.isRemote) {
|
|
worldObj.spawnParticle(particle, x, y, z, motionX, motionY, motionZ);
|
|
return;
|
|
}
|
|
|
|
if (worldObj instanceof WorldServer) {
|
|
((WorldServer) worldObj).func_147487_a(particle, x, y, z, 0, motionX, motionY, motionZ, 1.0D);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Spawn one cubic shell of explosion particles to get a blocky/voxel silhouette.
|
|
*/
|
|
protected void spawnVoxelShell(double shellRadius, int shellIndex, float visualRad) {
|
|
int steps = Math.max(2, Math.min(4, (int) Math.ceil(shellRadius * 0.55D)));
|
|
double cell = shellRadius / steps;
|
|
double jitter = Math.min(0.15D, cell * 0.20D);
|
|
double shellSpeed = 0.03D + (shellRadius / Math.max(1.0D, visualRad)) * 0.05D;
|
|
|
|
for (int ix = -steps; ix <= steps; ix++) {
|
|
for (int iy = -steps; iy <= steps; iy++) {
|
|
for (int iz = -steps; iz <= steps; iz++) {
|
|
int edge = Math.max(Math.abs(ix), Math.max(Math.abs(iy), Math.abs(iz)));
|
|
if (edge != steps) continue;
|
|
if (((ix + iy + iz + shellIndex) & 1) != 0) continue;
|
|
|
|
double px = posX + ix * cell + (rand.nextDouble() - 0.5D) * jitter;
|
|
double py = posY + iy * cell + (rand.nextDouble() - 0.5D) * jitter;
|
|
double pz = posZ + iz * cell + (rand.nextDouble() - 0.5D) * jitter;
|
|
|
|
double nx = ix;
|
|
double ny = iy;
|
|
double nz = iz;
|
|
double len = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
if (len < 0.001D) {
|
|
ny = 1.0D;
|
|
len = 1.0D;
|
|
}
|
|
nx /= len;
|
|
ny /= len;
|
|
nz /= len;
|
|
|
|
emitVoxelParticle("explode", px, py, pz, nx * shellSpeed, ny * shellSpeed, nz * shellSpeed);
|
|
|
|
if (((ix + iy + iz + shellIndex) % 3) == 0) {
|
|
emitVoxelParticle("smoke", px, py, pz, nx * shellSpeed * 0.35D, ny * shellSpeed * 0.35D, nz * shellSpeed * 0.35D);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Spawn debris particles using nearby block textures.
|
|
*/
|
|
protected void spawnVoxelDebris(float visualRad) {
|
|
if (worldObj == null) return;
|
|
|
|
int count = Math.max(4, Math.min(24, Math.round(visualRad * 2.5f)));
|
|
for (int i = 0; i < count; i++) {
|
|
double ox = (rand.nextDouble() - 0.5D) * visualRad * 1.2D;
|
|
double oy = (rand.nextDouble() - 0.35D) * visualRad * 0.9D;
|
|
double oz = (rand.nextDouble() - 0.5D) * visualRad * 1.2D;
|
|
|
|
int bx = MathHelper.floor_double(posX + ox);
|
|
int by = MathHelper.floor_double(posY + oy);
|
|
int bz = MathHelper.floor_double(posZ + oz);
|
|
|
|
Block block = worldObj.getBlock(bx, by, bz);
|
|
if (block == null || block == Blocks.air) continue;
|
|
|
|
int blockId = Block.getIdFromBlock(block);
|
|
if (blockId <= 0) continue;
|
|
int meta = worldObj.getBlockMetadata(bx, by, bz);
|
|
|
|
String particle = "blockcrack_" + blockId + "_" + Math.max(0, meta);
|
|
emitVoxelParticle(
|
|
particle,
|
|
posX + ox * 0.55D,
|
|
posY + oy * 0.55D,
|
|
posZ + oz * 0.55D,
|
|
ox * 0.03D, oy * 0.03D, oz * 0.03D
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Optional terrain damage for energy explosions.
|
|
* Controlled server-side by ConfigEnergy.EnableEnergyExplosionBlockDamage.
|
|
*/
|
|
protected void tryDestroyTerrain(float explosionRad) {
|
|
if (worldObj == null || worldObj.isRemote) return;
|
|
if (!ConfigEnergy.EnableEnergyExplosionBlockDamage) return;
|
|
if (!worldObj.getGameRules().getGameRuleBooleanValue("mobGriefing")) return;
|
|
|
|
float terrainRad = Math.max(0.5f, Math.min(explosionRad, EnergyCombatData.MAX_EXPLOSION_RADIUS));
|
|
int minX = MathHelper.floor_double(posX - terrainRad);
|
|
int maxX = MathHelper.floor_double(posX + terrainRad);
|
|
int minY = MathHelper.floor_double(posY - terrainRad);
|
|
int maxY = MathHelper.floor_double(posY + terrainRad);
|
|
int minZ = MathHelper.floor_double(posZ - terrainRad);
|
|
int maxZ = MathHelper.floor_double(posZ + terrainRad);
|
|
int worldMaxY = Math.max(0, worldObj.getActualHeight() - 1);
|
|
if (maxY < 0 || minY > worldMaxY) return;
|
|
float resistanceCutoff = Math.max(4.0f, terrainRad * 6.0f);
|
|
|
|
Explosion context = new Explosion(worldObj, this, posX, posY, posZ, terrainRad);
|
|
context.isFlaming = false;
|
|
context.isSmoking = true;
|
|
int lastChunkX = Integer.MIN_VALUE;
|
|
int lastChunkZ = Integer.MIN_VALUE;
|
|
boolean lastChunkLoaded = false;
|
|
|
|
for (int x = minX; x <= maxX; x++) {
|
|
for (int y = minY; y <= maxY; y++) {
|
|
for (int z = minZ; z <= maxZ; z++) {
|
|
double cx = x + 0.5D - posX;
|
|
double cy = y + 0.5D - posY;
|
|
double cz = z + 0.5D - posZ;
|
|
|
|
// Chebyshev metric yields a voxel/cubic blast shape.
|
|
double chebyshev = Math.max(Math.abs(cx), Math.max(Math.abs(cy), Math.abs(cz)));
|
|
if (chebyshev > terrainRad) continue;
|
|
|
|
// Soften cube corners a bit for a less artificial crater edge.
|
|
double normalized = chebyshev / terrainRad;
|
|
if (normalized > 0.72D && rand.nextDouble() < (normalized - 0.72D) * 1.4D) continue;
|
|
if (y < 0 || y > worldMaxY) continue;
|
|
|
|
int chunkX = x >> 4;
|
|
int chunkZ = z >> 4;
|
|
if (chunkX != lastChunkX || chunkZ != lastChunkZ) {
|
|
lastChunkX = chunkX;
|
|
lastChunkZ = chunkZ;
|
|
lastChunkLoaded = worldObj.getChunkProvider().chunkExists(chunkX, chunkZ);
|
|
}
|
|
if (!lastChunkLoaded) continue;
|
|
|
|
Block block = worldObj.getBlock(x, y, z);
|
|
if (block == null || block == Blocks.air || block == Blocks.bedrock) continue;
|
|
Material material = block.getMaterial();
|
|
if (material == null || material == Material.air) continue;
|
|
|
|
float resistance = block.getExplosionResistance(this, worldObj, x, y, z, posX, posY, posZ);
|
|
if (resistance > resistanceCutoff) continue;
|
|
|
|
try {
|
|
block.onBlockExploded(worldObj, x, y, z, context);
|
|
} catch (Exception ignored) {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================== LAUNCH HELPERS ====================
|
|
|
|
/**
|
|
* Snaps the projectile position to the owner's look vector on launch.
|
|
* This ensures projectiles launch aligned with the owner's facing direction
|
|
* rather than from the last anchor point position.
|
|
* Should be called in startMoving/startFiring BEFORE setting startX/Y/Z.
|
|
*/
|
|
protected void snapToLookVector() {
|
|
setLookVectorLaunchPosition(true);
|
|
}
|
|
|
|
/**
|
|
* Shared launch transition: exit charging, snap to look-vector launch origin,
|
|
* sync start position, and optionally reset interpolation state.
|
|
*
|
|
* @param resetInterpolationState true to reset prev/lastTick/interp state to launch origin.
|
|
* @return true when look-vector launch origin was resolved from owner/look data.
|
|
*/
|
|
protected boolean beginLookVectorLaunch(boolean resetInterpolationState) {
|
|
setCharging(false);
|
|
Entity owner = getOwnerEntity();
|
|
boolean positioned = false;
|
|
if (owner instanceof EntityLivingBase && shouldSnapLaunchToLookVector((EntityLivingBase) owner)) {
|
|
positioned = setLookVectorLaunchPosition((EntityLivingBase) owner, getOwnerLookVector(), true);
|
|
}
|
|
syncStartPositionToCurrent();
|
|
if (resetInterpolationState) {
|
|
syncPositionStateToCurrent(true);
|
|
}
|
|
return positioned;
|
|
}
|
|
|
|
/**
|
|
* Shared launch transition with explicit owner/look vector data.
|
|
*/
|
|
protected boolean beginLookVectorLaunch(EntityLivingBase owner, Vec3 look, boolean resetInterpolationState) {
|
|
setCharging(false);
|
|
boolean positioned = shouldSnapLaunchToLookVector(owner) && setLookVectorLaunchPosition(owner, look, true);
|
|
syncStartPositionToCurrent();
|
|
if (resetInterpolationState) {
|
|
syncPositionStateToCurrent(true);
|
|
}
|
|
return positioned;
|
|
}
|
|
|
|
/**
|
|
* Determines whether the projectile should snap to the look-vector launch origin on fire.
|
|
* By default, player-cast projectiles snap (launch from eye position along crosshair),
|
|
* and NPC projectiles keep their anchor charge origin.
|
|
* When {@code anchorData.launchFromAnchor} is true, snapping is disabled so the
|
|
* projectile launches from its configured anchor position instead.
|
|
*/
|
|
protected boolean shouldSnapLaunchToLookVector(EntityLivingBase owner) {
|
|
if (anchorData.launchFromAnchor) {
|
|
return false;
|
|
}
|
|
return owner instanceof EntityPlayer;
|
|
}
|
|
|
|
/**
|
|
* Default preview launch: snap to look-vector origin and fire along look.
|
|
*/
|
|
protected void startPreviewFiringDefault() {
|
|
beginLookVectorLaunch(true);
|
|
setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Default active launch: snap to look-vector origin and fire toward target when available.
|
|
*/
|
|
protected void startMovingTowardTargetDefault(EntityLivingBase target) {
|
|
beginLookVectorLaunch(false);
|
|
setMotionTowardTargetOrLookVector(target, posX, posY, posZ, getSpeed(), getSpeed(), 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Default active launch from fixed origin fields (used by beam-like projectiles).
|
|
*/
|
|
protected void startMovingTowardTargetFromStartDefault(EntityLivingBase target) {
|
|
beginLookVectorLaunch(false);
|
|
setMotionTowardTargetOrLookVector(target, startX, startY, startZ, getSpeed(), getSpeed(), 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Default active launch strictly along look vector.
|
|
*/
|
|
protected void startMovingAlongLookVectorDefault() {
|
|
beginLookVectorLaunch(false);
|
|
setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Resolve the owner's look vector.
|
|
* Falls back to yaw/pitch math if look vector is unavailable.
|
|
*/
|
|
protected Vec3 getOwnerLookVector() {
|
|
Entity owner = getOwnerEntity();
|
|
if (!(owner instanceof EntityLivingBase)) return null;
|
|
|
|
Vec3 look = owner.getLookVec();
|
|
if (look != null) return look;
|
|
|
|
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
|
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
|
return Vec3.createVectorHelper(
|
|
-Math.sin(yaw) * Math.cos(pitch),
|
|
-Math.sin(pitch),
|
|
Math.cos(yaw) * Math.cos(pitch)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Shared launch origin: owner eye position + look-vector offset.
|
|
*
|
|
* @param syncPositionState When true, sync prev/lastTick/interp targets to this position.
|
|
*/
|
|
protected boolean setLookVectorLaunchPosition(boolean syncPositionState) {
|
|
Entity owner = getOwnerEntity();
|
|
if (!(owner instanceof EntityLivingBase)) return false;
|
|
Vec3 look = getOwnerLookVector();
|
|
if (look == null) return false;
|
|
return setLookVectorLaunchPosition((EntityLivingBase) owner, look, syncPositionState);
|
|
}
|
|
|
|
/**
|
|
* Shared launch origin using provided owner/look data.
|
|
*/
|
|
protected boolean setLookVectorLaunchPosition(EntityLivingBase owner, Vec3 look, boolean syncPositionState) {
|
|
if (owner == null || look == null) return false;
|
|
|
|
double originX = owner.posX;
|
|
double originY = owner.posY + owner.getEyeHeight();
|
|
double originZ = owner.posZ;
|
|
|
|
float frontDist = computeLaunchFrontDistance(owner, look, originX, originY, originZ);
|
|
double newX = originX + look.xCoord * frontDist;
|
|
double newY = originY + look.yCoord * frontDist;
|
|
double newZ = originZ + look.zCoord * frontDist;
|
|
|
|
setPosition(newX, newY, newZ);
|
|
if (syncPositionState) {
|
|
syncPositionState(newX, newY, newZ, true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Radius used to clear the caster hitbox on launch.
|
|
* Subclasses may override if their effective launch footprint differs from {@code size}.
|
|
*/
|
|
protected float getLaunchClearanceRadius() {
|
|
return Math.max(0.1f, size * 0.5f);
|
|
}
|
|
|
|
private float computeLaunchFrontDistance(EntityLivingBase owner, Vec3 look, double originX, double originY, double originZ) {
|
|
// Legacy baseline offset retained as lower bound.
|
|
float baseline = size * 0.4f;
|
|
float clearance = getLaunchClearanceRadius() + 0.05f;
|
|
|
|
AxisAlignedBB bb = owner.boundingBox;
|
|
if (bb == null) {
|
|
return Math.max(baseline, owner.width * 0.5f + clearance);
|
|
}
|
|
|
|
double dx = look.xCoord;
|
|
double dy = look.yCoord;
|
|
double dz = look.zCoord;
|
|
|
|
double tx = computeRayExitT(originX, dx, bb.minX, bb.maxX);
|
|
double ty = computeRayExitT(originY, dy, bb.minY, bb.maxY);
|
|
double tz = computeRayExitT(originZ, dz, bb.minZ, bb.maxZ);
|
|
|
|
double tExit = Math.min(tx, Math.min(ty, tz));
|
|
if (Double.isInfinite(tExit) || tExit < 0) {
|
|
return Math.max(baseline, owner.width * 0.5f + clearance);
|
|
}
|
|
|
|
return (float) Math.max(baseline, tExit + clearance);
|
|
}
|
|
|
|
private double computeRayExitT(double origin, double dir, double min, double max) {
|
|
final double EPS = 1.0e-6;
|
|
if (dir > EPS) {
|
|
return (max - origin) / dir;
|
|
} else if (dir < -EPS) {
|
|
return (min - origin) / dir;
|
|
}
|
|
return Double.POSITIVE_INFINITY;
|
|
}
|
|
|
|
/**
|
|
* Set projectile motion along owner look vector.
|
|
*
|
|
* @return true if owner/look were available.
|
|
*/
|
|
protected boolean setMotionAlongLookVector(float speed) {
|
|
Vec3 look = getOwnerLookVector();
|
|
if (look == null) return false;
|
|
|
|
motionX = look.xCoord * speed;
|
|
motionY = look.yCoord * speed;
|
|
motionZ = look.zCoord * speed;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set motion along owner look vector or apply explicit fallback values.
|
|
* When {@code launchFromAnchor} is true, ray-casts from the owner's eye to find the
|
|
* crosshair target point and aims from the projectile's anchor position toward it.
|
|
*/
|
|
protected void setMotionAlongLookVectorOrFallback(float speed, double fallbackX, double fallbackY, double fallbackZ) {
|
|
if (anchorData.launchFromAnchor && setMotionTowardLookTarget(speed)) {
|
|
return;
|
|
}
|
|
if (!setMotionAlongLookVector(speed)) {
|
|
motionX = fallbackX;
|
|
motionY = fallbackY;
|
|
motionZ = fallbackZ;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ray-cast from the owner's eye along the look vector to find the crosshair target point,
|
|
* then set motion from the projectile's current position toward that point.
|
|
* Used when {@code launchFromAnchor} is true so projectiles fired from offset anchor
|
|
* positions converge to the crosshair rather than flying parallel to the look direction.
|
|
*/
|
|
protected boolean setMotionTowardLookTarget(float speed) {
|
|
Entity owner = getOwnerEntity();
|
|
if (!(owner instanceof EntityLivingBase)) return false;
|
|
Vec3 look = getOwnerLookVector();
|
|
if (look == null) return false;
|
|
|
|
EntityLivingBase livingOwner = (EntityLivingBase) owner;
|
|
double eyeX = livingOwner.posX;
|
|
double eyeY = livingOwner.posY + livingOwner.getEyeHeight();
|
|
double eyeZ = livingOwner.posZ;
|
|
|
|
double maxDist = 200.0;
|
|
Vec3 start = Vec3.createVectorHelper(eyeX, eyeY, eyeZ);
|
|
Vec3 end = Vec3.createVectorHelper(
|
|
eyeX + look.xCoord * maxDist,
|
|
eyeY + look.yCoord * maxDist,
|
|
eyeZ + look.zCoord * maxDist
|
|
);
|
|
|
|
double targetX, targetY, targetZ;
|
|
MovingObjectPosition hit = worldObj.rayTraceBlocks(start, end);
|
|
if (hit != null && hit.hitVec != null) {
|
|
targetX = hit.hitVec.xCoord;
|
|
targetY = hit.hitVec.yCoord;
|
|
targetZ = hit.hitVec.zCoord;
|
|
} else {
|
|
targetX = eyeX + look.xCoord * maxDist;
|
|
targetY = eyeY + look.yCoord * maxDist;
|
|
targetZ = eyeZ + look.zCoord * maxDist;
|
|
}
|
|
|
|
double dx = targetX - posX;
|
|
double dy = targetY - posY;
|
|
double dz = targetZ - posZ;
|
|
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
if (len <= 0.0001) return false;
|
|
|
|
motionX = (dx / len) * speed;
|
|
motionY = (dy / len) * speed;
|
|
motionZ = (dz / len) * speed;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set projectile motion toward a target from a given source point.
|
|
*
|
|
* @return true if target was valid and motion was updated.
|
|
*/
|
|
protected boolean setMotionTowardTarget(EntityLivingBase target, double sourceX, double sourceY, double sourceZ, float speed) {
|
|
if (target == null) return false;
|
|
|
|
double dx = target.posX - sourceX;
|
|
double dy = (target.posY + target.getEyeHeight()) - sourceY;
|
|
double dz = target.posZ - sourceZ;
|
|
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
if (len <= 0.0001) return false;
|
|
|
|
motionX = (dx / len) * speed;
|
|
motionY = (dy / len) * speed;
|
|
motionZ = (dz / len) * speed;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set motion toward target from source point; when target is unavailable,
|
|
* fall back to owner look vector, then explicit fallback values.
|
|
*/
|
|
protected void setMotionTowardTargetOrLookVector(EntityLivingBase target, double sourceX, double sourceY, double sourceZ, float speed,
|
|
double fallbackX, double fallbackY, double fallbackZ) {
|
|
if (!setMotionTowardTarget(target, sourceX, sourceY, sourceZ, speed)) {
|
|
setMotionAlongLookVectorOrFallback(speed, fallbackX, fallbackY, fallbackZ);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync start position (distance origin) to the current entity position.
|
|
*/
|
|
protected void syncStartPositionToCurrent() {
|
|
startX = posX;
|
|
startY = posY;
|
|
startZ = posZ;
|
|
}
|
|
|
|
/**
|
|
* Sync prev/lastTick and optional interpolation state to an explicit position.
|
|
*/
|
|
protected void syncPositionState(double x, double y, double z, boolean resetInterpolation) {
|
|
prevPosX = x;
|
|
prevPosY = y;
|
|
prevPosZ = z;
|
|
lastTickPosX = x;
|
|
lastTickPosY = y;
|
|
lastTickPosZ = z;
|
|
|
|
if (resetInterpolation) {
|
|
interpTargetX = x;
|
|
interpTargetY = y;
|
|
interpTargetZ = z;
|
|
interpTargetMotionX = motionX;
|
|
interpTargetMotionY = motionY;
|
|
interpTargetMotionZ = motionZ;
|
|
interpSteps = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync prev/lastTick and optional interpolation state to current entity position.
|
|
*/
|
|
protected void syncPositionStateToCurrent(boolean resetInterpolation) {
|
|
syncPositionState(posX, posY, posZ, resetInterpolation);
|
|
}
|
|
|
|
// ==================== CHARGING METHODS (projectile-specific) ====================
|
|
|
|
/**
|
|
* Shared charging-state bootstrap used by both world and preview setup paths.
|
|
*/
|
|
protected void setupChargingState(EnergyAnchorData anchor, int chargeDuration) {
|
|
setCharging(true);
|
|
this.chargeDuration = chargeDuration;
|
|
this.chargeTick = 0;
|
|
this.anchorData = anchor;
|
|
}
|
|
|
|
/**
|
|
* Shared preview bootstrap used by projectile preview setup methods.
|
|
*/
|
|
protected void setupPreviewState(EntityLivingBase owner, EnergyDisplayData display,
|
|
EnergyLightningData lightning, EnergyAnchorData anchor,
|
|
int chargeDuration) {
|
|
this.setPreviewMode(true);
|
|
this.setPreviewOwner(owner);
|
|
this.displayData = display != null ? display.copy() : new EnergyDisplayData();
|
|
this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData();
|
|
setupChargingState(anchor, chargeDuration);
|
|
}
|
|
|
|
/**
|
|
* Set size and render-size state to a single value.
|
|
*/
|
|
protected void setVisualSize(float value) {
|
|
this.size = value;
|
|
this.renderCurrentSize = value;
|
|
this.prevRenderSize = value;
|
|
}
|
|
|
|
/**
|
|
* Set entity/start state to a fixed position used as charging/launch origin.
|
|
*/
|
|
protected void setChargeOrigin(Vec3 pos) {
|
|
if (pos == null) return;
|
|
this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
|
|
syncPositionState(pos.xCoord, pos.yCoord, pos.zCoord, true);
|
|
this.startX = pos.xCoord;
|
|
this.startY = pos.yCoord;
|
|
this.startZ = pos.zCoord;
|
|
}
|
|
|
|
/**
|
|
* Resolve and apply charging/launch origin from anchor position.
|
|
*/
|
|
protected void setChargeOriginFromAnchor(EntityLivingBase owner, EnergyAnchorData anchor) {
|
|
if (owner == null || anchor == null) return;
|
|
setChargeOrigin(AnchorPointHelper.calculateAnchorPosition(owner, anchor));
|
|
}
|
|
|
|
/**
|
|
* Resolve and apply charging/launch origin from anchor position with forward offset.
|
|
*/
|
|
protected void setChargeOriginFromAnchor(EntityLivingBase owner, EnergyAnchorData anchor, float offsetDistance) {
|
|
if (owner == null || anchor == null) return;
|
|
setChargeOrigin(AnchorPointHelper.calculateAnchorPosition(owner, anchor, offsetDistance));
|
|
}
|
|
|
|
/**
|
|
* Zero current motion.
|
|
*/
|
|
protected void clearMotion() {
|
|
this.motionX = 0;
|
|
this.motionY = 0;
|
|
this.motionZ = 0;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
setupChargingState(anchor, chargeDuration);
|
|
this.targetSize = this.size;
|
|
setVisualSize(0.01f);
|
|
clearMotion();
|
|
}
|
|
|
|
/**
|
|
* 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).
|
|
* <p>
|
|
* 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 ====================
|
|
|
|
/**
|
|
* Accumulated proportional damage multiplier (0.0-1.0).
|
|
* Accumulates multiplicatively when the projectile breaks through barriers, is reflected,
|
|
* or any other system reduces its effective damage. Used by ability extenders to scale
|
|
* recalculated damage proportionally.
|
|
*/
|
|
private float damageMultiplier = 1.0f;
|
|
|
|
public float getDamageMultiplier() {
|
|
return damageMultiplier;
|
|
}
|
|
|
|
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.getExplosionRadius();
|
|
}
|
|
|
|
public void setExplosionRadius(float radius) {
|
|
combatData.setExplosionRadius(radius);
|
|
}
|
|
|
|
public float getExplosionDamageFalloff() {
|
|
return combatData.explosionDamageFalloff;
|
|
}
|
|
|
|
public void setExplosionDamageFalloff(float falloff) {
|
|
combatData.setExplosionDamageFalloff(falloff);
|
|
}
|
|
|
|
public int getHitType() {
|
|
return combatData.hitType.ordinal();
|
|
}
|
|
|
|
public void setHitType(int hitType) {
|
|
combatData.hitType = HitType.fromOrdinal(hitType);
|
|
}
|
|
|
|
public int getMultiHitDelayTicks() {
|
|
return combatData.multiHitDelayTicks;
|
|
}
|
|
|
|
public void setMultiHitDelayTicks(int delayTicks) {
|
|
combatData.multiHitDelayTicks = Math.max(1, delayTicks);
|
|
}
|
|
|
|
public int getMaxHits() {
|
|
return combatData.getMaxHits();
|
|
}
|
|
|
|
public void setMaxHits(int maxHits) {
|
|
combatData.setMaxHits(maxHits);
|
|
}
|
|
|
|
protected boolean canHitEntityNow(EntityLivingBase entity) {
|
|
if (entity == null) return false;
|
|
if (combatData.hitType != HitType.SINGLE && hasReachedMaxHits()) return false;
|
|
|
|
int entityId = entity.getEntityId();
|
|
switch (combatData.hitType) {
|
|
case PIERCE:
|
|
return !hitOnceEntities.contains(entityId);
|
|
case MULTI:
|
|
Integer lastHitTick = lastHitTickByEntity.get(entityId);
|
|
if (lastHitTick == null) return true;
|
|
int delay = Math.max(1, combatData.multiHitDelayTicks);
|
|
return ticksExisted - lastHitTick >= delay;
|
|
case SINGLE:
|
|
default:
|
|
return !hasHit;
|
|
}
|
|
}
|
|
|
|
protected void recordEntityHit(EntityLivingBase entity) {
|
|
if (entity == null) return;
|
|
int entityId = entity.getEntityId();
|
|
hitOnceEntities.add(entityId);
|
|
lastHitTickByEntity.put(entityId, ticksExisted);
|
|
hitCount++;
|
|
}
|
|
|
|
protected boolean hasReachedMaxHits() {
|
|
if (combatData.hitType == HitType.SINGLE) return false;
|
|
return hitCount >= combatData.getMaxHits();
|
|
}
|
|
|
|
protected boolean shouldTerminateAfterHit() {
|
|
if (combatData.hitType == HitType.SINGLE) return true;
|
|
return hasReachedMaxHits();
|
|
}
|
|
|
|
/**
|
|
* Shared block raytrace helper used by most projectile types.
|
|
*/
|
|
protected MovingObjectPosition rayTraceBlocks(double fromX, double fromY, double fromZ, double toX, double toY, double toZ) {
|
|
Vec3 currentPos = Vec3.createVectorHelper(fromX, fromY, fromZ);
|
|
Vec3 nextPos = Vec3.createVectorHelper(toX, toY, toZ);
|
|
return worldObj.func_147447_a(currentPos, nextPos, false, true, false);
|
|
}
|
|
|
|
/**
|
|
* Shared block-hit handling: fires block impact hook, optional explosion, and kills projectile.
|
|
*/
|
|
protected boolean handleBlockImpact(MovingObjectPosition blockHit, boolean explodeAtHitPoint) {
|
|
if (blockHit == null || blockHit.typeOfHit != MovingObjectPosition.MovingObjectType.BLOCK) return false;
|
|
|
|
if (!worldObj.isRemote) {
|
|
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
|
|
}
|
|
hasHit = true;
|
|
|
|
if (isExplosive()) {
|
|
if (explodeAtHitPoint && blockHit.hitVec != null) {
|
|
posX = blockHit.hitVec.xCoord;
|
|
posY = blockHit.hitVec.yCoord;
|
|
posZ = blockHit.hitVec.zCoord;
|
|
}
|
|
doExplosion();
|
|
}
|
|
|
|
this.setDead();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Apply projectile hit behavior to a target and return true if the projectile terminated.
|
|
*/
|
|
protected boolean processEntityHit(EntityLivingBase entity, double impactX, double impactY, double impactZ) {
|
|
if (entity == null || shouldIgnoreEntity(entity) || !canHitEntityNow(entity)) return false;
|
|
|
|
boolean successfulHit;
|
|
if (isExplosive()) {
|
|
Vec3 impactPoint = resolveEntityImpactPoint(entity, impactX, impactY, impactZ);
|
|
double oldX = posX;
|
|
double oldY = posY;
|
|
double oldZ = posZ;
|
|
posX = impactPoint.xCoord;
|
|
posY = impactPoint.yCoord;
|
|
posZ = impactPoint.zCoord;
|
|
successfulHit = doExplosion();
|
|
posX = oldX;
|
|
posY = oldY;
|
|
posZ = oldZ;
|
|
} else {
|
|
successfulHit = applyDamage(entity);
|
|
}
|
|
|
|
if (successfulHit) {
|
|
recordEntityHit(entity);
|
|
}
|
|
|
|
if ((successfulHit && shouldTerminateAfterHit()) || (!successfulHit && combatData.hitType == HitType.SINGLE)) {
|
|
hasHit = true;
|
|
this.setDead();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Resolve a stable impact point on the target so explosive collisions use actual contact.
|
|
*/
|
|
protected Vec3 resolveEntityImpactPoint(EntityLivingBase entity, double impactX, double impactY, double impactZ) {
|
|
if (entity == null || entity.boundingBox == null) {
|
|
return Vec3.createVectorHelper(impactX, impactY, impactZ);
|
|
}
|
|
|
|
Vec3 segmentStart = Vec3.createVectorHelper(posX, posY, posZ);
|
|
Vec3 segmentEnd = Vec3.createVectorHelper(impactX, impactY, impactZ);
|
|
AxisAlignedBB expandedBox = entity.boundingBox.expand(1.0e-4, 1.0e-4, 1.0e-4);
|
|
MovingObjectPosition intercept = expandedBox.calculateIntercept(segmentStart, segmentEnd);
|
|
if (intercept != null && intercept.hitVec != null) {
|
|
return intercept.hitVec;
|
|
}
|
|
|
|
Vec3 closest = closestPointOnBoundingBox(entity.boundingBox, impactX, impactY, impactZ);
|
|
return closest != null ? closest : Vec3.createVectorHelper(impactX, impactY, impactZ);
|
|
}
|
|
|
|
/**
|
|
* Scan entities in a hitbox and process the first/each valid hit based on hit mode.
|
|
*/
|
|
protected boolean processEntitiesInHitBox(AxisAlignedBB hitBox, double impactX, double impactY, double impactZ) {
|
|
@SuppressWarnings("unchecked")
|
|
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
|
|
for (EntityLivingBase entity : entities) {
|
|
if (processEntityHit(entity, impactX, impactY, impactZ)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected boolean handleSolidCollisionTermination() {
|
|
if (!this.isCollidedHorizontally && !this.isCollidedVertically) return false;
|
|
|
|
if (!worldObj.isRemote) {
|
|
hasHit = true;
|
|
if (isExplosive()) {
|
|
doExplosion();
|
|
}
|
|
}
|
|
this.setDead();
|
|
return true;
|
|
}
|
|
|
|
// ==================== 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;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasLightningEffect() {
|
|
return super.hasLightningEffect() || getBarrierSparkTicks() > 0;
|
|
}
|
|
|
|
// ==================== NBT ====================
|
|
|
|
@Override
|
|
protected void readEntityFromNBT(NBTTagCompound nbt) {
|
|
// Intentionally empty — ability entities are transient (not saved to world)
|
|
}
|
|
|
|
@Override
|
|
protected void writeEntityToNBT(NBTTagCompound nbt) {
|
|
// Intentionally empty — ability entities are transient (not saved to world)
|
|
}
|
|
|
|
@Override
|
|
protected void writeSpawnNBT(NBTTagCompound nbt) {
|
|
writeBaseNBT(nbt);
|
|
writeProjectileNBT(nbt);
|
|
}
|
|
|
|
@Override
|
|
protected void readSpawnNBT(NBTTagCompound nbt) {
|
|
readBaseNBT(nbt);
|
|
readProjectileNBT(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.reflected = nbt.getBoolean("Reflected");
|
|
|
|
this.motionX = nbt.getDouble("MotionX");
|
|
this.motionY = nbt.getDouble("MotionY");
|
|
this.motionZ = nbt.getDouble("MotionZ");
|
|
|
|
// Authoritative launch origin comes from spawn NBT (set during fire/startMoving).
|
|
// Sync full client position state to this origin to avoid first-tick spawn/interp pops.
|
|
this.setPosition(this.startX, this.startY, this.startZ);
|
|
this.prevPosX = this.startX;
|
|
this.prevPosY = this.startY;
|
|
this.prevPosZ = this.startZ;
|
|
this.lastTickPosX = this.startX;
|
|
this.lastTickPosY = this.startY;
|
|
this.lastTickPosZ = this.startZ;
|
|
this.interpTargetX = this.startX;
|
|
this.interpTargetY = this.startY;
|
|
this.interpTargetZ = this.startZ;
|
|
this.interpTargetMotionX = this.motionX;
|
|
this.interpTargetMotionY = this.motionY;
|
|
this.interpTargetMotionZ = this.motionZ;
|
|
this.interpSteps = 0;
|
|
|
|
// 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);
|
|
syncProjectileColorWatchers();
|
|
}
|
|
|
|
/**
|
|
* 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.setBoolean("Reflected", reflected);
|
|
|
|
nbt.setDouble("MotionX", motionX);
|
|
nbt.setDouble("MotionY", motionY);
|
|
nbt.setDouble("MotionZ", motionZ);
|
|
|
|
anchorData.writeNBT(nbt);
|
|
combatData.writeNBT(nbt);
|
|
homingData.writeNBT(nbt);
|
|
lifespanData.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;
|
|
}
|
|
}
|