Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Vidal committed 2026-02-12 16:24:26 -03:00
commit ab1de29e47
13 files changed
+2561 -985

No files matched your search

+1 -1
Submodule src/api updated: fd4c77d015...092b18fe95.
@@ -0,0 +1,754 @@
package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer;
import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityAbilityZone.AccentStyle;
import kamkeel.npcs.entity.EntityAbilityZone.ParticleMotion;
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
import kamkeel.npcs.entity.EntityAbilityZone.ZoneType;
import noppes.npcs.client.fx.CustomFX;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.entity.Render;
import net.minecraft.entity.Entity;
import net.minecraft.util.ResourceLocation;
import noppes.npcs.client.fx.ZoneParticleFX;
import org.lwjgl.opengl.GL11;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
/**
* Renders zone effects for Trap and Hazard abilities.
* Supports CIRCLE and SQUARE shapes with composable visual layers.
*/
@SideOnly(Side.CLIENT)
public class RenderAbilityZone extends Render {
private static final int CIRCLE_SEGMENTS = 32;
private static final int ACCENT_LINE_COUNT = 12;
private static final Random RANDOM = new Random();
// Track last particle spawn tick per entity to avoid duplicate spawns
private final Map<Integer, Integer> lastParticleTick = new HashMap<>();
// Persistent lightning state
private final Map<Integer, AttachedLightningRenderer.LightningState> lightningStates = new HashMap<>();
// ═══════════════════════════════════════════════════════════════════
// RENDER CONTEXT
// ═══════════════════════════════════════════════════════════════════
private static class Ctx {
EntityAbilityZone zone;
ZoneShape shape;
float radius, height;
float ir, ig, ib; // inner color
float outerR, outerG, outerB;
boolean outerEnabled;
float time, pulse, rotation, brightness;
boolean flash, newTick;
float particleDensity, particleScale, animSpeed, lightningDensity;
// Visual layer fields (always populated)
boolean groundFill, rings, border, accents, lightning, particles;
float groundAlpha, borderSpeed;
int ringCount, accentStyle, particleMotion, particleSize;
boolean particleGlow;
String particleDir;
}
// ═══════════════════════════════════════════════════════════════════
// MAIN RENDER
// ═══════════════════════════════════════════════════════════════════
@Override
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
EntityAbilityZone zone = (EntityAbilityZone) entity;
// Build context
Ctx c = new Ctx();
c.zone = zone;
c.shape = zone.getShape();
c.radius = zone.getRadius();
c.height = zone.getZoneHeight();
int inner = zone.getInnerColor();
c.ir = ((inner >> 16) & 0xFF) / 255.0f;
c.ig = ((inner >> 8) & 0xFF) / 255.0f;
c.ib = (inner & 0xFF) / 255.0f;
int outer = zone.getOuterColor();
c.outerR = ((outer >> 16) & 0xFF) / 255.0f;
c.outerG = ((outer >> 8) & 0xFF) / 255.0f;
c.outerB = (outer & 0xFF) / 255.0f;
c.outerEnabled = zone.isOuterColorEnabled();
c.particleDensity = zone.getParticleDensity();
c.particleScale = zone.getParticleScale();
c.animSpeed = zone.getAnimSpeed();
c.lightningDensity = zone.getLightningDensity();
// Visual layer fields — always populated
c.groundFill = zone.isGroundFill();
c.groundAlpha = zone.getGroundAlpha();
c.rings = zone.isRings();
c.ringCount = zone.getRingCount();
c.border = zone.isBorder();
c.borderSpeed = zone.getBorderSpeed();
c.accents = zone.isAccents();
c.accentStyle = zone.getAccentStyle();
c.lightning = zone.isLightning();
c.particles = zone.isParticles();
c.particleMotion = zone.getParticleMotion();
c.particleDir = zone.getParticleDir();
c.particleSize = zone.getParticleSize();
c.particleGlow = zone.isParticleGlow();
c.time = zone.ticksExisted + partialTicks;
c.pulse = c.time * 0.1f * c.animSpeed;
c.rotation = c.time * 1.5f * c.animSpeed;
c.brightness = 1.0f;
if (zone.getZoneType() == ZoneType.TRAP) {
if (!zone.isArmed()) {
c.brightness = 0.4f;
} else {
c.pulse = c.time * 0.2f;
}
}
c.flash = zone.getTriggerFlashTick() >= 0
&& zone.ticksExisted - zone.getTriggerFlashTick() < 4;
c.newTick = isNewTick(zone);
// Render
setupRenderState();
GL11.glPushMatrix();
GL11.glTranslated(x, y, z);
GL11.glDepthMask(false);
renderZone(c);
GL11.glDepthMask(true);
GL11.glPopMatrix();
restoreRenderState();
}
private boolean isNewTick(EntityAbilityZone zone) {
int id = zone.getEntityId();
int tick = zone.ticksExisted;
Integer last = lastParticleTick.get(id);
if (last == null || last != tick) {
lastParticleTick.put(id, tick);
return true;
}
return false;
}
private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityZone zone) {
int id = zone.getEntityId();
AttachedLightningRenderer.LightningState state = lightningStates.get(id);
if (state == null) {
state = new AttachedLightningRenderer.LightningState();
lightningStates.put(id, state);
}
return state;
}
// ═══════════════════════════════════════════════════════════════════
// UNIFIED RENDER — Composable visual layers
// ═══════════════════════════════════════════════════════════════════
private void renderZone(Ctx c) {
float rR = ringR(c), rG = ringG(c), rB = ringB(c);
// 1. Ground fill
if (c.groundFill) {
if (c.flash) {
renderShapeFill(c.shape, c.radius, 1, 1, 1, 0.5f);
} else {
renderShapeFill(c.shape, c.radius,
c.ir, c.ig, c.ib, c.groundAlpha * c.brightness);
}
}
// 2. Pulse rings
if (c.rings) {
int count = Math.max(1, Math.min(5, c.ringCount));
for (int i = 0; i < count; i++) {
float frac = (i + 1.0f) / (count + 1);
float ra = (0.4f + 0.3f * sin(c.pulse + i * Math.PI / 3)) * c.brightness;
float yOff = 0.02f + i * 0.01f;
renderRingLine(c.shape, c.radius * frac,
rR, rG, rB, ra, yOff);
}
// Always include outer edge ring
float edgeA = (0.5f + 0.4f * sin(c.pulse + count * Math.PI / 3)) * c.brightness;
renderRingLine(c.shape, c.radius, rR, rG, rB, edgeA, 0.02f + count * 0.01f);
}
// 3. Rotating border
if (c.border) {
renderRotatingBorder(c.shape, c.radius, rR, rG, rB,
0.7f * c.brightness, c.rotation * c.borderSpeed, 0.05f);
}
// 4. Vertical accents
if (c.accents) {
float aa = 0.5f * c.brightness * (0.3f + 0.2f * sin(c.pulse * 0.7f));
AccentStyle style = AccentStyle.values()[Math.min(c.accentStyle, AccentStyle.values().length - 1)];
switch (style) {
case SWAYING:
renderSwayingAccents(c, c.ir, c.ig, c.ib, aa, c.height);
break;
case FLICKERING:
renderFlickeringAccents(c, c.ir, c.ig, c.ib, aa, c.height);
break;
default:
renderVerticalAccents(c.shape, c.radius, c.ir, c.ig, c.ib, aa, c.height, c.rotation * 0.3f);
break;
}
}
// 5. Lightning
if (c.lightning) {
int innerColor = c.zone.getInnerColor();
int outerColor = c.outerEnabled ? c.zone.getOuterColor() : innerColor;
AttachedLightningRenderer.LightningState state = getLightningState(c.zone);
if (c.newTick) {
state.tick();
float density = 0.4f * c.brightness * c.lightningDensity;
if (RANDOM.nextFloat() < density) {
double[] p1 = randomPosInZone(c);
double[] p2 = randomPosInZone(c);
double y1 = RANDOM.nextDouble() * c.height * 0.4;
double y2 = RANDOM.nextDouble() * c.height * 0.4;
float disp = c.radius * 0.15f;
state.addArc(AttachedLightningRenderer.createArcBetween(
p1[0], y1, p1[2], p2[0], y2, p2[2], disp, outerColor, innerColor, 6));
}
if (RANDOM.nextFloat() < density * 0.5f * c.lightningDensity) {
double[] edgePos = randomEdgePos(c);
double ex = edgePos[0];
double ez = edgePos[1];
float disp = c.radius * 0.12f;
state.addArc(AttachedLightningRenderer.createArcBetween(
ex, 0, ez, ex + (RANDOM.nextDouble() - 0.5) * 0.5,
c.height * (0.3 + RANDOM.nextDouble() * 0.4),
ez + (RANDOM.nextDouble() - 0.5) * 0.5,
disp, outerColor, innerColor, 5));
}
}
state.render();
GL11.glDepthMask(false); // Reset after lightning
}
// 6. Particles
if (c.particles && c.newTick && c.particleDensity > 0) {
if (c.particleDir != null && !c.particleDir.isEmpty()) {
spawnCustomFXParticle(c);
} else {
spawnDefaultParticles(c);
}
}
}
/** Spawn CustomFX particles using a texture directory. */
private void spawnCustomFXParticle(Ctx c) {
int count = Math.round(3 * c.particleDensity);
for (int i = 0; i < count; i++) {
double[] pos = randomPosInZone(c);
double[] motion = getMotionForStyle(c.particleMotion);
CustomFX fx = new CustomFX(c.zone.worldObj, null, c.particleDir,
c.zone.posX + pos[0], c.zone.posY + pos[1], c.zone.posZ + pos[2],
motion[0], motion[1], motion[2]);
fx.setMaxAge(20 + RANDOM.nextInt(20));
fx.scaleX1 = c.particleScale * 10.0f;
fx.scaleY1 = c.particleScale * 10.0f;
fx.width = c.particleSize;
fx.height = c.particleSize;
fx.HEXColor = c.zone.getInnerColor();
fx.facePlayer = true;
fx.glows = c.particleGlow;
fx.alpha1 = 0.8f;
fx.alpha2 = 0.0f;
fx.alphaRate = 0.04f;
Minecraft.getMinecraft().effectRenderer.addEffect(fx);
}
}
/** Spawn default ZoneParticleFX with motion style from ParticleMotion enum. */
private void spawnDefaultParticles(Ctx c) {
int count = Math.round(3 * c.particleDensity);
ParticleMotion motion = ParticleMotion.values()[Math.min(c.particleMotion, ParticleMotion.values().length - 1)];
for (int i = 0; i < count; i++) {
double[] pos = randomPosInZone(c);
double mx, my, mz;
float gravity;
int age;
switch (motion) {
case DRIFTING:
mx = (RANDOM.nextDouble() - 0.5) * 0.005;
my = 0.005 + RANDOM.nextDouble() * 0.01;
mz = (RANDOM.nextDouble() - 0.5) * 0.005;
gravity = 0;
age = 40 + RANDOM.nextInt(20);
break;
case SPARKS:
mx = (RANDOM.nextDouble() - 0.5) * 0.08;
my = RANDOM.nextDouble() * 0.06;
mz = (RANDOM.nextDouble() - 0.5) * 0.08;
gravity = 0;
age = 4 + RANDOM.nextInt(6);
break;
default: // RISING
mx = (RANDOM.nextDouble() - 0.5) * 0.01;
my = 0.01 + RANDOM.nextDouble() * 0.02;
mz = (RANDOM.nextDouble() - 0.5) * 0.01;
gravity = -0.01f;
age = 30 + RANDOM.nextInt(20);
break;
}
float scale = (1.5f + RANDOM.nextFloat() * 1.0f) * c.particleScale;
spawnParticle(c.zone, pos[0], pos[1], pos[2], mx, my, mz,
c.ir, c.ig, c.ib, 0.7f, scale, age, gravity, c.particleGlow);
}
}
/** Get motion vector for a CustomFX particle based on ParticleMotion ordinal. */
private double[] getMotionForStyle(int motionOrdinal) {
ParticleMotion motion = ParticleMotion.values()[Math.min(motionOrdinal, ParticleMotion.values().length - 1)];
switch (motion) {
case DRIFTING:
return new double[]{
(RANDOM.nextDouble() - 0.5) * 0.005,
0.005 + RANDOM.nextDouble() * 0.01,
(RANDOM.nextDouble() - 0.5) * 0.005
};
case SPARKS:
return new double[]{
(RANDOM.nextDouble() - 0.5) * 0.08,
RANDOM.nextDouble() * 0.06,
(RANDOM.nextDouble() - 0.5) * 0.08
};
default: // RISING
return new double[]{
(RANDOM.nextDouble() - 0.5) * 0.01,
0.01 + RANDOM.nextDouble() * 0.02,
(RANDOM.nextDouble() - 0.5) * 0.01
};
}
}
// ═══════════════════════════════════════════════════════════════════
// STYLE HELPERS
// ═══════════════════════════════════════════════════════════════════
/** Get the ring/outer color (falls back to inner if outer not enabled). */
private float ringR(Ctx c) { return c.outerEnabled ? c.outerR : c.ir; }
private float ringG(Ctx c) { return c.outerEnabled ? c.outerG : c.ig; }
private float ringB(Ctx c) { return c.outerEnabled ? c.outerB : c.ib; }
private static float sin(double v) { return (float) Math.sin(v); }
/** Vertical accents with sinusoidal sway per line. */
private void renderSwayingAccents(Ctx c, float r, float g, float b, float a, float height) {
if (height <= 0.1f) return;
float rotRad = (float) Math.toRadians(c.rotation * 0.2f);
Tessellator tess = Tessellator.instance;
if (c.shape == ZoneShape.SQUARE) {
renderSquareSwayingAccents(c, r, g, b, a, height, rotRad, tess);
} else {
for (int i = 0; i < ACCENT_LINE_COUNT; i++) {
float baseAngle = (float) (Math.PI * 2 * i) / ACCENT_LINE_COUNT + rotRad;
double lx = Math.cos(baseAngle) * c.radius;
double lz = Math.sin(baseAngle) * c.radius;
float swayX = 0.15f * sin(c.time * 0.08f + i * 1.7f);
float swayZ = 0.15f * sin(c.time * 0.06f + i * 2.3f);
tess.startDrawing(GL11.GL_LINES);
tess.setColorRGBA_F(r, g, b, a);
tess.addVertex(lx, 0.01, lz);
tess.setColorRGBA_F(r, g, b, 0.0f);
tess.addVertex(lx + swayX, height, lz + swayZ);
tess.draw();
}
}
}
private void renderSquareSwayingAccents(Ctx c, float r, float g, float b, float a, float height, float rotRad, Tessellator tess) {
int perSide = Math.max(1, ACCENT_LINE_COUNT / 4);
int idx = 0;
for (int side = 0; side < 4; side++) {
for (int j = 0; j < perSide; j++) {
float frac = (j + 0.5f) / perSide;
double lx, lz;
switch (side) {
case 0: lx = -c.radius + frac * 2 * c.radius; lz = -c.radius; break;
case 1: lx = c.radius; lz = -c.radius + frac * 2 * c.radius; break;
case 2: lx = c.radius - frac * 2 * c.radius; lz = c.radius; break;
default: lx = -c.radius; lz = c.radius - frac * 2 * c.radius; break;
}
float swayX = 0.15f * sin(c.time * 0.08f + idx * 1.7f);
float swayZ = 0.15f * sin(c.time * 0.06f + idx * 2.3f);
tess.startDrawing(GL11.GL_LINES);
tess.setColorRGBA_F(r, g, b, a);
tess.addVertex(lx, 0.01, lz);
tess.setColorRGBA_F(r, g, b, 0.0f);
tess.addVertex(lx + swayX, height, lz + swayZ);
tess.draw();
idx++;
}
}
}
/** Vertical accents with per-line random alpha flicker. */
private void renderFlickeringAccents(Ctx c, float r, float g, float b, float baseAlpha, float height) {
if (height <= 0.1f) return;
float rotRad = (float) Math.toRadians(c.rotation * 0.4f);
Tessellator tess = Tessellator.instance;
if (c.shape == ZoneShape.SQUARE) {
renderSquareFlickeringAccents(c, r, g, b, baseAlpha, height, tess);
} else {
for (int i = 0; i < ACCENT_LINE_COUNT; i++) {
float baseAngle = (float) (Math.PI * 2 * i) / ACCENT_LINE_COUNT + rotRad;
double lx = Math.cos(baseAngle) * c.radius;
double lz = Math.sin(baseAngle) * c.radius;
float flicker = 0.3f + 0.7f * Math.abs(sin(c.time * 0.7f + i * 3.14f));
float lineAlpha = baseAlpha * flicker;
float lineHeight = height * (0.6f + 0.4f * flicker);
tess.startDrawing(GL11.GL_LINES);
tess.setColorRGBA_F(r, g, b, lineAlpha);
tess.addVertex(lx, 0.01, lz);
tess.setColorRGBA_F(r, g, b, 0.0f);
tess.addVertex(lx, lineHeight, lz);
tess.draw();
}
}
}
private void renderSquareFlickeringAccents(Ctx c, float r, float g, float b, float baseAlpha, float height, Tessellator tess) {
int perSide = Math.max(1, ACCENT_LINE_COUNT / 4);
int idx = 0;
for (int side = 0; side < 4; side++) {
for (int j = 0; j < perSide; j++) {
float frac = (j + 0.5f) / perSide;
double lx, lz;
switch (side) {
case 0: lx = -c.radius + frac * 2 * c.radius; lz = -c.radius; break;
case 1: lx = c.radius; lz = -c.radius + frac * 2 * c.radius; break;
case 2: lx = c.radius - frac * 2 * c.radius; lz = c.radius; break;
default: lx = -c.radius; lz = c.radius - frac * 2 * c.radius; break;
}
float flicker = 0.3f + 0.7f * Math.abs(sin(c.time * 0.7f + idx * 3.14f));
float lineAlpha = baseAlpha * flicker;
float lineHeight = height * (0.6f + 0.4f * flicker);
tess.startDrawing(GL11.GL_LINES);
tess.setColorRGBA_F(r, g, b, lineAlpha);
tess.addVertex(lx, 0.01, lz);
tess.setColorRGBA_F(r, g, b, 0.0f);
tess.addVertex(lx, lineHeight, lz);
tess.draw();
idx++;
}
}
}
/** Pick a random position within the zone's area. Returns {x, y, z} offsets from zone center. */
private double[] randomPosInZone(Ctx c) {
if (c.shape == ZoneShape.SQUARE) {
double px = (RANDOM.nextDouble() * 2 - 1) * c.radius;
double pz = (RANDOM.nextDouble() * 2 - 1) * c.radius;
return new double[]{px, 0, pz};
} else {
double angle = RANDOM.nextDouble() * Math.PI * 2;
double dist = Math.sqrt(RANDOM.nextDouble()) * c.radius;
double px = Math.cos(angle) * dist;
double pz = Math.sin(angle) * dist;
return new double[]{px, 0, pz};
}
}
/** Pick a random position on the edge of the zone. Returns {x, z}. */
private double[] randomEdgePos(Ctx c) {
if (c.shape == ZoneShape.SQUARE) {
int side = RANDOM.nextInt(4);
double frac = RANDOM.nextDouble() * 2 - 1;
switch (side) {
case 0: return new double[]{frac * c.radius, -c.radius};
case 1: return new double[]{frac * c.radius, c.radius};
case 2: return new double[]{-c.radius, frac * c.radius};
default: return new double[]{c.radius, frac * c.radius};
}
} else {
double angle = RANDOM.nextDouble() * Math.PI * 2;
double dist = c.radius * (0.5 + RANDOM.nextDouble() * 0.5);
return new double[]{Math.cos(angle) * dist, Math.sin(angle) * dist};
}
}
/** Spawn a zone particle at world position. */
private void spawnParticle(EntityAbilityZone zone, double offsetX, double offsetY, double offsetZ,
double mx, double my, double mz,
float r, float g, float b, float alpha,
float scale, int maxAge, float gravity, boolean glow) {
ZoneParticleFX particle = new ZoneParticleFX(
zone.worldObj,
zone.posX + offsetX, zone.posY + offsetY, zone.posZ + offsetZ,
mx, my, mz, r, g, b, alpha, scale, maxAge, gravity, glow);
Minecraft.getMinecraft().effectRenderer.addEffect(particle);
}
// ═══════════════════════════════════════════════════════════════════
// GL STATE
// ═══════════════════════════════════════════════════════════════════
private void setupRenderState() {
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glDisable(GL11.GL_CULL_FACE);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glDisable(GL11.GL_LIGHTING);
GL11.glShadeModel(GL11.GL_SMOOTH);
OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f);
}
private void restoreRenderState() {
GL11.glShadeModel(GL11.GL_FLAT);
GL11.glEnable(GL11.GL_LIGHTING);
GL11.glDisable(GL11.GL_BLEND);
GL11.glEnable(GL11.GL_CULL_FACE);
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glPopAttrib();
}
// ═══════════════════════════════════════════════════════════════════
// SHAPE FILL (Ground plane)
// ═══════════════════════════════════════════════════════════════════
private void renderShapeFill(ZoneShape shape, float radius, float r, float g, float b, float a) {
GL11.glEnable(GL11.GL_POLYGON_OFFSET_FILL);
GL11.glPolygonOffset(-1.0f, -1.0f);
switch (shape) {
case CIRCLE:
renderCircleFill(radius, r, g, b, a);
break;
case SQUARE:
renderSquareFill(radius, r, g, b, a);
break;
}
GL11.glDisable(GL11.GL_POLYGON_OFFSET_FILL);
}
private void renderCircleFill(float radius, float r, float g, float b, float a) {
Tessellator tess = Tessellator.instance;
tess.startDrawing(GL11.GL_TRIANGLE_FAN);
tess.setColorRGBA_F(r, g, b, a);
tess.addVertex(0, 0, 0);
for (int i = 0; i <= CIRCLE_SEGMENTS; i++) {
double angle = (Math.PI * 2 * i) / CIRCLE_SEGMENTS;
tess.addVertex(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
}
tess.draw();
}
private void renderSquareFill(float radius, float r, float g, float b, float a) {
Tessellator tess = Tessellator.instance;
tess.startDrawing(GL11.GL_QUADS);
tess.setColorRGBA_F(r, g, b, a);
tess.addVertex(-radius, 0, -radius);
tess.addVertex(radius, 0, -radius);
tess.addVertex(radius, 0, radius);
tess.addVertex(-radius, 0, radius);
tess.draw();
}
// ═══════════════════════════════════════════════════════════════════
// RING LINES (Concentric pulse rings)
// ═══════════════════════════════════════════════════════════════════
private void renderRingLine(ZoneShape shape, float radius,
float r, float g, float b, float a, float yOffset) {
switch (shape) {
case CIRCLE:
renderCircleRingLine(radius, r, g, b, a, yOffset);
break;
case SQUARE:
renderSquareRingLine(radius, r, g, b, a, yOffset);
break;
}
}
private void renderCircleRingLine(float radius, float r, float g, float b, float a, float yOffset) {
Tessellator tess = Tessellator.instance;
tess.startDrawing(GL11.GL_LINE_LOOP);
tess.setColorRGBA_F(r, g, b, a);
for (int i = 0; i < CIRCLE_SEGMENTS; i++) {
double angle = (Math.PI * 2 * i) / CIRCLE_SEGMENTS;
tess.addVertex(Math.cos(angle) * radius, yOffset, Math.sin(angle) * radius);
}
tess.draw();
}
private void renderSquareRingLine(float radius, float r, float g, float b, float a, float yOffset) {
Tessellator tess = Tessellator.instance;
tess.startDrawing(GL11.GL_LINE_LOOP);
tess.setColorRGBA_F(r, g, b, a);
tess.addVertex(-radius, yOffset, -radius);
tess.addVertex(radius, yOffset, -radius);
tess.addVertex(radius, yOffset, radius);
tess.addVertex(-radius, yOffset, radius);
tess.draw();
}
// ═══════════════════════════════════════════════════════════════════
// ROTATING BORDER
// ═══════════════════════════════════════════════════════════════════
private void renderRotatingBorder(ZoneShape shape, float radius,
float r, float g, float b, float a,
float rotationDeg, float yOffset) {
int dashCount = 16;
if (shape == ZoneShape.SQUARE) {
// Dashes along 4 rectangle edges with offset
float perimeter = radius * 8; // total perimeter = 4 * (2*radius)
float dashLen = perimeter / dashCount;
float offset = (rotationDeg % 360.0f) / 360.0f * perimeter;
Tessellator tess = Tessellator.instance;
for (int i = 0; i < dashCount; i += 2) {
float startDist = (i * dashLen + offset) % perimeter;
float endDist = startDist + dashLen;
tess.startDrawing(GL11.GL_LINE_STRIP);
tess.setColorRGBA_F(r, g, b, a);
int steps = 4;
for (int s = 0; s <= steps; s++) {
float d = startDist + (endDist - startDist) * s / steps;
double[] pos = perimeterPos(d % perimeter, radius);
tess.addVertex(pos[0], yOffset, pos[1]);
}
tess.draw();
}
} else {
float dashAngle = (float) (Math.PI * 2) / dashCount;
float rotRad = (float) Math.toRadians(rotationDeg);
Tessellator tess = Tessellator.instance;
for (int i = 0; i < dashCount; i += 2) {
float startAngle = i * dashAngle + rotRad;
float endAngle = startAngle + dashAngle;
tess.startDrawing(GL11.GL_LINE_STRIP);
tess.setColorRGBA_F(r, g, b, a);
int steps = 4;
for (int s = 0; s <= steps; s++) {
float segAngle = startAngle + (endAngle - startAngle) * s / steps;
tess.addVertex(Math.cos(segAngle) * radius, yOffset, Math.sin(segAngle) * radius);
}
tess.draw();
}
}
}
/** Convert a distance along the square perimeter to an x,z position. */
private double[] perimeterPos(float dist, float radius) {
float sideLen = radius * 2;
if (dist < sideLen) {
// Bottom edge: (-radius, -radius) to (radius, -radius)
return new double[]{-radius + dist, -radius};
}
dist -= sideLen;
if (dist < sideLen) {
// Right edge: (radius, -radius) to (radius, radius)
return new double[]{radius, -radius + dist};
}
dist -= sideLen;
if (dist < sideLen) {
// Top edge: (radius, radius) to (-radius, radius)
return new double[]{radius - dist, radius};
}
dist -= sideLen;
// Left edge: (-radius, radius) to (-radius, -radius)
return new double[]{-radius, radius - dist};
}
// ═══════════════════════════════════════════════════════════════════
// VERTICAL ACCENT LINES
// ═══════════════════════════════════════════════════════════════════
private void renderVerticalAccents(ZoneShape shape, float radius,
float r, float g, float b, float a,
float height, float rotationDeg) {
if (height <= 0.1f) return;
Tessellator tess = Tessellator.instance;
if (shape == ZoneShape.SQUARE) {
// Lines at corners + distributed along edges
int perSide = Math.max(1, ACCENT_LINE_COUNT / 4);
for (int side = 0; side < 4; side++) {
for (int j = 0; j < perSide; j++) {
float frac = (j + 0.5f) / perSide;
double lx, lz;
switch (side) {
case 0: lx = -radius + frac * 2 * radius; lz = -radius; break;
case 1: lx = radius; lz = -radius + frac * 2 * radius; break;
case 2: lx = radius - frac * 2 * radius; lz = radius; break;
default: lx = -radius; lz = radius - frac * 2 * radius; break;
}
tess.startDrawing(GL11.GL_LINES);
tess.setColorRGBA_F(r, g, b, a);
tess.addVertex(lx, 0.01, lz);
tess.setColorRGBA_F(r, g, b, 0.0f);
tess.addVertex(lx, height, lz);
tess.draw();
}
}
// Corner accent lines
float[][] corners = {{-radius, -radius}, {radius, -radius}, {radius, radius}, {-radius, radius}};
for (float[] corner : corners) {
tess.startDrawing(GL11.GL_LINES);
tess.setColorRGBA_F(r, g, b, a);
tess.addVertex(corner[0], 0.01, corner[1]);
tess.setColorRGBA_F(r, g, b, 0.0f);
tess.addVertex(corner[0], height, corner[1]);
tess.draw();
}
} else {
float rotRad = (float) Math.toRadians(rotationDeg);
for (int i = 0; i < ACCENT_LINE_COUNT; i++) {
float angle = (float) (Math.PI * 2 * i) / ACCENT_LINE_COUNT + rotRad;
double lx = Math.cos(angle) * radius;
double lz = Math.sin(angle) * radius;
tess.startDrawing(GL11.GL_LINES);
tess.setColorRGBA_F(r, g, b, a);
tess.addVertex(lx, 0.01, lz);
tess.setColorRGBA_F(r, g, b, 0.0f);
tess.addVertex(lx, height, lz);
tess.draw();
}
}
}
@Override
protected ResourceLocation getEntityTexture(Entity entity) {
return null;
}
}
@@ -85,6 +85,28 @@ public class AttachedLightningRenderer {
}
}
/**
* Age existing arcs and remove dead ones without spawning new arcs.
* Use with addArc() for custom arc generation.
*/
public void tick() {
Iterator<LightningArc> iter = arcs.iterator();
while (iter.hasNext()) {
LightningArc arc = iter.next();
arc.tick();
if (arc.isDead()) {
iter.remove();
}
}
}
/**
* Add a custom arc to the state.
*/
public void addArc(LightningArc arc) {
arcs.add(arc);
}
/**
* Render all active arcs in local space.
* Call this after glTranslated to entity position.
@@ -231,6 +253,17 @@ public class AttachedLightningRenderer {
}
}
/**
* Create a lightning arc between two arbitrary points.
*/
public static LightningArc createArcBetween(double x1, double y1, double z1,
double x2, double y2, double z2,
float displacement, int outerColor, int innerColor, int maxAge) {
int segments = 4 + rand.nextInt(3);
List<double[]> points = generateLightningPath(x1, y1, z1, x2, y2, z2, segments, displacement);
return new LightningArc(points, maxAge, outerColor, innerColor);
}
// ==================== INSTANT RENDERING (no persistence) ====================
/**
@@ -1,19 +1,11 @@
package kamkeel.npcs.controllers.data.ability.type;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.UserType;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.potion.Potion;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.api.ability.type.IAbilityHazard;
@@ -23,392 +15,105 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
/**
* Hazard ability: Creates persistent ground effect zones.
* Deals damage and/or applies debuffs to entities in the zone over time.
* Hazard ability: Creates persistent ground effect zones around the caster.
* Spawns one or more EntityAbilityZone entities in random positions
* within spawnRadius. Damage and zone logic handled by the entity.
*/
public class AbilityHazard extends Ability implements IAbilityHazard {
public class AbilityHazard extends AbilityZone implements IAbilityHazard {
public enum HazardShape {
CIRCLE,
RING,
CONE;
@Override
public String toString() {
switch (this) {
case CIRCLE: return "ability.shape.circle";
case RING: return "ability.shape.ring";
case CONE: return "ability.shape.cone";
default: return name();
}
}
}
public enum PlacementMode {
AT_CASTER,
AT_TARGET,
FOLLOW_CASTER,
FOLLOW_TARGET;
@Override
public String toString() {
switch (this) {
case AT_CASTER: return "ability.placement.atCaster";
case AT_TARGET: return "ability.placement.atTarget";
case FOLLOW_CASTER: return "ability.placement.followCaster";
case FOLLOW_TARGET: return "ability.placement.followTarget";
default: return name();
}
}
}
private int durationTicks = 100;
// Hazard-specific fields
private float radius = 4.0f;
private float innerRadius = 0.0f;
private float coneAngle = 45.0f;
private HazardShape shape = HazardShape.CIRCLE;
private PlacementMode placement = PlacementMode.AT_TARGET;
private float damagePerSecond = 1.0f;
private int damageInterval = 20;
private boolean ignoreInvulnFrames = false;
private int slownessLevel = -1;
private int weaknessLevel = -1;
private int poisonLevel = -1;
private int witherLevel = -1;
private int blindnessLevel = -1;
private int debuffDuration = 40;
private boolean affectsCaster = false;
private float heightAbove = 2.0f;
private float heightBelow = 1.0f;
// Offset parameters - hazard spawns near target, not exactly on them
private float minOffset = 0.0f; // Minimum offset from target
private float maxOffset = 2.0f; // Maximum offset from target
private boolean randomOffset = true; // Random or fixed offset
// Runtime state
private static final Random RANDOM = new Random();
private transient double zoneX, zoneY, zoneZ;
private transient boolean positionLocked = false;
private transient int ticksSinceDamage = 0;
private transient Set<Integer> damagedThisTick = new HashSet<>();
public AbilityHazard() {
super(100, new EnergyDisplayData(0x00CC00, 0x006600, true, 1.0f, 0.5f, 1.5f));
this.typeId = "ability.cnpc.hazard";
this.name = "Hazard";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.lockMovement = LockMovementType.WINDUP;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.telegraphType = TelegraphType.CIRCLE;
this.allowedBy = UserType.NPC_ONLY;
this.windUpAnimationName = "Ability_Hazard_Windup";
this.activeAnimationName = "Ability_Hazard_Active";
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public float getTelegraphRadius() {
return radius;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
TelegraphInstance instance = super.createTelegraph(caster, target);
if (instance == null) return null;
// Control telegraph following based on placement mode
switch (placement) {
case AT_CASTER:
case FOLLOW_CASTER:
// Telegraph stays at caster position, no following
instance.setEntityIdToFollow(-1);
instance.setX(caster.posX);
instance.setY(caster.posY);
instance.setZ(caster.posZ);
break;
case AT_TARGET:
// Telegraph follows target during windup, locks on execute
if (target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
break;
case FOLLOW_TARGET:
// Telegraph follows target during windup AND active phase
if (target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
break;
}
return instance;
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// AT_CASTER locks immediately, AT_TARGET follows during windup and locks on execute
if (tick == 0) {
switch (placement) {
case AT_CASTER:
zoneX = caster.posX;
zoneY = caster.posY;
zoneZ = caster.posZ;
positionLocked = true;
break;
case AT_TARGET:
// Position will be set from telegraph on execute
// Telegraph follows target during windup
positionLocked = false;
break;
default:
positionLocked = false;
break;
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
// Lock position from telegraph (which was following target) or calculate it now
TelegraphInstance telegraph = getTelegraphInstance();
switch (placement) {
case AT_CASTER:
// Already locked during windup
if (!positionLocked) {
zoneX = caster.posX;
zoneY = caster.posY;
zoneZ = caster.posZ;
}
break;
case AT_TARGET:
if (telegraph != null) {
double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(),
minOffset, maxOffset, randomOffset, RANDOM);
zoneX = pos[0];
zoneY = pos[1];
zoneZ = pos[2];
telegraph.setX(zoneX);
telegraph.setY(zoneY);
telegraph.setZ(zoneZ);
} else if (target != null) {
double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ,
minOffset, maxOffset, randomOffset, RANDOM);
zoneX = pos[0];
zoneY = pos[1];
zoneZ = pos[2];
} else {
zoneX = caster.posX;
zoneY = caster.posY;
zoneZ = caster.posZ;
}
break;
case FOLLOW_CASTER:
zoneX = caster.posX;
zoneY = caster.posY;
zoneZ = caster.posZ;
break;
case FOLLOW_TARGET:
if (target != null) {
zoneX = target.posX;
zoneY = target.posY;
zoneZ = target.posZ;
} else {
zoneX = caster.posX;
zoneY = caster.posY;
zoneZ = caster.posZ;
}
break;
}
positionLocked = true;
ticksSinceDamage = damageInterval;
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
// Check if hazard duration has ended
if (tick >= durationTicks) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (!isPreview()) {
damagedThisTick.clear();
ticksSinceDamage++;
activeEntities.clear();
switch (placement) {
case FOLLOW_CASTER:
zoneX = caster.posX;
zoneY = caster.posY;
zoneZ = caster.posZ;
break;
case FOLLOW_TARGET:
if (target != null) {
zoneX = target.posX;
zoneY = target.posY;
zoneZ = target.posZ;
}
break;
default:
break;
List<double[]> placedPositions = new ArrayList<>();
float minSeparation = radius * 2.0f;
for (int i = 0; i < zoneCount; i++) {
double[] pos = findSpawnPosition(caster, placedPositions, minSeparation);
placedPositions.add(pos);
EntityAbilityZone entity = EntityAbilityZone.createHazard(world, caster,
pos[0], caster.posY, pos[1],
zoneShape,
radius,
damagePerSecond, damageInterval,
ignoreInvulnFrames, affectsCaster,
durationTicks,
colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled,
zoneHeight,
particleDensity, particleScale, animSpeed, lightningDensity,
getEffects());
applyVisualToEntity(entity);
if (isPreview()) {
entity.setupPreview(caster);
}
if (ticksSinceDamage >= damageInterval) {
ticksSinceDamage = 0;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
zoneX - radius, zoneY - heightBelow, zoneZ - radius,
zoneX + radius, zoneY + heightAbove, zoneZ + radius
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
for (EntityLivingBase entity : entities) {
if (entity == caster && !affectsCaster) continue;
if (damagedThisTick.contains(entity.getEntityId())) continue;
if (!isInZone(entity, caster)) continue;
if (damagePerSecond > 0) {
if (ignoreInvulnFrames) {
entity.hurtResistantTime = 0;
}
// Apply damage with scripted event support (no knockback for hazard)
boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0);
if (!wasHit) continue; // Skip debuffs if hit was cancelled
}
applyDebuffs(entity);
damagedThisTick.add(entity.getEntityId());
}
}
spawnAbilityEntity(world, entity);
activeEntities.add(entity);
}
}
private boolean isInZone(EntityLivingBase entity, EntityLivingBase caster) {
double dx = entity.posX - zoneX;
double dz = entity.posZ - zoneZ;
double dist = Math.sqrt(dx * dx + dz * dz);
switch (shape) {
case CIRCLE:
return dist <= radius;
case RING:
return dist >= innerRadius && dist <= radius;
case CONE:
if (dist > radius) return false;
float casterYaw = caster.rotationYaw;
double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz));
double angleDiff = Math.abs(normalizeAngle(angleToEntity - casterYaw));
return angleDiff <= coneAngle / 2;
default:
return dist <= radius;
}
}
private double normalizeAngle(double angle) {
while (angle > 180) angle -= 360;
while (angle < -180) angle += 360;
return angle;
}
private void applyDebuffs(EntityLivingBase target) {
if (slownessLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, debuffDuration, slownessLevel));
}
if (weaknessLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.weakness.id, debuffDuration, weaknessLevel));
}
if (poisonLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.poison.id, debuffDuration, poisonLevel));
}
if (witherLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.wither.id, debuffDuration, witherLevel));
}
if (blindnessLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.blindness.id, debuffDuration, blindnessLevel));
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
damagedThisTick.clear();
positionLocked = false;
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
damagedThisTick.clear();
positionLocked = false;
}
// ═══════════════════════════════════════════════════════════════════
// NBT
// ═══════════════════════════════════════════════════════════════════
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setInteger("durationTicks", durationTicks);
writeZoneNBT(nbt);
nbt.setFloat("radius", radius);
nbt.setFloat("innerRadius", innerRadius);
nbt.setFloat("coneAngle", coneAngle);
nbt.setString("shape", shape.name());
nbt.setString("placement", placement.name());
nbt.setFloat("damagePerSecond", damagePerSecond);
nbt.setInteger("damageInterval", damageInterval);
nbt.setBoolean("ignoreInvulnFrames", ignoreInvulnFrames);
nbt.setInteger("slownessLevel", slownessLevel);
nbt.setInteger("weaknessLevel", weaknessLevel);
nbt.setInteger("poisonLevel", poisonLevel);
nbt.setInteger("witherLevel", witherLevel);
nbt.setInteger("blindnessLevel", blindnessLevel);
nbt.setInteger("debuffDuration", debuffDuration);
nbt.setBoolean("affectsCaster", affectsCaster);
nbt.setFloat("heightAbove", heightAbove);
nbt.setFloat("heightBelow", heightBelow);
nbt.setFloat("minOffset", minOffset);
nbt.setFloat("maxOffset", maxOffset);
nbt.setBoolean("randomOffset", randomOffset);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 100;
readZoneNBT(nbt, 100);
// Backward compat: old "shape" key with RING/CONE → ignore (base defaults to CIRCLE)
// The old HazardShape enum had CIRCLE/RING/CONE — only CIRCLE maps to new ZoneShape
if (nbt.hasKey("shape")) {
String oldShape = nbt.getString("shape");
if ("RING".equals(oldShape) || "CONE".equals(oldShape)) {
this.zoneShape = ZoneShape.CIRCLE;
}
}
this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f;
this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f;
this.coneAngle = nbt.hasKey("coneAngle") ? nbt.getFloat("coneAngle") : 45.0f;
try {
this.shape = HazardShape.valueOf(nbt.getString("shape"));
} catch (Exception e) {
this.shape = HazardShape.CIRCLE;
}
try {
this.placement = PlacementMode.valueOf(nbt.getString("placement"));
} catch (Exception e) {
this.placement = PlacementMode.AT_TARGET;
}
// Support legacy "damagePerTick" key for backwards compatibility
if (nbt.hasKey("damagePerSecond")) {
this.damagePerSecond = nbt.getFloat("damagePerSecond");
@@ -419,252 +124,60 @@ public class AbilityHazard extends Ability implements IAbilityHazard {
}
this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 20;
this.ignoreInvulnFrames = nbt.hasKey("ignoreInvulnFrames") && nbt.getBoolean("ignoreInvulnFrames");
this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : -1;
this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : -1;
this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : -1;
this.witherLevel = nbt.hasKey("witherLevel") ? nbt.getInteger("witherLevel") : -1;
this.blindnessLevel = nbt.hasKey("blindnessLevel") ? nbt.getInteger("blindnessLevel") : -1;
this.debuffDuration = nbt.hasKey("debuffDuration") ? nbt.getInteger("debuffDuration") : 40;
this.affectsCaster = nbt.hasKey("affectsCaster") && nbt.getBoolean("affectsCaster");
this.heightAbove = nbt.hasKey("heightAbove") ? nbt.getFloat("heightAbove") : 2.0f;
this.heightBelow = nbt.hasKey("heightBelow") ? nbt.getFloat("heightBelow") : 1.0f;
this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f;
this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 2.0f;
this.randomOffset = !nbt.hasKey("randomOffset") || nbt.getBoolean("randomOffset");
}
// Getters & Setters
public int getDurationTicks() {
return durationTicks;
}
// ═══════════════════════════════════════════════════════════════════
// HAZARD-SPECIFIC GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
public float getRadius() { return radius; }
public void setRadius(float radius) { this.radius = radius; }
public float getRadius() {
return radius;
}
public float getDamagePerSecond() { return damagePerSecond; }
public void setDamagePerSecond(float damagePerSecond) { this.damagePerSecond = damagePerSecond; }
public void setRadius(float radius) {
this.radius = radius;
}
public int getDamageInterval() { return damageInterval; }
public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; }
public float getInnerRadius() {
return innerRadius;
}
public boolean isIgnoreInvulnFrames() { return ignoreInvulnFrames; }
public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { this.ignoreInvulnFrames = ignoreInvulnFrames; }
public void setInnerRadius(float innerRadius) {
this.innerRadius = innerRadius;
}
public boolean isAffectsCaster() { return affectsCaster; }
public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; }
public float getConeAngle() {
return coneAngle;
}
public void setConeAngle(float coneAngle) {
this.coneAngle = coneAngle;
}
public HazardShape getShapeEnum() {
return shape;
}
public void setShapeEnum(HazardShape shape) {
this.shape = shape;
}
@Override
public int getShape() {
return shape.ordinal();
}
@Override
public void setShape(int shape) {
HazardShape[] values = HazardShape.values();
this.shape = shape >= 0 && shape < values.length ? values[shape] : HazardShape.CIRCLE;
}
public PlacementMode getPlacementEnum() {
return placement;
}
public void setPlacementEnum(PlacementMode placement) {
this.placement = placement;
}
@Override
public int getPlacement() {
return placement.ordinal();
}
@Override
public void setPlacement(int placement) {
PlacementMode[] values = PlacementMode.values();
this.placement = placement >= 0 && placement < values.length ? values[placement] : PlacementMode.AT_CASTER;
}
public float getDamagePerSecond() {
return damagePerSecond;
}
public void setDamagePerSecond(float damagePerSecond) {
this.damagePerSecond = damagePerSecond;
}
public int getDamageInterval() {
return damageInterval;
}
public void setDamageInterval(int damageInterval) {
this.damageInterval = damageInterval;
}
public boolean isIgnoreInvulnFrames() {
return ignoreInvulnFrames;
}
public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) {
this.ignoreInvulnFrames = ignoreInvulnFrames;
}
public int getSlownessLevel() {
return slownessLevel;
}
public void setSlownessLevel(int slownessLevel) {
this.slownessLevel = slownessLevel;
}
public int getWeaknessLevel() {
return weaknessLevel;
}
public void setWeaknessLevel(int weaknessLevel) {
this.weaknessLevel = weaknessLevel;
}
public int getPoisonLevel() {
return poisonLevel;
}
public void setPoisonLevel(int poisonLevel) {
this.poisonLevel = poisonLevel;
}
public int getWitherLevel() {
return witherLevel;
}
public void setWitherLevel(int witherLevel) {
this.witherLevel = witherLevel;
}
public int getBlindnessLevel() {
return blindnessLevel;
}
public void setBlindnessLevel(int blindnessLevel) {
this.blindnessLevel = blindnessLevel;
}
public int getDebuffDuration() {
return debuffDuration;
}
public void setDebuffDuration(int debuffDuration) {
this.debuffDuration = debuffDuration;
}
public boolean isAffectsCaster() {
return affectsCaster;
}
public void setAffectsCaster(boolean affectsCaster) {
this.affectsCaster = affectsCaster;
}
public float getHeightAbove() {
return heightAbove;
}
public void setHeightAbove(float heightAbove) {
this.heightAbove = heightAbove;
}
public float getHeightBelow() {
return heightBelow;
}
public void setHeightBelow(float heightBelow) {
this.heightBelow = heightBelow;
}
public float getMinOffset() {
return minOffset;
}
public void setMinOffset(float minOffset) {
this.minOffset = minOffset;
}
public float getMaxOffset() {
return maxOffset;
}
public void setMaxOffset(float maxOffset) {
this.maxOffset = maxOffset;
}
public boolean isRandomOffset() {
return randomOffset;
}
public void setRandomOffset(boolean randomOffset) {
this.randomOffset = randomOffset;
}
// Runtime getters
public double getZoneX() {
return zoneX;
}
public double getZoneY() {
return zoneY;
}
public double getZoneZ() {
return zoneZ;
}
// ═══════════════════════════════════════════════════════════════════
// GUI FIELD DEFINITIONS
// ═══════════════════════════════════════════════════════════════════
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000),
FieldDef.enumField("ability.shape", HazardShape.class, this::getShapeEnum, this::setShapeEnum)
.hover("ability.hover.shape"),
FieldDef.enumField("ability.placement", PlacementMode.class, this::getPlacementEnum, this::setPlacementEnum)
.hover("ability.hover.placement"),
FieldDef.section("ability.section.area"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getRadius, this::setRadius),
FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius)
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000),
FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum)
),
FieldDef.section("ability.section.zone"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius),
FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20)
),
FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight),
FieldDef.section("ability.section.area"),
FieldDef.floatField("gui.radius", this::getRadius, this::setRadius),
FieldDef.section("ability.section.damage"),
FieldDef.row(
FieldDef.floatField("ability.damagePerSecond", this::getDamagePerSecond, this::setDamagePerSecond),
FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval)
FieldDef.floatField("gui.dps", this::getDamagePerSecond, this::setDamagePerSecond),
FieldDef.intField("gui.interval", this::getDamageInterval, this::setDamageInterval)
),
FieldDef.section("ability.section.debuffs"),
FieldDef.row(
FieldDef.intField("ability.slownessLevel", this::getSlownessLevel, this::setSlownessLevel),
FieldDef.intField("ability.debuffDuration", this::getDebuffDuration, this::setDebuffDuration)
),
FieldDef.intField("ability.poisonLevel", this::getPoisonLevel, this::setPoisonLevel),
FieldDef.boolField("ability.ignoreInvulnFrames", this::isIgnoreInvulnFrames, this::setIgnoreInvulnFrames),
FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster)
.hover("ability.hover.affectsCaster"),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
addVisualFieldDefs(defs);
addTelegraphSizeField(defs);
}
}
@@ -1,17 +1,11 @@
package kamkeel.npcs.controllers.data.ability.type;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.UserType;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.api.ability.type.IAbilityTrap;
@@ -21,38 +15,18 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
/**
* Trap ability: Places a proximity-triggered trap.
* Features arm time, trigger radius, and various effects on trigger.
* Trap ability: Places proximity-triggered traps around the caster.
* Spawns one or more EntityAbilityZone entities in random positions
* within spawnRadius of the caster. Damage and trigger logic handled by the entity.
*/
public class AbilityTrap extends Ability implements IAbilityTrap {
public class AbilityTrap extends AbilityZone implements IAbilityTrap {
public enum TrapPlacement {
AT_CASTER,
AT_TARGET,
AHEAD_OF_CASTER;
@Override
public String toString() {
switch (this) {
case AT_CASTER: return "ability.trapPlace.atCaster";
case AT_TARGET: return "ability.trapPlace.atTarget";
case AHEAD_OF_CASTER: return "ability.trapPlace.aheadOfCaster";
default: return name();
}
}
}
private int durationTicks = 200;
private TrapPlacement placement = TrapPlacement.AT_TARGET;
private float placementDistance = 5.0f;
// Trap-specific fields
private float triggerRadius = 2.0f;
private int armTime = 20;
private int maxTriggers = 1;
@@ -62,240 +36,57 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
private float knockback = 0.5f;
private boolean visible = true;
// Offset parameters - trap spawns near target, not exactly on them
private float minOffset = 0.0f;
private float maxOffset = 1.5f;
private boolean randomOffset = true;
// Runtime state
private static final Random RANDOM = new Random();
private transient double trapX, trapY, trapZ;
private transient boolean armed = false;
private transient int triggerCount = 0;
private transient int ticksSinceLastTrigger = 0;
private transient Set<UUID> triggeredEntities = new HashSet<>();
public AbilityTrap() {
super(200, new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.0f, 0.5f, 1.5f));
this.typeId = "ability.cnpc.trap";
this.name = "Trap";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.lockMovement = LockMovementType.WINDUP;
this.cooldownTicks = 0;
this.windUpTicks = 20;
this.telegraphType = TelegraphType.CIRCLE;
this.allowedBy = UserType.NPC_ONLY;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public float getTelegraphRadius() {
return triggerRadius;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
TelegraphInstance instance = super.createTelegraph(caster, target);
if (instance == null) return null;
// Control telegraph following based on placement mode
switch (placement) {
case AT_CASTER:
case AHEAD_OF_CASTER:
// Telegraph at caster or ahead, no following
instance.setEntityIdToFollow(-1);
if (placement == TrapPlacement.AHEAD_OF_CASTER) {
double yaw = Math.toRadians(caster.rotationYaw);
instance.setX(caster.posX - Math.sin(yaw) * placementDistance);
instance.setY(caster.posY);
instance.setZ(caster.posZ + Math.cos(yaw) * placementDistance);
} else {
instance.setX(caster.posX);
instance.setY(caster.posY);
instance.setZ(caster.posZ);
}
break;
case AT_TARGET:
if (target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
break;
}
return instance;
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
armed = false;
triggerCount = 0;
ticksSinceLastTrigger = armTime;
triggeredEntities.clear();
// Use telegraph position if available (for AT_TARGET, it was following target)
TelegraphInstance telegraph = getTelegraphInstance();
switch (placement) {
case AT_CASTER:
trapX = caster.posX;
trapY = caster.posY;
trapZ = caster.posZ;
break;
case AT_TARGET:
if (telegraph != null) {
double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(),
minOffset, maxOffset, randomOffset, RANDOM);
trapX = pos[0];
trapY = pos[1];
trapZ = pos[2];
telegraph.setX(trapX);
telegraph.setY(trapY);
telegraph.setZ(trapZ);
} else if (target != null) {
double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ,
minOffset, maxOffset, randomOffset, RANDOM);
trapX = pos[0];
trapY = pos[1];
trapZ = pos[2];
} else {
trapX = caster.posX;
trapY = caster.posY;
trapZ = caster.posZ;
}
break;
case AHEAD_OF_CASTER:
double yaw = Math.toRadians(caster.rotationYaw);
trapX = caster.posX - Math.sin(yaw) * placementDistance;
trapY = caster.posY;
trapZ = caster.posZ + Math.cos(yaw) * placementDistance;
break;
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Check if trap duration has ended
if (tick >= durationTicks) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (!armed) {
if (tick >= armTime) {
armed = true;
activeEntities.clear();
List<double[]> placedPositions = new ArrayList<>();
float minSeparation = triggerRadius * 2.0f;
for (int i = 0; i < zoneCount; i++) {
double[] pos = findSpawnPosition(caster, placedPositions, minSeparation);
placedPositions.add(pos);
EntityAbilityZone entity = EntityAbilityZone.createTrap(world, caster,
pos[0], caster.posY, pos[1],
zoneShape,
triggerRadius, armTime, maxTriggers, triggerCooldown,
damage, damageRadius, knockback, durationTicks,
colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled,
zoneHeight,
particleDensity, particleScale, animSpeed, lightningDensity,
getEffects());
applyVisualToEntity(entity);
if (isPreview()) {
entity.setupPreview(caster);
}
return;
}
ticksSinceLastTrigger++;
if (maxTriggers > 0 && triggerCount >= maxTriggers) {
signalCompletion(); // All triggers used, trap is done
return;
}
if (ticksSinceLastTrigger < triggerCooldown) {
return;
}
if (!isPreview()) {
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
trapX - triggerRadius, trapY - 1, trapZ - triggerRadius,
trapX + triggerRadius, trapY + 2, trapZ + triggerRadius
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
for (EntityLivingBase entity : entities) {
if (entity == caster) continue;
if (entity.isDead) continue;
if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue;
double dx = entity.posX - trapX;
double dz = entity.posZ - trapZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist <= triggerRadius) {
triggerTrap(caster, entity, world);
return;
}
}
spawnAbilityEntity(world, entity);
activeEntities.add(entity);
}
}
private void triggerTrap(EntityLivingBase caster, EntityLivingBase triggerer, World world) {
triggerCount++;
ticksSinceLastTrigger = 0;
triggeredEntities.add(triggerer.getUniqueID());
Set<EntityLivingBase> affected = new HashSet<>();
if (damageRadius > 0) {
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
trapX - damageRadius, trapY - 1, trapZ - damageRadius,
trapX + damageRadius, trapY + 3, trapZ + damageRadius
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
for (EntityLivingBase entity : entities) {
if (entity == caster) continue;
double dx = entity.posX - trapX;
double dz = entity.posZ - trapZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist <= damageRadius) {
affected.add(entity);
}
}
} else {
affected.add(triggerer);
}
for (EntityLivingBase entity : affected) {
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback);
// Apply effects if the hit wasn't cancelled
if (wasHit) {
applyEffects(entity);
}
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
armed = false;
triggerCount = 0;
triggeredEntities.clear();
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
armed = false;
triggerCount = 0;
triggeredEntities.clear();
}
// ═══════════════════════════════════════════════════════════════════
// NBT
// ═══════════════════════════════════════════════════════════════════
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setString("placement", placement.name());
nbt.setFloat("placementDistance", placementDistance);
writeZoneNBT(nbt);
nbt.setFloat("triggerRadius", triggerRadius);
nbt.setInteger("armTime", armTime);
nbt.setInteger("maxTriggers", maxTriggers);
@@ -304,20 +95,11 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
nbt.setFloat("damageRadius", damageRadius);
nbt.setFloat("knockback", knockback);
nbt.setBoolean("visible", visible);
nbt.setFloat("minOffset", minOffset);
nbt.setFloat("maxOffset", maxOffset);
nbt.setBoolean("randomOffset", randomOffset);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 200;
try {
this.placement = TrapPlacement.valueOf(nbt.getString("placement"));
} catch (Exception e) {
this.placement = TrapPlacement.AT_TARGET;
}
this.placementDistance = nbt.hasKey("placementDistance") ? nbt.getFloat("placementDistance") : 5.0f;
readZoneNBT(nbt, 200);
this.triggerRadius = nbt.hasKey("triggerRadius") ? nbt.getFloat("triggerRadius") : 2.0f;
this.armTime = nbt.hasKey("armTime") ? nbt.getInteger("armTime") : 20;
this.maxTriggers = nbt.hasKey("maxTriggers") ? nbt.getInteger("maxTriggers") : 1;
@@ -326,168 +108,58 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f;
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f;
this.visible = !nbt.hasKey("visible") || nbt.getBoolean("visible");
this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f;
this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 1.5f;
this.randomOffset = !nbt.hasKey("randomOffset") || nbt.getBoolean("randomOffset");
}
// Getters & Setters
public int getDurationTicks() {
return durationTicks;
}
// ═══════════════════════════════════════════════════════════════════
// TRAP-SPECIFIC GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
public float getTriggerRadius() { return triggerRadius; }
public void setTriggerRadius(float triggerRadius) { this.triggerRadius = triggerRadius; }
public int getArmTime() { return armTime; }
public void setArmTime(int armTime) { this.armTime = armTime; }
public int getMaxTriggers() { return maxTriggers; }
public void setMaxTriggers(int maxTriggers) { this.maxTriggers = maxTriggers; }
public int getTriggerCooldown() { return triggerCooldown; }
public void setTriggerCooldown(int triggerCooldown) { this.triggerCooldown = triggerCooldown; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getDamageRadius() { return damageRadius; }
public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; }
public float getKnockback() { return knockback; }
public void setKnockback(float knockback) { this.knockback = knockback; }
public boolean isVisible() { return visible; }
public void setVisible(boolean visible) { this.visible = visible; }
public TrapPlacement getPlacementEnum() {
return placement;
}
public void setPlacementEnum(TrapPlacement placement) {
this.placement = placement;
}
@Override
public int getPlacement() {
return placement.ordinal();
}
@Override
public void setPlacement(int placement) {
TrapPlacement[] values = TrapPlacement.values();
this.placement = placement >= 0 && placement < values.length ? values[placement] : TrapPlacement.AT_TARGET;
}
public float getPlacementDistance() {
return placementDistance;
}
public void setPlacementDistance(float placementDistance) {
this.placementDistance = placementDistance;
}
public float getTriggerRadius() {
return triggerRadius;
}
public void setTriggerRadius(float triggerRadius) {
this.triggerRadius = triggerRadius;
}
public int getArmTime() {
return armTime;
}
public void setArmTime(int armTime) {
this.armTime = armTime;
}
public int getMaxTriggers() {
return maxTriggers;
}
public void setMaxTriggers(int maxTriggers) {
this.maxTriggers = maxTriggers;
}
public int getTriggerCooldown() {
return triggerCooldown;
}
public void setTriggerCooldown(int triggerCooldown) {
this.triggerCooldown = triggerCooldown;
}
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
public float getDamageRadius() {
return damageRadius;
}
public void setDamageRadius(float damageRadius) {
this.damageRadius = damageRadius;
}
public float getKnockback() {
return knockback;
}
public void setKnockback(float knockback) {
this.knockback = knockback;
}
public boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
public float getMinOffset() {
return minOffset;
}
public void setMinOffset(float minOffset) {
this.minOffset = minOffset;
}
public float getMaxOffset() {
return maxOffset;
}
public void setMaxOffset(float maxOffset) {
this.maxOffset = maxOffset;
}
public boolean isRandomOffset() {
return randomOffset;
}
public void setRandomOffset(boolean randomOffset) {
this.randomOffset = randomOffset;
}
// Runtime getters
public double getTrapX() {
return trapX;
}
public double getTrapY() {
return trapY;
}
public double getTrapZ() {
return trapZ;
}
public boolean isArmed() {
return armed;
}
// ═══════════════════════════════════════════════════════════════════
// GUI FIELD DEFINITIONS
// ═══════════════════════════════════════════════════════════════════
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks)
.range(1, 2000),
FieldDef.enumField("ability.placement", TrapPlacement.class, this::getPlacementEnum, this::setPlacementEnum)
.hover("ability.hover.placement"),
FieldDef.row(
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000),
FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum)
),
FieldDef.section("ability.section.zone"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius),
FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20)
),
FieldDef.row(
FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight),
FieldDef.boolField("ability.visible", this::isVisible, this::setVisible)
),
FieldDef.section("ability.section.trigger"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getTriggerRadius, this::setTriggerRadius),
FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime)
),
FieldDef.row(
FieldDef.intField("ability.maxTriggers", this::getMaxTriggers, this::setMaxTriggers),
FieldDef.intField("ability.triggerCooldown", this::getTriggerCooldown, this::setTriggerCooldown)
FieldDef.intField("gui.max", this::getMaxTriggers, this::setMaxTriggers),
FieldDef.intField("gui.cooldown", this::getTriggerCooldown, this::setTriggerCooldown)
),
FieldDef.section("ability.section.damage"),
FieldDef.row(
@@ -497,5 +169,8 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
addVisualFieldDefs(defs);
addTelegraphSizeField(defs);
}
}
@@ -0,0 +1,554 @@
package kamkeel.npcs.controllers.data.ability.type;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.UserType;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityAbilityZone.AccentStyle;
import kamkeel.npcs.entity.EntityAbilityZone.ParticleMotion;
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.client.gui.builder.FieldDef;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
/**
* Abstract base class for zone-based abilities (Trap, Hazard).
* Extracts shared fields: duration, shape, spawn, visual, lifecycle, and FieldDefs.
*/
public abstract class AbilityZone extends Ability {
// Zone properties
protected int durationTicks;
protected ZoneShape zoneShape = ZoneShape.CIRCLE;
protected float spawnRadius = 5.0f;
protected float telegraphSize = 5.0f;
protected int zoneCount = 1;
protected float zoneHeight = 2.0f;
// Visual parameters
protected float particleDensity = 1.0f;
protected float particleScale = 1.0f;
protected float animSpeed = 1.0f;
protected float lightningDensity = 1.0f;
protected EnergyDisplayData colorData;
// Visual layer fields (always active)
protected boolean groundFill = true;
protected float groundAlpha = 0.25f;
protected boolean rings = true;
protected int ringCount = 3;
protected boolean border = true;
protected float borderSpeed = 1.0f;
protected boolean accents = true;
protected int accentStyle = 0;
protected boolean lightning = false;
protected boolean particles = true;
protected int particleMotion = 0;
protected String particleDir = "";
protected int particleSize = 32;
protected boolean particleGlow = true;
// Runtime state
protected static final Random RANDOM = new Random();
protected transient List<EntityAbilityZone> activeEntities = new ArrayList<>();
protected AbilityZone(int defaultDuration, EnergyDisplayData defaultColors) {
this.durationTicks = defaultDuration;
this.colorData = defaultColors;
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.lockMovement = LockMovementType.WINDUP;
this.cooldownTicks = 0;
this.telegraphType = TelegraphType.CIRCLE;
this.allowedBy = UserType.NPC_ONLY;
}
// ═════════════════════════════════════════════════════════════════
// SHARED OVERRIDES
// ═════════════════════════════════════════════════════════════════
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public float getTelegraphRadius() {
return telegraphSize;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
TelegraphInstance instance = super.createTelegraph(caster, target);
if (instance == null) return null;
// Always show telegraph at caster position
instance.setEntityIdToFollow(-1);
instance.setX(caster.posX);
instance.setY(caster.posY);
instance.setZ(caster.posZ);
return instance;
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Remove dead entities
Iterator<EntityAbilityZone> it = activeEntities.iterator();
while (it.hasNext()) {
EntityAbilityZone entity = it.next();
if (entity == null || entity.isDead) {
it.remove();
}
}
if (activeEntities.isEmpty()) {
signalCompletion();
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
cleanup();
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
for (EntityAbilityZone entity : activeEntities) {
if (entity != null && !entity.isDead) {
killAbilityEntity(entity);
}
}
activeEntities.clear();
}
@Override
public int getMaxPreviewDuration() {
return durationTicks + 10;
}
// ═════════════════════════════════════════════════════════════════
// SHARED SPAWN HELPER
// ═════════════════════════════════════════════════════════════════
/**
* Find a non-overlapping spawn position within spawnRadius of the caster.
*/
protected double[] findSpawnPosition(EntityLivingBase caster, List<double[]> placedPositions, float minSeparation) {
double spawnX = caster.posX;
double spawnZ = caster.posZ;
for (int attempt = 0; attempt < 15; attempt++) {
double angle = RANDOM.nextDouble() * Math.PI * 2;
double dist = Math.sqrt(RANDOM.nextDouble()) * spawnRadius;
double candidateX = caster.posX + Math.cos(angle) * dist;
double candidateZ = caster.posZ + Math.sin(angle) * dist;
boolean overlaps = false;
for (double[] placed : placedPositions) {
double dx = candidateX - placed[0];
double dz = candidateZ - placed[1];
if (Math.sqrt(dx * dx + dz * dz) < minSeparation) {
overlaps = true;
break;
}
}
spawnX = candidateX;
spawnZ = candidateZ;
if (!overlaps) break;
}
return new double[]{spawnX, spawnZ};
}
/**
* Apply visual settings to a zone entity.
*/
protected void applyVisualToEntity(EntityAbilityZone entity) {
entity.applyVisual(groundFill, groundAlpha,
rings, ringCount, border, borderSpeed,
accents, accentStyle, lightning,
particles, particleMotion, particleDir,
particleSize, particleGlow);
}
// ═════════════════════════════════════════════════════════════════
// PRESET SUPPORT
// ═════════════════════════════════════════════════════════════════
/**
* Apply preset defaults by old style name (for backward compatibility).
*/
private void applyPresetDefaults(String styleName) {
switch (styleName) {
case "TOXIC":
groundFill = true; groundAlpha = 0.30f; rings = true; ringCount = 3;
border = false; borderSpeed = 1.0f; accents = true; accentStyle = 1;
lightning = false; particles = true; particleMotion = 0; particleGlow = true;
break;
case "INFERNO":
groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3;
border = true; borderSpeed = 2.0f; accents = true; accentStyle = 2;
lightning = false; particles = true; particleMotion = 0; particleGlow = true;
break;
case "ARCANE":
groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1;
border = true; borderSpeed = 0.8f; accents = true; accentStyle = 0;
lightning = false; particles = true; particleMotion = 1; particleGlow = true;
break;
case "ELECTRIC":
groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1;
border = false; borderSpeed = 1.0f; accents = false; accentStyle = 0;
lightning = true; particles = true; particleMotion = 2; particleGlow = true;
break;
case "FROST":
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
border = true; borderSpeed = 0.3f; accents = true; accentStyle = 0;
lightning = false; particles = true; particleMotion = 1; particleGlow = true;
break;
case "DEFAULT":
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
border = true; borderSpeed = 1.0f; accents = true; accentStyle = 0;
lightning = false; particles = false; particleMotion = 0; particleGlow = true;
break;
default:
break;
}
}
/**
* Client-only preset enum for the GUI dropdown.
* Not persisted — presets simply set field values.
*/
@SideOnly(Side.CLIENT)
public enum ZonePreset {
CUSTOM, DEFAULT, TOXIC, INFERNO, ARCANE, ELECTRIC, FROST;
@Override
public String toString() {
return "ability.preset." + name().toLowerCase();
}
}
/**
* Check current field values against each preset, return matching preset or CUSTOM.
*/
@SideOnly(Side.CLIENT)
public ZonePreset getCurrentPreset() {
if (matchesPreset(true, 0.25f, true, 3, true, 1.0f, true, 0, false, false, 0, true))
return ZonePreset.DEFAULT;
if (matchesPreset(true, 0.30f, true, 3, false, 1.0f, true, 1, false, true, 0, true))
return ZonePreset.TOXIC;
if (matchesPreset(true, 0.35f, true, 3, true, 2.0f, true, 2, false, true, 0, true))
return ZonePreset.INFERNO;
if (matchesPreset(true, 0.20f, true, 1, true, 0.8f, true, 0, false, true, 1, true))
return ZonePreset.ARCANE;
if (matchesPreset(true, 0.15f, false, 1, false, 1.0f, false, 0, true, true, 2, true))
return ZonePreset.ELECTRIC;
if (matchesPreset(true, 0.25f, true, 3, true, 0.3f, true, 0, false, true, 1, true))
return ZonePreset.FROST;
return ZonePreset.CUSTOM;
}
@SideOnly(Side.CLIENT)
private boolean matchesPreset(boolean gf, float ga, boolean r, int rc, boolean b, float bs,
boolean a, int as, boolean l, boolean p, int pm, boolean pg) {
return groundFill == gf && Math.abs(groundAlpha - ga) < 0.001f
&& rings == r && ringCount == rc && border == b && Math.abs(borderSpeed - bs) < 0.001f
&& accents == a && accentStyle == as && lightning == l
&& particles == p && particleMotion == pm && particleGlow == pg;
}
/**
* Apply a preset's field values. CUSTOM does nothing (user configures manually).
*/
@SideOnly(Side.CLIENT)
public void applyPreset(ZonePreset preset) {
if (preset == ZonePreset.CUSTOM) return;
applyPresetDefaults(preset.name());
}
// ═════════════════════════════════════════════════════════════════
// SHARED NBT
// ═════════════════════════════════════════════════════════════════
protected void writeZoneNBT(NBTTagCompound nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setString("zoneShape", zoneShape.name());
nbt.setFloat("spawnRadius", spawnRadius);
nbt.setFloat("telegraphSize", telegraphSize);
nbt.setInteger("zoneCount", zoneCount);
nbt.setFloat("zoneHeight", zoneHeight);
nbt.setFloat("particleDensity", particleDensity);
nbt.setFloat("particleScale", particleScale);
nbt.setFloat("animSpeed", animSpeed);
nbt.setFloat("lightningDensity", lightningDensity);
colorData.writeNBT(nbt);
// Visual layer fields — always written as top-level keys
nbt.setBoolean("groundFill", groundFill);
nbt.setFloat("groundAlpha", groundAlpha);
nbt.setBoolean("rings", rings);
nbt.setInteger("ringCount", ringCount);
nbt.setBoolean("border", border);
nbt.setFloat("borderSpeed", borderSpeed);
nbt.setBoolean("accents", accents);
nbt.setInteger("accentStyle", accentStyle);
nbt.setBoolean("lightning", lightning);
nbt.setBoolean("particles", particles);
nbt.setInteger("particleMotion", particleMotion);
nbt.setString("particleDir", particleDir);
nbt.setInteger("particleSize", particleSize);
nbt.setBoolean("particleGlow", particleGlow);
}
protected void readZoneNBT(NBTTagCompound nbt, int defaultDuration) {
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : defaultDuration;
if (nbt.hasKey("zoneShape")) {
try { this.zoneShape = ZoneShape.valueOf(nbt.getString("zoneShape")); }
catch (Exception e) { this.zoneShape = ZoneShape.CIRCLE; }
} else {
this.zoneShape = ZoneShape.CIRCLE;
}
this.spawnRadius = nbt.hasKey("spawnRadius") ? nbt.getFloat("spawnRadius") : 5.0f;
this.telegraphSize = nbt.hasKey("telegraphSize") ? nbt.getFloat("telegraphSize") : spawnRadius;
this.zoneCount = nbt.hasKey("zoneCount") ? nbt.getInteger("zoneCount") : 1;
this.zoneHeight = nbt.hasKey("zoneHeight") ? nbt.getFloat("zoneHeight") : 2.0f;
this.particleDensity = nbt.hasKey("particleDensity") ? nbt.getFloat("particleDensity") : 1.0f;
this.particleScale = nbt.hasKey("particleScale") ? nbt.getFloat("particleScale") : 1.0f;
this.animSpeed = nbt.hasKey("animSpeed") ? nbt.getFloat("animSpeed") : 1.0f;
this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 1.0f;
colorData.readNBT(nbt);
// Visual fields — backward compat with old zoneStyle format
if (nbt.hasKey("zoneStyle")) {
// OLD FORMAT: apply preset defaults, then overlay CustomVisual
applyPresetDefaults(nbt.getString("zoneStyle"));
if (nbt.hasKey("CustomVisual")) {
NBTTagCompound custom = nbt.getCompoundTag("CustomVisual");
this.groundFill = !custom.hasKey("groundFill") || custom.getBoolean("groundFill");
this.groundAlpha = custom.hasKey("groundAlpha") ? custom.getFloat("groundAlpha") : 0.25f;
this.rings = !custom.hasKey("rings") || custom.getBoolean("rings");
this.ringCount = custom.hasKey("ringCount") ? custom.getInteger("ringCount") : 3;
this.border = !custom.hasKey("border") || custom.getBoolean("border");
this.borderSpeed = custom.hasKey("borderSpeed") ? custom.getFloat("borderSpeed") : 1.0f;
this.accents = !custom.hasKey("accents") || custom.getBoolean("accents");
this.accentStyle = custom.hasKey("accentStyle") ? custom.getInteger("accentStyle") : 0;
this.lightning = custom.hasKey("lightning") && custom.getBoolean("lightning");
this.particles = !custom.hasKey("particles") || custom.getBoolean("particles");
this.particleMotion = custom.hasKey("particleMotion") ? custom.getInteger("particleMotion") : 0;
this.particleDir = custom.hasKey("particleDir") ? custom.getString("particleDir") : "";
this.particleSize = custom.hasKey("particleSize") ? custom.getInteger("particleSize") : 32;
this.particleGlow = !custom.hasKey("particleGlow") || custom.getBoolean("particleGlow");
}
} else {
// NEW FORMAT: read individual top-level keys
this.groundFill = !nbt.hasKey("groundFill") || nbt.getBoolean("groundFill");
this.groundAlpha = nbt.hasKey("groundAlpha") ? nbt.getFloat("groundAlpha") : 0.25f;
this.rings = !nbt.hasKey("rings") || nbt.getBoolean("rings");
this.ringCount = nbt.hasKey("ringCount") ? nbt.getInteger("ringCount") : 3;
this.border = !nbt.hasKey("border") || nbt.getBoolean("border");
this.borderSpeed = nbt.hasKey("borderSpeed") ? nbt.getFloat("borderSpeed") : 1.0f;
this.accents = !nbt.hasKey("accents") || nbt.getBoolean("accents");
this.accentStyle = nbt.hasKey("accentStyle") ? nbt.getInteger("accentStyle") : 0;
this.lightning = nbt.hasKey("lightning") && nbt.getBoolean("lightning");
this.particles = !nbt.hasKey("particles") || nbt.getBoolean("particles");
this.particleMotion = nbt.hasKey("particleMotion") ? nbt.getInteger("particleMotion") : 0;
this.particleDir = nbt.hasKey("particleDir") ? nbt.getString("particleDir") : "";
this.particleSize = nbt.hasKey("particleSize") ? nbt.getInteger("particleSize") : 32;
this.particleGlow = !nbt.hasKey("particleGlow") || nbt.getBoolean("particleGlow");
}
}
// ═════════════════════════════════════════════════════════════════
// SHARED GETTERS & SETTERS
// ═════════════════════════════════════════════════════════════════
public int getDurationTicks() { return durationTicks; }
public void setDurationTicks(int durationTicks) { this.durationTicks = Math.max(1, durationTicks); }
public ZoneShape getZoneShapeEnum() { return zoneShape; }
public void setZoneShapeEnum(ZoneShape shape) { this.zoneShape = shape; }
public int getZoneShapeOrdinal() { return zoneShape.ordinal(); }
public void setZoneShapeOrdinal(int shape) {
ZoneShape[] values = ZoneShape.values();
this.zoneShape = shape >= 0 && shape < values.length ? values[shape] : ZoneShape.CIRCLE;
}
public float getSpawnRadius() { return spawnRadius; }
public void setSpawnRadius(float spawnRadius) { this.spawnRadius = Math.max(0, spawnRadius); }
public float getTelegraphSize() { return telegraphSize; }
public void setTelegraphSize(float telegraphSize) { this.telegraphSize = Math.max(0, telegraphSize); }
public int getZoneCount() { return zoneCount; }
public void setZoneCount(int zoneCount) { this.zoneCount = Math.max(1, zoneCount); }
public float getZoneHeight() { return zoneHeight; }
public void setZoneHeight(float zoneHeight) { this.zoneHeight = Math.max(0.5f, zoneHeight); }
public float getParticleDensity() { return particleDensity; }
public void setParticleDensity(float v) { this.particleDensity = Math.max(0, Math.min(5, v)); }
public float getParticleScale() { return particleScale; }
public void setParticleScale(float v) { this.particleScale = Math.max(0.1f, Math.min(5, v)); }
public float getAnimSpeed() { return animSpeed; }
public void setAnimSpeed(float v) { this.animSpeed = Math.max(0.1f, Math.min(5, v)); }
public float getLightningDensity() { return lightningDensity; }
public void setLightningDensity(float v) { this.lightningDensity = Math.max(0, Math.min(3, v)); }
// Color getters/setters
public int getInnerColor() { return colorData.innerColor; }
public void setInnerColor(int color) { colorData.innerColor = color; }
public int getOuterColor() { return colorData.outerColor; }
public void setOuterColor(int color) { colorData.outerColor = color; }
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
public void setOuterColorEnabled(boolean enabled) { colorData.outerColorEnabled = enabled; }
// Visual layer getters/setters
public boolean isGroundFill() { return groundFill; }
public void setGroundFill(boolean v) { this.groundFill = v; }
public float getGroundAlpha() { return groundAlpha; }
public void setGroundAlpha(float v) { this.groundAlpha = Math.max(0, Math.min(1, v)); }
public boolean isRings() { return rings; }
public void setRings(boolean v) { this.rings = v; }
public int getRingCount() { return ringCount; }
public void setRingCount(int v) { this.ringCount = Math.max(1, Math.min(5, v)); }
public boolean isBorder() { return border; }
public void setBorder(boolean v) { this.border = v; }
public float getBorderSpeed() { return borderSpeed; }
public void setBorderSpeed(float v) { this.borderSpeed = Math.max(0.1f, Math.min(5, v)); }
public boolean isAccents() { return accents; }
public void setAccents(boolean v) { this.accents = v; }
public int getAccentStyle() { return accentStyle; }
public void setAccentStyle(int v) { this.accentStyle = Math.max(0, Math.min(AccentStyle.values().length - 1, v)); }
public AccentStyle getAccentStyleEnum() { return AccentStyle.values()[Math.max(0, Math.min(accentStyle, AccentStyle.values().length - 1))]; }
public void setAccentStyleEnum(AccentStyle s) { this.accentStyle = s.ordinal(); }
public boolean isLightning() { return lightning; }
public void setLightning(boolean v) { this.lightning = v; }
public boolean isParticles() { return particles; }
public void setParticles(boolean v) { this.particles = v; }
public int getParticleMotion() { return particleMotion; }
public void setParticleMotion(int v) { this.particleMotion = Math.max(0, Math.min(ParticleMotion.values().length - 1, v)); }
public ParticleMotion getParticleMotionEnum() { return ParticleMotion.values()[Math.max(0, Math.min(particleMotion, ParticleMotion.values().length - 1))]; }
public void setParticleMotionEnum(ParticleMotion m) { this.particleMotion = m.ordinal(); }
public String getParticleDir() { return particleDir; }
public void setParticleDir(String v) { this.particleDir = v != null ? v : ""; }
public int getParticleSize() { return particleSize; }
public void setParticleSize(int v) { this.particleSize = Math.max(1, Math.min(256, v)); }
public boolean isParticleGlow() { return particleGlow; }
public void setParticleGlow(boolean v) { this.particleGlow = v; }
// ═════════════════════════════════════════════════════════════════
// SHARED FIELD DEFINITIONS (client-only)
// ═════════════════════════════════════════════════════════════════
@SideOnly(Side.CLIENT)
protected void addVisualFieldDefs(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Preset dropdown
FieldDef.enumField("gui.preset", ZonePreset.class, this::getCurrentPreset, this::applyPreset)
.tab("ability.tab.visual"),
// Ground Fill
FieldDef.section("ability.section.ground").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isGroundFill, this::setGroundFill),
FieldDef.floatField("gui.alpha", this::getGroundAlpha, this::setGroundAlpha)
).tab("ability.tab.visual"),
// Rings
FieldDef.section("ability.section.rings").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isRings, this::setRings),
FieldDef.intField("gui.count", this::getRingCount, this::setRingCount).range(1, 5)
).tab("ability.tab.visual"),
// Border
FieldDef.section("ability.section.border").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isBorder, this::setBorder),
FieldDef.floatField("gui.speed", this::getBorderSpeed, this::setBorderSpeed)
).tab("ability.tab.visual"),
// Accents
FieldDef.section("ability.section.accents").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isAccents, this::setAccents),
FieldDef.enumField("gui.style", AccentStyle.class, this::getAccentStyleEnum, this::setAccentStyleEnum)
).tab("ability.tab.visual"),
// Lightning
FieldDef.section("ability.section.lightning").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isLightning, this::setLightning),
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
).tab("ability.tab.visual"),
// Particles
FieldDef.section("ability.section.particles").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isParticles, this::setParticles),
FieldDef.floatField("gui.density", this::getParticleDensity, this::setParticleDensity)
).tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.scale", this::getParticleScale, this::setParticleScale),
FieldDef.enumField("gui.motion", ParticleMotion.class, this::getParticleMotionEnum, this::setParticleMotionEnum)
).tab("ability.tab.visual"),
FieldDef.boolField("gui.glow", this::isParticleGlow, this::setParticleGlow)
.tab("ability.tab.visual"),
FieldDef.stringField("gui.texture", this::getParticleDir, this::setParticleDir)
.tab("ability.tab.visual").visibleWhen(this::isParticles),
FieldDef.intField("gui.size", this::getParticleSize, this::setParticleSize).range(1, 256)
.tab("ability.tab.visual").visibleWhen(this::isParticles),
// Animation
FieldDef.section("ability.section.animation").tab("ability.tab.visual"),
FieldDef.floatField("gui.speed", this::getAnimSpeed, this::setAnimSpeed)
.tab("ability.tab.visual"),
// Colors
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled)
));
}
@SideOnly(Side.CLIENT)
protected void addTelegraphSizeField(List<FieldDef> defs) {
defs.add(FieldDef.floatField("gui.size", this::getTelegraphSize, this::setTelegraphSize)
.tab("ability.tab.effects"));
}
}
@@ -0,0 +1,928 @@
package kamkeel.npcs.entity;
import cpw.mods.fml.common.network.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.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
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;
import java.util.UUID;
/**
* Shared zone entity for Trap and Hazard abilities.
* Handles rendering, positioning, and damage logic.
*/
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();
}
}
}
/** Tiny Y offset above ground to prevent z-clipping */
private static final double GROUND_OFFSET = 0.005;
// ═══════════════════════════════════════════════════════════════════
// COMMON PROPERTIES (synced via ByteBuf)
// ═══════════════════════════════════════════════════════════════════
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;
// Visual parameters (synced for renderer)
private float particleDensity = 1.0f;
private float particleScale = 1.0f;
private float animSpeed = 1.0f;
private float lightningDensity = 1.0f;
// Visual layer parameters (always active)
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; // AccentStyle ordinal
private boolean lightning = false;
private boolean particles = true;
private int particleMotion = 0; // ParticleMotion ordinal
private String particleDir = "";
private int particleSize = 32;
private boolean particleGlow = true;
// Lifetime
private long deathWorldTime = -1;
// Effects
private List<AbilityEffect> 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;
// ═══════════════════════════════════════════════════════════════════
// HAZARD-SPECIFIC PROPERTIES
// ═══════════════════════════════════════════════════════════════════
private float damagePerSecond = 1.0f;
private int damageInterval = 20;
private boolean ignoreInvulnFrames = false;
private boolean affectsCaster = false;
// ═══════════════════════════════════════════════════════════════════
// RUNTIME STATE (transient)
// ═══════════════════════════════════════════════════════════════════
// Trap runtime
private transient boolean armed = false;
private transient int triggerCount = 0;
private transient int ticksSinceLastTrigger = 0;
private transient Set<UUID> triggeredEntities = new HashSet<>();
private transient int triggerFlashTick = -1; // For visual flash on trigger
// Hazard runtime
private transient int ticksSinceDamage = 0;
private transient Set<Integer> damagedThisTick = new HashSet<>();
// Preview mode
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;
}
/**
* Full constructor for creating a zone entity.
* Use the static factory methods for cleaner creation.
* Position is snapped to ground level with a tiny offset to avoid z-clipping.
*/
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);
}
/**
* Create a Trap zone entity.
*/
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,
int innerColor, int outerColor, boolean outerColorEnabled,
float zoneHeight,
float particleDensity, float particleScale,
float animSpeed, float lightningDensity,
List<AbilityEffect> 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.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 (AbilityEffect e : effects) {
zone.effects.add(e.copy());
}
}
// Ready to trigger immediately after arm time
zone.ticksSinceLastTrigger = triggerCooldown;
return zone;
}
/**
* Create a Hazard zone entity.
*/
public static EntityAbilityZone createHazard(World world, EntityLivingBase owner,
double x, double y, double z,
ZoneShape shape,
float radius,
float damagePerSecond, int damageInterval,
boolean ignoreInvulnFrames, boolean affectsCaster,
int durationTicks,
int innerColor, int outerColor, boolean outerColorEnabled,
float zoneHeight,
float particleDensity, float particleScale,
float animSpeed, float lightningDensity,
List<AbilityEffect> 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.ignoreInvulnFrames = ignoreInvulnFrames;
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 (AbilityEffect e : effects) {
zone.effects.add(e.copy());
}
}
// Ready to deal damage immediately
zone.ticksSinceDamage = damageInterval;
return zone;
}
// ═══════════════════════════════════════════════════════════════════
// ENTITY LIFECYCLE
// ═══════════════════════════════════════════════════════════════════
@Override
protected void entityInit() {
// No DataWatcher needed
}
@Override
public void onUpdate() {
this.prevPosX = this.posX;
this.prevPosY = this.posY;
this.prevPosZ = this.posZ;
super.onUpdate();
// Set death time on first tick
if (deathWorldTime < 0 && worldObj != null) {
deathWorldTime = worldObj.getTotalWorldTime() + maxTicks;
}
// Check lifetime
if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) {
this.setDead();
return;
}
// Server handles damage
if (!worldObj.isRemote && !previewMode) {
switch (zoneType) {
case TRAP:
tickTrap();
break;
case HAZARD:
tickHazard();
break;
}
}
}
// ═══════════════════════════════════════════════════════════════════
// TRAP LOGIC (server-only)
// ═══════════════════════════════════════════════════════════════════
private void tickTrap() {
// Arm time countdown
if (!armed) {
if (ticksExisted >= armTime) {
armed = true;
}
return;
}
ticksSinceLastTrigger++;
// All triggers used
if (maxTriggers > 0 && triggerCount >= maxTriggers) {
this.setDead();
return;
}
// Cooldown between triggers
if (ticksSinceLastTrigger < triggerCooldown) {
return;
}
// Proximity detection
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.getUniqueID())) 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;
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;
}
// Apply knockback only if damage landed
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;
}
}
// Apply effects only if damage landed
if (hit) {
applyEffects(entity);
}
}
// Only count the trigger if at least one entity was actually damaged
if (anyDamaged) {
triggerCount++;
ticksSinceLastTrigger = 0;
triggeredEntities.add(triggerer.getUniqueID());
triggerFlashTick = ticksExisted;
}
}
// ═══════════════════════════════════════════════════════════════════
// HAZARD LOGIC (server-only)
// ═══════════════════════════════════════════════════════════════════
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 (!isInZone(entity)) continue;
if (damagePerSecond > 0) {
if (ignoreInvulnFrames) {
entity.hurtResistantTime = 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;
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);
}
}
private void applyEffects(EntityLivingBase entity) {
if (entity == null || effects.isEmpty()) return;
for (AbilityEffect 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;
}
// ═══════════════════════════════════════════════════════════════════
// RENDERING SUPPORT
// ═══════════════════════════════════════════════════════════════════
@Override
@SideOnly(Side.CLIENT)
public boolean isInRangeToRenderDist(double distance) {
return distance < 16384.0D; // 128 blocks
}
@Override
public boolean shouldRenderInPass(int pass) {
return pass == 1; // Translucent pass
}
@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 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; }
// Visual layer getters
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; }
/**
* Apply visual settings from ability data.
*/
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;
}
/**
* Apply preset defaults by old style name (for backward compatibility).
*/
private void applyPresetDefaults(String styleName) {
switch (styleName) {
case "TOXIC":
groundFill = true; groundAlpha = 0.30f; rings = true; ringCount = 3;
border = false; borderSpeed = 1.0f; accents = true; accentStyle = 1;
lightning = false; particles = true; particleMotion = 0; particleGlow = true;
break;
case "INFERNO":
groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3;
border = true; borderSpeed = 2.0f; accents = true; accentStyle = 2;
lightning = false; particles = true; particleMotion = 0; particleGlow = true;
break;
case "ARCANE":
groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1;
border = true; borderSpeed = 0.8f; accents = true; accentStyle = 0;
lightning = false; particles = true; particleMotion = 1; particleGlow = true;
break;
case "ELECTRIC":
groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1;
border = false; borderSpeed = 1.0f; accents = false; accentStyle = 0;
lightning = true; particles = true; particleMotion = 2; particleGlow = true;
break;
case "FROST":
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
border = true; borderSpeed = 0.3f; accents = true; accentStyle = 0;
lightning = false; particles = true; particleMotion = 1; particleGlow = true;
break;
default: // DEFAULT
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
border = true; borderSpeed = 1.0f; accents = true; accentStyle = 0;
lightning = false; particles = false; particleMotion = 0; particleGlow = true;
break;
}
}
// ═══════════════════════════════════════════════════════════════════
// NBT SERIALIZATION
// ═══════════════════════════════════════════════════════════════════
@Override
protected void readEntityFromNBT(NBTTagCompound nbt) {
this.zoneType = ZoneType.values()[nbt.getInteger("ZoneType")];
// Shape: read new string key first, fall back to old ordinal (always CIRCLE for old RING/CONE)
if (nbt.hasKey("ShapeName")) {
try {
this.shape = ZoneShape.valueOf(nbt.getString("ShapeName"));
} catch (Exception e) {
this.shape = ZoneShape.CIRCLE;
}
} else {
// Legacy: old ordinal data — RING(1) and CONE(2) map to CIRCLE
this.shape = ZoneShape.CIRCLE;
}
this.ownerEntityId = nbt.getInteger("OwnerId");
this.radius = nbt.getFloat("Radius");
this.durationTicks = nbt.getInteger("Duration");
this.maxTicks = nbt.getInteger("MaxTicks");
this.innerColor = nbt.getInteger("InnerColor");
this.outerColor = nbt.getInteger("OuterColor");
this.outerColorEnabled = nbt.getBoolean("OuterColorEnabled");
this.zoneHeight = nbt.hasKey("ZoneHeight") ? nbt.getFloat("ZoneHeight") : 2.0f;
this.particleDensity = nbt.hasKey("ParticleDensity") ? nbt.getFloat("ParticleDensity") : 1.0f;
this.particleScale = nbt.hasKey("ParticleScale") ? nbt.getFloat("ParticleScale") : 1.0f;
this.animSpeed = nbt.hasKey("AnimSpeed") ? nbt.getFloat("AnimSpeed") : 1.0f;
this.lightningDensity = nbt.hasKey("LightningDensity") ? nbt.getFloat("LightningDensity") : 1.0f;
this.deathWorldTime = nbt.hasKey("DeathWorldTime") ? nbt.getLong("DeathWorldTime") : -1;
// Visual fields — backward compat with old ZoneStyle format
if (nbt.hasKey("ZoneStyle")) {
// OLD FORMAT: apply preset defaults, then overlay CustomVisual
applyPresetDefaults(nbt.getString("ZoneStyle"));
if (nbt.hasKey("CustomVisual")) {
NBTTagCompound cv = nbt.getCompoundTag("CustomVisual");
this.groundFill = !cv.hasKey("GroundFill") || cv.getBoolean("GroundFill");
this.groundAlpha = cv.hasKey("GroundAlpha") ? cv.getFloat("GroundAlpha") : 0.25f;
this.rings = !cv.hasKey("Rings") || cv.getBoolean("Rings");
this.ringCount = cv.hasKey("RingCount") ? cv.getInteger("RingCount") : 3;
this.border = !cv.hasKey("Border") || cv.getBoolean("Border");
this.borderSpeed = cv.hasKey("BorderSpeed") ? cv.getFloat("BorderSpeed") : 1.0f;
this.accents = !cv.hasKey("Accents") || cv.getBoolean("Accents");
this.accentStyle = cv.hasKey("AccentStyle") ? cv.getInteger("AccentStyle") : 0;
this.lightning = cv.hasKey("Lightning") && cv.getBoolean("Lightning");
this.particles = !cv.hasKey("Particles") || cv.getBoolean("Particles");
this.particleMotion = cv.hasKey("ParticleMotion") ? cv.getInteger("ParticleMotion") : 0;
this.particleDir = cv.hasKey("ParticleDir") ? cv.getString("ParticleDir") : "";
this.particleSize = cv.hasKey("ParticleSize") ? cv.getInteger("ParticleSize") : 32;
this.particleGlow = !cv.hasKey("ParticleGlow") || cv.getBoolean("ParticleGlow");
}
} else {
// NEW FORMAT: read individual top-level keys
this.groundFill = !nbt.hasKey("GroundFill") || nbt.getBoolean("GroundFill");
this.groundAlpha = nbt.hasKey("GroundAlpha") ? nbt.getFloat("GroundAlpha") : 0.25f;
this.rings = !nbt.hasKey("Rings") || nbt.getBoolean("Rings");
this.ringCount = nbt.hasKey("RingCount") ? nbt.getInteger("RingCount") : 3;
this.border = !nbt.hasKey("Border") || nbt.getBoolean("Border");
this.borderSpeed = nbt.hasKey("BorderSpeed") ? nbt.getFloat("BorderSpeed") : 1.0f;
this.accents = !nbt.hasKey("Accents") || nbt.getBoolean("Accents");
this.accentStyle = nbt.hasKey("AccentStyle") ? nbt.getInteger("AccentStyle") : 0;
this.lightning = nbt.hasKey("Lightning") && nbt.getBoolean("Lightning");
this.particles = !nbt.hasKey("Particles") || nbt.getBoolean("Particles");
this.particleMotion = nbt.hasKey("ParticleMotion") ? nbt.getInteger("ParticleMotion") : 0;
this.particleDir = nbt.hasKey("ParticleDir") ? nbt.getString("ParticleDir") : "";
this.particleSize = nbt.hasKey("ParticleSize") ? nbt.getInteger("ParticleSize") : 32;
this.particleGlow = !nbt.hasKey("ParticleGlow") || nbt.getBoolean("ParticleGlow");
}
// Trap
this.armTime = nbt.getInteger("ArmTime");
this.triggerRadius = nbt.getFloat("TriggerRadius");
this.maxTriggers = nbt.getInteger("MaxTriggers");
this.triggerCooldown = nbt.getInteger("TriggerCooldown");
this.damage = nbt.getFloat("Damage");
this.damageRadius = nbt.getFloat("DamageRadius");
this.knockback = nbt.getFloat("Knockback");
// Hazard
this.damagePerSecond = nbt.getFloat("DamagePerSecond");
this.damageInterval = nbt.getInteger("DamageInterval");
this.ignoreInvulnFrames = nbt.getBoolean("IgnoreInvuln");
this.affectsCaster = nbt.getBoolean("AffectsCaster");
// Effects
effects.clear();
if (nbt.hasKey("Effects")) {
NBTTagList effectList = nbt.getTagList("Effects", 10);
for (int i = 0; i < effectList.tagCount(); i++) {
AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i));
if (effect != null && effect.isValid()) {
effects.add(effect);
}
}
}
}
@Override
protected void writeEntityToNBT(NBTTagCompound nbt) {
nbt.setInteger("ZoneType", zoneType.ordinal());
nbt.setString("ShapeName", shape.name());
nbt.setInteger("OwnerId", ownerEntityId);
nbt.setFloat("Radius", radius);
nbt.setInteger("Duration", durationTicks);
nbt.setInteger("MaxTicks", maxTicks);
nbt.setInteger("InnerColor", innerColor);
nbt.setInteger("OuterColor", outerColor);
nbt.setBoolean("OuterColorEnabled", outerColorEnabled);
nbt.setFloat("ZoneHeight", zoneHeight);
nbt.setFloat("ParticleDensity", particleDensity);
nbt.setFloat("ParticleScale", particleScale);
nbt.setFloat("AnimSpeed", animSpeed);
nbt.setFloat("LightningDensity", lightningDensity);
nbt.setLong("DeathWorldTime", deathWorldTime);
// Visual fields — always written as top-level keys
nbt.setBoolean("GroundFill", groundFill);
nbt.setFloat("GroundAlpha", groundAlpha);
nbt.setBoolean("Rings", rings);
nbt.setInteger("RingCount", ringCount);
nbt.setBoolean("Border", border);
nbt.setFloat("BorderSpeed", borderSpeed);
nbt.setBoolean("Accents", accents);
nbt.setInteger("AccentStyle", accentStyle);
nbt.setBoolean("Lightning", lightning);
nbt.setBoolean("Particles", particles);
nbt.setInteger("ParticleMotion", particleMotion);
nbt.setString("ParticleDir", particleDir);
nbt.setInteger("ParticleSize", particleSize);
nbt.setBoolean("ParticleGlow", particleGlow);
// Trap
nbt.setInteger("ArmTime", armTime);
nbt.setFloat("TriggerRadius", triggerRadius);
nbt.setInteger("MaxTriggers", maxTriggers);
nbt.setInteger("TriggerCooldown", triggerCooldown);
nbt.setFloat("Damage", damage);
nbt.setFloat("DamageRadius", damageRadius);
nbt.setFloat("Knockback", knockback);
// Hazard
nbt.setFloat("DamagePerSecond", damagePerSecond);
nbt.setInteger("DamageInterval", damageInterval);
nbt.setBoolean("IgnoreInvuln", ignoreInvulnFrames);
nbt.setBoolean("AffectsCaster", affectsCaster);
// Effects
NBTTagList effectList = new NBTTagList();
for (AbilityEffect effect : effects) {
effectList.appendTag(effect.writeNBT());
}
nbt.setTag("Effects", effectList);
}
// ═══════════════════════════════════════════════════════════════════
// SPAWN DATA (ByteBuf for client sync)
// ═══════════════════════════════════════════════════════════════════
@Override
public void writeSpawnData(ByteBuf buffer) {
buffer.writeInt(zoneType.ordinal());
ByteBufUtils.writeUTF8String(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 fields — always written individually
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.writeUTF8String(buffer, particleDir);
buffer.writeInt(particleSize);
buffer.writeBoolean(particleGlow);
// Trap-specific (client needs armTime for visual state)
buffer.writeInt(armTime);
// Effects via NBT in buffer
NBTTagCompound effectsNbt = new NBTTagCompound();
NBTTagList effectList = new NBTTagList();
for (AbilityEffect effect : effects) {
effectList.appendTag(effect.writeNBT());
}
effectsNbt.setTag("Effects", effectList);
ByteBufUtils.writeTag(buffer, effectsNbt);
}
@Override
public void readSpawnData(ByteBuf buffer) {
this.zoneType = ZoneType.values()[buffer.readInt()];
String shapeName = ByteBufUtils.readUTF8String(buffer);
try {
this.shape = ZoneShape.valueOf(shapeName);
} catch (Exception e) {
this.shape = ZoneShape.CIRCLE;
}
this.ownerEntityId = buffer.readInt();
this.radius = buffer.readFloat();
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 fields — always read individually
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.readUTF8String(buffer);
this.particleSize = buffer.readInt();
this.particleGlow = buffer.readBoolean();
this.armTime = buffer.readInt();
// Effects via NBT
NBTTagCompound effectsNbt = ByteBufUtils.readTag(buffer);
effects.clear();
if (effectsNbt != null && effectsNbt.hasKey("Effects")) {
NBTTagList effectList = effectsNbt.getTagList("Effects", 10);
for (int i = 0; i < effectList.tagCount(); i++) {
AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i));
if (effect != null && effect.isValid()) {
effects.add(effect);
}
}
}
}
}
@@ -91,6 +91,7 @@ import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityAbilitySweeper;
import kamkeel.npcs.entity.EntityAbilityZone;
import noppes.npcs.entity.old.EntityNPCDwarfFemale;
import noppes.npcs.entity.old.EntityNPCDwarfMale;
import noppes.npcs.entity.old.EntityNPCElfFemale;
@@ -292,6 +293,7 @@ public class CustomNpcs {
registerNewEntity(EntityAbilityLaser.class, "abilitylaser", 64, 3, true);
registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 64, 3, true);
registerNewEntity(EntityAbilitySweeper.class, "abilitysweeper", 64, 3, true);
registerNewEntity(EntityAbilityZone.class, "abilityzone", 64, 3, true);
new RecipeController();
@@ -151,12 +151,14 @@ import kamkeel.npcs.client.renderer.RenderAbilityDisc;
import kamkeel.npcs.client.renderer.RenderAbilityLaser;
import kamkeel.npcs.client.renderer.RenderAbilityBeam;
import kamkeel.npcs.client.renderer.RenderAbilitySweeper;
import kamkeel.npcs.client.renderer.RenderAbilityZone;
import kamkeel.npcs.client.renderer.lightning.LightningHandler;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityAbilitySweeper;
import kamkeel.npcs.entity.EntityAbilityZone;
import noppes.npcs.client.renderer.blocks.BlockBannerRenderer;
import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer;
import noppes.npcs.client.renderer.blocks.BlockBeamRenderer;
@@ -270,6 +272,7 @@ public class ClientProxy extends CommonProxy {
RenderingRegistry.registerEntityRenderingHandler(EntityAbilityLaser.class, new RenderAbilityLaser());
RenderingRegistry.registerEntityRenderingHandler(EntityAbilityBeam.class, new RenderAbilityBeam());
RenderingRegistry.registerEntityRenderingHandler(EntityAbilitySweeper.class, new RenderAbilitySweeper());
RenderingRegistry.registerEntityRenderingHandler(EntityAbilityZone.class, new RenderAbilityZone());
RenderingRegistry.registerEntityRenderingHandler(EntityCustomNpc.class, new RenderCustomNpc());
@@ -84,6 +84,10 @@ public class CustomFX extends EntityFX {
this.imageData = ClientCacheHandler.getImageData(this.directory);
}
public void setMaxAge(int age) {
this.particleMaxAge = age;
}
public static CustomFX fromScriptedParticle(ScriptParticle particle, World worldObj, Entity entity) {
EntityClientPlayerMP player = Minecraft.getMinecraft().thePlayer;
if (entity == player) {
@@ -0,0 +1,65 @@
package noppes.npcs.client.fx;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import net.minecraft.client.particle.EntityFX;
import net.minecraft.world.World;
/**
* Simple particle for zone visual effects (Trap/Hazard styles).
* Uses vanilla particle atlas with color tinting. Fades out over lifetime.
*/
@SideOnly(Side.CLIENT)
public class ZoneParticleFX extends EntityFX {
private final float startAlpha;
private final boolean glow;
public ZoneParticleFX(World world, double x, double y, double z,
double motionX, double motionY, double motionZ,
float r, float g, float b, float alpha,
float scale, int maxAge, float gravity, boolean glow) {
super(world, x, y, z, 0, 0, 0);
this.motionX = motionX;
this.motionY = motionY;
this.motionZ = motionZ;
this.particleRed = r;
this.particleGreen = g;
this.particleBlue = b;
this.particleAlpha = alpha;
this.startAlpha = alpha;
this.particleScale = scale;
this.particleMaxAge = maxAge;
this.particleGravity = gravity;
this.glow = glow;
this.noClip = true;
this.setParticleTextureIndex(0);
}
@Override
public int getBrightnessForRender(float partialTick) {
return glow ? 0xF000F0 : super.getBrightnessForRender(partialTick);
}
@Override
public void onUpdate() {
this.prevPosX = this.posX;
this.prevPosY = this.posY;
this.prevPosZ = this.posZ;
if (this.particleAge++ >= this.particleMaxAge) {
this.setDead();
return;
}
// Linear fade out
float lifeRatio = (float) this.particleAge / (float) this.particleMaxAge;
this.particleAlpha = this.startAlpha * (1.0f - lifeRatio);
// Movement
this.motionY -= 0.04D * (double) this.particleGravity;
this.posX += this.motionX;
this.posY += this.motionY;
this.posZ += this.motionZ;
}
}
@@ -25,7 +25,7 @@ public class ImageData {
public ImageData(String directory) {
this.location = new ResourceLocation(directory);
if (directory.startsWith("https://")) {
if (directory.startsWith("https://") || directory.startsWith("http://")) {
this.isUrl = true;
TextureManager texturemanager = Minecraft.getMinecraft().getTextureManager();
this.imageDownloadAlt = new ImageDownloadAlt(null, directory, new ResourceLocation("customnpcs:textures/gui/invisible.png"), new ImageBufferDownloadAlt(true, false));
@@ -1563,10 +1563,46 @@ ability.dash.defensive=Defensive
ability.counter.flat=Flat
ability.counter.percent=Percent
# Hazard Shape
# Zone Shape
ability.shape.circle=Circle
ability.shape.square=Square
ability.shape.ring=Ring
ability.shape.cone=Cone
ability.zoneShape=Shape
# Zone Presets
ability.preset.custom=Custom
ability.preset.default=Default
ability.preset.toxic=Toxic
ability.preset.inferno=Inferno
ability.preset.arcane=Arcane
ability.preset.electric=Electric
ability.preset.frost=Frost
# Accent Styles
ability.accentStyle.static=Static
ability.accentStyle.swaying=Swaying
ability.accentStyle.flickering=Flickering
# Particle Motion Styles
ability.particleMotion.rising=Rising
ability.particleMotion.drifting=Drifting
ability.particleMotion.sparks=Sparks
# Zone Settings
ability.section.zone=Zone Placement
ability.telegraphSize=Telegraph Size
ability.coneAngle=Cone Angle
ability.ignoreInvulnFrames=Ignore Invuln Frames
ability.visible=Visible
# Visual Layer Section Headers
ability.section.ground=Ground Fill
ability.section.rings=Rings
ability.section.border=Border
ability.section.accents=Accents
ability.section.lightning=Lightning
ability.section.particles=Particles
# Placement Mode
ability.placement.atCaster=At Caster
@@ -1662,6 +1698,7 @@ ability.section.timing=Timing
ability.section.movement=Movement
ability.section.sounds=Sounds
ability.section.animations=Animations
ability.section.animation=Animation
ability.section.telegraph=Telegraph
ability.section.beam=Beam
ability.section.sweep=Sweep
@@ -1711,6 +1748,19 @@ gui.animation=Animation
gui.duration=Duration
gui.amp=Amp
gui.speed=Speed
gui.size=Size
gui.alpha=Alpha
gui.count=Count
gui.style=Style
gui.scale=Scale
gui.motion=Motion
gui.glow=Glow
gui.texture=Texture
gui.dps=DPS
gui.interval=Interval
gui.cooldown=Cooldown
gui.max=Max
gui.preset=Preset
# Common Type Settings
ability.radius=Radius
@@ -1793,7 +1843,6 @@ ability.healAllies=Heal Allies
# Hazard
ability.shape=Shape
ability.placement=Placement
ability.damagePerSecond=Damage/Sec
ability.damageInterval=Damage Interval
ability.slownessLevel=Slowness Level
ability.debuffDuration=Debuff Duration
@@ -1802,8 +1851,6 @@ ability.affectsCaster=Affects Caster
# Trap
ability.armTime=Arm Time
ability.maxTriggers=Max Triggers
ability.triggerCooldown=Trigger Cooldown
# Sweeper
ability.sweepSpeed=Sweep Speed
@@ -1826,7 +1873,6 @@ ability.outerEnabled=Outer Glow
ability.outerWidth=Outer Width
ability.outerAlpha=Outer Alpha
ability.lightning=Lightning
ability.lightningDensity=Density
ability.lightningRadius=Radius
# Projectile Count / Settings
@@ -1930,7 +1976,6 @@ ability.hover.placement=Where the ability effect is placed (Caster, Target, etc.
ability.hover.lockTarget=Sweeper beam tracks the target's position
ability.hover.canCounter=Enable counterattack when blocking damage
ability.hover.counterType=Flat = fixed damage, Percent = % of blocked damage
ability.hover.shape=Shape of the hazard zone (Circle, Ring, Cone)
ability.hover.affectsCaster=Hazard can damage the NPC that created it
ability.hover.sweepMode=Swipe = single arc attack, Spin = continuous rotation