Files
CustomNPC-Plus/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java
T

649 lines
22 KiB
Java

package kamkeel.npcs.entity;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
import kamkeel.npcs.util.CNPCDebug;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.CustomNpcs;
import noppes.npcs.EventHooks;
import noppes.npcs.entity.EntityNPCInterface;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Base class for energy barrier entities (Dome, Panel).
* Provides shared health, damage, hit flash, knockback target,
* and barrier lifecycle management.
* Extends EntityEnergyAbility for shared visual/owner/charging state.
*/
public abstract class EntityAbilityBarrier extends EntityEnergyAbility {
/**
* Outcome of a projectile vs barrier interaction.
*/
public enum ProjectileHitResult {
PASS,
BLOCKED,
BROKEN;
public boolean isAbsorbed() {
return this == BLOCKED || this == BROKEN;
}
}
/**
* Full result of a projectile vs barrier hit after multiplier/event processing.
*/
public static class ProjectileHitOutcome {
public final ProjectileHitResult result;
public final float barrierHealthBefore;
public final float barrierHealthAfter;
public final float appliedBarrierDamage;
public final float remainingProjectileDamage;
public final boolean reflectEnabled;
public final float reflectStrengthPct;
public final boolean useHealth;
public ProjectileHitOutcome(ProjectileHitResult result,
float barrierHealthBefore,
float barrierHealthAfter,
float appliedBarrierDamage,
float remainingProjectileDamage,
boolean reflectEnabled,
float reflectStrengthPct,
boolean useHealth) {
this.result = result;
this.barrierHealthBefore = barrierHealthBefore;
this.barrierHealthAfter = barrierHealthAfter;
this.appliedBarrierDamage = appliedBarrierDamage;
this.remainingProjectileDamage = remainingProjectileDamage;
this.reflectEnabled = reflectEnabled;
this.reflectStrengthPct = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
this.useHealth = useHealth;
}
public boolean shouldReflect() {
return reflectEnabled && useHealth && result == ProjectileHitResult.BLOCKED;
}
}
// ==================== ACTIVE BARRIER REGISTRY ====================
private static final List<WeakReference<EntityAbilityBarrier>> activeBarriers = new ArrayList<>();
private boolean tracked = false;
public static void trackBarrier(EntityAbilityBarrier barrier) {
activeBarriers.add(new WeakReference<>(barrier));
}
/**
* Get all living barriers in the given world. Auto-cleans dead refs.
*/
public static List<EntityAbilityBarrier> getActiveBarriers(World world) {
List<EntityAbilityBarrier> result = new ArrayList<>();
Iterator<WeakReference<EntityAbilityBarrier>> it = activeBarriers.iterator();
while (it.hasNext()) {
EntityAbilityBarrier b = it.next().get();
if (b == null || b.isDead) {
it.remove();
} else if (b.worldObj == world) {
result.add(b);
}
}
return result;
}
public static void clearAllBarriers() {
activeBarriers.clear();
}
// ==================== BARRIER PROPERTIES ====================
protected EnergyBarrierData barrierData = new EnergyBarrierData();
protected float currentHealth;
protected int ticksAlive = 0;
// ==================== DATA WATCHER INDICES ====================
protected static final int DW_HEALTH_PERCENT = 21;
protected static final int DW_HIT_FLASH = 22;
public EntityAbilityBarrier(World world) {
super(world);
this.noClip = true;
}
@Override
protected void entityInit() {
super.entityInit(); // DW_CHARGING = 20
this.dataWatcher.addObject(DW_HEALTH_PERCENT, 1.0f);
this.dataWatcher.addObject(DW_HIT_FLASH, (byte) 0);
}
// ==================== ABSTRACT METHODS ====================
/**
* Check if a projectile is incoming toward this barrier.
* Dome uses spherical check, Panel uses planar check.
*/
public abstract boolean isIncomingProjectile(EntityEnergyProjectile projectile);
/**
* Setup charging state. Called during windup phase.
*/
public abstract void setupCharging(int duration);
/**
* Finish charging — snap to full size and become active.
*/
public abstract void finishCharging();
/**
* Process entity physics for this barrier.
* Handles solid wall (entity repositioning) and knockback (repulsion force).
*/
protected abstract void processEntityPhysics();
/**
* Get the maximum extent of this barrier for distance pre-filtering.
* Dome returns radius, Panel returns max(width, height) / 2.
*/
public abstract float getMaxExtent();
/**
* Check if a generic (non-CNPC+) projectile is incoming toward this barrier.
* Uses the same geometric logic as isIncomingProjectile but with raw position data.
*
* @param posX, posY, posZ Current (or predicted) position of the projectile
* @param motionX, motionY, motionZ Current velocity of the projectile
* @param prevPosX, prevPosY, prevPosZ Previous position of the projectile
* @param ownerEntityId Entity ID of the projectile's shooter
*/
public abstract boolean isIncomingGenericProjectile(
double posX, double posY, double posZ,
double motionX, double motionY, double motionZ,
double prevPosX, double prevPosY, double prevPosZ,
int ownerEntityId);
// ==================== GENERIC PROJECTILE HIT ====================
/**
* Apply damage from a generic (non-CNPC+) projectile to this barrier.
* Uses the typeId for damage multiplier lookup (e.g. "dbc.ki_attack").
*
* @param projectileEntity The entity that hit the barrier (for context)
* @param damage Base damage to apply
* @param typeId Type identifier for damage multiplier lookup
* @return true if the barrier absorbed the hit (projectile should be destroyed)
*/
public boolean onGenericProjectileHit(Entity projectileEntity, float damage, String typeId) {
float multiplier = barrierData.getMultiplier(typeId);
float finalDamage = damage * multiplier;
if (!worldObj.isRemote) {
float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, null, finalDamage);
if (eventDamage < 0) return false;
finalDamage = eventDamage;
}
triggerHitFlash();
if (!barrierData.useHealth) {
return true;
}
currentHealth -= finalDamage;
syncHealthPercent();
if (currentHealth <= 0) {
onBarrierDestroyed();
this.setDead();
}
return true;
}
// ==================== PROJECTILE HIT ====================
/**
* Apply damage to this barrier from a projectile.
* Returns true if the barrier absorbed the hit (projectile should be destroyed).
*/
public boolean onProjectileHit(EntityEnergyProjectile projectile, float baseDamage) {
return onProjectileHitDetailed(projectile, baseDamage).isAbsorbed();
}
/**
* Apply damage to this barrier from a projectile and return detailed outcome.
*/
public ProjectileHitResult onProjectileHitDetailed(EntityEnergyProjectile projectile, float baseDamage) {
return onProjectileHitResolved(projectile, baseDamage).result;
}
/**
* Apply damage to this barrier from a projectile and return full hit context.
*/
public ProjectileHitOutcome onProjectileHitResolved(EntityEnergyProjectile projectile, float baseDamage) {
// Get damage multiplier for this projectile type
String typeId = "";
if (projectile.getSourceAbility() != null) {
typeId = projectile.getSourceAbility().getTypeId();
}
float multiplier = barrierData.getMultiplier(typeId);
float damage = baseDamage * multiplier;
// Fire hit event (may cancel or modify damage)
if (!worldObj.isRemote) {
float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, projectile, damage);
if (eventDamage < 0) {
return new ProjectileHitOutcome(
ProjectileHitResult.PASS,
currentHealth,
currentHealth,
0.0f,
0.0f,
barrierData.reflect,
barrierData.reflectStrengthPct,
barrierData.useHealth
);
}
damage = eventDamage;
}
triggerHitFlash();
float healthBefore = currentHealth;
if (!barrierData.useHealth) {
return new ProjectileHitOutcome(
ProjectileHitResult.BLOCKED,
healthBefore,
healthBefore,
0.0f,
0.0f,
false,
barrierData.reflectStrengthPct,
false
);
}
currentHealth -= damage;
syncHealthPercent();
if (currentHealth <= 0) {
float remaining = Math.max(0.0f, damage - healthBefore);
onBarrierDestroyed();
this.setDead();
return new ProjectileHitOutcome(
ProjectileHitResult.BROKEN,
healthBefore,
0.0f,
damage,
remaining,
barrierData.reflect,
barrierData.reflectStrengthPct,
true
);
}
return new ProjectileHitOutcome(
ProjectileHitResult.BLOCKED,
healthBefore,
currentHealth,
damage,
0.0f,
barrierData.reflect,
barrierData.reflectStrengthPct,
true
);
}
// ==================== MELEE DAMAGE ====================
@Override
public boolean canBeCollidedWith() {
return barrierData.meleeEnabled;
}
@Override
public boolean isEntityInsideOpaqueBlock() {
return false;
}
@Override
protected boolean func_145771_j(double x, double y, double z) {
return false;
}
@Override
public boolean attackEntityFrom(DamageSource source, float amount) {
if (worldObj.isRemote || !barrierData.meleeEnabled) return false;
if (isCharging()) return false;
float damage = amount * barrierData.meleeDamageMultiplier;
// Fire hit event with null projectile (melee hit)
float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, null, damage);
if (eventDamage < 0) return false;
damage = eventDamage;
triggerHitFlash();
if (!barrierData.useHealth) return true;
currentHealth -= damage;
syncHealthPercent();
if (currentHealth <= 0) {
onBarrierDestroyed();
this.setDead();
}
return true;
}
// ==================== SHARED UPDATE LOGIC ====================
/**
* Shared barrier tick logic for onUpdate(). Handles spawned event, owner death,
* duration check, hit flash decay, and tick event.
* Call this after incrementing ticksAlive in subclass onUpdate().
*
* @return true if barrier died this tick (caller should return early)
*/
protected boolean updateBarrierTick() {
if (!worldObj.isRemote) {
// Track barrier and fire spawned event on first tick
if (ticksAlive == 1) {
if (!tracked) {
trackBarrier(this);
tracked = true;
}
EventHooks.onEnergyBarrierSpawned(this, ownerEntityId);
}
// Check owner death
if (ownerEntityId >= 0 && ticksAlive > 5) {
Entity owner = worldObj.getEntityByID(ownerEntityId);
if (owner != null) {
if (owner.isDead) {
onBarrierDestroyed();
this.setDead();
return true;
}
if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) {
onBarrierDestroyed();
this.setDead();
return true;
}
}
}
// Duration check
if (barrierData.useDuration && ticksAlive >= barrierData.durationTicks) {
onBarrierDestroyed();
this.setDead();
return true;
}
// Absolute hard lifetime cap (safety net for barriers with no duration)
if (ticksAlive > BARRIER_HARD_LIFETIME_CAP) {
onBarrierDestroyed();
this.setDead();
return true;
}
// Reset hit flash
if (getHitFlash() > 0) {
setHitFlash((byte) (getHitFlash() - 1));
}
// Fire tick event
EventHooks.onEnergyBarrierTick(this, ownerEntityId);
}
return false;
}
// ==================== HELPERS ====================
protected void onBarrierDestroyed() {
EventHooks.onEnergyBarrierDestroyed(this, ownerEntityId);
}
protected void syncHealthPercent() {
float percent = barrierData.useHealth && barrierData.maxHealth > 0
? Math.max(0, currentHealth / barrierData.maxHealth)
: 1.0f;
if (!worldObj.isRemote) {
this.dataWatcher.updateObject(DW_HEALTH_PERCENT, percent);
}
}
protected void triggerHitFlash() {
if (!worldObj.isRemote) {
setHitFlash((byte) 4);
}
}
protected void setHitFlash(byte value) {
this.dataWatcher.updateObject(DW_HIT_FLASH, value);
}
public byte getHitFlash() {
return this.dataWatcher.getWatchableObjectByte(DW_HIT_FLASH);
}
public float getHealthPercent() {
return this.dataWatcher.getWatchableObjectFloat(DW_HEALTH_PERCENT);
}
/**
* Client prediction only runs for the local player.
*/
protected EntityPlayer getClientPredictionPlayer() {
return worldObj != null && worldObj.isRemote ? CustomNpcs.proxy.getPlayer() : null;
}
/**
* Shared target filter used by Dome/Panel physics loops.
*/
protected boolean shouldSkipBarrierPhysicsTarget(EntityLivingBase entity, EntityPlayer clientPredictionPlayer) {
if (entity == null || !entity.isEntityAlive()) return true;
if (entity.getEntityId() == ownerEntityId) return true;
if (worldObj.isRemote && (clientPredictionPlayer == null || entity != clientPredictionPlayer)) return true;
return isAllyOfOwner(entity);
}
/**
* Check if an entity is an ally of this barrier's owner.
* Server uses full relationship logic; client uses a lightweight safe approximation.
*/
protected boolean isAllyOfOwner(EntityLivingBase entity) {
Entity owner = getOwnerEntity();
if (owner == null || !(owner instanceof EntityLivingBase)) return false;
if (worldObj != null && worldObj.isRemote) {
return isClientPredictedAlly((EntityLivingBase) owner, entity);
}
return AbilityTargetHelper.isAlly((EntityLivingBase) owner, entity);
}
/**
* Client-safe ally approximation used only for movement prediction.
* Avoids PlayerData/Party/Faction point lookups that are server-owned.
*/
protected boolean isClientPredictedAlly(EntityLivingBase owner, EntityLivingBase target) {
if (owner == null || target == null) return false;
if (owner == target) return true;
if (target instanceof EntityNPCInterface) {
EntityNPCInterface targetNpc = (EntityNPCInterface) target;
if (targetNpc.faction.isPassive) return true;
if (owner instanceof EntityNPCInterface) {
EntityNPCInterface ownerNpc = (EntityNPCInterface) owner;
return ownerNpc.faction.id == targetNpc.faction.id;
}
}
return false;
}
// ==================== CONTAINMENT & ABSORBING ====================
/**
* Check if an entity is geometrically inside this barrier's protected zone.
* Override in subclasses (dome checks sphere containment).
*/
public boolean isEntityInside(Entity entity) {
return false;
}
/**
* Find a barrier that would absorb damage for the given entity (its caster).
* Returns the barrier if: absorbing is enabled AND entity is the barrier's owner.
*/
public static EntityAbilityBarrier getAbsorbingBarrier(Entity entity) {
if (entity == null || entity.worldObj == null) return null;
List<EntityAbilityBarrier> barriers = getActiveBarriers(entity.worldObj);
for (EntityAbilityBarrier barrier : barriers) {
if (barrier.isDead) continue;
if (!barrier.barrierData.absorbing) continue;
if (barrier.ownerEntityId == entity.getEntityId()) {
return barrier;
}
}
return null;
}
/**
* Absorb damage from an external source. Reduces barrier health directly.
* Used by damage protection to redirect attacks on inside entities to the barrier.
*
* @param amount The damage amount to absorb
* @return true if damage was absorbed (barrier still alive or just destroyed)
*/
public boolean absorbDamage(float amount) {
if (worldObj.isRemote) return false;
triggerHitFlash();
if (!barrierData.useHealth) {
return true; // Duration-only: absorb but no health loss
}
currentHealth -= amount;
syncHealthPercent();
if (currentHealth <= 0) {
onBarrierDestroyed();
this.setDead();
}
return true;
}
/**
* Teleport an entity to a position. Uses network handler for players
* to ensure reliable client synchronization.
*/
protected void teleportEntity(EntityLivingBase ent, double x, double y, double z) {
if (ent instanceof EntityPlayerMP) {
EntityPlayerMP player = (EntityPlayerMP) ent;
player.playerNetServerHandler.setPlayerLocation(x, y, z, player.rotationYaw, player.rotationPitch);
} else {
ent.setPosition(x, y, z);
}
}
// ==================== GETTERS ====================
public EnergyBarrierData getBarrierData() {
return barrierData;
}
public float getCurrentHealth() {
return currentHealth;
}
public void setCurrentHealth(float health) {
this.currentHealth = health;
syncHealthPercent();
}
/**
* Override the barrier's max health and set current health to match.
* Used by extenders (e.g., DBC scaling) to dynamically set barrier HP at spawn time.
*/
public void setBarrierMaxHealth(float maxHealth) {
this.barrierData.setMaxHealth(maxHealth);
this.currentHealth = maxHealth;
syncHealthPercent();
}
public int getTicksAlive() {
return ticksAlive;
}
// ==================== NBT HELPERS ====================
/**
* Write shared barrier base fields to NBT.
* Call from subclass writeEntityToNBT() before writing type-specific fields.
*/
protected void writeBarrierBaseNBT(NBTTagCompound nbt) {
writeEnergyBaseNBT(nbt);
nbt.setInteger("TicksAlive", ticksAlive);
nbt.setFloat("CurrentHealth", currentHealth);
nbt.setBoolean("Charging", charging);
nbt.setInteger("ChargeTick", chargeTick);
nbt.setInteger("ChargeDuration", chargeDuration);
barrierData.writeNBT(nbt);
}
/**
* Read shared barrier base fields from NBT.
* Call from subclass readEntityFromNBT() before reading type-specific fields.
*/
protected void readBarrierBaseNBT(NBTTagCompound nbt) {
readEnergyBaseNBT(nbt);
this.ticksAlive = nbt.getInteger("TicksAlive");
this.currentHealth = nbt.getFloat("CurrentHealth");
if (Float.isNaN(currentHealth) || Float.isInfinite(currentHealth) || currentHealth < 0) currentHealth = barrierData.maxHealth;
this.charging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
this.chargeTick = nbt.getInteger("ChargeTick");
this.chargeDuration = nbt.getInteger("ChargeDuration");
this.dataWatcher.updateObject(DW_CHARGING, (byte) (charging ? 1 : 0));
barrierData.readNBT(nbt);
}
// ==================== DEBUG LOGGING ====================
/**
* Debug log called every tick for barriers. Subclasses override debugLogBarrierExtra().
*/
protected void debugLogBarrierTick() {
boolean isClient = worldObj.isRemote;
if (isClient ? !CNPCDebug.isClientEnabled("energy") : !CNPCDebug.isServerEnabled("energy"))
return;
String className = getClass().getSimpleName();
String base = String.format("[%s id=%d tick=%d] pos=(%.2f,%.2f,%.2f) charging=%b health=%.1f",
className, getEntityId(), ticksAlive,
posX, posY, posZ, isCharging(), currentHealth);
String extra = debugLogBarrierExtra();
String full = extra.isEmpty() ? base : base + " " + extra;
CNPCDebug.log("energy", isClient, full);
}
/**
* Subclass hook for barrier-specific debug data.
*/
protected String debugLogBarrierExtra() {
return "";
}
}