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

985 lines
39 KiB
Java

package kamkeel.npcs.entity;
import kamkeel.npcs.util.ByteBufUtils;
import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.EnergyController;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.NpcDamageSource;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Shared zone entity for Trap and Hazard abilities.
* ZoneType distinguishes behavior: TRAP (proximity-triggered burst) vs HAZARD (persistent AoE).
*/
public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnData {
// ═══════════════════════════════════════════════════════════════════
// ENUMS
// ═══════════════════════════════════════════════════════════════════
public enum ZoneType {TRAP, HAZARD}
public enum ZoneShape {
CIRCLE, SQUARE;
@Override
public String toString() {
switch (this) {
case CIRCLE:
return "ability.shape.circle";
case SQUARE:
return "ability.shape.square";
default:
return name();
}
}
}
public enum AccentStyle {
STATIC, SWAYING, FLICKERING;
@Override
public String toString() {
switch (this) {
case STATIC:
return "ability.accentStyle.static";
case SWAYING:
return "ability.accentStyle.swaying";
case FLICKERING:
return "ability.accentStyle.flickering";
default:
return name();
}
}
}
public enum ParticleMotion {
RISING, DRIFTING, SPARKS;
@Override
public String toString() {
switch (this) {
case RISING:
return "ability.particleMotion.rising";
case DRIFTING:
return "ability.particleMotion.drifting";
case SPARKS:
return "ability.particleMotion.sparks";
default:
return name();
}
}
}
private static final double GROUND_OFFSET = 0.005;
private static final byte TRIGGER_FLASH_STATUS = 60;
// ═══════════════════════════════════════════════════════════════════
// COMMON PROPERTIES
// ═══════════════════════════════════════════════════════════════════
private ZoneType zoneType = ZoneType.TRAP;
private ZoneShape shape = ZoneShape.CIRCLE;
private int ownerEntityId = -1;
private float radius = 4.0f;
private int durationTicks = 200;
private int maxTicks = 200;
private int innerColor = 0xFF6600;
private int outerColor = 0xFF0000;
private boolean outerColorEnabled = true;
private float zoneHeight = 2.0f;
private float particleDensity = 1.0f;
private float particleScale = 1.0f;
private float animSpeed = 1.0f;
private float lightningDensity = 1.0f;
// Visual layer fields
private boolean groundFill = true;
private float groundAlpha = 0.25f;
private boolean rings = true;
private int ringCount = 3;
private boolean border = true;
private float borderSpeed = 1.0f;
private boolean accents = true;
private int accentStyle = 0;
private boolean lightning = false;
private boolean particles = true;
private int particleMotion = 0;
private String particleDir = "";
private int particleSize = 32;
private boolean particleGlow = true;
private long deathWorldTime = -1;
private List<AbilityPotionEffect> effects = new ArrayList<>();
// ═══════════════════════════════════════════════════════════════════
// TRAP-SPECIFIC PROPERTIES
// ═══════════════════════════════════════════════════════════════════
private int armTime = 20;
private float triggerRadius = 2.0f;
private int maxTriggers = 1;
private int triggerCooldown = 20;
private float damage = 6.0f;
private float damageRadius = 0.0f;
private float knockback = 0.5f;
private boolean visible = true;
// ═══════════════════════════════════════════════════════════════════
// HAZARD-SPECIFIC PROPERTIES
// ═══════════════════════════════════════════════════════════════════
private float damagePerSecond = 1.0f;
private int damageInterval = 20;
private boolean ignoreIFrames = false;
private boolean affectsCaster = false;
// ═══════════════════════════════════════════════════════════════════
// RUNTIME STATE
// ═══════════════════════════════════════════════════════════════════
private transient boolean armed = false;
private transient int triggerCount = 0;
private transient int ticksSinceLastTrigger = 0;
private transient Set<Integer> triggeredEntities = new HashSet<>();
private transient int triggerFlashTick = -1;
private transient int ticksSinceDamage = 0;
private transient Set<Integer> damagedThisTick = new HashSet<>();
private transient Ability sourceAbility = null;
/**
* Custom damage data for script-created zones that don't have a sourceAbility.
* Used by EnergyController handlers (e.g. DBC Addon) to carry damage configuration
* directly on the entity. Synced via spawn data for client sync.
*/
private NBTTagCompound customDamageData = null;
private boolean previewMode = false;
private EntityLivingBase previewOwner = null;
// ═══════════════════════════════════════════════════════════════════
// CONSTRUCTORS
// ═══════════════════════════════════════════════════════════════════
public EntityAbilityZone(World world) {
super(world);
this.setSize(0.1f, 0.1f);
this.noClip = true;
this.isImmuneToFire = true;
this.ignoreFrustumCheck = true;
}
private EntityAbilityZone(World world, ZoneType type, EntityLivingBase owner,
double x, double y, double z) {
this(world);
this.zoneType = type;
this.ownerEntityId = owner.getEntityId();
// Snap to ground with tiny offset to prevent z-clipping
double groundY = Ability.findGroundLevel(world, x, y, z) + GROUND_OFFSET;
this.setPosition(x, groundY, z);
}
/**
* Initialize as a hazard zone with default values. Used by EnergyController factory.
* Scripts can then configure properties before calling spawn().
*/
public void initAsHazard(Entity owner, double x, double y, double z) {
this.zoneType = ZoneType.HAZARD;
this.ownerEntityId = owner.getEntityId();
double groundY = Ability.findGroundLevel(worldObj, x, y, z) + GROUND_OFFSET;
this.setPosition(x, groundY, z);
this.ticksSinceDamage = damageInterval;
}
/**
* Initialize as a trap zone with default values. Used by EnergyController factory.
* Scripts can then configure properties before calling spawn().
*/
public void initAsTrap(Entity owner, double x, double y, double z) {
this.zoneType = ZoneType.TRAP;
this.ownerEntityId = owner.getEntityId();
double groundY = Ability.findGroundLevel(worldObj, x, y, z) + GROUND_OFFSET;
this.setPosition(x, groundY, z);
this.ticksSinceLastTrigger = triggerCooldown;
}
public static EntityAbilityZone createTrap(World world, EntityLivingBase owner,
double x, double y, double z,
ZoneShape shape,
float triggerRadius, int armTime, int maxTriggers,
int triggerCooldown, float damage, float damageRadius,
float knockback, int durationTicks,
boolean ignoreIFrames,
int innerColor, int outerColor, boolean outerColorEnabled,
float zoneHeight,
float particleDensity, float particleScale,
float animSpeed, float lightningDensity,
boolean visible,
List<AbilityPotionEffect> effects) {
EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.TRAP, owner, x, y, z);
zone.shape = shape;
zone.radius = triggerRadius;
zone.triggerRadius = triggerRadius;
zone.armTime = armTime;
zone.maxTriggers = maxTriggers;
zone.triggerCooldown = triggerCooldown;
zone.damage = damage;
zone.damageRadius = damageRadius;
zone.knockback = knockback;
zone.durationTicks = durationTicks;
zone.maxTicks = durationTicks;
zone.ignoreIFrames = ignoreIFrames;
zone.innerColor = innerColor;
zone.outerColor = outerColor;
zone.outerColorEnabled = outerColorEnabled;
zone.zoneHeight = zoneHeight;
zone.particleDensity = particleDensity;
zone.particleScale = particleScale;
zone.animSpeed = animSpeed;
zone.lightningDensity = lightningDensity;
zone.visible = visible;
if (effects != null) {
for (AbilityPotionEffect e : effects) {
zone.effects.add(e.copy());
}
}
zone.ticksSinceLastTrigger = triggerCooldown;
return zone;
}
public static EntityAbilityZone createHazard(World world, EntityLivingBase owner,
double x, double y, double z,
ZoneShape shape,
float radius,
float damagePerSecond, int damageInterval,
boolean ignoreIFrames, boolean affectsCaster,
int durationTicks,
int innerColor, int outerColor, boolean outerColorEnabled,
float zoneHeight,
float particleDensity, float particleScale,
float animSpeed, float lightningDensity,
List<AbilityPotionEffect> effects) {
EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.HAZARD, owner, x, y, z);
zone.shape = shape;
zone.radius = radius;
zone.damagePerSecond = damagePerSecond;
zone.damageInterval = damageInterval;
zone.ignoreIFrames = ignoreIFrames;
zone.affectsCaster = affectsCaster;
zone.durationTicks = durationTicks;
zone.maxTicks = durationTicks;
zone.innerColor = innerColor;
zone.outerColor = outerColor;
zone.outerColorEnabled = outerColorEnabled;
zone.zoneHeight = zoneHeight;
zone.particleDensity = particleDensity;
zone.particleScale = particleScale;
zone.animSpeed = animSpeed;
zone.lightningDensity = lightningDensity;
if (effects != null) {
for (AbilityPotionEffect e : effects) {
zone.effects.add(e.copy());
}
}
zone.ticksSinceDamage = damageInterval;
return zone;
}
// ═══════════════════════════════════════════════════════════════════
// ENTITY LIFECYCLE
// ═══════════════════════════════════════════════════════════════════
@Override
protected void entityInit() {
}
@Override
protected void readEntityFromNBT(NBTTagCompound nbt) {
this.setDead();
}
@Override
protected void writeEntityToNBT(NBTTagCompound nbt) {
// Intentionally empty — ability entities are transient (not saved to world)
}
@Override
public void handleHealthUpdate(byte id) {
if (id == TRIGGER_FLASH_STATUS) {
this.triggerFlashTick = this.ticksExisted;
} else {
super.handleHealthUpdate(id);
}
}
@Override
public void onUpdate() {
this.prevPosX = this.posX;
this.prevPosY = this.posY;
this.prevPosZ = this.posZ;
super.onUpdate();
if (deathWorldTime < 0 && worldObj != null) {
deathWorldTime = worldObj.getTotalWorldTime() + maxTicks;
}
if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) {
this.setDead();
return;
}
if (!worldObj.isRemote && !previewMode) {
switch (zoneType) {
case TRAP:
tickTrap();
break;
case HAZARD:
tickHazard();
break;
}
}
}
// ═══════════════════════════════════════════════════════════════════
// TRAP LOGIC
// ═══════════════════════════════════════════════════════════════════
private void tickTrap() {
if (!armed) {
if (ticksExisted >= armTime) {
armed = true;
}
return;
}
ticksSinceLastTrigger++;
if (maxTriggers > 0 && triggerCount >= maxTriggers) {
this.setDead();
return;
}
if (ticksSinceLastTrigger < triggerCooldown) {
return;
}
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
posX - triggerRadius, posY - 0.5, posZ - triggerRadius,
posX + triggerRadius, posY + zoneHeight, posZ + triggerRadius
);
Entity owner = getOwner();
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
for (EntityLivingBase entity : entities) {
if (owner != null && entity == owner) continue;
if (entity.isDead) continue;
if (maxTriggers == 1 && triggeredEntities.contains(entity.getEntityId())) continue;
if (owner instanceof EntityLivingBase && !AbilityTargetHelper.shouldAffect((EntityLivingBase) owner, entity, TargetFilter.ENEMIES, false)) continue;
double dx = entity.posX - posX;
double dz = entity.posZ - posZ;
boolean inRange;
if (shape == ZoneShape.SQUARE) {
inRange = Math.abs(dx) <= triggerRadius && Math.abs(dz) <= triggerRadius;
} else {
double dist = Math.sqrt(dx * dx + dz * dz);
inRange = dist <= triggerRadius;
}
if (inRange) {
triggerTrap(entity);
return;
}
}
}
private void triggerTrap(EntityLivingBase triggerer) {
Entity owner = getOwner();
Set<EntityLivingBase> affected = new HashSet<>();
if (damageRadius > 0) {
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
posX - damageRadius, posY - 0.5, posZ - damageRadius,
posX + damageRadius, posY + zoneHeight, posZ + damageRadius
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
for (EntityLivingBase entity : entities) {
if (owner != null && entity == owner) continue;
if (owner instanceof EntityLivingBase && !AbilityTargetHelper.shouldAffect((EntityLivingBase) owner, entity, TargetFilter.ENEMIES, false)) continue;
double dx = entity.posX - posX;
double dz = entity.posZ - posZ;
if (shape == ZoneShape.SQUARE) {
if (Math.abs(dx) <= damageRadius && Math.abs(dz) <= damageRadius) {
affected.add(entity);
}
} else {
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist <= damageRadius) {
affected.add(entity);
}
}
}
} else {
affected.add(triggerer);
}
boolean anyDamaged = false;
for (EntityLivingBase entity : affected) {
boolean hit = applyDamage(entity, owner, damage);
if (hit) {
anyDamaged = true;
}
if (hit && knockback > 0) {
double dx = entity.posX - posX;
double dz = entity.posZ - posZ;
double len = Math.sqrt(dx * dx + dz * dz);
if (len > 0) {
entity.addVelocity((dx / len) * knockback * 0.5, 0.1, (dz / len) * knockback * 0.5);
entity.velocityChanged = true;
}
}
if (hit) {
applyEffects(entity);
}
}
if (anyDamaged) {
triggerCount++;
ticksSinceLastTrigger = 0;
triggeredEntities.add(triggerer.getEntityId());
triggerFlashTick = ticksExisted;
worldObj.setEntityState(this, TRIGGER_FLASH_STATUS);
}
}
// ═══════════════════════════════════════════════════════════════════
// HAZARD LOGIC
// ═══════════════════════════════════════════════════════════════════
private void tickHazard() {
damagedThisTick.clear();
ticksSinceDamage++;
if (ticksSinceDamage < damageInterval) {
return;
}
ticksSinceDamage = 0;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
posX - radius, posY - 0.5, posZ - radius,
posX + radius, posY + zoneHeight, posZ + radius
);
Entity owner = getOwner();
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
for (EntityLivingBase entity : entities) {
if (owner != null && entity == owner && !affectsCaster) continue;
if (damagedThisTick.contains(entity.getEntityId())) continue;
if (entity != owner && owner instanceof EntityLivingBase && !AbilityTargetHelper.shouldAffect((EntityLivingBase) owner, entity, TargetFilter.ENEMIES, false)) continue;
if (!isInZone(entity)) continue;
if (damagePerSecond > 0) {
applyDamage(entity, owner, damagePerSecond);
}
applyEffects(entity);
damagedThisTick.add(entity.getEntityId());
}
}
private boolean isInZone(EntityLivingBase entity) {
double dx = entity.posX - posX;
double dz = entity.posZ - posZ;
switch (shape) {
case CIRCLE:
return Math.sqrt(dx * dx + dz * dz) <= radius;
case SQUARE:
return Math.abs(dx) <= radius && Math.abs(dz) <= radius;
default:
return Math.sqrt(dx * dx + dz * dz) <= radius;
}
}
// ═══════════════════════════════════════════════════════════════════
// DAMAGE
// ═══════════════════════════════════════════════════════════════════
private boolean applyDamage(EntityLivingBase target, Entity owner, float dmg) {
if (dmg <= 0) return false;
int previousHurtResistantTime = Ability.clearHurtResistanceIfNeeded(target, ignoreIFrames);
try {
// Route through ability extender (e.g. DBC Addon damage scaling)
boolean handled = false;
if (sourceAbility != null && owner instanceof EntityLivingBase) {
double dx = target.posX - posX;
double dz = target.posZ - posZ;
handled = AbilityController.Instance.fireOnAbilityDamage(
sourceAbility, (EntityLivingBase) owner, target,
dmg, 0.0f, 0.0f, dx, dz, 1.0f);
}
// 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;
handled = EnergyController.Instance.fireOnEnergyDamage(
this, (EntityLivingBase) owner, target,
dmg, 0.0f, 0.0f, dx, dz, 1.0f, customDamageData);
}
if (!handled) {
if (owner instanceof EntityNPCInterface) {
return target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg);
} else if (owner instanceof EntityPlayer) {
return target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg);
} else if (owner instanceof EntityLivingBase) {
return target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg);
} else {
return target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg);
}
}
return true;
} finally {
Ability.restoreHurtResistanceIfNeeded(target, ignoreIFrames, previousHurtResistantTime);
}
}
private void applyEffects(EntityLivingBase entity) {
if (entity == null || effects.isEmpty()) return;
for (AbilityPotionEffect effect : effects) {
effect.apply(entity);
}
}
// ═══════════════════════════════════════════════════════════════════
// ENTITY HELPERS
// ═══════════════════════════════════════════════════════════════════
private Entity getOwner() {
if (previewMode && previewOwner != null) return previewOwner;
if (ownerEntityId == -1) return null;
return worldObj.getEntityByID(ownerEntityId);
}
public void setupPreview(EntityLivingBase owner) {
this.previewMode = true;
this.previewOwner = owner;
}
public Ability getSourceAbility() {
return sourceAbility;
}
public void setSourceAbility(Ability ability) {
this.sourceAbility = ability;
}
public NBTTagCompound getCustomDamageData() {
return customDamageData;
}
public void setCustomDamageData(NBTTagCompound data) {
this.customDamageData = data;
}
// ═══════════════════════════════════════════════════════════════════
// RENDERING SUPPORT
// ═══════════════════════════════════════════════════════════════════
@Override
@SideOnly(Side.CLIENT)
public boolean isInRangeToRenderDist(double distance) {
return distance < 16384.0D;
}
@Override
public boolean shouldRenderInPass(int pass) {
return pass == 1;
}
@Override
public float getBrightness(float partialTicks) {
return 1.0f;
}
@Override
@SideOnly(Side.CLIENT)
public int getBrightnessForRender(float partialTicks) {
return 0xF000F0;
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS FOR RENDERER
// ═══════════════════════════════════════════════════════════════════
public ZoneType getZoneType() {
return zoneType;
}
public ZoneShape getShape() {
return shape;
}
public float getRadius() {
return radius;
}
public int getInnerColor() {
return innerColor;
}
public int getOuterColor() {
return outerColor;
}
public boolean isOuterColorEnabled() {
return outerColorEnabled;
}
public float getZoneHeight() {
return zoneHeight;
}
public boolean isArmed() {
return armed || ticksExisted >= armTime;
}
public boolean isVisible() {
return visible;
}
public int getTriggerFlashTick() {
return triggerFlashTick;
}
public int getDurationTicks() {
return durationTicks;
}
public int getMaxTicks() {
return maxTicks;
}
public float getParticleDensity() {
return particleDensity;
}
public float getParticleScale() {
return particleScale;
}
public float getAnimSpeed() {
return animSpeed;
}
public float getLightningDensity() {
return lightningDensity;
}
public boolean isGroundFill() {
return groundFill;
}
public float getGroundAlpha() {
return groundAlpha;
}
public boolean isRings() {
return rings;
}
public int getRingCount() {
return ringCount;
}
public boolean isBorder() {
return border;
}
public float getBorderSpeed() {
return borderSpeed;
}
public boolean isAccents() {
return accents;
}
public int getAccentStyle() {
return accentStyle;
}
public boolean isLightning() {
return lightning;
}
public boolean isParticles() {
return particles;
}
public int getParticleMotion() {
return particleMotion;
}
public String getParticleDir() {
return particleDir;
}
public int getParticleSize() {
return particleSize;
}
public boolean isParticleGlow() {
return particleGlow;
}
public void applyVisual(boolean groundFill, float groundAlpha,
boolean rings, int ringCount,
boolean border, float borderSpeed,
boolean accents, int accentStyle,
boolean lightning,
boolean particles, int particleMotion,
String particleDir, int particleSize, boolean particleGlow) {
this.groundFill = groundFill;
this.groundAlpha = groundAlpha;
this.rings = rings;
this.ringCount = ringCount;
this.border = border;
this.borderSpeed = borderSpeed;
this.accents = accents;
this.accentStyle = accentStyle;
this.lightning = lightning;
this.particles = particles;
this.particleMotion = particleMotion;
this.particleDir = particleDir != null ? particleDir : "";
this.particleSize = particleSize;
this.particleGlow = particleGlow;
}
// ═══════════════════════════════════════════════════════════════════
// PUBLIC SETTERS (for EnergyController / scripting API)
// ═══════════════════════════════════════════════════════════════════
public void setShape(ZoneShape shape) { this.shape = shape; }
public void setRadius(float radius) { this.radius = radius; }
public void setZoneHeight(float height) { this.zoneHeight = height; }
public void setDurationTicks(int ticks) { this.durationTicks = ticks; this.maxTicks = ticks; }
public void setInnerColor(int color) { this.innerColor = color; }
public void setOuterColor(int color) { this.outerColor = color; }
public void setOuterColorEnabled(boolean enabled) { this.outerColorEnabled = enabled; }
public void setParticleDensity(float density) { this.particleDensity = density; }
public void setParticleScale(float scale) { this.particleScale = scale; }
public void setAnimSpeed(float speed) { this.animSpeed = speed; }
public void setIgnoreIFrames(boolean ignore) { this.ignoreIFrames = ignore; }
// Hazard-specific
public float getDamagePerSecond() { return damagePerSecond; }
public void setDamagePerSecond(float dps) { this.damagePerSecond = dps; }
public int getDamageInterval() { return damageInterval; }
public void setDamageInterval(int ticks) { this.damageInterval = ticks; }
public boolean isAffectsCaster() { return affectsCaster; }
public void setAffectsCaster(boolean affects) { this.affectsCaster = affects; }
// Trap-specific
public float getTriggerRadius() { return triggerRadius; }
public void setTriggerRadius(float radius) { this.triggerRadius = radius; }
public int getArmTime() { return armTime; }
public void setArmTime(int ticks) { this.armTime = ticks; }
public int getMaxTriggers() { return maxTriggers; }
public void setMaxTriggers(int max) { this.maxTriggers = max; }
public int getTriggerCooldown() { return triggerCooldown; }
public void setTriggerCooldown(int ticks) { this.triggerCooldown = ticks; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getDamageRadius() { return damageRadius; }
public void setDamageRadius(float radius) { this.damageRadius = radius; }
public float getKnockback() { return knockback; }
public void setKnockback(float knockback) { this.knockback = knockback; }
public void setVisible(boolean visible) { this.visible = visible; }
public boolean isIgnoreIFrames() { return ignoreIFrames; }
public int getOwnerEntityId() { return ownerEntityId; }
// ═══════════════════════════════════════════════════════════════════
// NBT SERIALIZATION
// ═══════════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════════
// SPAWN DATA (ByteBuf for client sync)
// ═══════════════════════════════════════════════════════════════════
@Override
public void writeSpawnData(ByteBuf buffer) {
buffer.writeInt(zoneType.ordinal());
ByteBufUtils.writeString(buffer, shape.name());
buffer.writeInt(ownerEntityId);
buffer.writeFloat(radius);
buffer.writeInt(durationTicks);
buffer.writeInt(maxTicks);
buffer.writeInt(innerColor);
buffer.writeInt(outerColor);
buffer.writeBoolean(outerColorEnabled);
buffer.writeFloat(zoneHeight);
buffer.writeFloat(particleDensity);
buffer.writeFloat(particleScale);
buffer.writeFloat(animSpeed);
buffer.writeFloat(lightningDensity);
// Visual layer fields
buffer.writeBoolean(groundFill);
buffer.writeFloat(groundAlpha);
buffer.writeBoolean(rings);
buffer.writeInt(ringCount);
buffer.writeBoolean(border);
buffer.writeFloat(borderSpeed);
buffer.writeBoolean(accents);
buffer.writeInt(accentStyle);
buffer.writeBoolean(lightning);
buffer.writeBoolean(particles);
buffer.writeInt(particleMotion);
ByteBufUtils.writeString(buffer, particleDir);
buffer.writeInt(particleSize);
buffer.writeBoolean(particleGlow);
// Trap-specific
buffer.writeInt(armTime);
buffer.writeBoolean(visible);
// Effects
NBTTagCompound effectsNbt = new NBTTagCompound();
NBTTagList effectList = new NBTTagList();
for (AbilityPotionEffect effect : effects) {
effectList.appendTag(effect.writeNBT());
}
effectsNbt.setTag("Effects", effectList);
try {
ByteBufUtils.writeNBT(buffer, effectsNbt);
} catch (java.io.IOException e) {
noppes.npcs.LogWriter.error("Error writing zone effects spawn data", e);
}
// Custom damage data
boolean hasDamageData = customDamageData != null;
buffer.writeBoolean(hasDamageData);
if (hasDamageData) {
try {
ByteBufUtils.writeNBT(buffer, customDamageData);
} catch (java.io.IOException e) {
noppes.npcs.LogWriter.error("Error writing zone custom damage data", e);
}
}
}
@Override
public void readSpawnData(ByteBuf buffer) {
int zoneTypeOrd = buffer.readInt();
this.zoneType = (zoneTypeOrd >= 0 && zoneTypeOrd < ZoneType.values().length) ? ZoneType.values()[zoneTypeOrd] : ZoneType.TRAP;
String shapeName = ByteBufUtils.readString(buffer);
try {
this.shape = ZoneShape.valueOf(shapeName);
} catch (Exception e) {
this.shape = ZoneShape.CIRCLE;
}
this.ownerEntityId = buffer.readInt();
this.radius = Math.max(0.1f, Math.min(100.0f, buffer.readFloat()));
if (Float.isNaN(radius) || Float.isInfinite(radius)) radius = 5.0f;
this.durationTicks = buffer.readInt();
this.maxTicks = buffer.readInt();
this.innerColor = buffer.readInt();
this.outerColor = buffer.readInt();
this.outerColorEnabled = buffer.readBoolean();
this.zoneHeight = buffer.readFloat();
this.particleDensity = buffer.readFloat();
this.particleScale = buffer.readFloat();
this.animSpeed = buffer.readFloat();
this.lightningDensity = buffer.readFloat();
// Visual layer fields
this.groundFill = buffer.readBoolean();
this.groundAlpha = buffer.readFloat();
this.rings = buffer.readBoolean();
this.ringCount = buffer.readInt();
this.border = buffer.readBoolean();
this.borderSpeed = buffer.readFloat();
this.accents = buffer.readBoolean();
this.accentStyle = buffer.readInt();
this.lightning = buffer.readBoolean();
this.particles = buffer.readBoolean();
this.particleMotion = buffer.readInt();
this.particleDir = ByteBufUtils.readString(buffer);
this.particleSize = buffer.readInt();
this.particleGlow = buffer.readBoolean();
this.armTime = buffer.readInt();
this.visible = buffer.readBoolean();
// Effects
NBTTagCompound effectsNbt = null;
try {
effectsNbt = ByteBufUtils.readNBT(buffer);
} catch (java.io.IOException e) {
noppes.npcs.LogWriter.error("Error reading zone effects spawn data", e);
}
effects.clear();
if (effectsNbt != null && effectsNbt.hasKey("Effects")) {
NBTTagList effectList = effectsNbt.getTagList("Effects", 10);
for (int i = 0; i < effectList.tagCount(); i++) {
AbilityPotionEffect effect = AbilityPotionEffect.fromNBT(effectList.getCompoundTagAt(i));
if (effect != null && effect.isValid()) {
effects.add(effect);
}
}
}
// Custom damage data
if (buffer.readableBytes() > 0 && buffer.readBoolean()) {
try {
this.customDamageData = ByteBufUtils.readNBT(buffer);
} catch (java.io.IOException e) {
noppes.npcs.LogWriter.error("Error reading zone custom damage data", e);
}
}
}
}