diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyPillar.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyPillar.java index cd47b4dbf..60c9620c2 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyPillar.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyPillar.java @@ -11,20 +11,13 @@ import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; /** - * Renders EntityAbilityPillar as a vertical energy column. + * Renders EntityAbilityPillar. * - * Shape modes: - * - CIRCLE: cylindrical pillar with triangulated segments. - * - SQUARE: rectangular prism pillar. + * Radius: snaps immediately to targetRadius when charging ends — no interpolation. + * Height: grows linearly with no easing, at the speed defined in pillarData. * - * Both modes render three color layers (outer glow, mid, inner core). - * - * Origin modes: - * - FROM_GROUND: posY = ground base. Pillar grows upward (baseY = posY, tipY = posY + height). - * - FROM_ABOVE: posY = top anchor (ceiling). Pillar grows downward - * (baseY = posY, tipY = posY - height). - * - * During charging, a flat disc grows at ground level (FROM_GROUND) or ceiling level (FROM_ABOVE). + * FROM_GROUND: baseY = posY, tipY = posY + height (grows up). + * FROM_ABOVE: baseY = posY, tipY = posY - height (grows down from ceiling anchor). */ @SideOnly(Side.CLIENT) public class RenderEnergyPillar extends RenderEnergy { @@ -39,7 +32,6 @@ public class RenderEnergyPillar extends RenderEnergy { if (shouldSkipInitialActiveRender(pillar)) return; setupRenderState(); - float proximityAlpha = getProximityAlphaFactor(pillar, x, y, z); if (pillar.isCharging()) { @@ -48,7 +40,9 @@ public class RenderEnergyPillar extends RenderEnergy { return; } - float radius = pillar.getInterpolatedPillarRadius(partialTicks); + // Radius: use raw value directly — snaps on charging end, no interpolation needed + float radius = pillar.getPillarRadius(); + // Height: linearly interpolated between prev and current (no easing) float height = pillar.getInterpolatedPillarHeight(partialTicks); if (radius <= 0.01f || height <= 0.01f) { @@ -56,8 +50,6 @@ public class RenderEnergyPillar extends RenderEnergy { return; } - // FROM_GROUND: base at y, tip grows up. - // FROM_ABOVE: posY is the top anchor. Base = y, tip = y - height (grows down). float baseY, tipY; if (pillar.getPillarOrigin() == PillarOrigin.FROM_ABOVE) { baseY = (float) y; @@ -103,16 +95,17 @@ public class RenderEnergyPillar extends RenderEnergy { private void renderCharging(EntityAbilityPillar pillar, double x, double y, double z, float partialTicks, float proximityAlpha) { - float radius = pillar.getTargetRadius() * 2; + // Use targetRadius directly — pillarRadius during charging is only 0.01 initially + // and grows via DataWatcher which is unavailable on preview entities. + float targetRadius = pillar.getPillarData().targetRadius; float chargeProgress = pillar.getInterpolatedChargeProgress(partialTicks); - float renderRadius = radius * chargeProgress; + float renderRadius = targetRadius * chargeProgress; if (renderRadius <= 0.01f) return; float pulseTime = pillar.ticksExisted + partialTicks; renderRadius *= (1.0f + (float) Math.sin(pulseTime * 0.2f) * 0.08f); - // Flat disc at the anchor point float baseY = (float) y; float tipY = baseY + 0.05f; @@ -174,7 +167,6 @@ public class RenderEnergyPillar extends RenderEnergy { Tessellator tess = Tessellator.instance; - // Lateral faces tess.startDrawingQuads(); tess.setColorRGBA_F(r, g, b, alpha); for (int i = 0; i < CIRCLE_SEGMENTS; i++) { @@ -182,9 +174,7 @@ public class RenderEnergyPillar extends RenderEnergy { float z1 = (float) cz + sinTable[i] * radius; float x2 = (float) cx + cosTable[i + 1] * radius; float z2 = (float) cz + sinTable[i + 1] * radius; - float nx = (cosTable[i] + cosTable[i + 1]) * 0.5f; - float nz = (sinTable[i] + sinTable[i + 1]) * 0.5f; - tess.setNormal(nx, 0, nz); + tess.setNormal((cosTable[i] + cosTable[i + 1]) * 0.5f, 0, (sinTable[i] + sinTable[i + 1]) * 0.5f); tess.addVertex(x1, baseY, z1); tess.addVertex(x2, baseY, z2); tess.addVertex(x2, tipY, z2); @@ -195,7 +185,6 @@ public class RenderEnergyPillar extends RenderEnergy { float capMinY = Math.min(baseY, tipY); float capMaxY = Math.max(baseY, tipY); - // Bottom cap tess.startDrawingQuads(); tess.setColorRGBA_F(r, g, b, alpha); tess.setNormal(0, -1, 0); @@ -211,7 +200,6 @@ public class RenderEnergyPillar extends RenderEnergy { } tess.draw(); - // Top cap tess.startDrawingQuads(); tess.setColorRGBA_F(r, g, b, alpha); tess.setNormal(0, 1, 0); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyPillarData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyPillarData.java index 20eee0967..ff0a728ac 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyPillarData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyPillarData.java @@ -13,8 +13,8 @@ public class EnergyPillarData { public float targetRadius = 2.0f; public float targetHeight = 10.0f; - public float radiusGrowSpeed = 0.1f; - public float heightGrowSpeed = 0.2f; + public float radiusGrowSpeed = 0.5f; + public float heightGrowSpeed = 1.0f; public PillarMode mode = PillarMode.ANCHORED; public PillarOrigin origin = PillarOrigin.FROM_GROUND; public PillarShape shape = PillarShape.CIRCLE; @@ -37,6 +37,70 @@ public class EnergyPillarData { this.spawnDelay = Math.max(0, spawnDelay); } + public float getTargetRadius() { + return targetRadius; + } + + public void setTargetRadius(float targetRadius) { + this.targetRadius = targetRadius; + } + + public float getTargetHeight() { + return targetHeight; + } + + public void setTargetHeight(float targetHeight) { + this.targetHeight = targetHeight; + } + + public float getRadiusGrowSpeed() { + return radiusGrowSpeed; + } + + public void setRadiusGrowSpeed(float radiusGrowSpeed) { + this.radiusGrowSpeed = radiusGrowSpeed; + } + + public float getHeightGrowSpeed() { + return heightGrowSpeed; + } + + public void setHeightGrowSpeed(float heightGrowSpeed) { + this.heightGrowSpeed = heightGrowSpeed; + } + + public PillarMode getMode() { + return mode; + } + + public void setMode(PillarMode mode) { + this.mode = mode; + } + + public PillarOrigin getOrigin() { + return origin; + } + + public void setOrigin(PillarOrigin origin) { + this.origin = origin; + } + + public PillarShape getShape() { + return shape; + } + + public void setShape(PillarShape shape) { + this.shape = shape; + } + + public int getSpawnDelay() { + return spawnDelay; + } + + public void setSpawnDelay(int spawnDelay) { + this.spawnDelay = spawnDelay; + } + public void writeNBT(NBTTagCompound nbt) { nbt.setFloat("targetRadius", targetRadius); nbt.setFloat("targetHeight", targetHeight); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyZone.java index f934e4df9..00e29d086 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyZone.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyZone.java @@ -210,6 +210,14 @@ public abstract class AbilityEnergyZone extends Abil return getSpawnPosition(caster, target, index); } + /** + * Return the entity ID the charge visual should follow during charging, or -1 for none. + * Default: -1 (static). Override to make the charge visual track a moving entity. + */ + protected int getChargeVisualFollowEntityId(EntityLivingBase caster, EntityLivingBase target, int index) { + return -1; + } + protected void spawnZoneEntity(E entity, int index) { if (entity == null) return; spawnAbilityEntity(entity); @@ -236,14 +244,14 @@ public abstract class AbilityEnergyZone extends Abil if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return; if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return; - - double[] windupPos = getWindupSpawnPosition(caster, target, index); E previewEntity = createZoneEntityAt(caster, target, index, windupPos); previewEntity.setPreviewMode(true); previewEntity.setPreviewOwner(caster); setupEntityCharging(previewEntity, index); previewEntity.setChargeDuration(Math.max(1, chargeDuration)); + int followId = getChargeVisualFollowEntityId(caster, target, index); + previewEntity.setFollowEntityId(followId); String id = getChargeVisualId(caster, index); chargeVisualIds[index] = id; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityPillar.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityPillar.java index ee9a9380a..44d26ff0c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityPillar.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityPillar.java @@ -2,13 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type.energy; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -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.controllers.data.ability.data.energy.EnergyPillarData; -import kamkeel.npcs.controllers.data.ability.enums.HitType; +import kamkeel.npcs.controllers.data.ability.data.energy.*; import kamkeel.npcs.controllers.data.ability.enums.LockMode; import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; @@ -26,44 +20,48 @@ import noppes.npcs.client.gui.builder.FieldDef; import java.util.List; -/** - * Pillar zone ability — spawns one or more energy pillars that rise from the ground - * or fall from the sky. - * - * ANCHORED: pillar spawns at the target's position at execute time and stays there. - * When count > 1, each subsequent pillar gets a random nearby position. - * - * MOVING: pillar spawns in front of the caster at execute time and travels forward. - * With homing: steers toward target. - * Without homing: straight line along caster look vector or toward target. - * When count > 1, pillarOffset spaces pillars along the chosen OffsetAxis. - * - * Charge visual appears at the target/spawn position during windup (tick 1), - * growing in radius at ground level — exactly above the telegraph. - */ +import static kamkeel.npcs.entity.EntityAbilityPillar.MIN_HEIGHT; +import static kamkeel.npcs.entity.EntityAbilityPillar.MIN_RADIUS; +import static kamkeel.npcs.entity.EntityAbilityPillar.MAX_ZONE_HEIGHT; +import static kamkeel.npcs.entity.EntityAbilityPillar.MAX_ZONE_RADIUS; -// TODO 1. add another mode, static, 2. fix charging radius, rotation, etc +/** + * Pillar zone ability. + * + * ANCHORED: spawns at target position at execute time. + * - trackTarget: predicts where target will be based on current velocity, spawns there. + * - followTarget (requires homing): pillar chases target after spawn. + * When count > 1: each pillar after index 0 gets a random nearby position. + * + * MOVING: spawns in front of caster and travels forward. + * - Without homing: straight line. + * - With homing: steers toward target. + * When count > 1: pillarOffset spaces pillars along offsetAxis relative to look vector. + * OffsetAxis.Z = along look direction (one behind the other). + * OffsetAxis.X = perpendicular to look direction (side by side). + * + * TODO 1. fix charging visual for ABOVE origin; 2. fix hitbox for ABOVE origin; 3. fix trackTarget to actually track the player + */ public class AbilityPillar extends AbilityEnergyZone { - // ==================== PILLAR CONFIG ==================== + // ==================== CONFIG ==================== private EnergyPillarData pillarData = new EnergyPillarData(); private EnergyHomingData homingData = new EnergyHomingData(); - /** Spacing between pillars when count > 1. 0 = same position. */ private float pillarOffset = 0f; - - /** Axis along which pillarOffset is applied in MOVING mode. */ private OffsetAxis offsetAxis = OffsetAxis.Z; + /** ANCHORED only: follow target after spawn (requires homing). */ + private boolean followTarget = false; + + /** ANCHORED only: predict target position at spawn time from target velocity. */ + private boolean trackTarget = false; + // ==================== CONSTRUCTOR ==================== public AbilityPillar() { - super( - new EnergyDisplayData(), - new EnergyCombatData(), - new EnergyLifespanData() - ); + super(new EnergyDisplayData(), new EnergyCombatData(), new EnergyLifespanData()); this.typeId = "ability.cnpc.pillar"; this.name = "Pillar"; this.targetingMode = TargetingMode.AGGRO_TARGET; @@ -88,8 +86,6 @@ public class AbilityPillar extends AbilityEnergyZone { @Override protected void setupEntityCharging(EntityAbilityPillar entity, int index) { - // Charge visual: entity is already at the correct ground position. - // Just set it up for charging — it will grow in radius at that position. entity.setupCharging(windUpTicks); } @@ -111,75 +107,99 @@ public class AbilityPillar extends AbilityEnergyZone { // ==================== SPAWN POSITIONS ==================== - /** - * Spawn position at execute time. - * MOVING: in front of caster with offset per index along offsetAxis. - * ANCHORED index 0: pre-calculated telegraph position. - * ANCHORED index > 0: random position near target. - */ @Override protected double[] getSpawnPosition(EntityLivingBase caster, EntityLivingBase target, int index) { if (pillarData.mode == PillarMode.MOVING) { - Vec3 look = caster.getLookVec(); - if (look == null) return new double[]{caster.posX, caster.posY, caster.posZ}; - float baseDist = Math.max(1.0f, pillarData.targetRadius); - double ox = offsetAxis == OffsetAxis.X ? pillarOffset * index : 0; - double oz = offsetAxis == OffsetAxis.Z ? pillarOffset * index : 0; - return new double[]{ - caster.posX + look.xCoord * baseDist + ox, - caster.posY, - caster.posZ + look.zCoord * baseDist + oz - }; + return getMovingSpawnPosition(caster, index); } + // ANCHORED: spawn at target's current position at execute time if (index == 0) { - return super.getSpawnPosition(caster, target, index); + if (target == null) return new double[]{caster.posX, caster.posY, caster.posZ}; + double spawnX = target.posX; + double spawnZ = target.posZ; + if (trackTarget) { + // Predict where target will be based on current motion and spawnDelay + double prediction = pillarData.spawnDelay * homingData.getHomingStrength(); + spawnX += target.motionX * prediction; + spawnZ += target.motionZ * prediction; + } + return new double[]{spawnX, target.posY, spawnZ}; } + // index > 0: random nearby position double baseX = target != null ? target.posX : caster.posX; double baseZ = target != null ? target.posZ : caster.posZ; double baseY = target != null ? target.posY : caster.posY; float spread = Math.max(pillarData.targetRadius * 2.0f, 3.0f); double angle = Math.random() * Math.PI * 2; double dist = Math.sqrt(Math.random()) * spread; - return new double[]{ - baseX + Math.cos(angle) * dist, - baseY, - baseZ + Math.sin(angle) * dist - }; + return new double[]{baseX + Math.cos(angle) * dist, baseY, baseZ + Math.sin(angle) * dist}; + } + + @Override + protected int getChargeVisualFollowEntityId(EntityLivingBase caster, EntityLivingBase target, int index) { + // ANCHORED: charge visual follows target during charging + // MOVING: charge visual is static at spawn point + if (pillarData.mode == PillarMode.ANCHORED && target != null) { + return target.getEntityId(); + } + return -1; } - /** - * Windup spawn position for charge visual. - * Always uses the target/telegraph position so the charge visual - * appears at ground level exactly above the telegraph. - */ @Override protected double[] getWindupSpawnPosition(EntityLivingBase caster, EntityLivingBase target, int index) { if (pillarData.mode == PillarMode.MOVING) { - // Charge visual appears at the same spot the pillar will spawn - Vec3 look = caster.getLookVec(); - if (look == null) return new double[]{caster.posX, caster.posY, caster.posZ}; - float baseDist = Math.max(1.0f, pillarData.targetRadius); - double ox = offsetAxis == OffsetAxis.X ? pillarOffset * index : 0; - double oz = offsetAxis == OffsetAxis.Z ? pillarOffset * index : 0; - return new double[]{ - caster.posX + look.xCoord * baseDist + ox, - caster.posY, - caster.posZ + look.zCoord * baseDist + oz - }; + return getMovingSpawnPosition(caster, index); } - // ANCHORED: use pre-calculated telegraph position for index 0, - // target position for the rest if (index == 0 && !preCalculatedPositions.isEmpty()) { return preCalculatedPositions.get(0); } - if (target != null) { - return new double[]{target.posX, target.posY, target.posZ}; - } + if (target != null) return new double[]{target.posX, target.posY, target.posZ}; return new double[]{caster.posX, caster.posY, caster.posZ}; } + /** + * Spawn position for MOVING mode. + * OffsetAxis.Z = offset along look direction (one behind the other). + * OffsetAxis.X = offset perpendicular to look direction (side by side). + */ + private double[] getMovingSpawnPosition(EntityLivingBase caster, int index) { + Vec3 look = caster.getLookVec(); + if (look == null) return new double[]{caster.posX, caster.posY, caster.posZ}; + + float baseDist = Math.max(1.0f, pillarData.targetRadius); + + // Forward direction (normalized XZ) + double fwdX = look.xCoord; + double fwdZ = look.zCoord; + double fwdLen = Math.sqrt(fwdX * fwdX + fwdZ * fwdZ); + if (fwdLen > 0) { fwdX /= fwdLen; fwdZ /= fwdLen; } + + // Right direction = perpendicular to forward in XZ plane + double rightX = fwdZ; + double rightZ = -fwdX; + + double ox, oz; + if (offsetAxis == OffsetAxis.Z) { + // Along look direction + ox = fwdX * pillarOffset * index; + oz = fwdZ * pillarOffset * index; + } else { + // Perpendicular to look direction + // Center the group: offset by -(count-1)/2 * spacing + index * spacing + double centerOffset = -(zoneCount - 1) * 0.5 * pillarOffset + index * pillarOffset; + ox = rightX * centerOffset; + oz = rightZ * centerOffset; + } + + return new double[]{ + caster.posX + fwdX * baseDist + ox, + caster.posY, + caster.posZ + fwdZ * baseDist + oz + }; + } + // ==================== EXECUTE ==================== @Override @@ -192,22 +212,21 @@ public class AbilityPillar extends AbilityEnergyZone { entity.setTarget(target); } + if (pillarData.mode == PillarMode.ANCHORED) { + entity.setFollowTarget(followTarget && homingData.isHoming()); + } + if (pillarData.mode == PillarMode.MOVING && !homingData.isHoming()) { + Vec3 look = caster.getLookVec(); double mx = 0, mz = 0; if (target != null) { double dx = target.posX - entity.posX; double dz = target.posZ - entity.posZ; double len = Math.sqrt(dx * dx + dz * dz); - if (len > 0) { - mx = (dx / len) * homingData.getSpeed(); - mz = (dz / len) * homingData.getSpeed(); - } - } else { - Vec3 look = caster.getLookVec(); - if (look != null) { - mx = look.xCoord * homingData.getSpeed(); - mz = look.zCoord * homingData.getSpeed(); - } + if (len > 0) { mx = (dx / len) * homingData.getSpeed(); mz = (dz / len) * homingData.getSpeed(); } + } else if (look != null) { + mx = look.xCoord * homingData.getSpeed(); + mz = look.zCoord * homingData.getSpeed(); } entity.setInitialMotion(mx, mz); } @@ -219,16 +238,11 @@ public class AbilityPillar extends AbilityEnergyZone { @Override public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster)) { - return null; - } + if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster)) return null; - Telegraph telegraph; - if (pillarData.shape == PillarShape.SQUARE) { - telegraph = Telegraph.square(pillarData.targetRadius); - } else { - telegraph = Telegraph.circle(pillarData.targetRadius); - } + Telegraph telegraph = pillarData.shape == PillarShape.SQUARE + ? Telegraph.square(pillarData.targetRadius) + : Telegraph.circle(pillarData.targetRadius); telegraph.setDurationTicks(windUpTicks); telegraph.setColor(windUpColor); @@ -239,18 +253,13 @@ public class AbilityPillar extends AbilityEnergyZone { double telegraphX, telegraphZ, telegraphY; if (pillarData.mode == PillarMode.MOVING) { - Vec3 look = caster.getLookVec(); - if (look != null) { - float dist = Math.max(1.0f, pillarData.targetRadius); - telegraphX = caster.posX + look.xCoord * dist; - telegraphZ = caster.posZ + look.zCoord * dist; - } else { - telegraphX = caster.posX; - telegraphZ = caster.posZ; - } + double[] pos = getMovingSpawnPosition(caster, 0); + telegraphX = pos[0]; + telegraphZ = pos[2]; telegraphY = findGroundLevel(caster.worldObj, telegraphX, caster.posY, telegraphZ); } else { if (target == null) return null; + // Regular: start at target's current position, telegraph follows target telegraphX = target.posX; telegraphZ = target.posZ; telegraphY = findGroundLevel(caster.worldObj, telegraphX, target.posY, telegraphZ); @@ -262,8 +271,14 @@ public class AbilityPillar extends AbilityEnergyZone { TelegraphInstance instance = new TelegraphInstance( telegraph, telegraphX, telegraphY, telegraphZ, caster.rotationYaw); instance.setCasterEntityId(caster.getEntityId()); - instance.setEntityIdToFollow(-1); + // ANCHORED: telegraph follows target in real time (regular and trackTarget) + // MOVING: telegraph is static at spawn point + if (pillarData.mode == PillarMode.ANCHORED && target != null) { + instance.setEntityIdToFollow(target.getEntityId()); + } else { + instance.setEntityIdToFollow(-1); + } return instance; } @@ -280,6 +295,8 @@ public class AbilityPillar extends AbilityEnergyZone { homingData.writeNBT(nbt); nbt.setFloat("pillarOffset", pillarOffset); nbt.setInteger("offsetAxis", offsetAxis.ordinal()); + nbt.setBoolean("followTarget", followTarget); + nbt.setBoolean("trackTarget", trackTarget); } @Override @@ -290,6 +307,8 @@ public class AbilityPillar extends AbilityEnergyZone { int axisOrd = nbt.hasKey("offsetAxis") ? nbt.getInteger("offsetAxis") : 1; this.offsetAxis = (axisOrd >= 0 && axisOrd < OffsetAxis.values().length) ? OffsetAxis.values()[axisOrd] : OffsetAxis.Z; + this.followTarget = nbt.hasKey("followTarget") && nbt.getBoolean("followTarget"); + this.trackTarget = nbt.hasKey("trackTarget") && nbt.getBoolean("trackTarget"); } // ==================== GUI ==================== @@ -298,47 +317,54 @@ public class AbilityPillar extends AbilityEnergyZone { @Override protected void addTypeDefinitions(List defs) { FieldDef.insertAfter(defs, "ability.fireDelay", FieldDef.row( - FieldDef.floatField("ability.pillar.offset", () -> pillarOffset, v -> pillarOffset = Math.max(0f, v)).range(0f, 20f), - FieldDef.enumField("ability.pillar.offsetAxis", OffsetAxis.class, () -> offsetAxis, v -> offsetAxis = v) - ).visibleWhen(() -> zoneCount > 1)); + FieldDef.floatField("ability.pillar.offset", this::getPillarOffset, this::setPillarOffset).range(0f, 20f), + FieldDef.enumField("ability.pillar.offsetAxis", OffsetAxis.class, this::getOffsetAxis, this::setOffsetAxis) + ).visibleWhen(() -> zoneCount > 1 && pillarData.mode == PillarMode.MOVING)); defs.add(FieldDef.section("ability.section.pillar")); defs.add(FieldDef.enumField("ability.pillarMode", PillarMode.class, - () -> pillarData.mode, v -> pillarData.mode = v)); + this::getMode, this::setMode)); defs.add(FieldDef.enumField("ability.pillarOrigin", EntityAbilityPillar.PillarOrigin.class, - () -> pillarData.origin, v -> pillarData.origin = v)); + this::getOrigin, this::setOrigin)); defs.add(FieldDef.enumField("ability.pillarShape", PillarShape.class, - () -> pillarData.shape, v -> pillarData.shape = v)); + this::getShape, this::setShape)); - defs.add(FieldDef.intField("ability.pillar.spawnDelay", () -> pillarData.spawnDelay, v -> pillarData.spawnDelay = Math.max(0, v)) - .range(0, 200)); + defs.add(FieldDef.intField("ability.pillar.spawnDelay", + this::getSpawnDelay, this::setSpawnDelay).range(0, getMaxLifetime())); defs.add(FieldDef.section("ability.section.size")); defs.add(FieldDef.row( - FieldDef.floatField("ability.pillar.targetRadius", () -> pillarData.targetRadius, v -> pillarData.targetRadius = v).range(0.1f, 64f), - FieldDef.floatField("ability.pillar.targetHeight", () -> pillarData.targetHeight, v -> pillarData.targetHeight = v).range(0.1f, 64f) + FieldDef.floatField("ability.pillar.targetRadius", this::getTargetRadius, this::setTargetRadius).range(0.1f, 64f), + FieldDef.floatField("ability.pillar.targetHeight", this::getTargetHeight, this::setTargetHeight).range(0.1f, 64f) )); defs.add(FieldDef.row( - FieldDef.floatField("ability.pillar.radiusGrowSpeed", () -> pillarData.radiusGrowSpeed, v -> pillarData.radiusGrowSpeed = Math.max(0.01f, v)), - FieldDef.floatField("ability.pillar.heightGrowSpeed", () -> pillarData.heightGrowSpeed, v -> pillarData.heightGrowSpeed = Math.max(0.01f, v)) + FieldDef.floatField("ability.pillar.radiusGrowSpeed", this::getRadiusGrowSpeed, this::setRadiusGrowSpeed), + FieldDef.floatField("ability.pillar.heightGrowSpeed", this::getHeightGrowSpeed, this::setRadiusGrowSpeed) )); defs.add(FieldDef.section("ability.section.movement")); - defs.add(FieldDef.floatField("ability.speed", () -> homingData.getSpeed(), v -> homingData.setSpeed(v)) + defs.add(FieldDef.floatField("ability.speed", this::getPillarSpeed, this::setPillarSpeed) .range(0.01f, 10f)); - defs.add(FieldDef.boolField("ability.homing", () -> homingData.isHoming(), v -> homingData.setHoming(v))); + defs.add(FieldDef.boolField("ability.homing", this::isHoming, this::setHoming)); defs.add(FieldDef.row( - FieldDef.floatField("ability.homingStrength", () -> homingData.getHomingStrength(), v -> homingData.setHomingStrength(v)).range(0f, 1f), - FieldDef.floatField("ability.homingRange", () -> homingData.getHomingRange(), v -> homingData.setHomingRange(v)) - ).visibleWhen(() -> homingData.isHoming())); + FieldDef.floatField("ability.homingStrength", this::getHomingStrength, this::setHomingStrength), + FieldDef.floatField("ability.homingRange", this::getHomingRange, this::setHomingRange) + ).visibleWhen(this::isHoming)); + + // ANCHORED-only options + defs.add(FieldDef.boolField("ability.pillar.trackTarget", this::isTrackTarget, this::setTrackTarget) + .visibleWhen(() -> pillarData.mode == PillarMode.ANCHORED)); + + defs.add(FieldDef.boolField("ability.pillar.followTarget", this::isFollowTarget, this::setFollowTarget) + .visibleWhen(() -> pillarData.mode == PillarMode.ANCHORED && homingData.isHoming())); defs.add(FieldDef.section("ability.section.combat")); @@ -354,12 +380,111 @@ public class AbilityPillar extends AbilityEnergyZone { public EnergyPillarData getPillarData() { return pillarData; } public void setPillarData(EnergyPillarData data) { this.pillarData = data != null ? data : new EnergyPillarData(); } - public EnergyHomingData getHomingData() { return homingData; } - public void setHomingData(EnergyHomingData data) { this.homingData = data != null ? data : new EnergyHomingData(); } - public float getPillarOffset() { return pillarOffset; } - public void setPillarOffset(float offset) { this.pillarOffset = Math.max(0f, offset); } + public void setPillarOffset(float offset) { this.pillarOffset = Math.max(0f, Math.min(20f, offset)); } public OffsetAxis getOffsetAxis() { return offsetAxis; } public void setOffsetAxis(OffsetAxis axis) { this.offsetAxis = axis != null ? axis : OffsetAxis.Z; } + + public boolean isFollowTarget() { return followTarget; } + public void setFollowTarget(boolean v) { this.followTarget = v; } + + public boolean isTrackTarget() { return trackTarget; } + public void setTrackTarget(boolean v) { this.trackTarget = v; } + + public float getTargetRadius() { + return pillarData.targetRadius; + } + + public void setTargetRadius(float targetRadius) { + pillarData.targetRadius = Math.max(MIN_RADIUS, Math.min(MAX_ZONE_RADIUS, targetRadius)); + } + + public float getTargetHeight() { + return pillarData.targetHeight; + } + + public void setTargetHeight(float targetHeight) { + pillarData.targetHeight = Math.max(MIN_HEIGHT, Math.min(MAX_ZONE_HEIGHT, targetHeight)); + } + + public float getRadiusGrowSpeed() { + return pillarData.radiusGrowSpeed; + } + + public void setRadiusGrowSpeed(float radiusGrowSpeed) { + pillarData.radiusGrowSpeed = Math.max(0.01f, radiusGrowSpeed); + } + + public float getHeightGrowSpeed() { + return pillarData.heightGrowSpeed; + } + + public void setHeightGrowSpeed(float heightGrowSpeed) { + pillarData.heightGrowSpeed = Math.max(0.01f, heightGrowSpeed); + } + + public PillarMode getMode() { + return pillarData.mode; + } + + public void setMode(PillarMode mode) { + pillarData.mode = mode; + } + + public EntityAbilityPillar.PillarOrigin getOrigin() { + return pillarData.origin; + } + + public void setOrigin(EntityAbilityPillar.PillarOrigin origin) { + pillarData.origin = origin; + } + + public PillarShape getShape() { + return pillarData.shape; + } + + public void setShape(PillarShape shape) { + pillarData.shape = shape; + } + + public int getSpawnDelay() { + return pillarData.spawnDelay; + } + + public void setSpawnDelay(int spawnDelay) { + pillarData.spawnDelay = Math.max(0, Math.min(getMaxLifetime(), spawnDelay)); + } + + public float getPillarSpeed() { + return homingData.speed; + } + + public void setPillarSpeed(float speed) { + homingData.speed = speed; + } + + public boolean isHoming() { + return homingData.homing; + } + + public void setHoming(boolean homing) { + homingData.homing = homing; + } + + public float getHomingStrength() { + return homingData.homingStrength; + } + + public void setHomingStrength(float strength) { + homingData.homingStrength = strength; + } + + public float getHomingRange() { + return homingData.homingRange; + } + + public void setHomingRange(float range) { + homingData.homingRange = range; + } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityPillar.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityPillar.java index e50d19f23..43ac81e5e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityPillar.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityPillar.java @@ -8,79 +8,62 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.Vec3; import net.minecraft.world.World; import java.util.List; /** - * Pillar zone entity — a column of energy that rises from the ground or falls from the sky. - * - * Behavior modes: - * - ANCHORED: spawns at a fixed position and stays there. - * - MOVING: spawns in front of the caster and travels forward, - * optionally homing toward a target. Without homing, travels in a straight - * line along the initial direction set by setInitialMotion(). + * Pillar zone entity. * * Lifecycle: * 1. Charging — charge visual grows in radius at ground level during windup. - * 2. spawnDelay — entity is alive but does not grow and does not deal damage. - * 3. Active — pillarHeight grows toward targetHeight; damage checks begin. + * 2. spawnDelay — entity is alive, not growing, no damage. + * 3. Active — pillarHeight grows linearly at heightGrowSpeed; damage checks begin. * - * FROM_ABOVE: posY is always the top anchor. The pillar grows downward. - * Hitbox spans from posY - height to posY. - * FROM_GROUND: posY is the ground anchor. The pillar grows upward. - * Hitbox spans from posY to posY + height. + * Radius snaps to targetRadius immediately when charging ends — no interpolation. + * Height grows linearly with no easing. + * + * FROM_ABOVE: posY = top anchor. Hitbox: posY - height to posY. + * FROM_GROUND: posY = ground anchor. Hitbox: posY to posY + height. + * + * ANCHORED modes: + * - followTarget: pillar moves directly toward target each tick (requires homing=true). + * - trackTarget: spawn position predicted once from target velocity at spawn time. */ public class EntityAbilityPillar extends EntityEnergyZone { - // ==================== CONSTANTS ==================== - - private static final float MIN_HEIGHT = 0.05f; - + public static final float MIN_HEIGHT = 0.05f; + public static final float MIN_RADIUS = 0.01f; private static final int DW_PILLAR_RADIUS = 21; private static final int DW_PILLAR_HEIGHT = 22; // ==================== ENUMS ==================== - public enum PillarMode { - ANCHORED, - MOVING - } + public enum PillarMode { ANCHORED, MOVING } - public enum PillarOrigin { - FROM_GROUND, - FROM_ABOVE - } + public enum PillarOrigin { FROM_GROUND, FROM_ABOVE } public enum PillarShape { - CIRCLE, - SQUARE; + CIRCLE, SQUARE; public TelegraphType getTelegraphType() { - if (this == CIRCLE) return TelegraphType.CIRCLE; - else return TelegraphType.SQUARE; + return this == CIRCLE ? TelegraphType.CIRCLE : TelegraphType.SQUARE; } } - public enum OffsetAxis { - X, - Z - } + public enum OffsetAxis { X, Z } - // ==================== PILLAR CONFIG ==================== + // ==================== CONFIG ==================== private EnergyPillarData pillarData = new EnergyPillarData(); - // ==================== PILLAR DIMENSIONS ==================== + // ==================== DIMENSIONS ==================== private float pillarRadius = 0.01f; private float pillarHeight = MIN_HEIGHT; - private float renderPillarRadius = 0.01f; + // Height interpolation only — radius snaps immediately private float renderPillarHeight = MIN_HEIGHT; - - private float prevRenderPillarRadius = 0.01f; private float prevRenderPillarHeight = MIN_HEIGHT; // ==================== HOMING ==================== @@ -88,6 +71,11 @@ public class EntityAbilityPillar extends EntityEnergyZone { protected EnergyHomingData homingData = new EnergyHomingData(); private int targetEntityId = -1; + // ==================== ANCHORED MODES ==================== + + /** If true, pillar follows target directly each tick (ANCHORED + homing only). */ + private boolean followTarget = false; + // ==================== RUNTIME STATE ==================== private boolean growing = false; @@ -105,7 +93,6 @@ public class EntityAbilityPillar extends EntityEnergyZone { EnergyCombatData combat, EnergyHomingData homing, EnergyLightningData lightning, EnergyLifespanData lifespan) { super(world); - this.ownerEntityId = owner != null ? owner.getEntityId() : -1; this.pillarData = pillarData != null ? pillarData.copy() : new EnergyPillarData(); this.displayData = display != null ? display.copy() : new EnergyDisplayData(); @@ -114,9 +101,8 @@ public class EntityAbilityPillar extends EntityEnergyZone { this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData(); this.lifespanData = lifespan != null ? lifespan.copy() : new EnergyLifespanData(); - // FROM_ABOVE: posY is the top anchor — position at target height, not ground - if (pillarData != null && pillarData.origin == PillarOrigin.FROM_ABOVE) { - this.setPosition(x, y + pillarData.targetHeight, z); + if (this.pillarData.origin == PillarOrigin.FROM_ABOVE) { + this.setPosition(x, y + this.pillarData.targetHeight, z); } else { snapToGround(x, y, z); } @@ -135,7 +121,7 @@ public class EntityAbilityPillar extends EntityEnergyZone { this.dataWatcher.addObject(DW_PILLAR_HEIGHT, MIN_HEIGHT); } - // ==================== INTERNAL DIMENSION SETTERS ==================== + // ==================== INTERNAL SETTERS ==================== private void setPillarRadiusInternal(float value) { this.pillarRadius = value; @@ -157,9 +143,10 @@ public class EntityAbilityPillar extends EntityEnergyZone { this.targetEntityId = target != null ? target.getEntityId() : -1; } - /** - * Set straight-line motion for MOVING mode without homing. - */ + public void setFollowTarget(boolean follow) { + this.followTarget = follow; + } + public void setInitialMotion(double mx, double mz) { this.motionX = mx; this.motionZ = mz; @@ -175,22 +162,19 @@ public class EntityAbilityPillar extends EntityEnergyZone { setupCharging(chargeDuration); setPillarRadiusInternal(0.01f); setPillarHeightInternal(MIN_HEIGHT); - this.renderPillarRadius = 0.01f; - this.prevRenderPillarRadius = 0.01f; this.renderPillarHeight = MIN_HEIGHT; this.prevRenderPillarHeight = MIN_HEIGHT; } - /** - * Called by the ability when charging ends. - * Starts the spawnDelay countdown. No damage is dealt during spawnDelay. - */ public void startGrowing() { setCharging(false); this.spawnDelayTick = 0; this.growing = pillarData.spawnDelay <= 0; + // Radius snaps immediately to target — no interpolation setPillarRadiusInternal(pillarData.targetRadius); setPillarHeightInternal(MIN_HEIGHT); + this.renderPillarHeight = MIN_HEIGHT; + this.prevRenderPillarHeight = MIN_HEIGHT; syncHitbox(); } @@ -199,6 +183,7 @@ public class EntityAbilityPillar extends EntityEnergyZone { @Override protected void updateCharging() { chargeTick++; + // Radius grows during charging for the visual disc effect float progress = getChargeProgress(); setPillarRadiusInternal(pillarData.targetRadius * progress); setPillarHeightInternal(MIN_HEIGHT); @@ -209,20 +194,20 @@ public class EntityAbilityPillar extends EntityEnergyZone { @Override protected void updateZone() { - prevRenderPillarRadius = renderPillarRadius; prevRenderPillarHeight = renderPillarHeight; if (worldObj.isRemote) { float dwRadius = this.dataWatcher.getWatchableObjectFloat(DW_PILLAR_RADIUS); float dwHeight = this.dataWatcher.getWatchableObjectFloat(DW_PILLAR_HEIGHT); + // Radius: snap directly, no interpolation this.pillarRadius = dwRadius; + // Height: linear tracking, no easing this.pillarHeight = dwHeight; - renderPillarRadius += (pillarRadius - renderPillarRadius) * 0.15f; - renderPillarHeight += (pillarHeight - renderPillarHeight) * 0.15f; + renderPillarHeight = pillarHeight; return; } - // spawnDelay: entity is alive but not growing and not dealing damage + // spawnDelay: no growth, no damage if (!growing) { spawnDelayTick++; if (spawnDelayTick >= pillarData.spawnDelay) { @@ -231,26 +216,27 @@ public class EntityAbilityPillar extends EntityEnergyZone { return; } - if (pillarRadius < pillarData.targetRadius) { - setPillarRadiusInternal(Math.min(pillarData.targetRadius, pillarRadius + pillarData.radiusGrowSpeed)); - } + // Linear height growth — no easing, exact speed per tick if (pillarHeight < pillarData.targetHeight) { setPillarHeightInternal(Math.min(pillarData.targetHeight, pillarHeight + pillarData.heightGrowSpeed)); } + // Radius stays at targetRadius (set in startGrowing) syncHitbox(); if (pillarData.mode == PillarMode.MOVING) { updateMovement(); + } else if (pillarData.mode == PillarMode.ANCHORED && followTarget) { + updateFollowTarget(); } - // Only deal damage once growing has started and height is meaningful + // No damage during spawnDelay — only when growing if (pillarHeight > MIN_HEIGHT) { checkEntityCollision(); } } - // ==================== MOVEMENT (MOVING mode) ==================== + // ==================== MOVEMENT ==================== private void updateMovement() { if (!homingData.isHoming()) { @@ -292,10 +278,25 @@ public class EntityAbilityPillar extends EntityEnergyZone { } /** - * Re-anchor Y position after horizontal movement. - * FROM_GROUND: snap to ground. - * FROM_ABOVE: snap to ground + targetHeight (top anchor stays at ceiling height). + * ANCHORED followTarget: moves directly toward target each tick. */ + private void updateFollowTarget() { + Entity target = getTargetEntity(); + if (target == null || !target.isEntityAlive()) return; + + double dx = target.posX - posX; + double dz = target.posZ - posZ; + double dist = Math.sqrt(dx * dx + dz * dz); + + if (dist > 0.1) { + float speed = homingData.getSpeed(); + double step = Math.min(dist, speed); + this.posX += (dx / dist) * step; + this.posZ += (dz / dist) * step; + repositionY(); + } + } + private void repositionY() { if (pillarData.origin == PillarOrigin.FROM_ABOVE) { double groundY = kamkeel.npcs.controllers.data.ability.Ability.findGroundLevel(worldObj, posX, posY, posZ); @@ -331,11 +332,6 @@ public class EntityAbilityPillar extends EntityEnergyZone { } } - /** - * Build the hit box based on origin. - * FROM_GROUND: posY (base) to posY + height (tip). - * FROM_ABOVE: posY - height (tip) to posY (base/ceiling). - */ private AxisAlignedBB buildHitBox() { float minY, maxY; if (pillarData.origin == PillarOrigin.FROM_ABOVE) { @@ -347,19 +343,15 @@ public class EntityAbilityPillar extends EntityEnergyZone { } return AxisAlignedBB.getBoundingBox( posX - pillarRadius, minY, posZ - pillarRadius, - posX + pillarRadius, maxY, posZ + pillarRadius - ); + posX + pillarRadius, maxY, posZ + pillarRadius); } private boolean isInPillar(EntityLivingBase entity) { double dx = entity.posX - posX; double dz = entity.posZ - posZ; switch (pillarData.shape) { - case SQUARE: - return Math.abs(dx) <= pillarRadius && Math.abs(dz) <= pillarRadius; - case CIRCLE: - default: - return Math.sqrt(dx * dx + dz * dz) <= pillarRadius; + case SQUARE: return Math.abs(dx) <= pillarRadius && Math.abs(dz) <= pillarRadius; + case CIRCLE: default: return Math.sqrt(dx * dx + dz * dz) <= pillarRadius; } } @@ -377,7 +369,7 @@ public class EntityAbilityPillar extends EntityEnergyZone { return distance < range * range; } - // ==================== ENTITY HELPERS ==================== + // ==================== HELPERS ==================== public Entity getTargetEntity() { if (targetEntityId == -1) return null; @@ -388,20 +380,15 @@ public class EntityAbilityPillar extends EntityEnergyZone { public float getPillarRadius() { return pillarRadius; } public float getPillarHeight() { return pillarHeight; } + public float getInterpolatedPillarHeight(float partialTicks) { + return prevRenderPillarHeight + (renderPillarHeight - prevRenderPillarHeight) * partialTicks; + } public PillarMode getPillarMode() { return pillarData.mode; } public PillarOrigin getPillarOrigin() { return pillarData.origin; } public PillarShape getPillarShape() { return pillarData.shape; } public boolean isGrowing() { return growing; } public EnergyPillarData getPillarData() { return pillarData; } - public float getInterpolatedPillarRadius(float partialTicks) { - return prevRenderPillarRadius + (renderPillarRadius - prevRenderPillarRadius) * partialTicks; - } - - public float getInterpolatedPillarHeight(float partialTicks) { - return prevRenderPillarHeight + (renderPillarHeight - prevRenderPillarHeight) * partialTicks; - } - // ==================== SETTERS ==================== public void setPillarData(EnergyPillarData data) { this.pillarData = data != null ? data.copy() : new EnergyPillarData(); } @@ -417,6 +404,7 @@ public class EntityAbilityPillar extends EntityEnergyZone { nbt.setFloat("PillarHeight", pillarHeight); nbt.setInteger("SpawnDelayTick", spawnDelayTick); nbt.setBoolean("Growing", growing); + nbt.setBoolean("FollowTarget", followTarget); nbt.setInteger("TargetEntityId", targetEntityId); nbt.setDouble("MotionX", motionX); nbt.setDouble("MotionZ", motionZ); @@ -438,13 +426,12 @@ public class EntityAbilityPillar extends EntityEnergyZone { this.dataWatcher.updateObject(DW_PILLAR_HEIGHT, height); } - this.renderPillarRadius = radius; - this.prevRenderPillarRadius = radius; this.renderPillarHeight = height; this.prevRenderPillarHeight = height; this.spawnDelayTick = nbt.getInteger("SpawnDelayTick"); this.growing = nbt.getBoolean("Growing"); + this.followTarget = nbt.hasKey("FollowTarget") && nbt.getBoolean("FollowTarget"); this.targetEntityId = nbt.hasKey("TargetEntityId") ? nbt.getInteger("TargetEntityId") : -1; this.motionX = nbt.getDouble("MotionX"); this.motionZ = nbt.getDouble("MotionZ"); diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyZone.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyZone.java index 6a8d7d1fe..08d03c57f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyZone.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyZone.java @@ -53,8 +53,8 @@ public abstract class EntityEnergyZone extends EntityEnergyAbility { // ==================== SAFETY CONSTANTS ==================== - protected static final float MAX_ZONE_RADIUS = 64.0f; - protected static final float MAX_ZONE_HEIGHT = 64.0f; + public static final float MAX_ZONE_RADIUS = 64.0f; + public static final float MAX_ZONE_HEIGHT = 64.0f; protected static final double GROUND_OFFSET = 0.05; // ==================== LIFESPAN ==================== @@ -78,6 +78,12 @@ public abstract class EntityEnergyZone extends EntityEnergyAbility { protected int hitCount = 0; + /** + * Client-side only: if >= 0, the preview entity updates its position + * to match this entity each tick during charging (charge visual follows target). + */ + protected int followEntityId = -1; + // ==================== SIZE — LOGICAL ==================== /** Current radius used for collision and damage checks. */ @@ -144,6 +150,15 @@ public abstract class EntityEnergyZone extends EntityEnergyAbility { if (isCharging()) { this.renderRadius = this.radius; this.renderHeight = this.height; + + // Follow entity position during charging preview (e.g. charge visual follows target) + if (followEntityId != -1 && worldObj != null) { + Entity followed = worldObj.getEntityByID(followEntityId); + if (followed instanceof EntityLivingBase && !followed.isDead) { + snapToGround(followed.posX, followed.posY, followed.posZ); + } + } + updateCharging(); return; } else { @@ -415,6 +430,9 @@ public abstract class EntityEnergyZone extends EntityEnergyAbility { public float getTargetHeight() { return targetHeight; } public void setTargetHeight(float height) { this.targetHeight = sanitize(height, 1.0f, MAX_ZONE_HEIGHT); } + public int getFollowEntityId() { return followEntityId; } + public void setFollowEntityId(int id) { this.followEntityId = id; } + public void setEffects(List effects) { if (effects == null || effects.isEmpty()) { this.effects = new ArrayList<>(); @@ -461,6 +479,7 @@ public abstract class EntityEnergyZone extends EntityEnergyAbility { nbt.setBoolean("Charging", isCharging()); nbt.setInteger("ChargeDuration", chargeDuration); nbt.setInteger("ChargeTick", chargeTick); + nbt.setInteger("ChargeFollowEntityId", followEntityId); NBTTagList effectsList = new NBTTagList(); for (AbilityPotionEffect effect : effects) { @@ -502,6 +521,7 @@ public abstract class EntityEnergyZone extends EntityEnergyAbility { } this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 0; this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.followEntityId = nbt.hasKey("ChargeFollowEntityId") ? nbt.getInteger("ChargeFollowEntityId") : -1; this.effects.clear(); if (nbt.hasKey("Effects")) {