diff --git a/src/api b/src/api index fd4c77d01..092b18fe9 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit fd4c77d015add1f6f3012a1681561b2774f2f997 +Subproject commit 092b18fe95d57353d16960411e5289ac3bbfa6ef diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java new file mode 100644 index 000000000..6c555aca8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java @@ -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 lastParticleTick = new HashMap<>(); + + // Persistent lightning state + private final Map 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; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java index d68c4037b..2910a9566 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java @@ -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 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 points = generateLightningPath(x1, y1, z1, x2, y2, z2, segments, displacement); + return new LightningArc(points, maxAge, outerColor, innerColor); + } + // ==================== INSTANT RENDERING (no persistence) ==================== /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index f1ae7e719..89dd8b00d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -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,93 +15,28 @@ 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 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; @@ -117,21 +44,6 @@ public class AbilityHazard extends Ability implements IAbilityHazard { 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; @@ -193,222 +105,71 @@ public class AbilityHazard extends Ability implements IAbilityHazard { @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 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 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 +180,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 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); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 494192e04..173efa0de 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -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 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 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 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 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 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 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); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java new file mode 100644 index 000000000..94ceea56d --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -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 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 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 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 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 defs) { + defs.add(FieldDef.floatField("gui.size", this::getTelegraphSize, this::setTelegraphSize) + .tab("ability.tab.effects")); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java new file mode 100644 index 000000000..2f2bc00e9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -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 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 triggeredEntities = new HashSet<>(); + private transient int triggerFlashTick = -1; // For visual flash on trigger + + // Hazard runtime + private transient int ticksSinceDamage = 0; + private transient Set 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 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 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 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 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 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 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); + } + } + } + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 0bc081429..933450557 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -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(); diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 4331ab0ea..2970d835a 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -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()); diff --git a/src/main/java/noppes/npcs/client/fx/CustomFX.java b/src/main/java/noppes/npcs/client/fx/CustomFX.java index 7a296d8e8..79d5f452c 100644 --- a/src/main/java/noppes/npcs/client/fx/CustomFX.java +++ b/src/main/java/noppes/npcs/client/fx/CustomFX.java @@ -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) { diff --git a/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java b/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java new file mode 100644 index 000000000..80f6f687b --- /dev/null +++ b/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java @@ -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; + } +} diff --git a/src/main/java/noppes/npcs/client/renderer/ImageData.java b/src/main/java/noppes/npcs/client/renderer/ImageData.java index 15c383e40..8e6f651c5 100644 --- a/src/main/java/noppes/npcs/client/renderer/ImageData.java +++ b/src/main/java/noppes/npcs/client/renderer/ImageData.java @@ -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)); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 9319f1b36..408136c34 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -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