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