yet more pillar fixes

This commit is contained in:
Vidal committed 2026-03-15 01:50:52 -03:00
1 parent 9546500b4b
commit 956d2e27be
6 files changed
+438 -246

No files matched your search

@@ -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);
@@ -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);
@@ -210,6 +210,14 @@ public abstract class AbilityEnergyZone<E extends EntityEnergyZone> 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<E extends EntityEnergyZone> 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;
@@ -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<EntityAbilityPillar> {
// ==================== 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<EntityAbilityPillar> {
@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<EntityAbilityPillar> {
// ==================== 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<EntityAbilityPillar> {
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<EntityAbilityPillar> {
@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<EntityAbilityPillar> {
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<EntityAbilityPillar> {
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<EntityAbilityPillar> {
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<EntityAbilityPillar> {
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<EntityAbilityPillar> {
@Override
protected void addTypeDefinitions(List<FieldDef> 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<EntityAbilityPillar> {
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;
}
}
@@ -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");
@@ -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<AbilityPotionEffect> 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")) {