Merge branch 'dev' into syntax-highlighter-unified

This commit is contained in:
Kam committed 2026-02-12 20:33:44 -05:00
commit 105d274955
97 files changed
+8850 -6462

No files matched your search

+1 -1
Submodule src/api updated: f383fc3037...456bb2cbfa.
@@ -43,7 +43,16 @@ public class RenderAbilityDisc extends RenderAbilityProjectile {
GL11.glPushMatrix();
GL11.glScalef(scale, scale, scale);
// Apply ONLY Y-axis rotation for flat spin
if (disc.isVertical()) {
// Vertical mode: thin edge faces direction of travel (like a saw blade)
// 1. Spin in local space (Y-axis)
// 2. Tilt 90° to stand up (X-axis)
// 3. Orient to face travel direction (Y-axis world yaw)
GL11.glRotatef(disc.getTravelYaw(), 0.0f, 1.0f, 0.0f);
GL11.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
}
// Apply Y-axis rotation for spin
GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f);
// Inner scale defines the core size
@@ -31,7 +31,7 @@ public abstract class RenderAbilityProjectile extends Render {
*/
protected void setupRenderState() {
GL11.glPushMatrix();
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_LIGHTING_BIT);
GL11.glDisable(GL11.GL_CULL_FACE);
GL11.glEnable(GL12.GL_RESCALE_NORMAL);
GL11.glEnable(GL11.GL_ALPHA_TEST);
@@ -0,0 +1,779 @@
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()) {
if (c.particleDir.startsWith("mc:")) {
spawnVanillaParticle(c);
} else {
spawnCustomFXParticle(c);
}
} else {
spawnDefaultParticles(c);
}
}
}
/** Spawn vanilla Minecraft particles using mc: prefix (e.g. "mc:flame"). */
private void spawnVanillaParticle(Ctx c) {
String particleName = c.particleDir.substring(3);
int count = Math.round(3 * c.particleDensity);
for (int i = 0; i < count; i++) {
double[] pos = randomPosInZone(c);
double[] motion = getMotionForStyle(c.particleMotion);
c.zone.worldObj.spawnParticle(particleName,
c.zone.posX + pos[0], c.zone.posY + pos[1], c.zone.posZ + pos[2],
motion[0], motion[1], motion[2]);
}
}
/** 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],
0, 0, 0);
fx.motionX = motion[0];
fx.motionY = motion[1];
fx.motionZ = motion[2];
fx.setMaxAge(20 + RANDOM.nextInt(20));
fx.width = -1;
fx.height = -1;
fx.scaleX1 = c.particleScale * 10.0f;
fx.scaleY1 = c.particleScale * 10.0f;
fx.scaleX2 = fx.scaleX1;
fx.scaleY2 = fx.scaleY1;
fx.scaleXRate = 0;
fx.scaleYRate = 0;
fx.HEXColor = c.zone.getInnerColor();
fx.facePlayer = true;
fx.glows = c.particleGlow;
fx.noClip = true;
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;
}
}
@@ -110,6 +110,9 @@ public class TelegraphRenderer {
case POINT:
renderPoint(red, green, blue, alpha);
break;
case SQUARE:
renderSquare(telegraph.getRadius(), red, green, blue, alpha);
break;
default:
break;
}
@@ -258,6 +261,30 @@ public class TelegraphRenderer {
tessellator.draw();
}
private void renderSquare(float radius, float r, float g, float b, float a) {
Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
tessellator.setColorRGBA_F(r, g, b, a);
tessellator.addVertex(-radius, 0, -radius);
tessellator.addVertex(radius, 0, -radius);
tessellator.addVertex(radius, 0, radius);
tessellator.addVertex(-radius, 0, radius);
tessellator.draw();
// Draw border
tessellator.startDrawing(GL11.GL_LINE_LOOP);
tessellator.setColorRGBA_F(r, g, b, Math.min(1.0f, a * 2));
tessellator.addVertex(-radius, 0.01, -radius);
tessellator.addVertex(radius, 0.01, -radius);
tessellator.addVertex(radius, 0.01, radius);
tessellator.addVertex(-radius, 0.01, radius);
tessellator.draw();
}
// ==================== GUI PREVIEW RENDERING ====================
/**
@@ -322,6 +349,9 @@ public class TelegraphRenderer {
case POINT:
renderPoint(red, green, blue, alpha);
break;
case SQUARE:
renderSquare(telegraph.getRadius(), red, green, blue, alpha);
break;
default:
break;
}
@@ -85,20 +85,39 @@ 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.
* Note: Expects the caller to have already set up GL state (disabled textures,
* disabled lighting, disabled culling, enabled blending). This avoids nesting
* glPushAttrib calls which can overflow the GL attrib stack (depth 16) when
* other mods also push attribs during entity rendering.
*/
public void render() {
if (arcs.isEmpty()) return;
// Setup GL state
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glDisable(GL11.GL_LIGHTING);
GL11.glDisable(GL11.GL_CULL_FACE);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glDepthMask(false);
Tessellator tess = Tessellator.instance;
@@ -114,7 +133,6 @@ public class AttachedLightningRenderer {
}
GL11.glDepthMask(true);
GL11.glPopAttrib();
}
public int getArcCount() {
@@ -235,10 +253,23 @@ 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) ====================
/**
* Render a single instant lightning arc (no fading, drawn this frame only).
* Expects the caller to have already set up GL state (disabled textures,
* disabled lighting, disabled culling, enabled blending).
*/
public static void renderInstantArc(float radius, int outerColor, int innerColor) {
double theta = rand.nextDouble() * Math.PI * 2;
@@ -254,12 +285,6 @@ public class AttachedLightningRenderer {
List<double[]> points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement);
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glDisable(GL11.GL_LIGHTING);
GL11.glDisable(GL11.GL_CULL_FACE);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glDepthMask(false);
Tessellator tess = Tessellator.instance;
@@ -267,7 +292,6 @@ public class AttachedLightningRenderer {
renderBoltPath(tess, points, innerColor, 0.012f, 0.7f);
GL11.glDepthMask(true);
GL11.glPopAttrib();
}
/**
@@ -2,7 +2,12 @@ package kamkeel.npcs.command;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.controllers.data.ability.UserType;
import net.minecraft.command.ICommandSender;
import net.minecraft.entity.player.EntityPlayerMP;
import noppes.npcs.NoppesUtilServer;
import noppes.npcs.controllers.PlayerDataController;
import noppes.npcs.controllers.data.PlayerData;
import java.util.ArrayList;
import java.util.Collections;
@@ -122,5 +127,192 @@ public class AbilityCommand extends CommandKamkeelBase {
}
sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange());
sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No"));
sendResult(sender, " Allowed By: \u00A7e" + ability.getAllowedBy().name());
}
@SubCommand(
desc = "List all built-in abilities available for players"
)
public void prebuilts(ICommandSender sender, String[] args) {
Set<String> names = AbilityController.Instance.getAbilityNames();
if (names.isEmpty()) {
sendResult(sender, "No built-in abilities found.");
return;
}
List<String> playerAbilities = new ArrayList<>();
for (String name : names) {
Ability ability = AbilityController.Instance.getAbility(name);
if (ability != null && ability.getAllowedBy().allowsPlayer()) {
playerAbilities.add(name);
}
}
if (playerAbilities.isEmpty()) {
sendResult(sender, "No player-usable built-in abilities found.");
return;
}
Collections.sort(playerAbilities);
sendResult(sender, "Built-in Player Abilities (" + playerAbilities.size() + "):");
for (String name : playerAbilities) {
Ability ability = AbilityController.Instance.getAbility(name);
String userType = ability.getAllowedBy() == UserType.PLAYER_ONLY ? "\u00A7d[PLAYER]" : "\u00A7a[BOTH]";
sendResult(sender, " - \u00A7b" + name + " " + userType);
}
}
@SubCommand(
desc = "Unlock an ability for a player",
usage = "<player> <ability>"
)
public void unlock(ICommandSender sender, String[] args) {
if (args.length < 2) {
sendError(sender, "Usage: /kam ability unlock <player> <ability>");
return;
}
String playerName = args[0];
String abilityKey = joinArgs(args, 1);
// Find player
EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName);
if (player == null) {
sendError(sender, "Player not found: " + playerName);
return;
}
// Resolve ability
Ability ability = AbilityController.Instance.resolveAbility(abilityKey);
if (ability == null) {
sendError(sender, "Ability not found: " + abilityKey);
return;
}
// Use the ability's canonical ID for storage (registry key for built-in, UUID for custom)
String canonicalKey = ability.getId();
if (canonicalKey == null || canonicalKey.isEmpty()) {
canonicalKey = abilityKey; // Fallback to user-provided key
}
String displayName = ability.getName() != null ? ability.getName() : canonicalKey;
// Check if ability allows players
if (!ability.getAllowedBy().allowsPlayer()) {
sendError(sender, "Ability '\u00A7b" + displayName + "\u00A7c' is NPC-only and cannot be given to players.");
return;
}
// Get player data and unlock
PlayerData data = PlayerDataController.Instance.getPlayerData(player);
if (data.abilityData.hasUnlockedAbility(canonicalKey)) {
sendError(sender, "Player already has ability: " + displayName);
return;
}
data.abilityData.unlockAbility(canonicalKey);
sendResult(sender, "Unlocked ability '\u00A7b" + displayName + "\u00A77' for player \u00A7a" + playerName);
}
@SubCommand(
desc = "Lock (revoke) an ability from a player",
usage = "<player> <ability>"
)
public void lock(ICommandSender sender, String[] args) {
if (args.length < 2) {
sendError(sender, "Usage: /kam ability lock <player> <ability>");
return;
}
String playerName = args[0];
String abilityKey = joinArgs(args, 1);
// Find player
EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName);
if (player == null) {
sendError(sender, "Player not found: " + playerName);
return;
}
// Resolve ability to get canonical key
Ability ability = AbilityController.Instance.resolveAbility(abilityKey);
String canonicalKey = abilityKey;
String displayName = abilityKey;
if (ability != null) {
canonicalKey = ability.getId() != null ? ability.getId() : abilityKey;
displayName = ability.getName() != null ? ability.getName() : canonicalKey;
}
// Get player data and lock
PlayerData data = PlayerDataController.Instance.getPlayerData(player);
if (!data.abilityData.hasUnlockedAbility(canonicalKey)) {
sendError(sender, "Player doesn't have ability: " + displayName);
return;
}
data.abilityData.lockAbility(canonicalKey);
sendResult(sender, "Locked ability '\u00A7b" + displayName + "\u00A77' from player \u00A7a" + playerName);
}
@SubCommand(
desc = "List abilities unlocked for a player",
usage = "<player>"
)
public void player(ICommandSender sender, String[] args) {
if (args.length < 1) {
sendError(sender, "Usage: /kam ability player <player>");
return;
}
String playerName = args[0];
// Find player
EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName);
if (player == null) {
sendError(sender, "Player not found: " + playerName);
return;
}
PlayerData data = PlayerDataController.Instance.getPlayerData(player);
String[] abilityKeys = data.abilityData.getUnlockedAbilities();
if (abilityKeys.length == 0) {
sendResult(sender, "Player \u00A7a" + playerName + "\u00A77 has no unlocked abilities.");
return;
}
sendResult(sender, "Abilities for \u00A7a" + playerName + "\u00A77 (" + abilityKeys.length + "):");
int selected = data.abilityData.getSelectedIndex();
for (int i = 0; i < abilityKeys.length; i++) {
String key = abilityKeys[i];
Ability ability = AbilityController.Instance.resolveAbility(key);
String displayName = ability != null && ability.getName() != null ? ability.getName() : key;
String prefix = (i == selected) ? "\u00A7e> " : " ";
// Show display name and key if different
if (!displayName.equals(key)) {
sendResult(sender, prefix + "\u00A7b" + displayName + " \u00A78[" + key + "]");
} else {
sendResult(sender, prefix + "\u00A7b" + displayName);
}
}
}
private String joinArgs(String[] args, int startIndex) {
StringBuilder sb = new StringBuilder();
for (int i = startIndex; i < args.length; i++) {
if (i > startIndex) sb.append(" ");
sb.append(args[i]);
}
return sb.toString();
}
/**
* Get list of player-usable ability keys (for tab completion).
* Uses registry keys for built-in abilities (no spaces) and UUIDs for custom.
* Used by CommandKamkeel for <ability> usage token.
*/
public static List<String> getPlayerAbilityNames() {
List<String> keys = new ArrayList<>(AbilityController.Instance.getPlayerAbilityKeys());
Collections.sort(keys);
return keys;
}
}
@@ -20,6 +20,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static kamkeel.npcs.command.AbilityCommand.getPlayerAbilityNames;
import static kamkeel.npcs.command.EffectCommand.getSortedEffectNames;
public class CommandKamkeel extends CommandBase {
@@ -135,6 +136,9 @@ public class CommandKamkeel extends CommandBase {
} else if (usage.equals("<effectName>")) {
List<String> keys = getSortedEffectNames();
return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()]));
} else if (usage.equals("<ability>")) {
List<String> keys = getPlayerAbilityNames();
return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()]));
}
}
}
@@ -32,6 +32,7 @@ import noppes.npcs.scripted.event.player.PlayerAbilityEvent;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import net.minecraft.entity.Entity;
@@ -67,7 +68,8 @@ public abstract class Ability implements IAbility {
// Feedback
protected LockMovementType lockMovement = LockMovementType.WINDUP;
protected LockType lockType = LockType.BOTH;
protected RotationMode rotationMode = RotationMode.FREE;
protected LockMovementType rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
protected int windUpColor = 0x80FF4400; // Telegraph color during wind up
protected int activeColor = 0xC0FF0000; // Telegraph warning/active color
@@ -100,6 +102,13 @@ public abstract class Ability implements IAbility {
// Configurable potion effects
protected List<AbilityEffect> effects = new ArrayList<>();
// Burst
protected boolean burstEnabled = false;
protected int burstAmount = 0; // number of EXTRA repetitions
protected int burstDelay = 0; // ticks between bursts
protected boolean burstReplayAnimations = true;
protected boolean burstOverlap = false; // if true, don't wait for entity death between bursts
// ═══════════════════════════════════════════════════════════════════
// EXECUTION STATE (not saved, reset each combat)
// ═══════════════════════════════════════════════════════════════════
@@ -108,7 +117,18 @@ public abstract class Ability implements IAbility {
protected transient int currentTick = 0;
protected transient EntityLivingBase currentTarget;
protected transient long executionStartTime;
protected transient TelegraphInstance telegraphInstance;
protected transient List<TelegraphInstance> telegraphInstances = new ArrayList<>();
// Burst execution state
protected transient int burstIndex = 0;
protected transient List<Entity> burstEntities = new ArrayList<>();
// ═══════════════════════════════════════════════════════════════════
// PREVIEW STATE (client-side only, not saved)
// ═══════════════════════════════════════════════════════════════════
protected transient boolean previewMode = false;
protected transient PreviewEntityHandler previewEntityHandler;
// ═══════════════════════════════════════════════════════════════════
// ABSTRACT METHODS
@@ -148,6 +168,50 @@ public abstract class Ability implements IAbility {
return true;
}
/**
* Override to provide variant templates for this ability type.
* When a user creates this ability type, they will be shown a selection dialog
* with the returned variants. If empty or single-entry, the dialog is skipped.
*/
public List<AbilityVariant> getVariants() {
return Collections.emptyList();
}
/**
* Whether this ability type supports the burst system.
* Override to return false for abilities that shouldn't repeat (Guard, Heal, Sweeper, etc.).
*/
public boolean allowBurst() {
return true;
}
/**
* Whether this ability supports overlap mode for burst (entities from previous bursts keep flying).
* Override to return true for entity-spawning projectile abilities (Orb, Beam, Disc, LaserShot).
*/
public boolean allowOverlap() {
return false;
}
/**
* Whether this ability is ready for auto-completion during burst overlap mode.
* Override to delay auto-completion until all staggered projectiles have been fired.
* @param activeTick the current tick within the ACTIVE phase
*/
public boolean isReadyForBurstCompletion(int activeTick) {
return true;
}
/**
* Called when burst re-fires without replay animations (burstReplayAnimations=false).
* Entity-spawning abilities should override to spawn and fire entities in one step,
* since onWindUpTick was not called (windup was skipped).
* Default: does nothing (ability goes directly to onExecute with existing state).
*/
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
// Override in entity-spawning abilities (Orb, Beam, Disc, LaserShot)
}
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
}
@@ -328,6 +392,28 @@ public abstract class Ability implements IAbility {
return 45.0f;
}
/**
* Returns true if the caster is a player (not an NPC).
* Use this instead of checking {@code target != null} to make NPC/Player
* code paths explicit and readable.
* <p>
* NPCs always have an aggro target passed to ability methods.
* Players never do — they use look direction for aiming instead.
*/
protected final boolean isPlayerCaster(EntityLivingBase caster) {
return caster instanceof EntityPlayer;
}
/**
* Whether the telegraph should remain visible during the ACTIVE phase.
* Default: false (telegraph removed at WINDUP→ACTIVE transition).
* Override for abilities where the active phase IS the attack (e.g., sweeping/spinning attacks)
* so the telegraph stays visible while damage is being dealt.
*/
public boolean keepTelegraphDuringActive() {
return false;
}
/**
* Returns true if this ability controls NPC movement during ACTIVE phase.
* When true, AI pathfinding will be blocked during ability execution.
@@ -411,26 +497,33 @@ public abstract class Ability implements IAbility {
defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks)
.tab("General").range(0, 10000));
defs.add(FieldDef.section("ability.section.movement").tab("General"));
defs.add(FieldDef.row(
FieldDef.stringEnumField("ability.lockPosition", LockMovementType.getDisplayKeys(),
defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(),
() -> this.getLockMovement().getDisplayKey(),
v -> {
String[] keys = LockMovementType.getDisplayKeys();
for (int i = 0; i < keys.length; i++) {
if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; }
for (LockMovementType t : LockMovementType.values()) {
if (t.getDisplayKey().equals(v)) { this.setLockMovement(t); break; }
}
})
.hover("ability.hover.lockPosition"),
FieldDef.stringEnumField("ability.lockType", LockType.getDisplayKeys(),
() -> this.getLockType().getDisplayKey(),
.hover("ability.hover.lockMovement")
.tab("General"));
defs.add(FieldDef.row(
FieldDef.stringEnumField("ability.rotationMode", RotationMode.getDisplayKeys(),
() -> this.getRotationMode().getDisplayKey(),
v -> {
String[] keys = LockType.getDisplayKeys();
for (int i = 0; i < keys.length; i++) {
if (keys[i].equals(v)) { this.setLockType(LockType.fromOrdinal(i)); break; }
for (RotationMode m : RotationMode.values()) {
if (m.getDisplayKey().equals(v)) { this.setRotationMode(m); break; }
}
})
.hover("ability.hover.lockType")
.visibleWhen(() -> this.lockMovement != LockMovementType.NO)
.hover("ability.hover.rotationMode"),
FieldDef.stringEnumField("ability.rotationPhase", getRotationPhaseKeys(),
() -> this.getRotationPhase().getDisplayKey(),
v -> {
for (LockMovementType t : LockMovementType.values()) {
if (t.getDisplayKey().equals(v)) { this.setRotationPhase(t); break; }
}
})
.hover("ability.hover.rotationPhase")
.visibleWhen(() -> this.rotationMode != RotationMode.FREE)
).tab("General"));
defs.add(FieldDef.row(
FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible)
@@ -439,6 +532,25 @@ public abstract class Ability implements IAbility {
.range(0, 1000).visibleWhen(this::isInterruptible)
).tab("General"));
// ── Burst section ────────────────────────────────────────────
if (allowBurst()) {
defs.add(FieldDef.section("ability.section.burst").tab("General"));
defs.add(FieldDef.boolField("ability.burstEnabled", this::isBurstEnabled, this::setBurstEnabled)
.tab("General"));
defs.add(FieldDef.row(
FieldDef.intField("ability.burstAmount", this::getBurstAmount, this::setBurstAmount).range(1, 100),
FieldDef.intField("ability.burstDelay", this::getBurstDelay, this::setBurstDelay).range(0, 1000)
).tab("General").visibleWhen(this::isBurstEnabled));
defs.add(FieldDef.boolField("ability.burstReplayAnimations", this::isBurstReplayAnimations, this::setBurstReplayAnimations)
.tab("General").visibleWhen(this::isBurstEnabled)
.hover("ability.hover.burstReplay"));
if (allowOverlap()) {
defs.add(FieldDef.boolField("ability.burstOverlap", this::isBurstOverlap, this::setBurstOverlap)
.tab("General").visibleWhen(this::isBurstEnabled)
.hover("ability.hover.burstOverlap"));
}
}
// ── Target tab ───────────────────────────────────────────────
defs.add(FieldDef.row(
FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)).range(0, 100),
@@ -580,6 +692,9 @@ public abstract class Ability implements IAbility {
case POINT:
telegraph = new Telegraph("", TelegraphType.POINT);
break;
case SQUARE:
telegraph = Telegraph.square(getTelegraphRadius());
break;
default:
return null;
}
@@ -599,12 +714,12 @@ public abstract class Ability implements IAbility {
instance.setEntityIdToFollow(caster.getEntityId());
// For LINE/CONE telegraphs: track target direction during windup
// Only track if movement is NOT locked during windup (locked = NPC can't rotate)
// Only track if rotation is NOT locked during windup (NPC can still turn)
if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) {
if (!lockMovement.locksWindup()) {
if (!isRotationLockedDuringWindup()) {
instance.setTargetEntityId(target.getEntityId());
}
// If windup is locked, yaw stays fixed at creation time
// If rotation is locked, yaw stays fixed at creation time
}
} else if (target != null) {
// AOE_TARGET abilities: telegraph follows target during windup
@@ -614,6 +729,17 @@ public abstract class Ability implements IAbility {
return instance;
}
/**
* Create all telegraph instances for this ability.
* Override for abilities that need multiple telegraphs (e.g., zone abilities).
* Default wraps createTelegraph() in a singleton list.
*/
public List<TelegraphInstance> createTelegraphs(EntityLivingBase caster, EntityLivingBase target) {
TelegraphInstance instance = createTelegraph(caster, target);
if (instance == null) return new ArrayList<>();
return new ArrayList<>(Collections.singletonList(instance));
}
/**
* Find the ground level at a given position.
* Searches downward from the given Y to find a solid block.
@@ -682,17 +808,17 @@ public abstract class Ability implements IAbility {
}
/**
* Get the current telegraph instance.
* Get the current telegraph instances.
*/
public TelegraphInstance getTelegraphInstance() {
return telegraphInstance;
public List<TelegraphInstance> getTelegraphInstances() {
return telegraphInstances;
}
/**
* Set the telegraph instance (called by DataAbilities).
* Set the telegraph instances (called by DataAbilities).
*/
public void setTelegraphInstance(TelegraphInstance instance) {
this.telegraphInstance = instance;
public void setTelegraphInstances(List<TelegraphInstance> instances) {
this.telegraphInstances = instances != null ? instances : new ArrayList<>();
}
// ═══════════════════════════════════════════════════════════════════
@@ -707,6 +833,8 @@ public abstract class Ability implements IAbility {
this.currentTick = 0;
this.currentTarget = target;
this.executionStartTime = System.currentTimeMillis();
this.burstIndex = 0;
this.burstEntities.clear();
if (windUpTicks <= 0) {
this.phase = AbilityPhase.ACTIVE;
@@ -749,6 +877,19 @@ public abstract class Ability implements IAbility {
return true;
}
break;
case BURST_DELAY:
// Waiting between burst repetitions - free movement/rotation
if (currentTick >= burstDelay) {
if (burstReplayAnimations && getWindUpTicks() > 0) {
phase = AbilityPhase.WINDUP;
} else {
phase = AbilityPhase.ACTIVE;
}
currentTick = 0;
return true;
}
break;
}
return false;
}
@@ -762,6 +903,19 @@ public abstract class Ability implements IAbility {
*/
public boolean signalCompletion() {
if (phase == AbilityPhase.ACTIVE) {
// Check if more burst iterations remain
if (burstEnabled && burstAmount > 0 && burstIndex < burstAmount) {
if (!burstOverlap) {
cleanup();
}
burstIndex++;
phase = AbilityPhase.BURST_DELAY;
currentTick = 0;
return true;
}
// Final completion - let burst entities die naturally (don't force-kill)
burstEntities.clear();
cleanup();
phase = AbilityPhase.IDLE;
currentTick = 0;
@@ -775,9 +929,10 @@ public abstract class Ability implements IAbility {
* Called when NPC takes direct damage during WINDUP.
*/
public void interrupt() {
cleanupBurstEntities();
cleanup();
if (interruptible && phase == AbilityPhase.WINDUP) {
// Interrupted during windup - go to dazed state
if (interruptible && (phase == AbilityPhase.WINDUP || phase == AbilityPhase.BURST_DELAY)) {
// Interrupted during windup or burst delay - go to dazed state
phase = AbilityPhase.DAZED;
currentTick = 0;
} else {
@@ -785,8 +940,9 @@ public abstract class Ability implements IAbility {
phase = AbilityPhase.IDLE;
currentTick = 0;
}
burstIndex = 0;
currentTarget = null;
telegraphInstance = null;
telegraphInstances.clear();
}
/**
@@ -805,7 +961,7 @@ public abstract class Ability implements IAbility {
* @return true if the ability should be interrupted
*/
public boolean canInterrupt(DamageSource source) {
if (!interruptible || phase != AbilityPhase.WINDUP) {
if (!interruptible || (phase != AbilityPhase.WINDUP && phase != AbilityPhase.BURST_DELAY)) {
return false;
}
@@ -832,11 +988,15 @@ public abstract class Ability implements IAbility {
* Reset all execution state (called on combat end, NPC death, target lost, etc.)
*/
public void reset() {
cleanupBurstEntities();
cleanup();
phase = AbilityPhase.IDLE;
currentTick = 0;
burstIndex = 0;
currentTarget = null;
telegraphInstance = null;
telegraphInstances.clear();
previewMode = false;
previewEntityHandler = null;
}
/**
@@ -848,91 +1008,73 @@ public abstract class Ability implements IAbility {
// Override in subclasses to clean up spawned entities, etc.
}
/**
* Kill all entities tracked during burst overlap and clear the list.
*/
protected void cleanupBurstEntities() {
for (Entity e : burstEntities) {
if (e != null && !e.isDead) {
killAbilityEntity(e);
}
}
burstEntities.clear();
}
// ═══════════════════════════════════════════════════════════════════
// CLIENT-SIDE PREVIEW METHODS
// PREVIEW MODE SUPPORT
// ═══════════════════════════════════════════════════════════════════
/**
* Get total active phase duration for preview (in ticks).
* Override in subclasses to provide accurate preview duration.
*
* @return Duration of active phase in ticks (default: 40 = 2 seconds)
* Whether this ability is running in preview mode (GUI preview).
* When true, abilities should skip damage, effects, sounds, and particles
* but still run their core logic (entity spawning, movement, timing).
*/
public int getPreviewActiveDuration() {
return 40;
public boolean isPreview() {
return previewMode;
}
public void setPreviewMode(boolean preview) {
this.previewMode = preview;
}
public void setPreviewEntityHandler(PreviewEntityHandler handler) {
this.previewEntityHandler = handler;
}
/**
* Called during preview windup tick (client-side only).
* Override for visual updates during windup preview.
* Do NOT apply damage or world effects here.
*
* @param npc The preview NPC
* @param tick Current tick within windup phase
* Spawn an entity during ability execution.
* In preview mode, routes to PreviewEntityHandler instead of world.spawnEntityInWorld().
*/
@SideOnly(Side.CLIENT)
public void onPreviewWindUpTick(EntityNPCInterface npc, int tick) {
// Default: no-op. Override in subclasses for visual effects.
protected void spawnAbilityEntity(World world, Entity entity) {
if (previewMode && previewEntityHandler != null) {
previewEntityHandler.onEntitySpawned(entity);
} else {
world.spawnEntityInWorld(entity);
}
if (burstEnabled && burstOverlap) {
burstEntities.add(entity);
}
}
/**
* Called during preview active tick (client-side only).
* Override for visual updates during active preview.
* Do NOT apply damage or world effects here.
*
* @param npc The preview NPC
* @param tick Current tick within active phase
* Kill an entity spawned by this ability.
* In preview mode, notifies PreviewEntityHandler of removal.
*/
@SideOnly(Side.CLIENT)
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
// Default: no-op. Override in subclasses for visual effects.
protected void killAbilityEntity(Entity entity) {
if (entity == null) return;
entity.setDead();
if (previewMode && previewEntityHandler != null) {
previewEntityHandler.onEntityRemoved(entity);
}
}
/**
* Create a preview entity for GUI display (client-side only).
* The entity should be in preview mode and not apply damage.
* Override in abilities that spawn entities (Beam, Orb, Disc, etc.)
*
* @param npc The preview NPC
* @return Preview entity, or null if ability doesn't spawn entities
* Maximum duration (in ticks) for preview before auto-stopping.
* Override in movement abilities that have variable duration.
* Default: 200 ticks (10 seconds).
*/
@SideOnly(Side.CLIENT)
public Entity createPreviewEntity(EntityNPCInterface npc) {
return null; // Override in entity-spawning abilities
}
/**
* Whether to spawn preview entity during WINDUP phase (true) or ACTIVE phase (false).
* Most abilities (Orb, Beam, Disc) spawn during windup for charging effect.
* Laser spawns at active phase since it has no charging state.
*/
public boolean spawnPreviewDuringWindup() {
return true; // Default: spawn during windup for charging
}
// ── Preview target (set by executor for abilities that need targeting) ──
protected transient EntityLivingBase previewTarget;
/**
* Set the fake target entity for preview mode.
* Called by AbilityPreviewExecutor before starting preview.
*/
public void setPreviewTarget(EntityLivingBase target) {
this.previewTarget = target;
}
public EntityLivingBase getPreviewTarget() {
return previewTarget;
}
/**
* Called once when transitioning from WINDUP to ACTIVE in preview mode.
* Override in movement-based abilities to initiate movement.
*/
@SideOnly(Side.CLIENT)
public void onPreviewExecute(EntityNPCInterface npc) {
// Default: no-op. Override in movement abilities.
public int getMaxPreviewDuration() {
return 200;
}
// ═══════════════════════════════════════════════════════════════════
@@ -978,7 +1120,8 @@ public abstract class Ability implements IAbility {
nbt.setInteger("recovery", dazedTicks);
nbt.setBoolean("interruptible", interruptible);
nbt.setInteger("lockMovement", lockMovement.ordinal());
nbt.setInteger("lockType", lockType.ordinal());
nbt.setInteger("rotationMode", rotationMode.ordinal());
nbt.setInteger("rotationPhase", rotationPhase.ordinal());
nbt.setInteger("windUpColor", windUpColor);
nbt.setInteger("activeColor", activeColor);
nbt.setString("windUpSound", windUpSound);
@@ -994,6 +1137,13 @@ public abstract class Ability implements IAbility {
nbt.setInteger("allowedBy", allowedBy.ordinal());
nbt.setBoolean("ignoreCooldown", ignoreCooldown);
// Burst
nbt.setBoolean("burstEnabled", burstEnabled);
nbt.setInteger("burstAmount", burstAmount);
nbt.setInteger("burstDelay", burstDelay);
nbt.setBoolean("burstReplayAnimations", burstReplayAnimations);
nbt.setBoolean("burstOverlap", burstOverlap);
// Conditions
NBTTagList condList = new NBTTagList();
for (Condition c : conditions) condList.appendTag(c.writeNBT());
@@ -1029,7 +1179,30 @@ public abstract class Ability implements IAbility {
dazedTicks = nbt.getInteger("recovery");
interruptible = nbt.getBoolean("interruptible");
lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement"));
lockType = nbt.hasKey("lockType") ? LockType.fromOrdinal(nbt.getInteger("lockType")) : LockType.BOTH;
if (nbt.hasKey("rotationMode")) {
// Current format
rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode"));
rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase"));
} else if (nbt.hasKey("lockType")) {
// Legacy format migration: lockType was LockType enum (0=MOVEMENT, 1=ROTATION, 2=BOTH)
int oldLockType = nbt.getInteger("lockType");
if (oldLockType == 0) {
// MOVEMENT only: movement locked, rotation was free
rotationMode = RotationMode.FREE;
} else if (oldLockType == 1) {
// ROTATION only: rotation locked same timing as old lockMovement, movement was free
rotationMode = RotationMode.LOCKED;
rotationPhase = lockMovement;
lockMovement = LockMovementType.NO;
} else {
// BOTH: both locked same timing
rotationMode = RotationMode.LOCKED;
rotationPhase = lockMovement;
}
} else {
rotationMode = RotationMode.FREE;
rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
}
windUpColor = nbt.getInteger("windUpColor");
activeColor = nbt.getInteger("activeColor");
windUpSound = nbt.getString("windUpSound");
@@ -1051,6 +1224,13 @@ public abstract class Ability implements IAbility {
allowedBy = nbt.hasKey("allowedBy") ? UserType.fromOrdinal(nbt.getInteger("allowedBy")) : UserType.BOTH;
ignoreCooldown = nbt.hasKey("ignoreCooldown") && nbt.getBoolean("ignoreCooldown");
// Burst
burstEnabled = nbt.hasKey("burstEnabled") && nbt.getBoolean("burstEnabled");
burstAmount = nbt.hasKey("burstAmount") ? nbt.getInteger("burstAmount") : 0;
burstDelay = nbt.hasKey("burstDelay") ? nbt.getInteger("burstDelay") : 0;
burstReplayAnimations = !nbt.hasKey("burstReplayAnimations") || nbt.getBoolean("burstReplayAnimations");
burstOverlap = nbt.hasKey("burstOverlap") && nbt.getBoolean("burstOverlap");
// Conditions
conditions.clear();
NBTTagList condList = nbt.getTagList("conditions", 10);
@@ -1098,6 +1278,10 @@ public abstract class Ability implements IAbility {
return typeId;
}
public boolean isBuiltIn() {
return false;
}
public int getWeight() {
return weight;
}
@@ -1194,12 +1378,20 @@ public abstract class Ability implements IAbility {
this.lockMovement = lockMovement;
}
public LockType getLockType() {
return lockType;
public RotationMode getRotationMode() {
return rotationMode;
}
public void setLockType(LockType lockType) {
this.lockType = lockType;
public void setRotationMode(RotationMode rotationMode) {
this.rotationMode = rotationMode;
}
public LockMovementType getRotationPhase() {
return rotationPhase;
}
public void setRotationPhase(LockMovementType rotationPhase) {
this.rotationPhase = rotationPhase;
}
/**
@@ -1221,21 +1413,39 @@ public abstract class Ability implements IAbility {
}
/**
* API method: Get lock type as integer.
* @return 0=MOVEMENT, 1=ROTATION, 2=BOTH
* API method: Get rotation mode as integer.
* @return 0=FREE, 1=LOCKED, 2=TRACK
*/
@Override
public int getLockTypeOrdinal() {
return lockType.ordinal();
public int getRotationModeType() {
return rotationMode.ordinal();
}
/**
* API method: Set lock type from integer.
* @param type 0=MOVEMENT, 1=ROTATION, 2=BOTH
* API method: Set rotation mode from integer.
* @param type 0=FREE, 1=LOCKED, 2=TRACK
*/
@Override
public void setLockTypeOrdinal(int type) {
this.lockType = LockType.fromOrdinal(type);
public void setRotationModeType(int type) {
this.rotationMode = RotationMode.fromOrdinal(type);
}
/**
* API method: Get rotation phase as integer.
* @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
*/
@Override
public int getRotationPhaseType() {
return rotationPhase.ordinal();
}
/**
* API method: Set rotation phase from integer.
* @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
*/
@Override
public void setRotationPhaseType(int type) {
this.rotationPhase = LockMovementType.fromOrdinal(type);
}
/**
@@ -1243,7 +1453,7 @@ public abstract class Ability implements IAbility {
*/
@Override
public boolean isMovementLockedDuringWindup() {
return lockMovement.locksWindup() && lockType.locksMovement();
return lockMovement.locksWindup();
}
/**
@@ -1251,7 +1461,7 @@ public abstract class Ability implements IAbility {
*/
@Override
public boolean isMovementLockedDuringActive() {
return lockMovement.locksActive() && lockType.locksMovement();
return lockMovement.locksActive();
}
/**
@@ -1259,7 +1469,7 @@ public abstract class Ability implements IAbility {
*/
@Override
public boolean isRotationLockedDuringWindup() {
return lockMovement.locksWindup() && lockType.locksRotation();
return rotationMode == RotationMode.LOCKED && rotationPhase.locksWindup();
}
/**
@@ -1267,7 +1477,7 @@ public abstract class Ability implements IAbility {
*/
@Override
public boolean isRotationLockedDuringActive() {
return lockMovement.locksActive() && lockType.locksRotation();
return rotationMode == RotationMode.LOCKED && rotationPhase.locksActive();
}
/**
@@ -1279,6 +1489,8 @@ public abstract class Ability implements IAbility {
return isMovementLockedDuringWindup();
case ACTIVE:
return isMovementLockedDuringActive();
case BURST_DELAY:
return false; // Free movement during burst delay
default:
return false;
}
@@ -1293,6 +1505,25 @@ public abstract class Ability implements IAbility {
return isRotationLockedDuringWindup();
case ACTIVE:
return isRotationLockedDuringActive();
case BURST_DELAY:
return false; // Free rotation during burst delay
default:
return false;
}
}
/**
* Check if hit scan (force face target) is active during the current phase.
*/
public boolean isHitScanForCurrentPhase() {
if (rotationMode != RotationMode.TRACK) return false;
switch (phase) {
case WINDUP:
return rotationPhase.locksWindup();
case ACTIVE:
return rotationPhase.locksActive();
case BURST_DELAY:
return false; // Free rotation during burst delay
default:
return false;
}
@@ -1481,6 +1712,63 @@ public abstract class Ability implements IAbility {
effects.clear();
}
// Burst getters/setters
public boolean isBurstEnabled() {
return burstEnabled;
}
public void setBurstEnabled(boolean burstEnabled) {
this.burstEnabled = burstEnabled;
}
public int getBurstAmount() {
return burstAmount;
}
public void setBurstAmount(int burstAmount) {
this.burstAmount = Math.max(0, burstAmount);
}
public int getBurstDelay() {
return burstDelay;
}
public void setBurstDelay(int burstDelay) {
this.burstDelay = Math.max(0, burstDelay);
}
public boolean isBurstReplayAnimations() {
return burstReplayAnimations;
}
public void setBurstReplayAnimations(boolean burstReplayAnimations) {
this.burstReplayAnimations = burstReplayAnimations;
}
public boolean isBurstOverlap() {
return burstOverlap;
}
public void setBurstOverlap(boolean burstOverlap) {
this.burstOverlap = burstOverlap;
}
public int getBurstIndex() {
return burstIndex;
}
/**
* Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that).
*/
private static String[] getRotationPhaseKeys() {
return new String[]{
"ability.lockMove.windup",
"ability.lockMove.active",
"ability.lockMove.both"
};
}
// ═══════════════════════════════════════════════════════════════════
// SHIT FROM THE ASS
// ═══════════════════════════════════════════════════════════════════
@@ -45,6 +45,12 @@ public class AbilityController implements IAbilityHandler {
/** External field providers for ability GUI (e.g., DBC Addon). Client-side only. */
private final List<IAbilityFieldProvider> fieldProviders = new ArrayList<>();
/** External variant definitions registered by addons, keyed by typeId. */
private final Map<String, List<AbilityVariant>> externalVariants = new LinkedHashMap<>();
/** TypeIds that produce BuiltInAbility instances (non-editable in GUI). */
private final Set<String> builtInTypeIds = new HashSet<>();
public AbilityController() {
registerBuiltinTypes();
}
@@ -170,7 +176,9 @@ public class AbilityController implements IAbilityHandler {
if (abilities.containsKey(name)) {
LogWriter.info("AbilityController: Overwriting built-in ability: " + name);
}
ability.setName(name);
if (!ability.isBuiltIn()) {
ability.setName(name);
}
abilities.put(name, ability);
LogWriter.info("Registered ability: " + name);
}
@@ -180,30 +188,93 @@ public class AbilityController implements IAbilityHandler {
// ═══════════════════════════════════════════════════════════════════════════
/**
* Resolve an ability by key. Checks built-in abilities first (by name),
* then custom abilities (by UUID). Returns a copy safe to modify.
* Resolve an ability by key. Checks built-in abilities first (by name or registry key),
* then custom abilities (by UUID or name). Returns a copy safe to modify for
* custom abilities, or the singleton instance for built-in abilities.
*
* @param key The ability name (built-in) or UUID (custom)
* @return A copy of the resolved ability, or null if not found
* @param key The ability name, registry key (built-in) or UUID (custom)
* @return The resolved ability, or null if not found
*/
public Ability resolveAbility(String key) {
if (key == null || key.isEmpty()) return null;
// Check built-in abilities first
// Check built-in abilities first (exact match by name)
Ability builtIn = abilities.get(key);
if (builtIn != null) {
return fromNBT(builtIn.writeNBT());
return builtIn;
}
// Check custom abilities
// Check built-in abilities by name (case-insensitive)
for (Map.Entry<String, Ability> entry : abilities.entrySet()) {
if (entry.getKey().equalsIgnoreCase(key)) {
return entry.getValue();
}
}
// Check built-in abilities by registry key / ID
for (Ability ability : abilities.values()) {
if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) {
return ability;
}
}
// Check custom abilities (exact UUID match)
Ability custom = customAbilities.get(key);
if (custom != null) {
return fromNBT(custom.writeNBT());
}
// Check custom abilities by name (case-insensitive)
for (Ability ability : customAbilities.values()) {
if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) {
return fromNBT(ability.writeNBT());
}
}
return null;
}
/**
* Get all keys that can be used to look up abilities (for tab completion).
* Returns registry keys for built-in abilities (no spaces) and UUIDs for custom.
*/
public Set<String> getAbilityKeys() {
Set<String> keys = new LinkedHashSet<>();
// Built-in abilities - use the ID (registry key for BuiltInAbility)
for (Ability ability : abilities.values()) {
String id = ability.getId();
if (id != null && !id.isEmpty()) {
keys.add(id);
}
}
// Custom abilities - use UUID
keys.addAll(customAbilities.keySet());
return keys;
}
/**
* Get keys for player-usable abilities only (for tab completion).
*/
public Set<String> getPlayerAbilityKeys() {
Set<String> keys = new LinkedHashSet<>();
// Built-in abilities - use the ID (registry key for BuiltInAbility)
for (Ability ability : abilities.values()) {
if (ability.getAllowedBy().allowsPlayer()) {
String id = ability.getId();
if (id != null && !id.isEmpty()) {
keys.add(id);
}
}
}
// Custom abilities - use UUID (if player-usable)
for (Map.Entry<String, Ability> entry : customAbilities.entrySet()) {
if (entry.getValue().getAllowedBy().allowsPlayer()) {
keys.add(entry.getKey());
}
}
return keys;
}
/**
* Check if an ability key exists (either built-in or custom).
*/
@@ -215,12 +286,22 @@ public class AbilityController implements IAbilityHandler {
// ACCESSORS
// ═══════════════════════════════════════════════════════════════════════════
/** Get a built-in ability by name. Returns the stored instance (not a copy). */
/** Get a built-in ability by map key. Returns the stored instance (not a copy). */
public Ability getAbility(String name) {
return abilities.get(name);
}
/** Get all built-in ability names. */
/** Get a built-in ability by its display name (ability.getName()). */
public Ability getAbilityByDisplayName(String displayName) {
for (Ability ability : abilities.values()) {
if (ability.getName().equals(displayName)) {
return ability;
}
}
return null;
}
/** Get all built-in ability map keys. */
public Set<String> getAbilityNames() {
return new LinkedHashSet<>(abilities.keySet());
}
@@ -272,6 +353,10 @@ public class AbilityController implements IAbilityHandler {
return typeFactories.keySet().toArray(new String[0]);
}
public boolean isBuiltInType(String typeId) {
return builtInTypeIds.contains(typeId);
}
@Override
public boolean hasType(String typeId) {
return typeFactories.containsKey(typeId);
@@ -320,6 +405,9 @@ public class AbilityController implements IAbilityHandler {
LogWriter.info("AbilityController: Overwriting type: " + typeId);
}
typeFactories.put(typeId, factory);
if (temp.isBuiltIn()) {
builtInTypeIds.add(typeId);
}
}
/**
@@ -369,13 +457,9 @@ public class AbilityController implements IAbilityHandler {
registerType("cnpc:sweeper", AbilitySweeper::new);
registerType("cnpc:projectile", AbilityProjectile::new);
registerType("cnpc:orb", AbilityOrb::new);
registerType("cnpc:dual_orb", AbilityOrbDual::new);
registerType("cnpc:orb_barrage", AbilityOrbBarrage::new);
registerType("cnpc:dual_disc", AbilityDisc::new);
registerType("cnpc:dual_disc", AbilityDiscDual::new);
registerType("cnpc:disc", AbilityDisc::new);
registerType("cnpc:laser_shot", AbilityLaserShot::new);
registerType("cnpc:beam", AbilityBeam::new);
registerType("cnpc:dual_beam", AbilityBeamDual::new);
// Movement abilities
registerType("cnpc:charge", AbilityCharge::new);
@@ -427,4 +511,41 @@ public class AbilityController implements IAbilityHandler {
public List<IAbilityFieldProvider> getFieldProviders() {
return fieldProviders;
}
// ═══════════════════════════════════════════════════════════════════════════
// EXTERNAL VARIANT REGISTRY
// ═══════════════════════════════════════════════════════════════════════════
/**
* Register an external variant for an ability type.
* External mods can call this to add variant presets that appear
* alongside built-in variants in the variant selection SubGui.
*
* @param typeId The ability typeId (e.g., "ability.cnpc.beam")
* @param variant The variant to register
*/
public void registerVariant(String typeId, AbilityVariant variant) {
externalVariants.computeIfAbsent(typeId, k -> new ArrayList<>()).add(variant);
}
/**
* Get all variants for a given ability type (built-in + external).
* Returns the combined list of variants from the ability's getVariants()
* and any externally registered variants.
*
* @param typeId The ability typeId
* @return Combined list of variants, empty if none
*/
public List<AbilityVariant> getVariantsForType(String typeId) {
List<AbilityVariant> result = new ArrayList<>();
Ability temp = create(typeId);
if (temp != null) {
result.addAll(temp.getVariants());
}
List<AbilityVariant> ext = externalVariants.get(typeId);
if (ext != null) {
result.addAll(ext);
}
return result;
}
}
@@ -20,7 +20,8 @@ public class AbilityEffect {
BLINDNESS("potion.blindness", Potion.blindness.id),
NAUSEA("potion.confusion", Potion.confusion.id),
HUNGER("potion.hunger", Potion.hunger.id),
MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id);
MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id),
BURN("ability.effect.burn", -1);
private final String langKey;
private final int potionId;
@@ -83,6 +84,11 @@ public class AbilityEffect {
return;
}
if (type == EffectType.BURN) {
entity.setFire(Math.max(1, durationTicks / 20));
return;
}
int potionId = type.getPotionId();
if (potionId >= 0) {
entity.addPotionEffect(new PotionEffect(potionId, durationTicks, amplifier));
@@ -4,5 +4,6 @@ public enum AbilityPhase {
IDLE, // Not executing
WINDUP, // Charging up (telegraph visible)
ACTIVE, // Executing (damage happens)
DAZED // Stunned after interrupt during WINDUP (cannot attack)
DAZED, // Stunned after interrupt during WINDUP (cannot attack)
BURST_DELAY // Waiting between burst repetitions (free movement/rotation)
}
@@ -0,0 +1,54 @@
package kamkeel.npcs.controllers.data.ability;
import java.util.function.Consumer;
/**
* Represents a variant/template for an ability type.
* When a user creates an ability that has registered variants,
* they are presented with a selection dialog before the ability is created.
*
* Each variant has a display name (lang key) and a configurator that
* applies preset values to the newly created ability.
*/
public class AbilityVariant {
private final String displayKey;
private final String group;
private final Consumer<Ability> configurator;
/**
* @param displayKey Lang key for the variant display name (e.g., "ability.variant.dual")
* @param configurator Lambda that configures the ability with preset values
*/
public AbilityVariant(String displayKey, Consumer<Ability> configurator) {
this(displayKey, null, configurator);
}
/**
* @param displayKey Lang key for the variant display name
* @param group Optional group header shown in the variant selection GUI (e.g., "DBC Addon")
* @param configurator Lambda that configures the ability with preset values
*/
public AbilityVariant(String displayKey, String group, Consumer<Ability> configurator) {
this.displayKey = displayKey;
this.group = group;
this.configurator = configurator;
}
public String getDisplayKey() {
return displayKey;
}
public String getGroup() {
return group;
}
/**
* Apply this variant's preset configuration to the given ability.
*/
public void apply(Ability ability) {
if (configurator != null) {
configurator.accept(ability);
}
}
}
@@ -10,7 +10,8 @@ public enum AnchorPoint {
RIGHT_HAND("Right Hand"), // Right arm/hand position
LEFT_HAND("Left Hand"), // Left arm/hand position
ABOVE_HEAD("Above Head"), // Above the entity
CHEST("Chest"); // Chest level, centered
CHEST("Chest"), // Chest level, centered
EYE("Eye"); // Eye level, centered
private final String displayName;
@@ -55,6 +56,7 @@ public enum AnchorPoint {
case LEFT_HAND: return "ability.anchor.leftHand";
case ABOVE_HEAD: return "ability.anchor.aboveHead";
case CHEST: return "ability.anchor.chest";
case EYE: return "ability.anchor.eye";
default: return name();
}
}
@@ -21,7 +21,13 @@ public abstract class BuiltInAbility extends Ability {
this.registryKey = registryKey;
this.id = registryKey;
this.name = registryKey;
this.typeId = registryKey;
// Convert "namespace:name" to "ability.namespace.name" for GUI filter compatibility
this.typeId = "ability." + registryKey.replace(':', '.');
}
@Override
public boolean isBuiltIn() {
return true;
}
/**
@@ -0,0 +1,13 @@
package kamkeel.npcs.controllers.data.ability;
import net.minecraft.entity.Entity;
/**
* Interface for routing entity spawn/kill calls during preview mode.
* In preview mode, entities are not added to the world but instead
* tracked by the GUI for rendering in the 3D viewport.
*/
public interface PreviewEntityHandler {
void onEntitySpawned(Entity entity);
void onEntityRemoved(Entity entity);
}
@@ -0,0 +1,53 @@
package kamkeel.npcs.controllers.data.ability;
/**
* Defines what happens to NPC rotation during ability phases.
* Used with a LockMovementType to control WHEN it applies.
*/
public enum RotationMode {
/**
* NPC rotates normally via AI look helper.
*/
FREE,
/**
* Freeze rotation at the moment the phase starts.
* NPC stays facing the direction it was looking when the phase began.
*/
LOCKED,
/**
* Force NPC to continuously face its target (hit scan).
* Overrides AI look helper every tick for precise aiming.
*/
TRACK;
public String getDisplayKey() {
switch (this) {
case FREE:
return "ability.rotation.free";
case LOCKED:
return "ability.rotation.locked";
case TRACK:
return "ability.rotation.track";
default:
return "ability.rotation.free";
}
}
public static String[] getDisplayKeys() {
return new String[]{
"ability.rotation.free",
"ability.rotation.locked",
"ability.rotation.track"
};
}
public static RotationMode fromOrdinal(int ordinal) {
RotationMode[] values = values();
if (ordinal >= 0 && ordinal < values.length) {
return values[ordinal];
}
return FREE;
}
}
@@ -0,0 +1,60 @@
package kamkeel.npcs.controllers.data.ability.data;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import net.minecraft.nbt.NBTTagCompound;
/**
* Per-projectile data: anchor point + optional color override.
* All projectiles share the primary EnergyDisplayData; this class
* allows individual projectiles to override just the inner/outer colors.
*/
public class ProjectileData {
public EnergyAnchorData anchor;
public boolean colorOverride = false;
public int innerColor = 0xFFFFFF;
public int outerColor = 0x8888FF;
public ProjectileData(AnchorPoint defaultAnchor) {
this.anchor = new EnergyAnchorData(defaultAnchor);
}
/**
* Returns the display data to use for this projectile.
* If colorOverride is false, returns the primary display data directly.
* If colorOverride is true, returns a copy with overridden inner/outer colors.
*/
public EnergyDisplayData resolveDisplay(EnergyDisplayData primary) {
if (!colorOverride) return primary;
EnergyDisplayData resolved = primary.copy();
resolved.innerColor = this.innerColor;
resolved.outerColor = this.outerColor;
return resolved;
}
public void writeNBT(NBTTagCompound nbt) {
anchor.writeNBT(nbt);
nbt.setBoolean("colorOverride", colorOverride);
if (colorOverride) {
nbt.setInteger("overrideInnerColor", innerColor);
nbt.setInteger("overrideOuterColor", outerColor);
}
}
public void readNBT(NBTTagCompound nbt) {
anchor.readNBT(nbt);
colorOverride = nbt.getBoolean("colorOverride");
if (colorOverride) {
innerColor = nbt.hasKey("overrideInnerColor") ? nbt.getInteger("overrideInnerColor") : 0xFFFFFF;
outerColor = nbt.hasKey("overrideOuterColor") ? nbt.getInteger("overrideOuterColor") : 0x8888FF;
}
}
public ProjectileData copy() {
ProjectileData copy = new ProjectileData(anchor.anchorPoint);
copy.anchor = anchor.copy();
copy.colorOverride = colorOverride;
copy.innerColor = innerColor;
copy.outerColor = outerColor;
return copy;
}
}
@@ -23,4 +23,5 @@ public class AbilityFieldDefs {
() -> getter.get(),
v -> setter.accept((List<AbilityEffect>) v));
}
}
@@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.RotationMode;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
@@ -12,7 +13,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
@@ -23,30 +23,39 @@ import noppes.npcs.api.ability.type.IAbilityEnergyBeam;
import java.util.Arrays;
import java.util.List;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
/**
* Energy Beam ability: A homing head with a trailing path.
* The beam stays attached to the origin and curves as it homes in on target.
* Cannot cross over itself (self-intersection prevention).
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
*/
public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
// Shape properties (standalone)
private static final int MAX_PROJECTILES = 8;
// Beam shape properties (shared)
private float beamWidth = 0.4f;
private float headSize = 0.6f;
// Data classes
private final EnergyDisplayData colorData = new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f);
// Projectile count and fire stagger
private int projectileCount = 1;
private int fireDelay = 0;
// Shared visual data (all projectiles use these)
private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f);
private EnergyLightningData lightningData = new EnergyLightningData();
private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Shared combat/movement data
private final EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f);
private final EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f);
private final EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200);
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Transient state for beam entity (used during windup charging)
private transient EntityAbilityBeam beamEntity = null;
// Per-projectile data (anchor + optional color override)
private ProjectileData[] projectiles;
// Transient entity state
private transient EntityAbilityBeam[] beamEntities;
public AbilityBeam() {
this.typeId = "ability.cnpc.beam";
@@ -57,11 +66,93 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
this.cooldownTicks = 0;
this.windUpTicks = 40;
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
this.rotationMode = RotationMode.TRACK;
this.rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animations
this.windUpAnimationName = "Ability_Beam_Windup";
this.activeAnimationName = "Ability_Beam_Active";
initProjectiles(1);
}
private void initProjectiles(int count) {
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
this.projectiles = new ProjectileData[projectileCount];
for (int i = 0; i < projectileCount; i++) {
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
}
}
private AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0: return AnchorPoint.RIGHT_HAND;
case 1: return AnchorPoint.LEFT_HAND;
default: return AnchorPoint.FRONT;
}
}
public void setProjectileCount(int count) {
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
if (newCount == projectileCount) return;
ProjectileData[] oldProjectiles = projectiles;
initProjectiles(newCount);
if (oldProjectiles != null) {
int copyCount = Math.min(oldProjectiles.length, newCount);
for (int i = 0; i < copyCount; i++) {
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
}
}
}
public int getProjectileCount() {
return projectileCount;
}
public int getFireDelay() {
return fireDelay;
}
public void setFireDelay(int delay) {
this.fireDelay = Math.max(0, delay);
}
/** @deprecated Use {@link #getFireDelay()} */
public int getDualFireDelay() { return fireDelay; }
/** @deprecated Use {@link #setFireDelay(int)} */
public void setDualFireDelay(int delay) { setFireDelay(delay); }
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Beam");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilityBeam beam = (AbilityBeam) a;
a.setName("Dual Beam");
beam.setProjectileCount(2);
beam.setFireDelay(0);
// Set color override for second beam
beam.projectiles[1].colorOverride = true;
beam.projectiles[1].innerColor = 0xFFFFFF;
beam.projectiles[1].outerColor = 0xFF0000;
a.setWindUpTicks(80);
a.setWindUpAnimationName("Ability_BeamDual_Windup");
a.setActiveAnimationName("Ability_BeamDual_Active");
})
);
}
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
@@ -74,51 +165,118 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
signalCompletion();
return;
/**
* Create beam entities.
* NPC: target is the aggro target — beams will home toward it.
* Player: target is null — beams fire in caster's look direction.
*/
private EntityAbilityBeam[] createBeamEntities(EntityLivingBase caster, EntityLivingBase target, World world) {
EntityAbilityBeam[] entities = new EntityAbilityBeam[projectileCount];
float offsetDist = 1.0f;
for (int i = 0; i < projectileCount; i++) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor, offsetDist);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
entities[i] = new EntityAbilityBeam(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
beamWidth, headSize,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
lockMovement.locksActive());
entities[i].setEffects(this.effects);
entities[i].setSourceAbility(this);
}
// Start firing the beam that was spawned during windup
if (beamEntity != null && !beamEntity.isDead) {
beamEntity.startFiring(target);
if (projectileCount == 2 && entities[0] != null && entities[1] != null) {
entities[0].setSiblingEntityId(entities[1].getEntityId());
entities[1].setSiblingEntityId(entities[0].getEntityId());
}
return entities;
}
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
private void fireBeamEntity(EntityAbilityBeam beam, EntityLivingBase target) {
if (beam == null || beam.isDead) return;
if (isPreview()) {
beam.startPreviewFiring();
} else {
beam.startFiring(target);
}
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
if (world.isRemote && !isPreview()) return;
// Spawn beam in charging mode on first tick of windup
if (tick == 1) {
beamEntities = createBeamEntities(caster, target, world);
float offsetDist = 1.0f;
for (int i = 0; i < projectileCount; i++) {
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
if (isPreview()) {
beamEntities[i].setupPreview(caster, beamWidth, headSize, resolved, lightningData, projectiles[i].anchor, windUpTicks, offsetDist);
} else {
beamEntities[i].setupCharging(projectiles[i].anchor, windUpTicks, offsetDist);
}
spawnAbilityEntity(world, beamEntities[i]);
}
}
}
// Create beam in charging mode - follows caster based on anchor point during windup
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist);
beamEntity = new EntityAbilityBeam(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
beamWidth, headSize,
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData,
lockMovement.locksActive());
beamEntity.setupCharging(anchorData, windUpTicks, offsetDist);
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
beamEntities = createBeamEntities(caster, target, world);
for (EntityAbilityBeam beam : beamEntities) {
spawnAbilityEntity(world, beam);
}
beamEntity.setEffects(this.effects);
beamEntity.setSourceAbility(this);
world.spawnEntityInWorld(beamEntity);
// Fire first projectile immediately, let onActiveTick handle the rest with fireDelay
fireBeamEntity(beamEntities[0], target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireBeamEntity(beamEntities[i], target);
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (beamEntities == null) return;
fireBeamEntity(beamEntities[0], target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireBeamEntity(beamEntities[i], target);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (beamEntity == null || beamEntity.isDead) {
beamEntity = null;
if (beamEntities == null) {
signalCompletion();
return;
}
if (fireDelay > 0) {
for (int i = 1; i < projectileCount; i++) {
if (tick == fireDelay * i) {
fireBeamEntity(beamEntities[i], target);
}
}
}
boolean allDead = true;
for (EntityAbilityBeam beam : beamEntities) {
if (beam != null && !beam.isDead) {
allDead = false;
break;
}
}
if (allDead) {
signalCompletion();
}
}
@@ -134,20 +292,25 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
@Override
public void cleanup() {
// Despawn beam entity if still alive
if (beamEntity != null && !beamEntity.isDead) {
beamEntity.setDead();
if (beamEntities != null) {
for (int i = 0; i < beamEntities.length; i++) {
if (beamEntities[i] != null && !beamEntities[i].isDead) {
beamEntities[i].setDead();
}
beamEntities[i] = null;
}
}
beamEntity = null;
beamEntities = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
// NPC: telegraph on aggro target position
// Player: no telegraph (beam fires in look direction, no fixed position to mark)
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
return null;
}
// Create circle telegraph at target position
Telegraph telegraph = Telegraph.circle(headSize * 2.0f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
@@ -155,7 +318,6 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
@@ -172,176 +334,276 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("beamWidth", beamWidth);
nbt.setFloat("headSize", headSize);
anchorData.writeNBT(nbt);
colorData.writeNBT(nbt);
nbt.setInteger("projectileCount", projectileCount);
nbt.setInteger("fireDelay", fireDelay);
// Shared visual data
displayData.writeNBT(nbt);
lightningData.writeNBT(nbt);
// Shared combat/movement data
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lightningData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
NBTTagCompound projNbt = new NBTTagCompound();
projectiles[i].writeNBT(projNbt);
nbt.setTag("Projectile_" + i, projNbt);
}
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f;
this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f;
anchorData.readNBT(nbt);
colorData.readNBT(nbt);
int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1;
initProjectiles(count);
this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0;
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lightningData.readNBT(nbt);
lifespanData.readNBT(nbt);
// Shared visual data
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
}
// Getters & Setters - Standalone fields
// ═══════════════════════════════════════════════════════════════════
// API GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public float getBeamWidth() { return beamWidth; }
public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
public float getHeadSize() { return headSize; }
public void setHeadSize(float headSize) { this.headSize = headSize; }
// Getters & Setters - Color data
public int getInnerColor() { return colorData.innerColor; }
public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; }
public int getOuterColor() { return colorData.outerColor; }
public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; }
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; }
public float getOuterColorWidth() { return colorData.outerColorWidth; }
public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; }
public float getOuterColorAlpha() { return colorData.outerColorAlpha; }
public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; }
public float getRotationSpeed() { return colorData.rotationSpeed; }
public void setRotationSpeed(float rotationSpeed) { this.colorData.rotationSpeed = rotationSpeed; }
// Getters & Setters - Combat data
public float getDamage() { return combatData.damage; }
public void setDamage(float damage) { this.combatData.damage = damage; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float knockback) { this.combatData.knockback = knockback; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; }
// Getters & Setters - Homing data
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { this.homingData.speed = speed; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean homing) { this.homingData.homing = homing; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; }
// Getters & Setters - Lightning data
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; }
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; }
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; }
// Getters & Setters - Lifespan data
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; }
// Getters & Setters - Anchor point
public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; }
public float getAnchorOffsetX() { return anchorData.anchorOffsetX; }
public float getAnchorOffsetY() { return anchorData.anchorOffsetY; }
public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; }
public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; }
public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; }
public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; }
public float getDamage() { return combatData.damage; }
public void setDamage(float damage) { this.combatData.damage = damage; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float knockback) { this.combatData.knockback = knockback; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean homing) { this.homingData.homing = homing; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; }
// Shared display data (primary colors)
public int getInnerColor() { return displayData.innerColor; }
public void setInnerColor(int innerColor) { displayData.innerColor = innerColor; }
public int getOuterColor() { return displayData.outerColor; }
public void setOuterColor(int outerColor) { displayData.outerColor = outerColor; }
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { displayData.outerColorEnabled = outerColorEnabled; }
public float getOuterColorWidth() { return displayData.outerColorWidth; }
public void setOuterColorWidth(float outerColorWidth) { displayData.outerColorWidth = outerColorWidth; }
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
public void setOuterColorAlpha(float outerColorAlpha) { displayData.outerColorAlpha = outerColorAlpha; }
public float getRotationSpeed() { return displayData.rotationSpeed; }
public void setRotationSpeed(float rotationSpeed) { displayData.rotationSpeed = rotationSpeed; }
// Shared lightning data
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; }
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; }
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; }
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
public void setLightningFadeTime(int lightningFadeTime) { lightningData.lightningFadeTime = lightningFadeTime; }
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { projectiles[0].anchor.anchorPoint = anchorPoint; }
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
@Override
public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); }
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
@Override
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
@Override
@SideOnly(Side.CLIENT)
public Entity createPreviewEntity(EntityNPCInterface npc) {
if (npc == null || npc.worldObj == null) return null;
// Indexed API methods
public int getInnerColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.innerColor : displayData.innerColor;
}
public void setInnerColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.innerColor = color;
}
EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj);
beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, 1.0f);
return beam;
public int getOuterColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.outerColor : displayData.outerColor;
}
public void setOuterColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.outerColor = color;
}
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
private int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
}
@Override
public int getPreviewActiveDuration() {
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
// ═══════════════════════════════════════════════════════════════════
// GUI FIELD DEFINITIONS
// ═══════════════════════════════════════════════════════════════════
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.section("ability.section.beam"),
FieldDef.row(
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth),
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
.hover("ability.hover.homing"),
FieldDef.row(
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
.visibleWhen(this::isHoming),
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange)
.visibleWhen(this::isHoming)
),
FieldDef.section("ability.section.explosive"),
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive)
.hover("ability.hover.explosive"),
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
.visibleWhen(this::isExplosive),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
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),
FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.effects").tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.range(0.01f, 100f).visibleWhen(this::hasLightningEffect),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual")
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
));
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
defs.add(FieldDef.section("ability.section.beam"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth),
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize)
));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
));
defs.add(FieldDef.section("ability.section.homing"));
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange).visibleWhen(this::isHoming)
));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
// Visual tab - shared primary colors
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
// Visual tab - shared effects
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
// Visual tab - per-projectile sections
for (int i = 0; i < MAX_PROJECTILES; i++) {
final int idx = i;
String sectionKey = "ability.section.projectile" + (i + 1);
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
.visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v),
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v)
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
// Color override (only when multiple projectiles)
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
}
}
}
@@ -1,517 +0,0 @@
package kamkeel.npcs.controllers.data.ability.type;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import noppes.npcs.client.gui.builder.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.util.ValueUtil;
import java.util.Arrays;
import java.util.List;
public class AbilityBeamDual extends Ability {
private float beamWidth = 0.4f;
private float headSize = 0.6f;
private boolean dualFire= true;
private int dualFireDelay = 0;
// Data classes
private final EnergyDisplayData[] displayData = new EnergyDisplayData[]{
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f),
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 6.0f),
};
private final EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f);
private final EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f);
private final EnergyLightningData[] lightningData = new EnergyLightningData[]{
new EnergyLightningData(), new EnergyLightningData()
};
private final EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200);
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND)
};
private final EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{
new EnergyTrajectoryData(), new EnergyTrajectoryData()
};
// Transient state for beam entity (used during windup charging)
private transient EntityAbilityBeam beamEntity = null;
private transient EntityAbilityBeam beamEntity2 = null;
public AbilityBeamDual() {
this.typeId = "ability.cnpc.beam_dual";
this.name = "Beam Dual";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 80;
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animations
this.windUpAnimationName = "Ability_BeamDual_Windup";
this.activeAnimationName = "Ability_BeamDual_Active";
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
signalCompletion();
return;
}
// Start firing the beam that was spawned during windup
if (beamEntity != null && !beamEntity.isDead) {
beamEntity.startFiring(target);
}
if (dualFire) {
if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) {
beamEntity2.startFiring(target);
}
} else if (beamEntity2 != null) {
beamEntity2.setDead();
}
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
// Spawn beam in charging mode on first tick of windup
if (tick == 1) {
float offsetDist = 1.0f;
// Create beam in charging mode - follows caster based on anchor point during windup
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist);
beamEntity = new EntityAbilityBeam(
world, caster, target,
spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord,
beamWidth, headSize,
displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0],
lockMovement.locksActive());
Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1], offsetDist);
beamEntity2 = new EntityAbilityBeam(
world, caster, target,
spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord,
beamWidth, headSize,
displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1],
lockMovement.locksActive());
beamEntity.setSiblingEntityId(beamEntity2.getEntityId());
beamEntity2.setSiblingEntityId(beamEntity.getEntityId());
beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist);
beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist);
beamEntity.setEffects(this.effects);
if (dualFire) beamEntity2.setEffects(this.effects);
beamEntity.setSourceAbility(this);
beamEntity2.setSourceAbility(this);
world.spawnEntityInWorld(beamEntity);
world.spawnEntityInWorld(beamEntity2);
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (dualFire) {
if ((beamEntity == null || beamEntity.isDead) && (beamEntity2 == null || beamEntity2.isDead)) {
beamEntity = null;
beamEntity2 = null;
signalCompletion();
}
if (tick == dualFireDelay && beamEntity2 != null && !beamEntity2.isDead) {
beamEntity2.startFiring(target);
}
} else {
if (beamEntity == null || beamEntity.isDead) {
beamEntity = null;
signalCompletion();
}
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
@Override
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
// Despawn beam entity if still alive
if (beamEntity != null && !beamEntity.isDead) {
beamEntity.setDead();
}
if (beamEntity2 != null && !beamEntity2.isDead) {
beamEntity2.setDead();
}
beamEntity = null;
beamEntity2 = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create circle telegraph at target position
Telegraph telegraph = Telegraph.circle(headSize * 2.0f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return headSize * 2.0f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("beamWidth", beamWidth);
nbt.setFloat("headSize", headSize);
nbt.setBoolean("dualFire", dualFire);
nbt.setInteger("dualFireDelay", dualFireDelay);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
NBTTagCompound beam1 = new NBTTagCompound();
NBTTagCompound beam2 = new NBTTagCompound();
for (int i = 0; i < 2; i++) {
NBTTagCompound beam = i == 0 ? beam1 : beam2;
anchorData[i].writeNBT(beam);
displayData[i].writeNBT(beam);
lightningData[i].writeNBT(beam);
trajectoryData[i].writeNBT(beam);
}
nbt.setTag("Beam_1", beam1);
nbt.setTag("Beam_2", beam2);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
NBTTagCompound beam1 = nbt.hasKey("Beam_1") ? nbt.getCompoundTag("Beam_1") : new NBTTagCompound();
NBTTagCompound beam2 = nbt.hasKey("Beam_2") ? nbt.getCompoundTag("Beam_2") : new NBTTagCompound();
this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f;
this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f;
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0;
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
for (int i = 0; i < 2; i++) {
NBTTagCompound beam = i == 0 ? beam1 : beam2;
anchorData[i].readNBT(beam);
displayData[i].readNBT(beam);
lightningData[i].readNBT(beam);
trajectoryData[i].readNBT(beam);
}
}
// Getters & Setters - Standalone fields
public float getBeamWidth() { return beamWidth; }
public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
public float getHeadSize() { return headSize; }
public void setHeadSize(float headSize) { this.headSize = headSize; }
public boolean isDualFire() { return dualFire; }
public void setDualFire(boolean dualFire) { this.dualFire = dualFire; }
public int getDualFireDelay() { return dualFireDelay; }
public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; }
private EnergyDisplayData getColorData(int beam) {
beam = ValueUtil.clamp(beam, 0, 1);
return displayData[beam];
}
private EnergyLightningData getLightningData(int beam) {
beam = ValueUtil.clamp(beam, 0, 1);
return lightningData[beam];
}
private EnergyAnchorData getAnchorData(int beam) {
beam = ValueUtil.clamp(beam, 0, 1);
return anchorData[beam];
}
// Getters & Setters - Color data
public int getInnerColor(int beam) { return getColorData(beam).innerColor; }
public void setInnerColor(int beam, int innerColor) { getColorData(beam).innerColor = innerColor; }
public int getOuterColor(int beam) { return getColorData(beam).outerColor; }
public void setOuterColor(int beam, int outerColor) { getColorData(beam).outerColor = outerColor; }
public boolean isOuterColorEnabled(int beam) { return getColorData(beam).outerColorEnabled; }
public void setOuterColorEnabled(int beam, boolean enabled) { getColorData(beam).outerColorEnabled = enabled; }
public float getOuterColorWidth(int beam) { return getColorData(beam).outerColorWidth; }
public void setOuterColorWidth(int beam, float width) { getColorData(beam).outerColorWidth = width; }
public float getOuterColorAlpha(int beam) { return getColorData(beam).outerColorAlpha; }
public void setOuterColorAlpha(int beam, float alpha) { getColorData(beam).outerColorAlpha = alpha; }
public float getRotationSpeed(int beam) { return getColorData(beam).rotationSpeed; }
public void setRotationSpeed(int beam, float speed) { getColorData(beam).rotationSpeed = speed; }
public int getInnerColor() { return getInnerColor(0); }
public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); }
public int getOuterColor() { return getOuterColor(0); }
public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); }
public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); }
public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); }
public float getOuterColorWidth() { return getOuterColorWidth(0); }
public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); }
public float getOuterColorAlpha() { return getOuterColorAlpha(0); }
public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); }
public float getRotationSpeed() { return getRotationSpeed(0); }
public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); }
// Getters & Setters - Combat data
public float getDamage() { return combatData.damage; }
public void setDamage(float damage) { this.combatData.damage = damage; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float knockback) { this.combatData.knockback = knockback; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; }
// Getters & Setters - Homing data
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { this.homingData.speed = speed; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean homing) { this.homingData.homing = homing; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; }
// Getters & Setters - Lightning data
public boolean hasLightningEffect(int beam) { return getLightningData(beam).lightningEffect; }
public void setLightningEffect(int beam, boolean lightningEffect) { getLightningData(beam).lightningEffect = lightningEffect; }
public float getLightningDensity(int beam) { return getLightningData(beam).lightningDensity; }
public void setLightningDensity(int beam, float lightningDensity) { getLightningData(beam).lightningDensity = lightningDensity; }
public float getLightningRadius(int beam) { return getLightningData(beam).lightningRadius; }
public void setLightningRadius(int beam, float lightningRadius) { getLightningData(beam).lightningRadius = lightningRadius; }
public boolean hasLightningEffect() { return hasLightningEffect(0); }
public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); }
public float getLightningDensity() { return getLightningDensity(0); }
public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); }
public float getLightningRadius() { return getLightningRadius(0); }
public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); }
// Getters & Setters - Lifespan data
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; }
// Getters & Setters - Anchor point
public AnchorPoint getAnchorPointEnum(int beam) { return getAnchorData(beam).anchorPoint; }
public void setAnchorPointEnum(int beam, AnchorPoint anchorPoint) { getAnchorData(beam).anchorPoint = anchorPoint; }
public float getAnchorOffsetX(int beam) { return getAnchorData(beam).anchorOffsetX; }
public void setAnchorOffsetX(int beam, float x) { getAnchorData(beam).anchorOffsetX = x; }
public float getAnchorOffsetY(int beam) { return getAnchorData(beam).anchorOffsetY; }
public void setAnchorOffsetY(int beam, float y) { getAnchorData(beam).anchorOffsetY = y; }
public float getAnchorOffsetZ(int beam) { return getAnchorData(beam).anchorOffsetZ; }
public void setAnchorOffsetZ(int beam, float z) { getAnchorData(beam).anchorOffsetZ = z; }
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
//@Override
public int getAnchorPoint(int beam) { return getAnchorData(beam).anchorPoint.ordinal(); }
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
//@Override
public void setAnchorPoint(int beam, int point) { getAnchorData(beam).anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.section("ability.section.beam"),
FieldDef.row(
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth),
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.dual"),
FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire),
FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay)
.visibleWhen(this::isDualFire),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
.hover("ability.hover.homing"),
FieldDef.row(
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
.visibleWhen(this::isHoming),
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange)
.visibleWhen(this::isHoming)
),
FieldDef.section("ability.section.explosive"),
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive)
.hover("ability.hover.explosive"),
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
.visibleWhen(this::isExplosive),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
// Visual tab - Beam 1
FieldDef.section("ability.section.beam1").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(0), v -> setInnerColor(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(0), v -> setOuterColor(0, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v))
.visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(0))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(0), v -> setRotationSpeed(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(0), v -> setLightningEffect(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(0), v -> setLightningDensity(0, v))
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(0)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(0), v -> setLightningRadius(0, v))
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(0))
).tab("ability.tab.visual"),
// Visual tab - Beam 2
FieldDef.section("ability.section.beam2").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(1), v -> setInnerColor(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(1), v -> setOuterColor(1, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v))
.visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(1))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(1), v -> setRotationSpeed(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(1), v -> setLightningEffect(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(1), v -> setLightningDensity(1, v))
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(1), v -> setLightningRadius(1, v))
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1))
).tab("ability.tab.visual")
));
}
@Override
@SideOnly(Side.CLIENT)
public Entity createPreviewEntity(EntityNPCInterface npc) {
if (npc == null || npc.worldObj == null) return null;
EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj);
beam.setupPreview(npc, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, 1.0f);
return beam;
}
@Override
public int getPreviewActiveDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
}
@@ -38,9 +38,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
// Runtime state (transient)
private transient double startX, startY, startZ;
private transient double prevTickX, prevTickZ;
private transient Vec3 chargeDirection;
private transient Set<Integer> hitEntities = new HashSet<>();
private transient float lockedYaw;
private transient int maxActiveTicks;
public AbilityCharge() {
this.typeId = "ability.cnpc.charge";
@@ -56,6 +58,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
this.showTelegraph = true;
this.windUpSound = "mob.zombie.wood";
this.activeSound = "mob.zombie.attack";
this.windUpAnimationName = "Ability_Charge_Windup";
this.activeAnimationName = "Ability_Active_Windup";
}
@Override
@@ -87,11 +91,13 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
}
/**
* Locks the charge direction based on current target position.
* Called once at windup start - direction won't change even if target moves.
* Locks the charge direction. Called once at windup start — direction won't change even if target moves.
* NPC: charges toward aggro target.
* Player: charges in look direction.
*/
private void lockChargeDirection(EntityLivingBase caster, EntityLivingBase target) {
if (target != null) {
if (!isPlayerCaster(caster) && target != null) {
// NPC: charge toward aggro target
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
double len = Math.sqrt(dx * dx + dz * dz);
@@ -102,6 +108,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
}
} else {
// Player: charge in look direction
float yaw = (float) Math.toRadians(caster.rotationYaw);
chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
}
@@ -114,8 +121,13 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
startX = caster.posX;
startY = caster.posY;
startZ = caster.posZ;
prevTickX = caster.posX;
prevTickZ = caster.posZ;
hitEntities.clear();
// Safety timeout: expected ticks + buffer to prevent infinite charge
maxActiveTicks = chargeSpeed > 0 ? (int)(maxRange / chargeSpeed) + 10 : 10;
// If direction wasn't set during windup (shouldn't happen), set it now
if (chargeDirection == null) {
lockChargeDirection(caster, target);
@@ -135,10 +147,35 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (chargeDirection == null) return;
// Safety timeout or missing state: force-complete to prevent stuck NPC
if (!isPreview() && (chargeDirection == null || tick > maxActiveTicks)) {
stopMomentum(caster);
signalCompletion();
return;
}
if (chargeDirection == null) {
signalCompletion();
return;
}
// Enforce rotation every tick
enforceLockedRotation(caster);
if (!isPreview()) {
enforceLockedRotation(caster);
}
// Stall detection: if entity hasn't moved since last tick, it's stuck against a wall
if (!isPreview() && tick > 1) {
double dx = caster.posX - prevTickX;
double dz = caster.posZ - prevTickZ;
if (dx * dx + dz * dz < 0.0001) {
stopMomentum(caster);
signalCompletion();
return;
}
}
prevTickX = caster.posX;
prevTickZ = caster.posZ;
// Calculate distance traveled
double distanceTraveled = Math.sqrt(
@@ -148,14 +185,13 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
// Check if reached max distance
if (distanceTraveled >= maxRange) {
caster.motionX = 0;
caster.motionZ = 0;
caster.velocityChanged = true;
stopMomentum(caster);
signalCompletion();
return;
}
if (isChargeBlocked(caster)) {
// Block detection (skip in preview - no real world collision)
if (!isPreview() && isChargeBlocked(caster)) {
stopMomentum(caster);
signalCompletion();
return;
@@ -165,10 +201,12 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
caster.motionX = chargeDirection.xCoord * chargeSpeed;
caster.motionY = 0;
caster.motionZ = chargeDirection.zCoord * chargeSpeed;
caster.velocityChanged = true;
if (!isPreview()) {
caster.velocityChanged = true;
}
// Server-side collision damage
if (!world.isRemote) {
// Server-side collision damage (skip in preview)
if (!world.isRemote && !isPreview()) {
AxisAlignedBB hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth);
@SuppressWarnings("unchecked")
@@ -211,7 +249,9 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
private void stopMomentum(EntityLivingBase caster) {
caster.motionX = 0;
caster.motionZ = 0;
caster.velocityChanged = true;
if (!isPreview()) {
caster.velocityChanged = true;
}
}
private boolean isChargeBlocked(EntityLivingBase caster) {
@@ -226,6 +266,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
super.reset();
chargeDirection = null;
hitEntities.clear();
maxActiveTicks = 0;
}
@Override
@@ -249,9 +290,10 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
return null;
}
// Calculate direction to target at this moment (locked)
// NPC: telegraph points toward aggro target
// Player: telegraph points in look direction
float yaw;
if (target != null) {
if (!isPlayerCaster(caster) && target != null) {
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
yaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
@@ -277,66 +319,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
return instance;
}
// ==================== PREVIEW MODE ====================
private transient double previewDirX, previewDirZ;
private transient double previewStartX, previewStartZ;
private transient boolean previewCharging = false;
@Override
@SideOnly(Side.CLIENT)
public void onPreviewExecute(EntityNPCInterface npc) {
previewStartX = npc.posX;
previewStartZ = npc.posZ;
previewCharging = false;
// Calculate direction toward fake target
if (previewTarget != null) {
double dx = previewTarget.posX - npc.posX;
double dz = previewTarget.posZ - npc.posZ;
double len = Math.sqrt(dx * dx + dz * dz);
if (len > 0) {
previewDirX = dx / len;
previewDirZ = dz / len;
} else {
float yaw = (float) Math.toRadians(npc.rotationYaw);
previewDirX = -Math.sin(yaw);
previewDirZ = Math.cos(yaw);
}
} else {
float yaw = (float) Math.toRadians(npc.rotationYaw);
previewDirX = -Math.sin(yaw);
previewDirZ = Math.cos(yaw);
}
previewCharging = true;
}
@Override
@SideOnly(Side.CLIENT)
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
if (!previewCharging) return;
// Check distance traveled
double distTraveled = Math.sqrt(
Math.pow(npc.posX - previewStartX, 2) +
Math.pow(npc.posZ - previewStartZ, 2)
);
if (distTraveled >= maxRange) {
previewCharging = false;
return;
}
npc.prevPosX = npc.posX;
npc.prevPosY = npc.posY;
npc.prevPosZ = npc.posZ;
npc.posX += previewDirX * chargeSpeed;
npc.posZ += previewDirZ * chargeSpeed;
}
@Override
public int getPreviewActiveDuration() {
public int getMaxPreviewDuration() {
return (int) Math.ceil(maxRange / chargeSpeed) + 5;
}
@@ -3,6 +3,8 @@ 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.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
@@ -60,6 +62,8 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
// Runtime state
private transient Set<Integer> hitEntities = new HashSet<>();
private transient float currentRotation = 0.0f;
private transient boolean activeSoundPlayed = false;
private transient float sweepBaseYaw = 0.0f;
public AbilityCutter() {
this.typeId = "ability.cnpc.cutter";
@@ -86,6 +90,54 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
return new TargetingMode[]{TargetingMode.AOE_SELF};
}
/**
* Calculate how many ticks the active sweep phase takes.
* SWIPE: arcAngle / sweepSpeed ticks
* SPIN: spinDurationTicks
*/
private int getActiveDurationTicks() {
if (sweepMode == SweepMode.SPIN) {
return spinDurationTicks;
}
return sweepSpeed > 0 ? (int) Math.ceil(arcAngle / sweepSpeed) : 1;
}
@Override
public boolean keepTelegraphDuringActive() {
return true;
}
/**
* Creates a CONE telegraph that lasts through both windup and active sweep phases.
* Warning color transitions when the sweep actually begins (at windUpTicks).
*/
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) {
return null;
}
Telegraph telegraph = Telegraph.cone(getTelegraphLength(), getTelegraphAngle());
int totalDuration = windUpTicks + getActiveDurationTicks();
telegraph.setDurationTicks(totalDuration);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
// Warning transition starts when the sweep begins (active phase)
telegraph.setWarningStartTick(windUpTicks);
telegraph.setHeightOffset(telegraphHeightOffset);
double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ);
TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(caster.getEntityId());
if (target != null && !isRotationLockedDuringWindup()) {
instance.setTargetEntityId(target.getEntityId());
}
return instance;
}
@Override
public float getTelegraphRadius() {
return range;
@@ -105,39 +157,76 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
hitEntities.clear();
currentRotation = -arcAngle / 2.0f;
activeSoundPlayed = false;
// Lock sweep direction toward target at execute time to prevent
// AI look-at jitter from misdirecting the sweep at close range
if (target != null) {
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
if (dx * dx + dz * dz > 0.0001) {
sweepBaseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
} else {
sweepBaseYaw = caster.rotationYaw;
}
} else {
sweepBaseYaw = caster.rotationYaw;
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
if (world.isRemote && !isPreview()) return;
switch (sweepMode) {
case SWIPE:
float prevRotation = currentRotation;
currentRotation += sweepSpeed;
// Play active sound when sweep reaches center (where target typically stands)
if (!activeSoundPlayed && currentRotation >= 0) {
playActiveSound(caster, world);
}
if (currentRotation > arcAngle / 2.0f) {
// Final sweep — check from prevRotation to end of arc
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f);
if (!isPreview()) {
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f);
}
signalCompletion();
return;
}
// Check entities between previous and current sweep position
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation);
if (!isPreview()) {
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation);
}
break;
case SPIN:
// Play active sound at spin start
if (!activeSoundPlayed) {
playActiveSound(caster, world);
}
if (tick >= spinDurationTicks) {
signalCompletion(); // Spin duration complete
signalCompletion();
return;
}
float prevSpin = currentRotation;
currentRotation = (currentRotation + sweepSpeed) % 360.0f;
hitEntities.clear();
performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation);
if (!isPreview()) {
hitEntities.clear();
performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation);
}
break;
}
}
private void playActiveSound(EntityLivingBase caster, World world) {
activeSoundPlayed = true;
if (activeSound != null && !activeSound.isEmpty()) {
world.playSoundAtEntity(caster, activeSound, 1.0f, 1.0f);
}
}
/**
* Check for entities between two sweep angles, ensuring no gaps between ticks.
* For SWIPE mode, this covers the continuous arc from startAngle to endAngle.
@@ -167,7 +256,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist < minDist || dist > maxDist) continue;
if (!isInSweepRange(dx, dz, caster.rotationYaw, minAngle, maxAngle)) continue;
if (!isInSweepRange(dx, dz, sweepBaseYaw, minAngle, maxAngle)) continue;
hitEntities.add(entity.getEntityId());
@@ -210,12 +299,16 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
hitEntities.clear();
currentRotation = 0.0f;
activeSoundPlayed = false;
sweepBaseYaw = 0.0f;
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
hitEntities.clear();
currentRotation = 0.0f;
activeSoundPlayed = false;
sweepBaseYaw = 0.0f;
}
@Override
@@ -87,13 +87,14 @@ public class AbilityDash extends Ability implements IAbilityDash {
// Type-specific parameters
private DashMode dashMode = DashMode.DEFENSIVE;
private float dashDistance = 4.0f;
private float dashSpeed = 1.5f;
private float dashSpeed = 0.5f;
// Runtime state
private transient Vec3 dashDirection;
private transient double startX, startY, startZ;
private transient double prevTickX, prevTickZ;
private transient DashDirection chosenDirection;
private transient int maxActiveTicks;
public AbilityDash() {
this.typeId = "ability.cnpc.dash";
@@ -139,15 +140,19 @@ public class AbilityDash extends Ability implements IAbilityDash {
prevTickX = caster.posX;
prevTickZ = caster.posZ;
// Safety timeout: expected ticks + generous buffer to prevent infinite dash
maxActiveTicks = dashSpeed > 0 ? (int)(dashDistance / dashSpeed) + 10 : 10;
// Choose random direction based on mode
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
? AGGRESSIVE_DIRECTIONS
: DEFENSIVE_DIRECTIONS;
chosenDirection = directions[RANDOM.nextInt(directions.length)];
// Calculate dash direction relative to target
// NPC: dash direction is relative to aggro target facing
// Player: dash direction is relative to look direction
float baseYaw;
if (target != null) {
if (!isPlayerCaster(caster) && target != null) {
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
@@ -165,21 +170,30 @@ public class AbilityDash extends Ability implements IAbilityDash {
Math.cos(yawRad)
);
// Small upward impulse for a skip/hop arc (gravity handles the descent)
caster.motionY = 0.2;
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (dashDirection == null) return;
// Safety timeout or missing state: force-complete to prevent stuck NPC
if (!isPreview() && (dashDirection == null || tick > maxActiveTicks)) {
stopDash(caster);
signalCompletion();
return;
}
if (dashDirection == null) {
signalCompletion();
return;
}
// Stall detection: if entity hasn't moved since last tick, it's stuck against a wall
if (tick > 1) {
double movedThisTick = Math.sqrt(
Math.pow(caster.posX - prevTickX, 2) +
Math.pow(caster.posZ - prevTickZ, 2));
if (movedThisTick < 0.01) {
caster.motionX = 0;
caster.motionZ = 0;
caster.velocityChanged = true;
if (!isPreview() && tick > 1) {
double dx = caster.posX - prevTickX;
double dz = caster.posZ - prevTickZ;
if (dx * dx + dz * dz < 0.0001) {
stopDash(caster);
signalCompletion();
return;
}
@@ -187,57 +201,56 @@ public class AbilityDash extends Ability implements IAbilityDash {
prevTickX = caster.posX;
prevTickZ = caster.posZ;
// Calculate distance traveled
double distanceTraveled = Math.sqrt(
Math.pow(caster.posX - startX, 2) +
Math.pow(caster.posZ - startZ, 2)
);
// Check if reached max distance
if (distanceTraveled >= dashDistance) {
caster.motionX = 0;
caster.motionZ = 0;
caster.velocityChanged = true;
double travelDx = caster.posX - startX;
double travelDz = caster.posZ - startZ;
if (travelDx * travelDx + travelDz * travelDz >= (double) dashDistance * dashDistance) {
stopDash(caster);
signalCompletion();
return;
}
if (isDashBlocked(caster)) {
caster.motionX = 0;
caster.motionZ = 0;
caster.velocityChanged = true;
// Block detection (skip in preview - no real world collision)
if (!isPreview() && isDashBlocked(caster)) {
stopDash(caster);
signalCompletion();
return;
}
// Move caster
// Move caster (motionY left to gravity for skip arc)
caster.motionX = dashDirection.xCoord * dashSpeed;
caster.motionY = 0;
caster.motionZ = dashDirection.zCoord * dashSpeed;
caster.velocityChanged = true;
if (!isPreview()) {
caster.velocityChanged = true;
// Trail particles
world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
// Trail particles
world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
}
}
private void stopDash(EntityLivingBase caster) {
caster.motionX = 0;
caster.motionZ = 0;
if (!isPreview()) {
caster.velocityChanged = true;
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
caster.motionX = 0;
caster.motionZ = 0;
caster.velocityChanged = true;
stopDash(caster);
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
caster.motionX = 0;
caster.motionZ = 0;
caster.velocityChanged = true;
stopDash(caster);
}
@Override
public void cleanup() {
dashDirection = null;
chosenDirection = null;
maxActiveTicks = 0;
}
private boolean isDashBlocked(EntityLivingBase caster) {
@@ -247,66 +260,8 @@ public class AbilityDash extends Ability implements IAbilityDash {
return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty();
}
// ==================== PREVIEW MODE ====================
private transient double previewDirX, previewDirZ;
private transient double previewStartX, previewStartZ;
private transient boolean previewDashing = false;
@Override
@SideOnly(Side.CLIENT)
public void onPreviewExecute(EntityNPCInterface npc) {
previewStartX = npc.posX;
previewStartZ = npc.posZ;
previewDashing = false;
// Pick a random direction based on mode
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
? AGGRESSIVE_DIRECTIONS
: DEFENSIVE_DIRECTIONS;
DashDirection dir = directions[RANDOM.nextInt(directions.length)];
float baseYaw;
if (previewTarget != null) {
double dx = previewTarget.posX - npc.posX;
double dz = previewTarget.posZ - npc.posZ;
baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
} else {
baseYaw = npc.rotationYaw;
}
float dashYaw = baseYaw + dir.getAngleOffset();
float yawRad = (float) Math.toRadians(dashYaw);
previewDirX = -Math.sin(yawRad);
previewDirZ = Math.cos(yawRad);
previewDashing = true;
}
@Override
@SideOnly(Side.CLIENT)
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
if (!previewDashing) return;
double distTraveled = Math.sqrt(
Math.pow(npc.posX - previewStartX, 2) +
Math.pow(npc.posZ - previewStartZ, 2)
);
if (distTraveled >= dashDistance) {
previewDashing = false;
return;
}
npc.prevPosX = npc.posX;
npc.prevPosY = npc.posY;
npc.prevPosZ = npc.posZ;
npc.posX += previewDirX * dashSpeed;
npc.posZ += previewDirZ * dashSpeed;
}
@Override
public int getPreviewActiveDuration() {
public int getMaxPreviewDuration() {
return (int) Math.ceil(dashDistance / dashSpeed) + 5;
}
@@ -330,7 +285,7 @@ public class AbilityDash extends Ability implements IAbilityDash {
this.dashMode = DashMode.DEFENSIVE;
}
this.dashDistance = nbt.hasKey("dashDistance") ? nbt.getFloat("dashDistance") : 4.0f;
this.dashSpeed = nbt.hasKey("dashSpeed") ? nbt.getFloat("dashSpeed") : 1.5f;
this.dashSpeed = nbt.hasKey("dashSpeed") ? nbt.getFloat("dashSpeed") : 0.5f;
}
// Getters & Setters
@@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
@@ -23,33 +22,45 @@ import noppes.npcs.api.ability.type.IAbilityDisc;
import java.util.Arrays;
import java.util.List;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
/**
* Disc ability: Spawns a flat spinning disc projectile.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
* Has optional boomerang behavior to return to owner.
*/
public class AbilityDisc extends Ability implements IAbilityDisc {
// Disc geometry
private static final int MAX_PROJECTILES = 8;
// Disc geometry (shared)
private float discRadius = 1.0f;
private float discThickness = 0.2f;
private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward)
// Boomerang properties
// Boomerang properties (shared)
private boolean boomerang = false;
private int boomerangDelay = 40;
// Energy data classes
private final EnergyDisplayData colorData = new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f);
// Projectile count and fire stagger
private int projectileCount = 1;
private int fireDelay = 0;
// Shared visual data (all projectiles use these)
private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f);
private EnergyLightningData lightningData = new EnergyLightningData();
private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Shared combat/movement data
private final EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f);
private final EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f);
private final EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200);
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Transient state for disc entity (used during windup charging)
private transient EntityAbilityDisc discEntity = null;
// Per-projectile data (anchor + optional color override)
private ProjectileData[] projectiles;
// Transient entity state
private transient EntityAbilityDisc[] discEntities;
public AbilityDisc() {
this.typeId = "ability.cnpc.disc";
@@ -62,9 +73,91 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animations
this.windUpAnimationName = "Ability_Disc_Windup";
this.activeAnimationName = "Ability_Disc_Active";
initProjectiles(1);
}
private void initProjectiles(int count) {
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
this.projectiles = new ProjectileData[projectileCount];
for (int i = 0; i < projectileCount; i++) {
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
}
}
/**
* Disc anchor order is REVERSED from Orb/Beam: [0]=LEFT_HAND, [1]=RIGHT_HAND
*/
private AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0: return AnchorPoint.RIGHT_HAND;
case 1: return AnchorPoint.LEFT_HAND;
default: return AnchorPoint.FRONT;
}
}
public void setProjectileCount(int count) {
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
if (newCount == projectileCount) return;
ProjectileData[] oldProjectiles = projectiles;
initProjectiles(newCount);
if (oldProjectiles != null) {
int copyCount = Math.min(oldProjectiles.length, newCount);
for (int i = 0; i < copyCount; i++) {
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
}
}
}
public int getProjectileCount() {
return projectileCount;
}
public int getFireDelay() {
return fireDelay;
}
public void setFireDelay(int delay) {
this.fireDelay = Math.max(0, delay);
}
/** @deprecated Use {@link #getFireDelay()} */
public int getDualFireDelay() { return fireDelay; }
/** @deprecated Use {@link #setFireDelay(int)} */
public void setDualFireDelay(int delay) { setFireDelay(delay); }
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Disc");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilityDisc disc = (AbilityDisc) a;
a.setName("Dual Disc");
disc.setProjectileCount(2);
disc.setFireDelay(5);
// Set color override for second disc
disc.projectiles[1].colorOverride = true;
disc.projectiles[1].innerColor = 0xFFFFFF;
disc.projectiles[1].outerColor = 0x8800FF;
a.setWindUpAnimationName("Ability_DiscDual_Windup");
a.setActiveAnimationName("Ability_DiscDual_Active");
})
);
}
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
@@ -77,49 +170,120 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
signalCompletion();
return;
/**
* Create disc entities.
* NPC: target is the aggro target — discs will home toward it.
* Player: target is null — discs fire in caster's look direction.
*/
private EntityAbilityDisc[] createDiscEntities(EntityLivingBase caster, EntityLivingBase target, World world) {
EntityAbilityDisc[] entities = new EntityAbilityDisc[projectileCount];
for (int i = 0; i < projectileCount; i++) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
entities[i] = new EntityAbilityDisc(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
discRadius, discThickness,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
boomerang, boomerangDelay);
entities[i].setEffects(this.effects);
entities[i].setSourceAbility(this);
}
// Start moving the disc that was spawned during windup
if (discEntity != null && !discEntity.isDead) {
discEntity.startMoving(target);
if (projectileCount == 2 && entities[0] != null && entities[1] != null) {
entities[0].setSiblingEntityId(entities[1].getEntityId());
entities[1].setSiblingEntityId(entities[0].getEntityId());
}
return entities;
}
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
private void fireDiscEntity(EntityAbilityDisc disc, EntityLivingBase target) {
if (disc == null || disc.isDead) return;
if (isPreview()) {
disc.startPreviewFiring();
} else {
disc.startMoving(target);
}
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
if (world.isRemote && !isPreview()) return;
// Spawn disc in charging mode on first tick of windup
if (tick == 1) {
// Create disc in charging mode - follows caster based on anchor point during windup
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
discEntity = new EntityAbilityDisc(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
discRadius, discThickness,
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData,
boomerang, boomerangDelay);
discEntity.setupCharging(anchorData, windUpTicks);
discEntities = createDiscEntities(caster, target, world);
for (int i = 0; i < projectileCount; i++) {
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
if (isPreview()) {
discEntities[i].setupPreview(caster, discRadius, discThickness, resolved, lightningData, projectiles[i].anchor, windUpTicks, vertical);
} else {
discEntities[i].setupCharging(projectiles[i].anchor, windUpTicks, vertical);
}
spawnAbilityEntity(world, discEntities[i]);
}
}
}
discEntity.setEffects(this.effects);
discEntity.setSourceAbility(this);
world.spawnEntityInWorld(discEntity);
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
discEntities = createDiscEntities(caster, target, world);
for (EntityAbilityDisc disc : discEntities) {
spawnAbilityEntity(world, disc);
}
// Fire first projectile immediately, let onActiveTick handle the rest with fireDelay
fireDiscEntity(discEntities[0], target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireDiscEntity(discEntities[i], target);
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (discEntities == null) return;
// Fire first projectile immediately
fireDiscEntity(discEntities[0], target);
// Fire remaining projectiles if no delay
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireDiscEntity(discEntities[i], target);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (discEntity == null || discEntity.isDead) {
discEntity = null;
if (discEntities == null) {
signalCompletion();
return;
}
// Fire staggered projectiles
if (fireDelay > 0) {
for (int i = 1; i < projectileCount; i++) {
if (tick == fireDelay * i) {
fireDiscEntity(discEntities[i], target);
}
}
}
// Check if all entities are dead
boolean allDead = true;
for (EntityAbilityDisc disc : discEntities) {
if (disc != null && !disc.isDead) {
allDead = false;
break;
}
}
if (allDead) {
signalCompletion();
}
}
@@ -135,16 +299,22 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
@Override
public void cleanup() {
// Despawn disc entity if still alive
if (discEntity != null && !discEntity.isDead) {
discEntity.setDead();
if (discEntities != null) {
for (int i = 0; i < discEntities.length; i++) {
if (discEntities[i] != null && !discEntities[i].isDead) {
discEntities[i].setDead();
}
discEntities[i] = null;
}
}
discEntity = null;
discEntities = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
// NPC: telegraph on aggro target position
// Player: no telegraph (discs fire in look direction, no fixed position to mark)
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
return null;
}
@@ -173,173 +343,297 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("discRadius", discRadius);
nbt.setFloat("discThickness", discThickness);
nbt.setBoolean("vertical", vertical);
nbt.setBoolean("boomerang", boomerang);
nbt.setInteger("boomerangDelay", boomerangDelay);
anchorData.writeNBT(nbt);
colorData.writeNBT(nbt);
nbt.setInteger("projectileCount", projectileCount);
nbt.setInteger("fireDelay", fireDelay);
// Shared visual data
displayData.writeNBT(nbt);
lightningData.writeNBT(nbt);
// Shared combat/movement data
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lightningData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
NBTTagCompound projNbt = new NBTTagCompound();
projectiles[i].writeNBT(projNbt);
nbt.setTag("Projectile_" + i, projNbt);
}
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f;
this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f;
this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical");
this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang");
this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40;
anchorData.readNBT(nbt);
colorData.readNBT(nbt);
int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1;
initProjectiles(count);
this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0;
// Shared combat/movement data
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lightningData.readNBT(nbt);
lifespanData.readNBT(nbt);
// Shared visual data
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
}
// Getters & Setters
// ═══════════════════════════════════════════════════════════════════
// API GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { homingData.speed = speed; }
public float getDiscRadius() { return discRadius; }
public void setDiscRadius(float discRadius) { this.discRadius = discRadius; }
public float getDiscThickness() { return discThickness; }
public void setDiscThickness(float discThickness) { this.discThickness = discThickness; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; }
public float getDamage() { return combatData.damage; }
public void setDamage(float damage) { combatData.damage = damage; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float knockback) { combatData.knockback = knockback; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean homing) { homingData.homing = homing; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; }
public boolean isVertical() { return vertical; }
public void setVertical(boolean vertical) { this.vertical = vertical; }
public boolean isBoomerang() { return boomerang; }
public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; }
public int getBoomerangDelay() { return boomerangDelay; }
public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float d) { lifespanData.maxDistance = d; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int t) { lifespanData.maxLifetime = t; }
public float getDamage() { return combatData.damage; }
public void setDamage(float d) { combatData.damage = d; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float k) { combatData.knockback = k; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float k) { combatData.knockbackUp = k; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean h) { homingData.homing = h; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float s) { homingData.homingStrength = s; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float r) { homingData.homingRange = r; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean explosive) { combatData.explosive = explosive; }
public void setExplosive(boolean e) { combatData.explosive = e; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; }
public void setExplosionRadius(float r) { combatData.explosionRadius = r; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; }
public int getInnerColor() { return colorData.innerColor; }
public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; }
public int getOuterColor() { return colorData.outerColor; }
public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; }
public float getOuterColorWidth() { return colorData.outerColorWidth; }
public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; }
public float getOuterColorAlpha() { return colorData.outerColorAlpha; }
public void setOuterColorAlpha(float outerColorAlpha) { colorData.outerColorAlpha = outerColorAlpha; }
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; }
public float getRotationSpeed() { return colorData.rotationSpeed; }
public void setRotationSpeed(float rotationSpeed) { colorData.rotationSpeed = rotationSpeed; }
public void setExplosionDamageFalloff(float f) { combatData.explosionDamageFalloff = f; }
// Shared display data (primary colors)
public int getInnerColor() { return displayData.innerColor; }
public void setInnerColor(int c) { displayData.innerColor = c; }
public int getOuterColor() { return displayData.outerColor; }
public void setOuterColor(int c) { displayData.outerColor = c; }
public float getOuterColorWidth() { return displayData.outerColorWidth; }
public void setOuterColorWidth(float w) { displayData.outerColorWidth = w; }
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
public void setOuterColorAlpha(float a) { displayData.outerColorAlpha = a; }
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
public void setOuterColorEnabled(boolean e) { displayData.outerColorEnabled = e; }
public float getRotationSpeed() { return displayData.rotationSpeed; }
public void setRotationSpeed(float s) { displayData.rotationSpeed = s; }
// Shared lightning data
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; }
public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; }
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; }
public void setLightningDensity(float d) { lightningData.lightningDensity = d; }
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; }
public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; }
public float getAnchorOffsetX() { return anchorData.anchorOffsetX; }
public float getAnchorOffsetY() { return anchorData.anchorOffsetY; }
public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; }
public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; }
public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; }
public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; }
public void setLightningRadius(float r) { lightningData.lightningRadius = r; }
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
public void setLightningFadeTime(int t) { lightningData.lightningFadeTime = t; }
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
public void setAnchorPointEnum(AnchorPoint p) { projectiles[0].anchor.anchorPoint = p; }
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
@Override
public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); }
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
@Override
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
@Override
@SideOnly(Side.CLIENT)
public Entity createPreviewEntity(EntityNPCInterface npc) {
if (npc == null || npc.worldObj == null) return null;
// Indexed API methods
public int getInnerColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.innerColor : displayData.innerColor;
}
public void setInnerColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.innerColor = color;
}
EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj);
disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks);
return disc;
public int getOuterColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.outerColor : displayData.outerColor;
}
public void setOuterColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.outerColor = color;
}
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
private int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
}
@Override
public int getPreviewActiveDuration() {
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
// ═══════════════════════════════════════════════════════════════════
// GUI FIELD DEFINITIONS
// ═══════════════════════════════════════════════════════════════════
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.section("ability.section.disc"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
.hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
.visibleWhen(this::isHoming),
FieldDef.section("ability.section.boomerang"),
FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang)
.hover("ability.hover.boomerang"),
FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay)
.visibleWhen(this::isBoomerang),
FieldDef.section("ability.section.explosive"),
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive)
.hover("ability.hover.explosive"),
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
.visibleWhen(this::isExplosive),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
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),
FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.effects").tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.range(0.01f, 100f).visibleWhen(this::hasLightningEffect),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual")
// Type tab - projectile count and fire delay
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
// Type tab - disc properties
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
));
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
defs.add(FieldDef.section("ability.section.disc"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
));
defs.add(FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical)
.hover("ability.hover.vertical"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
));
defs.add(FieldDef.section("ability.section.homing"));
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming));
defs.add(FieldDef.section("ability.section.boomerang"));
defs.add(FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang).hover("ability.hover.boomerang"));
defs.add(FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay).visibleWhen(this::isBoomerang));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
// Visual tab - shared primary colors
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
// Visual tab - shared effects
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
// Visual tab - per-projectile sections
for (int i = 0; i < MAX_PROJECTILES; i++) {
final int idx = i;
String sectionKey = "ability.section.projectile" + (i + 1);
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
.visibleWhen(() -> idx < projectileCount));
// Anchor point
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v),
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v)
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
// Color override (only when multiple projectiles)
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
}
}
}
@@ -1,527 +0,0 @@
package kamkeel.npcs.controllers.data.ability.type;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import noppes.npcs.client.gui.builder.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.util.ValueUtil;
import java.util.Arrays;
import java.util.List;
public class AbilityDiscDual extends Ability {
// Disc geometry
private float discRadius = 1.0f;
private float discThickness = 0.2f;
// Boomerang properties
private boolean boomerang = false;
private int boomerangDelay = 40;
// Dual properties
private boolean dualFire = true;
private int dualFireDelay = 10;
// Energy data classes
private final EnergyDisplayData[] displayData = new EnergyDisplayData[]{
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f),
new EnergyDisplayData(0xFFFFFF, 0x8800FF, true, 0.4f, 0.5f, 5.0f)
};
private final EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f);
private final EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f);
private final EnergyLightningData[] lightningData = new EnergyLightningData[]{
new EnergyLightningData(), new EnergyLightningData()
};
private final EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200);
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
new EnergyAnchorData(AnchorPoint.LEFT_HAND), new EnergyAnchorData(AnchorPoint.RIGHT_HAND)
};
private final EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{
new EnergyTrajectoryData(), new EnergyTrajectoryData()
};
// Transient state for disc entity (used during windup charging)
private transient EntityAbilityDisc discEntity = null;
private transient EntityAbilityDisc discEntity2 = null;
public AbilityDiscDual() {
this.typeId = "ability.cnpc.disc_dual";
this.name = "Disc Dual";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 30.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 60;
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animations
this.windUpAnimationName = "Ability_DiscDual_Windup";
this.activeAnimationName = "Ability_DiscDual_Active";
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
signalCompletion();
return;
}
// Start moving the orb that was spawned during windup
if (discEntity != null && !discEntity.isDead) {
discEntity.startMoving(target);
}
if (dualFire) {
if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) {
discEntity2.startMoving(target);
}
} else if (discEntity2 != null) {
discEntity2.setDead();
}
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
// Spawn disc in charging mode on first tick of windup
if (tick == 1) {
// Create disc in charging mode - follows caster based on anchor point during windup
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]);
discEntity = new EntityAbilityDisc(
world, caster, target,
spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord,
discRadius, discThickness,
displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0],
boomerang, boomerangDelay);
Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]);
discEntity2 = new EntityAbilityDisc(
world, caster, target,
spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord,
discRadius, discThickness,
displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1],
boomerang, boomerangDelay);
discEntity.setSiblingEntityId(discEntity2.getEntityId());
discEntity2.setSiblingEntityId(discEntity.getEntityId());
discEntity.setupCharging(anchorData[0], windUpTicks);
discEntity2.setupCharging(anchorData[1], windUpTicks);
discEntity.setEffects(this.effects);
if (dualFire) {
discEntity2.setEffects(this.effects);
}
discEntity.setSourceAbility(this);
discEntity2.setSourceAbility(this);
world.spawnEntityInWorld(discEntity);
world.spawnEntityInWorld(discEntity2);
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (dualFire) {
if ((discEntity == null || discEntity.isDead) && (discEntity2 == null || discEntity2.isDead)) {
discEntity = null;
discEntity2 = null;
signalCompletion();
}
if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) {
discEntity2.startMoving(target);
}
} else {
if (discEntity == null || discEntity.isDead) {
discEntity = null;
signalCompletion();
}
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
@Override
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
// Despawn disc entity if still alive
if (discEntity != null && !discEntity.isDead) {
discEntity.setDead();
}
if (discEntity2 != null && !discEntity2.isDead) {
discEntity2.setDead();
}
discEntity = null;
discEntity2 = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create circle telegraph at target position
Telegraph telegraph = Telegraph.circle(discRadius * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return discRadius * 1.5f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("discRadius", discRadius);
nbt.setFloat("discThickness", discThickness);
nbt.setBoolean("boomerang", boomerang);
nbt.setInteger("boomerangDelay", boomerangDelay);
nbt.setBoolean("dualFire", dualFire);
nbt.setInteger("dualFireDelay", dualFireDelay);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
NBTTagCompound disc1 = new NBTTagCompound();
NBTTagCompound disc2 = new NBTTagCompound();
for (int i = 0; i < 2; i++) {
NBTTagCompound disc = i == 0 ? disc1 : disc2;
anchorData[i].writeNBT(disc);
displayData[i].writeNBT(disc);
lightningData[i].writeNBT(disc);
trajectoryData[i].writeNBT(disc);
}
nbt.setTag("Disc_1", disc1);
nbt.setTag("Disc_2", disc2);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
NBTTagCompound disc1 = nbt.hasKey("Disc_1") ? nbt.getCompoundTag("Disc_1") : new NBTTagCompound();
NBTTagCompound disc2 = nbt.hasKey("Disc_2") ? nbt.getCompoundTag("Disc_2") : new NBTTagCompound();
this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f;
this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f;
this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang");
this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40;
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0;
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
for (int i = 0; i < 2; i++) {
NBTTagCompound disc = i == 0 ? disc1 : disc2;
anchorData[i].readNBT(disc);
displayData[i].readNBT(disc);
lightningData[i].readNBT(disc);
trajectoryData[i].readNBT(disc);
}
}
// Getters & Setters
private EnergyDisplayData getColorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return displayData[orb];
}
private EnergyLightningData getLightningData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return lightningData[orb];
}
private EnergyAnchorData getAnchorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return anchorData[orb];
}
public boolean isDualFire() { return dualFire; }
public void setDualFire(boolean dualFire) { this.dualFire = dualFire; }
public int getDualFireDelay() { return dualFireDelay; }
public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; }
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { homingData.speed = speed; }
public float getDiscRadius() { return discRadius; }
public void setDiscRadius(float discRadius) { this.discRadius = discRadius; }
public float getDiscThickness() { return discThickness; }
public void setDiscThickness(float discThickness) { this.discThickness = discThickness; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; }
public float getDamage() { return combatData.damage; }
public void setDamage(float damage) { combatData.damage = damage; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float knockback) { combatData.knockback = knockback; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean homing) { homingData.homing = homing; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; }
public boolean isBoomerang() { return boomerang; }
public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; }
public int getBoomerangDelay() { return boomerangDelay; }
public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean explosive) { combatData.explosive = explosive; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; }
public int getInnerColor(int orb) { return getColorData(orb).innerColor; }
public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; }
public int getOuterColor(int orb) { return getColorData(orb).outerColor; }
public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; }
public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; }
public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; }
public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; }
public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; }
public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; }
public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; }
public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; }
public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; }
public int getInnerColor() { return getInnerColor(0); }
public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); }
public int getOuterColor() { return getOuterColor(0); }
public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); }
public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); }
public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); }
public float getOuterColorWidth() { return getOuterColorWidth(0); }
public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); }
public float getOuterColorAlpha() { return getOuterColorAlpha(0); }
public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); }
public float getRotationSpeed() { return getRotationSpeed(0); }
public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); }
public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; }
public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; }
public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; }
public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; }
public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; }
public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; }
public boolean hasLightningEffect() { return hasLightningEffect(0); }
public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); }
public float getLightningDensity() { return getLightningDensity(0); }
public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); }
public float getLightningRadius() { return getLightningRadius(0); }
public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); }
public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; }
public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; }
public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; }
public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; }
public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; }
public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; }
public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; }
public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; }
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
//@Override
public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); }
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
//@Override
public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.section("ability.section.disc"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.dual"),
FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire),
FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay)
.visibleWhen(this::isDualFire),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
.hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
.visibleWhen(this::isHoming),
FieldDef.section("ability.section.boomerang"),
FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang)
.hover("ability.hover.boomerang"),
FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay)
.visibleWhen(this::isBoomerang),
FieldDef.section("ability.section.explosive"),
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive)
.hover("ability.hover.explosive"),
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
.visibleWhen(this::isExplosive),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
// Visual tab - Disc 1
FieldDef.section("ability.section.disc1").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(0), v -> setInnerColor(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(0), v -> setOuterColor(0, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v))
.visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(0))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(0), v -> setRotationSpeed(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(0), v -> setLightningEffect(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(0), v -> setLightningDensity(0, v))
.range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(0), v -> setLightningRadius(0, v))
.range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0))
).tab("ability.tab.visual"),
// Visual tab - Disc 2
FieldDef.section("ability.section.disc2").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(1), v -> setInnerColor(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(1), v -> setOuterColor(1, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v))
.visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(1))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(1), v -> setRotationSpeed(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(1), v -> setLightningEffect(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(1), v -> setLightningDensity(1, v))
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(1), v -> setLightningRadius(1, v))
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1))
).tab("ability.tab.visual")
));
}
@Override
@SideOnly(Side.CLIENT)
public Entity createPreviewEntity(EntityNPCInterface npc) {
if (npc == null || npc.worldObj == null) return null;
EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj);
disc.setupPreview(npc, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks);
return disc;
}
@Override
public int getPreviewActiveDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
}
@@ -66,7 +66,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
}
@Override
public boolean hasDamage() {
public boolean allowBurst() {
return false;
}
@@ -128,16 +128,22 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
lastAttacker = attacker;
lastDamageTaken = damage;
float reduction = ValueUtil.clamp(getDamageReductionFactor(), 0, damage);
float newHealth = ValueUtil.clamp(caster.getHealth() + reduction, 0, caster.getMaxHealth());
caster.setHealth(newHealth);
if (!canCounter || !counterEligible || counterTriggered || !isDirectHit(source)) {
caster.setHealth(newHealth);
// Players: damage reduction handled via LivingHurtEvent (event.ammount modification)
// NPCs: called AFTER damageEntity() so health was already reduced — heal back here
if (caster instanceof EntityPlayer) {
if (canCounter && counterEligible && !counterTriggered && isDirectHit(source)) {
counterTriggered = true;
}
} else {
caster.setHealth(ValueUtil.clamp(caster.getHealth() + damage, 0, caster.getMaxHealth()));
counterTriggered = true;
float reduction = ValueUtil.clamp(getDamageReductionFactor(), 0, damage);
float newHealth = ValueUtil.clamp(caster.getHealth() + reduction, 0, caster.getMaxHealth());
if (canCounter && counterEligible && !counterTriggered && isDirectHit(source)) {
caster.setHealth(ValueUtil.clamp(caster.getHealth() + damage, 0, caster.getMaxHealth()));
counterTriggered = true;
} else {
caster.setHealth(newHealth);
}
}
}
@@ -337,6 +343,10 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
this.counterAnimationId = counterAnimationId;
}
public boolean isCounterTriggered() {
return counterTriggered;
}
public String getCounterAnimationName() {
return counterAnimationName;
}
@@ -1,18 +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.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;
@@ -22,640 +15,165 @@ 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 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;
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;
public class AbilityHazard extends AbilityZone implements IAbilityHazard {
// Hazard-specific fields
private float radius = 2.0f;
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(300, 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.windUpAnimationName = "Ability_Zone_Windup";
this.activeAnimationName = "Ability_Zone_Active";
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public float getTelegraphRadius() {
public float getZoneRadius() {
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:
// Use telegraph position if available, apply offset
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];
// Update telegraph to show actual hazard position
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) return;
// Check if hazard duration has ended
if (tick >= durationTicks) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
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;
}
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());
// Use pre-calculated positions from telegraph phase, or generate new ones
List<double[]> positions;
if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) {
positions = new ArrayList<>(preCalculatedPositions);
preCalculatedPositions.clear();
} else {
positions = new ArrayList<>();
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);
positions.add(pos);
}
}
}
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);
for (int i = 0; i < zoneCount; i++) {
double[] pos = positions.get(i);
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;
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);
}
spawnAbilityEntity(world, entity);
activeEntities.add(entity);
}
}
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;
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");
} else if (nbt.hasKey("damagePerTick")) {
this.damagePerSecond = nbt.getFloat("damagePerTick");
} else {
this.damagePerSecond = 1.0f;
}
readZoneNBT(nbt, 300);
this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 2.0f;
this.damagePerSecond = nbt.hasKey("damagePerSecond") ? nbt.getFloat("damagePerSecond") : 1.0f;
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) {
addPresetFieldDef(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);
}
}
@@ -63,6 +63,11 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
return false;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
@@ -75,27 +80,31 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) return;
if (world.isRemote && !isPreview()) return;
getHealedAllies().clear();
if (!isPreview()) {
getHealedAllies().clear();
// Always find allies if we're healing them (needed for both instant and HoT)
if (healAllies && healRadius > 0) {
findAlliesInRadius(caster, world);
// Always find allies if we're healing them (needed for both instant and HoT)
if (healAllies && healRadius > 0) {
findAlliesInRadius(caster, world);
}
if (instantHeal) {
// Instant heal - apply all healing now
if (healSelf) {
healEntity(caster);
spawnHealParticles(world, caster);
}
for (EntityLivingBase ally : getHealedAllies()) {
healEntity(ally);
spawnHealParticles(world, ally);
}
}
}
if (instantHeal) {
// Instant heal - apply all healing now
if (healSelf) {
healEntity(caster);
spawnHealParticles(world, caster);
}
for (EntityLivingBase ally : getHealedAllies()) {
healEntity(ally);
spawnHealParticles(world, ally);
}
// Instant heal completes immediately
signalCompletion();
}
@@ -104,7 +113,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote || instantHeal) return;
if ((world.isRemote && !isPreview()) || instantHeal) return;
// Heal over time - distribute heal across duration ticks
if (tick % 10 == 0) {
@@ -68,51 +68,53 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
// Calculate forward and right vectors from caster yaw
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double forwardX = -Math.sin(yawRad);
double forwardZ = Math.cos(yawRad);
double rightX = forwardZ; // perpendicular right
double rightZ = -forwardX;
if (!isPreview()) {
// Calculate forward and right vectors from caster yaw
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double forwardX = -Math.sin(yawRad);
double forwardZ = Math.cos(yawRad);
double rightX = forwardZ; // perpendicular right
double rightZ = -forwardX;
// Search area: AABB that encompasses the rectangle
float searchDist = Math.max(hitLength, hitWidth) + 1.0f;
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
caster, caster.boundingBox.expand(searchDist, 2, searchDist));
// Search area: AABB that encompasses the rectangle
float searchDist = Math.max(hitLength, hitWidth) + 1.0f;
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
caster, caster.boundingBox.expand(searchDist, 2, searchDist));
boolean anyHit = false;
for (Entity entity : entities) {
if (!(entity instanceof EntityLivingBase) || entity == caster) continue;
EntityLivingBase livingTarget = (EntityLivingBase) entity;
boolean anyHit = false;
for (Entity entity : entities) {
if (!(entity instanceof EntityLivingBase) || entity == caster) continue;
EntityLivingBase livingTarget = (EntityLivingBase) entity;
double dx = livingTarget.posX - caster.posX;
double dz = livingTarget.posZ - caster.posZ;
double dx = livingTarget.posX - caster.posX;
double dz = livingTarget.posZ - caster.posZ;
// Project onto forward direction (must be in front, within hitLength)
double forwardDist = dx * forwardX + dz * forwardZ;
if (forwardDist < 0 || forwardDist > hitLength) continue;
// Project onto forward direction (must be in front, within hitLength)
double forwardDist = dx * forwardX + dz * forwardZ;
if (forwardDist < 0 || forwardDist > hitLength) continue;
// Project onto right direction (must be within hitWidth to each side)
double sideDist = dx * rightX + dz * rightZ;
if (Math.abs(sideDist) > hitWidth) continue;
// Project onto right direction (must be within hitWidth to each side)
double sideDist = dx * rightX + dz * rightZ;
if (Math.abs(sideDist) > hitWidth) continue;
// Entity is within the rectangle - apply damage
boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback);
if (wasHit) {
applyEffects(livingTarget);
anyHit = true;
// Entity is within the rectangle - apply damage
boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback);
if (wasHit) {
applyEffects(livingTarget);
anyHit = true;
}
}
}
// Play hit sound even if nothing was hit (the attack still happens)
if (!anyHit) {
world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f);
// Play hit sound even if nothing was hit (the attack still happens)
if (!anyHit) {
world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f);
}
}
}
@@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.RotationMode;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -12,7 +13,6 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
@@ -22,7 +22,6 @@ import noppes.npcs.api.ability.type.IAbilityLaserShot;
import java.util.Arrays;
import java.util.List;
import noppes.npcs.entity.EntityNPCInterface;
/**
* Laser Shot ability: Fast expanding thin line that pierces through targets.
@@ -40,7 +39,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
private final EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f);
private final EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100);
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Transient state for laser entity (used for movement locking)
@@ -55,6 +54,8 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
this.cooldownTicks = 0;
this.windUpTicks = 15;
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
this.rotationMode = RotationMode.TRACK;
this.rotationPhase = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
// Default built-in animations
@@ -62,6 +63,11 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
this.activeAnimationName = "Ability_Laser_Active";
}
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean isTargetingModeLocked() {
return false;
@@ -82,31 +88,55 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
return laserWidth * 2.0f; // Make telegraph slightly wider for visibility
}
/**
* Spawn laser entity.
* NPC: target is the aggro target — laser expands toward it.
* Player: target is null — laser expands in caster's look direction.
*/
private EntityAbilityLaser spawnLaserEntity(EntityLivingBase caster, EntityLivingBase target, World world) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
EntityAbilityLaser entity = new EntityAbilityLaser(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
laserWidth,
colorData, combatData, lightningData, lifespanData, trajectoryData,
expansionSpeed, lingerTicks
);
entity.setEffects(this.effects);
entity.setSourceAbility(this);
spawnAbilityEntity(world, entity);
return entity;
}
private void fireLaserEntity(EntityAbilityLaser laser, EntityLivingBase target) {
if (laser == null || laser.isDead) return;
laser.setLockVerticalDirection(true);
laser.startMoving(target);
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
if (world.isRemote && !isPreview()) return;
if (tick == 1) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
laserEntity = new EntityAbilityLaser(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
laserWidth,
colorData, combatData, lightningData, lifespanData, trajectoryData,
expansionSpeed, lingerTicks
);
laserEntity.setupCharging(anchorData, windUpTicks);
laserEntity.setEffects(this.effects);
laserEntity.setSourceAbility(this);
world.spawnEntityInWorld(laserEntity);
laserEntity = spawnLaserEntity(caster, target, world);
if (isPreview()) {
laserEntity.setupPreview(caster, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance);
} else {
laserEntity.setupCharging(anchorData, windUpTicks);
}
}
}
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
laserEntity = spawnLaserEntity(caster, target, world);
fireLaserEntity(laserEntity, target);
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
@@ -116,10 +146,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
return;
}
laserEntity.startMoving(target);
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
fireLaserEntity(laserEntity, target);
}
@Override
@@ -235,25 +262,10 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
@Override
@SideOnly(Side.CLIENT)
public Entity createPreviewEntity(EntityNPCInterface npc) {
if (npc == null || npc.worldObj == null) return null;
EntityAbilityLaser laser = new EntityAbilityLaser(npc.worldObj);
laser.setupPreview(npc, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance);
return laser;
}
@Override
public int getPreviewActiveDuration() {
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60;
}
@Override
public boolean spawnPreviewDuringWindup() {
return false; // Laser has no charging, spawns at active phase
}
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
@@ -282,6 +294,12 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.tab("ability.tab.visual"),
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"),
@@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
@@ -23,29 +22,38 @@ import noppes.npcs.api.ability.type.IAbilityOrb;
import java.util.Arrays;
import java.util.List;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
/**
* Orb ability: Spawns a homing projectile sphere that tracks target.
* The EntityAbilityOrb handles all movement, collision, and damage logic.
* This ability just configures and spawns the orb entity.
* Orb ability: Spawns homing projectile sphere(s) that track target.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
*/
public class AbilityOrb extends Ability implements IAbilityOrb {
// Ability-specific properties
private static final int MAX_PROJECTILES = 8;
// Orb properties
private float orbSize = 1.0f;
// Data classes for energy properties
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
private final EnergyCombatData combatData = new EnergyCombatData();
private final EnergyDisplayData displayData = new EnergyDisplayData();
private final EnergyHomingData homingData = new EnergyHomingData();
private final EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData();
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Projectile count and fire stagger
private int projectileCount = 1;
private int fireDelay = 0;
// Transient state for orb entity (used during windup charging)
private transient EntityAbilityOrb orbEntity = null;
// Shared visual data (all projectiles use these)
private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f);
private EnergyLightningData lightningData = new EnergyLightningData();
private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Shared combat/movement data
private final EnergyCombatData combatData = new EnergyCombatData();
private final EnergyHomingData homingData = new EnergyHomingData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData();
// Per-projectile data (anchor + optional color override)
private ProjectileData[] projectiles;
// Transient entity state
private transient EntityAbilityOrb[] orbEntities;
public AbilityOrb() {
this.typeId = "ability.cnpc.orb";
@@ -58,9 +66,101 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animation
this.windUpAnimationName = "Ability_Orb_Windup";
this.activeAnimationName = "Ability_Orb_Active";
initProjectiles(1);
}
private void initProjectiles(int count) {
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
this.projectiles = new ProjectileData[projectileCount];
for (int i = 0; i < projectileCount; i++) {
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
}
}
private AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0: return AnchorPoint.RIGHT_HAND;
case 1: return AnchorPoint.LEFT_HAND;
default: return AnchorPoint.FRONT;
}
}
public void setProjectileCount(int count) {
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
if (newCount == projectileCount) return;
ProjectileData[] oldProjectiles = projectiles;
initProjectiles(newCount);
// Preserve existing projectile data
if (oldProjectiles != null) {
int copyCount = Math.min(oldProjectiles.length, newCount);
for (int i = 0; i < copyCount; i++) {
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
}
}
}
public int getProjectileCount() {
return projectileCount;
}
public int getFireDelay() {
return fireDelay;
}
public void setFireDelay(int delay) {
this.fireDelay = Math.max(0, delay);
}
/** @deprecated Use {@link #getFireDelay()} */
public int getDualFireDelay() { return fireDelay; }
/** @deprecated Use {@link #setFireDelay(int)} */
public void setDualFireDelay(int delay) { setFireDelay(delay); }
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Orb");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilityOrb orb = (AbilityOrb) a;
a.setName("Dual Orb");
orb.setProjectileCount(2);
orb.setFireDelay(5);
a.setWindUpAnimationName("Ability_OrbDual_Windup");
a.setActiveAnimationName("Ability_OrbDual_Active");
}),
new AbilityVariant("ability.variant.barrage", a -> {
AbilityOrb orb = (AbilityOrb) a;
a.setName("Orb Barrage");
orb.setOrbSize(0.5f);
orb.setDamage(4.0f);
orb.setKnockback(0.0f);
orb.setKnockbackUp(0.0f);
a.setMaxRange(75.0f);
a.setBurstEnabled(true);
a.setBurstAmount(15);
a.setBurstDelay(5);
a.setBurstReplayAnimations(false);
a.setBurstOverlap(true);
a.setWindUpAnimationName("Ability_OrbBarrage_Windup");
a.setActiveAnimationName("Ability_OrbBarrage_Active");
})
);
}
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
@@ -73,54 +173,124 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
signalCompletion();
return;
/**
* Create orb entities.
* NPC: target is the aggro target — orbs will home toward it.
* Player: target is null — orbs fire in caster's look direction.
*/
private EntityAbilityOrb[] createOrbEntities(EntityLivingBase caster, EntityLivingBase target, World world) {
EntityAbilityOrb[] entities = new EntityAbilityOrb[projectileCount];
for (int i = 0; i < projectileCount; i++) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
entities[i] = new EntityAbilityOrb(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData);
entities[i].setEffects(this.effects);
entities[i].setSourceAbility(this);
}
// Start moving the orb that was spawned during windup
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.startMoving(target);
if (projectileCount == 2 && entities[0] != null && entities[1] != null) {
entities[0].setSiblingEntityId(entities[1].getEntityId());
entities[1].setSiblingEntityId(entities[0].getEntityId());
}
return entities;
}
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
private void fireOrbEntity(EntityAbilityOrb orb, EntityLivingBase target) {
if (orb == null || orb.isDead) return;
if (isPreview()) {
orb.startPreviewFiring();
} else {
orb.startMoving(target);
}
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
if (world.isRemote && !isPreview()) return;
// Spawn orb in charging mode on first tick of windup
if (tick == 1) {
// Create orb in charging mode - follows caster based on anchor point during windup
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
orbEntity = new EntityAbilityOrb(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
displayData, combatData, homingData, lightningData, lifespanData, trajectoryData);
orbEntity.setupCharging(anchorData, windUpTicks);
orbEntities = createOrbEntities(caster, target, world);
for (int i = 0; i < projectileCount; i++) {
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
if (isPreview()) {
orbEntities[i].setupPreview(caster, orbSize, resolved, lightningData, projectiles[i].anchor, windUpTicks);
} else {
orbEntities[i].setupCharging(projectiles[i].anchor, windUpTicks);
}
spawnAbilityEntity(world, orbEntities[i]);
}
}
}
orbEntity.setEffects(this.effects);
orbEntity.setSourceAbility(this);
world.spawnEntityInWorld(orbEntity);
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
orbEntities = createOrbEntities(caster, target, world);
for (EntityAbilityOrb orb : orbEntities) {
spawnAbilityEntity(world, orb);
}
// Fire first projectile immediately, let onActiveTick handle the rest with fireDelay
fireOrbEntity(orbEntities[0], target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireOrbEntity(orbEntities[i], target);
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (orbEntities == null) return;
// Fire first projectile immediately
fireOrbEntity(orbEntities[0], target);
// Fire remaining projectiles if no delay
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireOrbEntity(orbEntities[i], target);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (orbEntity == null || orbEntity.isDead) {
orbEntity = null;
if (orbEntities == null) {
signalCompletion();
return;
}
// Fire staggered projectiles
if (fireDelay > 0) {
for (int i = 1; i < projectileCount; i++) {
if (tick == fireDelay * i) {
fireOrbEntity(orbEntities[i], target);
}
}
}
// Check if all entities are dead
boolean allDead = true;
for (EntityAbilityOrb orb : orbEntities) {
if (orb != null && !orb.isDead) {
allDead = false;
break;
}
}
if (allDead) {
signalCompletion();
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
// Nothing to clean up - entity manages itself
}
@Override
@@ -130,20 +300,25 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
@Override
public void cleanup() {
// Despawn orb entity if still alive
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.setDead();
if (orbEntities != null) {
for (int i = 0; i < orbEntities.length; i++) {
if (orbEntities[i] != null && !orbEntities[i].isDead) {
orbEntities[i].setDead();
}
orbEntities[i] = null;
}
}
orbEntity = null;
orbEntities = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
// NPC: telegraph on aggro target position
// Player: no telegraph (orbs fire in look direction, no fixed position to mark)
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
return null;
}
// Create small circle telegraph at target position
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
@@ -151,7 +326,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
@@ -167,307 +341,270 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("orbSize", orbSize);
anchorData.writeNBT(nbt);
combatData.writeNBT(nbt);
nbt.setInteger("projectileCount", projectileCount);
nbt.setInteger("fireDelay", fireDelay);
// Shared visual data
displayData.writeNBT(nbt);
homingData.writeNBT(nbt);
lightningData.writeNBT(nbt);
// Shared combat/movement data
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
trajectoryData.writeNBT(nbt);
// Backward compat: old key was "orbSpeed"
nbt.setFloat("orbSpeed", homingData.speed);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
NBTTagCompound projNbt = new NBTTagCompound();
projectiles[i].writeNBT(projNbt);
nbt.setTag("Projectile_" + i, projNbt);
}
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
anchorData.readNBT(nbt);
int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1;
initProjectiles(count);
this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0;
// Shared combat/movement data
combatData.readNBT(nbt);
displayData.readNBT(nbt);
homingData.readNBT(nbt);
lightningData.readNBT(nbt);
lifespanData.readNBT(nbt);
trajectoryData.readNBT(nbt);
// Backward compat: old key was "orbSpeed"
if (nbt.hasKey("orbSpeed")) {
homingData.speed = nbt.getFloat("orbSpeed");
// Shared visual data
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
}
// Getters & Setters
public float getOrbSpeed() {
return homingData.speed;
// ═══════════════════════════════════════════════════════════════════
// API GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public float getOrbSpeed() { return homingData.speed; }
public void setOrbSpeed(float speed) { homingData.speed = speed; }
public float getOrbSize() { return orbSize; }
public void setOrbSize(float size) { this.orbSize = size; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float d) { lifespanData.maxDistance = d; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int t) { lifespanData.maxLifetime = t; }
public float getDamage() { return combatData.damage; }
public void setDamage(float d) { combatData.damage = d; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float k) { combatData.knockback = k; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float k) { combatData.knockbackUp = k; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean h) { homingData.homing = h; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float s) { homingData.homingStrength = s; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float r) { homingData.homingRange = r; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean e) { combatData.explosive = e; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float r) { combatData.explosionRadius = r; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float f) { combatData.explosionDamageFalloff = f; }
// Shared display data (primary colors)
public int getInnerColor() { return displayData.innerColor; }
public void setInnerColor(int c) { displayData.innerColor = c; }
public int getOuterColor() { return displayData.outerColor; }
public void setOuterColor(int c) { displayData.outerColor = c; }
public float getOuterColorWidth() { return displayData.outerColorWidth; }
public void setOuterColorWidth(float w) { displayData.outerColorWidth = w; }
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
public void setOuterColorAlpha(float a) { displayData.outerColorAlpha = a; }
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
public void setOuterColorEnabled(boolean e) { displayData.outerColorEnabled = e; }
public float getRotationSpeed() { return displayData.rotationSpeed; }
public void setRotationSpeed(float s) { displayData.rotationSpeed = s; }
// Shared lightning data
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; }
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float d) { lightningData.lightningDensity = d; }
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float r) { lightningData.lightningRadius = r; }
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
public void setLightningFadeTime(int t) { lightningData.lightningFadeTime = t; }
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
public void setAnchorPointEnum(AnchorPoint p) { projectiles[0].anchor.anchorPoint = p; }
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
@Override
public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
// Indexed API methods
public int getInnerColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.innerColor : displayData.innerColor;
}
public void setInnerColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.innerColor = color;
}
public void setOrbSpeed(float orbSpeed) {
homingData.speed = orbSpeed;
public int getOuterColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.outerColor : displayData.outerColor;
}
public void setOuterColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.outerColor = color;
}
public float getOrbSize() {
return orbSize;
}
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
public void setOrbSize(float orbSize) {
this.orbSize = orbSize;
}
public float getMaxDistance() {
return lifespanData.maxDistance;
}
public void setMaxDistance(float maxDistance) {
lifespanData.maxDistance = maxDistance;
}
public int getMaxLifetime() {
return lifespanData.maxLifetime;
}
public void setMaxLifetime(int maxLifetime) {
lifespanData.maxLifetime = maxLifetime;
}
public float getDamage() {
return combatData.damage;
}
public void setDamage(float damage) {
combatData.damage = damage;
}
public float getKnockback() {
return combatData.knockback;
}
public void setKnockback(float knockback) {
combatData.knockback = knockback;
}
public float getKnockbackUp() {
return combatData.knockbackUp;
}
public void setKnockbackUp(float knockbackUp) {
combatData.knockbackUp = knockbackUp;
}
public boolean isHoming() {
return homingData.homing;
}
public void setHoming(boolean homing) {
homingData.homing = homing;
}
public float getHomingStrength() {
return homingData.homingStrength;
}
public void setHomingStrength(float homingStrength) {
homingData.homingStrength = homingStrength;
}
public float getHomingRange() {
return homingData.homingRange;
}
public void setHomingRange(float homingRange) {
homingData.homingRange = homingRange;
}
public boolean isExplosive() {
return combatData.explosive;
}
public void setExplosive(boolean explosive) {
combatData.explosive = explosive;
}
public float getExplosionRadius() {
return combatData.explosionRadius;
}
public void setExplosionRadius(float explosionRadius) {
combatData.explosionRadius = explosionRadius;
}
public float getExplosionDamageFalloff() {
return combatData.explosionDamageFalloff;
}
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
combatData.explosionDamageFalloff = explosionDamageFalloff;
}
public int getInnerColor() {
return displayData.innerColor;
}
public void setInnerColor(int innerColor) {
displayData.innerColor = innerColor;
}
public int getOuterColor() {
return displayData.outerColor;
}
public void setOuterColor(int outerColor) {
displayData.outerColor = outerColor;
}
public float getOuterColorWidth() {
return displayData.outerColorWidth;
}
public void setOuterColorWidth(float outerColorWidth) {
displayData.outerColorWidth = outerColorWidth;
}
public float getOuterColorAlpha() {
return displayData.outerColorAlpha;
}
public void setOuterColorAlpha(float outerColorAlpha) {
displayData.outerColorAlpha = outerColorAlpha;
}
public boolean isOuterColorEnabled() {
return displayData.outerColorEnabled;
}
public void setOuterColorEnabled(boolean outerColorEnabled) {
displayData.outerColorEnabled = outerColorEnabled;
}
public float getRotationSpeed() {
return displayData.rotationSpeed;
}
public void setRotationSpeed(float rotationSpeed) {
displayData.rotationSpeed = rotationSpeed;
}
public boolean hasLightningEffect() {
return lightningData.lightningEffect;
}
public void setLightningEffect(boolean lightningEffect) {
lightningData.lightningEffect = lightningEffect;
}
public float getLightningDensity() {
return lightningData.lightningDensity;
}
public void setLightningDensity(float lightningDensity) {
lightningData.lightningDensity = lightningDensity;
}
public float getLightningRadius() {
return lightningData.lightningRadius;
}
public void setLightningRadius(float lightningRadius) {
lightningData.lightningRadius = lightningRadius;
}
public int getLightningFadeTime() {
return lightningData.lightningFadeTime;
}
public void setLightningFadeTime(int lightningFadeTime) {
lightningData.lightningFadeTime = lightningFadeTime;
}
public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; }
public float getAnchorOffsetX() { return anchorData.anchorOffsetX; }
public float getAnchorOffsetY() { return anchorData.anchorOffsetY; }
public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; }
public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; }
public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; }
public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; }
public int getAnchorPoint() {
return anchorData.anchorPoint.ordinal();
private int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
}
@Override
public void setAnchorPoint(int point) {
this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point);
}
@Override
@SideOnly(Side.CLIENT)
public Entity createPreviewEntity(EntityNPCInterface npc) {
if (npc == null || npc.worldObj == null) return null;
EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj);
orb.setupPreview(npc, orbSize, displayData, lightningData, anchorData, windUpTicks);
return orb;
}
@Override
public int getPreviewActiveDuration() {
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
// ═══════════════════════════════════════════════════════════════════
// GUI FIELD DEFINITIONS
// ═══════════════════════════════════════════════════════════════════
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
),
FieldDef.row(
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
FieldDef.section("ability.section.explosive"),
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"),
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
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),
FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.effects").tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual")
// Type tab - projectile count and fire delay
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
// Type tab - orb properties
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
));
defs.add(FieldDef.row(
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
));
defs.add(FieldDef.section("ability.section.homing"));
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
// Visual tab - shared primary colors
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
// Visual tab - shared effects
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
// Visual tab - per-projectile sections
for (int i = 0; i < MAX_PROJECTILES; i++) {
final int idx = i;
String sectionKey = "ability.section.projectile" + (i + 1);
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
.visibleWhen(() -> idx < projectileCount));
// Anchor point
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v),
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v)
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
// Color override (only when multiple projectiles)
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
}
}
}
@@ -1,614 +0,0 @@
package kamkeel.npcs.controllers.data.ability.type;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import net.minecraftforge.common.util.Constants;
import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.util.ValueUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AbilityOrbBarrage extends Ability {
// Ability-specific properties
private float orbSize = 0.5f;
private int orbAmount = 16;
private int firingSpeed = 2;
private boolean dualCharging = true;
// Data classes for energy properties
private final EnergyDisplayData colorData = new EnergyDisplayData();
private final EnergyCombatData combatData = new EnergyCombatData(4.0f, 0.0f, 0.0f, false, 1.5f, 0.5f);
private final EnergyHomingData homingData = new EnergyHomingData();
private final EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData();
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND)
};
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Array to store entities
private transient List<EntityAbilityOrb> orbEntities = new ArrayList<>();
private transient EntityAbilityOrb[] chargingEntities = null;
// Field to keep track of the currently firing orb
private int currentOrb = 0;
public AbilityOrbBarrage() {
this.typeId = "ability.cnpc.orb_barrage";
this.name = "Orb Barrage";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 75.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animation
this.windUpAnimationName = "Ability_BeamDual_Windup";
this.activeAnimationName = "Ability_BeamDual_Active";
}
// @Override
// @SideOnly(Side.CLIENT)
// public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
// return new SubGuiAbilityOrbBarrage(this, callback);
// }
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
signalCompletion();
return;
}
for (int i = 0; i < chargingEntities.length; i++) {
EntityAbilityOrb orbEntity = chargingEntities[i];
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.setDead();
}
chargingEntities[i] = null;
}
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
// Spawn orb in charging mode on first tick of windup
if (tick == 1) {
// Create orb in charging mode - follows caster based on anchor point during windup
if (dualCharging) {
chargingEntities = new EntityAbilityOrb[2];
} else {
chargingEntities = new EntityAbilityOrb[1];
}
for (int i = 0; i < chargingEntities.length; i++) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[i]);
EntityAbilityOrb orbEntity = new EntityAbilityOrb(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData
);
orbEntity.setupCharging(anchorData[i], windUpTicks);
orbEntity.setSourceAbility(this);
chargingEntities[i] = orbEntity;
world.spawnEntityInWorld(orbEntity);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (!orbEntities.isEmpty()) {
boolean allDead = true;
for (EntityAbilityOrb orbEntity : orbEntities) {
if (orbEntity != null && !orbEntity.isDead) {
allDead = false;
break;
}
}
if (allDead && currentOrb >= orbAmount) {
signalCompletion();
return;
}
}
if (currentOrb >= orbAmount) {
return;
}
int ticksPerOrb = Math.max(1, 20 / firingSpeed);
if (tick % ticksPerOrb != 0) {
return;
}
EnergyAnchorData anchor;
if (dualCharging) {
anchor = anchorData[currentOrb % 2];
} else {
anchor = anchorData[0];
}
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchor);
EntityAbilityOrb orb = new EntityAbilityOrb(
world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
orbSize, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData
);
orb.setSourceAbility(this);
world.spawnEntityInWorld(orb);
orbEntities.add(orb);
currentOrb++;
}
@Override
public boolean canInterrupt(DamageSource source) {
// Can be interrupted if the caster is hit when the beam is moving
if (!interruptible) {
return false;
}
// Only direct physical hits can interrupt, not magic, fire, or other indirect damage
if (source == null) {
return false;
}
// Reject indirect damage types
if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion()) {
return false;
}
// Reject damage without a direct attacker entity
if (source.getEntity() == null) {
return false;
}
// Direct hit from an entity - can interrupt
return true;
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
currentOrb = 0;
if (chargingEntities != null) {
for (int i = 0; i < chargingEntities.length; i++) {
if (chargingEntities[i] != null && !chargingEntities[i].isDead) {
chargingEntities[i].setDead();
}
chargingEntities[i] = null;
}
chargingEntities = null;
}
if (orbEntities != null && !orbEntities.isEmpty()) {
for (EntityAbilityOrb orbEntity : orbEntities) {
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.setDead();
}
}
orbEntities.clear();
}
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create small circle telegraph at target position
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return orbSize * 1.5f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("orbSize", orbSize);
nbt.setInteger("orbAmount", orbAmount);
nbt.setInteger("firingSpeed", firingSpeed);
nbt.setBoolean("dualCharging", dualCharging);
colorData.writeNBT(nbt);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lightningData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
trajectoryData.writeNBT(nbt);
for (int i = 0; i < anchorData.length; i++) {
NBTTagCompound comp = new NBTTagCompound();
anchorData[i].writeNBT(comp);
nbt.setTag("Anchor_" + i, comp);
}
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
this.orbAmount = nbt.hasKey("orbAmount") ? nbt.getInteger("orbAmount") : 16;
this.firingSpeed = nbt.hasKey("firingSpeed") ? nbt.getInteger("firingSpeed") : 2;
this.dualCharging = !nbt.hasKey("dualCharging") || nbt.getBoolean("dualCharging");
colorData.readNBT(nbt);
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lightningData.readNBT(nbt);
lifespanData.readNBT(nbt);
trajectoryData.readNBT(nbt);
for (int i = 0; i < anchorData.length; i++) {
if (nbt.hasKey("Anchor_" + i, Constants.NBT.TAG_COMPOUND)) {
anchorData[i].readNBT(nbt.getCompoundTag("Anchor_" + i));
} else {
anchorData[i] = new EnergyAnchorData(i == 0 ? AnchorPoint.RIGHT_HAND : AnchorPoint.LEFT_HAND);
}
}
}
// Getters & Setters
public float getOrbSpeed() {
return homingData.speed;
}
public void setOrbSpeed(float orbSpeed) {
homingData.speed = orbSpeed;
}
public float getOrbSize() {
return orbSize;
}
public void setOrbSize(float orbSize) {
this.orbSize = orbSize;
}
public int getOrbAmount() { return orbAmount; }
public void setOrbAmount(int orbAmount) { this.orbAmount = orbAmount; }
public int getFiringSpeed() { return firingSpeed; }
public void setFiringSpeed(int firingSpeed) { this.firingSpeed = firingSpeed; }
public boolean isDualCharging() { return dualCharging; }
public void setDualCharging(boolean dualCharging) { this.dualCharging = dualCharging; }
public float getMaxDistance() {
return lifespanData.maxDistance;
}
public void setMaxDistance(float maxDistance) {
lifespanData.maxDistance = maxDistance;
}
public int getMaxLifetime() {
return lifespanData.maxLifetime;
}
public void setMaxLifetime(int maxLifetime) {
lifespanData.maxLifetime = maxLifetime;
}
public float getDamage() {
return combatData.damage;
}
public void setDamage(float damage) {
combatData.damage = damage;
}
public float getKnockback() {
return combatData.knockback;
}
public void setKnockback(float knockback) {
combatData.knockback = knockback;
}
public float getKnockbackUp() {
return combatData.knockbackUp;
}
public void setKnockbackUp(float knockbackUp) {
combatData.knockbackUp = knockbackUp;
}
public boolean isHoming() {
return homingData.homing;
}
public void setHoming(boolean homing) {
homingData.homing = homing;
}
public float getHomingStrength() {
return homingData.homingStrength;
}
public void setHomingStrength(float homingStrength) {
homingData.homingStrength = homingStrength;
}
public float getHomingRange() {
return homingData.homingRange;
}
public void setHomingRange(float homingRange) {
homingData.homingRange = homingRange;
}
public boolean isExplosive() {
return combatData.explosive;
}
public void setExplosive(boolean explosive) {
combatData.explosive = explosive;
}
public float getExplosionRadius() {
return combatData.explosionRadius;
}
public void setExplosionRadius(float explosionRadius) {
combatData.explosionRadius = explosionRadius;
}
public float getExplosionDamageFalloff() {
return combatData.explosionDamageFalloff;
}
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
combatData.explosionDamageFalloff = explosionDamageFalloff;
}
public int getInnerColor() {
return colorData.innerColor;
}
public void setInnerColor(int innerColor) {
colorData.innerColor = innerColor;
}
public int getOuterColor() {
return colorData.outerColor;
}
public void setOuterColor(int outerColor) {
colorData.outerColor = outerColor;
}
public float getOuterColorWidth() {
return colorData.outerColorWidth;
}
public void setOuterColorWidth(float outerColorWidth) {
colorData.outerColorWidth = outerColorWidth;
}
public float getOuterColorAlpha() {
return colorData.outerColorAlpha;
}
public void setOuterColorAlpha(float outerColorAlpha) {
colorData.outerColorAlpha = outerColorAlpha;
}
public boolean isOuterColorEnabled() {
return colorData.outerColorEnabled;
}
public void setOuterColorEnabled(boolean outerColorEnabled) {
colorData.outerColorEnabled = outerColorEnabled;
}
public float getRotationSpeed() {
return colorData.rotationSpeed;
}
public void setRotationSpeed(float rotationSpeed) {
colorData.rotationSpeed = rotationSpeed;
}
public boolean hasLightningEffect() {
return lightningData.lightningEffect;
}
public void setLightningEffect(boolean lightningEffect) {
lightningData.lightningEffect = lightningEffect;
}
public float getLightningDensity() {
return lightningData.lightningDensity;
}
public void setLightningDensity(float lightningDensity) {
lightningData.lightningDensity = lightningDensity;
}
public float getLightningRadius() {
return lightningData.lightningRadius;
}
public void setLightningRadius(float lightningRadius) {
lightningData.lightningRadius = lightningRadius;
}
public int getLightningFadeTime() {
return lightningData.lightningFadeTime;
}
public void setLightningFadeTime(int lightningFadeTime) {
lightningData.lightningFadeTime = lightningFadeTime;
}
private EnergyAnchorData getAnchorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return anchorData[orb];
}
public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; }
public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; }
public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; }
public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; }
public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; }
public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; }
public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; }
public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; }
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
//@Override
public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); }
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
//@Override
public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
@Override
@SideOnly(Side.CLIENT)
public Entity createPreviewEntity(EntityNPCInterface npc) {
if (npc == null || npc.worldObj == null) return null;
EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj);
orb.setupPreview(npc, orbSize, colorData, lightningData, anchorData[0], windUpTicks);
return orb;
}
@Override
public int getPreviewActiveDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
),
FieldDef.row(
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.barrage"),
FieldDef.row(
FieldDef.intField("ability.amount", this::getOrbAmount, this::setOrbAmount),
FieldDef.intField("ability.firingSpeed", this::getFiringSpeed, this::setFiringSpeed)
),
FieldDef.boolField("ability.dualCharge", this::isDualCharging, this::setDualCharging),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
FieldDef.section("ability.section.explosive"),
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"),
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
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),
FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.effects").tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.range(0.01f, 100f).visibleWhen(this::hasLightningEffect),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual")
));
}
}
@@ -1,586 +0,0 @@
package kamkeel.npcs.controllers.data.ability.type;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import noppes.npcs.client.gui.builder.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.util.ValueUtil;
import java.util.Arrays;
import java.util.List;
public class AbilityOrbDual extends Ability {
// Ability-specific properties
private float orbSize = 1.0f;
private boolean dualFire= true;
private int dualFireDelay = 5;
// Data classes for energy properties
public final EnergyDisplayData[] displayData = new EnergyDisplayData[]{
new EnergyDisplayData(), new EnergyDisplayData()
};
public final EnergyCombatData combatData = new EnergyCombatData();
public final EnergyHomingData homingData = new EnergyHomingData();
public final EnergyLightningData[] lightningData = new EnergyLightningData[]{
new EnergyLightningData(), new EnergyLightningData()
};
public final EnergyLifespanData lifespanData = new EnergyLifespanData();
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND)
};
private EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{
new EnergyTrajectoryData(), new EnergyTrajectoryData()
};
// Transient state for orb entity (used during windup charging)
private transient EntityAbilityOrb orbEntity = null;
private transient EntityAbilityOrb orbEntity2 = null;
public AbilityOrbDual() {
this.typeId = "ability.cnpc.orb_dual";
this.name = "Orb Dual";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 25.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animation
this.windUpAnimationName = "Ability_OrbDual_Windup";
this.activeAnimationName = "Ability_OrbDual_Active";
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
signalCompletion();
return;
}
// Start moving the orb that was spawned during windup
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.startMoving(target);
}
if (dualFire) {
if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) {
orbEntity2.startMoving(target);
}
} else if (orbEntity2 != null) {
orbEntity2.setDead();
}
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
// Spawn orb in charging mode on first tick of windup
if (tick == 1) {
// Create orb in charging mode - follows caster based on anchor point during windup
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]);
orbEntity = new EntityAbilityOrb(
world, caster, target,
spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, orbSize,
displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0]);
Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]);
orbEntity2 = new EntityAbilityOrb(
world, caster, target,
spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, orbSize,
displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1]);
orbEntity.setSiblingEntityId(orbEntity2.getEntityId());
orbEntity2.setSiblingEntityId(orbEntity.getEntityId());
orbEntity.setupCharging(anchorData[0], windUpTicks);
orbEntity2.setupCharging(anchorData[1], windUpTicks);
orbEntity.setEffects(this.effects);
if (dualFire) {
orbEntity2.setEffects(this.effects);
}
orbEntity.setSourceAbility(this);
orbEntity2.setSourceAbility(this);
world.spawnEntityInWorld(orbEntity);
world.spawnEntityInWorld(orbEntity2);
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (dualFire) {
if ((orbEntity == null || orbEntity.isDead) && (orbEntity2 == null || orbEntity2.isDead)) {
orbEntity = null;
orbEntity2 = null;
signalCompletion();
}
if (tick == dualFireDelay && orbEntity2 != null && !orbEntity2.isDead) {
orbEntity2.startMoving(target);
}
} else {
if (orbEntity == null || orbEntity.isDead) {
orbEntity = null;
signalCompletion();
}
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
@Override
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
// Despawn orb entity if still alive
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.setDead();
}
if (orbEntity2 != null && !orbEntity2.isDead) {
orbEntity2.setDead();
}
orbEntity = null;
orbEntity2 = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create small circle telegraph at target position
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return orbSize * 1.5f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("orbSize", orbSize);
nbt.setBoolean("dualFire", dualFire);
nbt.setInteger("dualFireDelay", dualFireDelay);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
// Backward compat: old key was "orbSpeed"
nbt.setFloat("orbSpeed", homingData.speed);
NBTTagCompound orb1 = new NBTTagCompound();
NBTTagCompound orb2 = new NBTTagCompound();
for (int i = 0; i < 2; i++) {
NBTTagCompound orb = i == 0 ? orb1 : orb2;
anchorData[i].writeNBT(orb);
displayData[i].writeNBT(orb);
lightningData[i].writeNBT(orb);
trajectoryData[i].writeNBT(orb);
}
nbt.setTag("Orb_1", orb1);
nbt.setTag("Orb_2", orb2);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
NBTTagCompound orb1 = nbt.hasKey("Orb_1") ? nbt.getCompoundTag("Orb_1") : new NBTTagCompound();
NBTTagCompound orb2 = nbt.hasKey("Orb_2") ? nbt.getCompoundTag("Orb_2") : new NBTTagCompound();
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0;
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
// Backward compat: old key was "orbSpeed"
if (nbt.hasKey("orbSpeed")) {
homingData.speed = nbt.getFloat("orbSpeed");
}
for (int i = 0; i < 2; i++) {
NBTTagCompound orb = i == 0 ? orb1 : orb2;
anchorData[i].readNBT(orb);
displayData[i].readNBT(orb);
lightningData[i].readNBT(orb);
trajectoryData[i].readNBT(orb);
}
}
// Getters & Setters
private EnergyDisplayData getColorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return displayData[orb];
}
private EnergyLightningData getLightningData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return lightningData[orb];
}
private EnergyAnchorData getAnchorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return anchorData[orb];
}
public float getOrbSpeed() {
return homingData.speed;
}
public void setOrbSpeed(float orbSpeed) {
homingData.speed = orbSpeed;
}
public float getOrbSize() {
return orbSize;
}
public void setOrbSize(float orbSize) {
this.orbSize = orbSize;
}
public boolean isDualFire() {
return dualFire;
}
public void setDualFire(boolean dualFire) {
this.dualFire = dualFire;
}
public int getDualFireDelay() {
return dualFireDelay;
}
public void setDualFireDelay(int dualFireDelay) {
this.dualFireDelay = dualFireDelay;
}
public float getMaxDistance() {
return lifespanData.maxDistance;
}
public void setMaxDistance(float maxDistance) {
lifespanData.maxDistance = maxDistance;
}
public int getMaxLifetime() {
return lifespanData.maxLifetime;
}
public void setMaxLifetime(int maxLifetime) {
lifespanData.maxLifetime = maxLifetime;
}
public float getDamage() {
return combatData.damage;
}
public void setDamage(float damage) {
combatData.damage = damage;
}
public float getKnockback() {
return combatData.knockback;
}
public void setKnockback(float knockback) {
combatData.knockback = knockback;
}
public float getKnockbackUp() {
return combatData.knockbackUp;
}
public void setKnockbackUp(float knockbackUp) {
combatData.knockbackUp = knockbackUp;
}
public boolean isHoming() {
return homingData.homing;
}
public void setHoming(boolean homing) {
homingData.homing = homing;
}
public float getHomingStrength() {
return homingData.homingStrength;
}
public void setHomingStrength(float homingStrength) {
homingData.homingStrength = homingStrength;
}
public float getHomingRange() {
return homingData.homingRange;
}
public void setHomingRange(float homingRange) {
homingData.homingRange = homingRange;
}
public boolean isExplosive() {
return combatData.explosive;
}
public void setExplosive(boolean explosive) {
combatData.explosive = explosive;
}
public float getExplosionRadius() {
return combatData.explosionRadius;
}
public void setExplosionRadius(float explosionRadius) {
combatData.explosionRadius = explosionRadius;
}
public float getExplosionDamageFalloff() {
return combatData.explosionDamageFalloff;
}
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
combatData.explosionDamageFalloff = explosionDamageFalloff;
}
public int getInnerColor(int orb) { return getColorData(orb).innerColor; }
public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; }
public int getOuterColor(int orb) { return getColorData(orb).outerColor; }
public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; }
public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; }
public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; }
public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; }
public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; }
public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; }
public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; }
public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; }
public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; }
public int getInnerColor() { return getInnerColor(0); }
public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); }
public int getOuterColor() { return getOuterColor(0); }
public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); }
public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); }
public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); }
public float getOuterColorWidth() { return getOuterColorWidth(0); }
public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); }
public float getOuterColorAlpha() { return getOuterColorAlpha(0); }
public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); }
public float getRotationSpeed() { return getRotationSpeed(0); }
public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); }
public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; }
public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; }
public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; }
public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; }
public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; }
public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; }
public boolean hasLightningEffect() { return hasLightningEffect(0); }
public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); }
public float getLightningDensity() { return getLightningDensity(0); }
public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); }
public float getLightningRadius() { return getLightningRadius(0); }
public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); }
public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; }
public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; }
public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; }
public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; }
public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; }
public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; }
public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; }
public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; }
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
//@Override
public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); }
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
//@Override
public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
),
FieldDef.row(
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.dual"),
FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire),
FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay)
.visibleWhen(this::isDualFire),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
FieldDef.section("ability.section.explosive"),
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"),
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
// Visual tab - Orb 1
FieldDef.section("ability.section.orb1").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(0), v -> setInnerColor(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(0), v -> setOuterColor(0, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v))
.visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(0))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(0), v -> setRotationSpeed(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(0), v -> setLightningEffect(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(0), v -> setLightningDensity(0, v))
.range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(0), v -> setLightningRadius(0, v))
.range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0))
).tab("ability.tab.visual"),
// Visual tab - Orb 2
FieldDef.section("ability.section.orb2").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(1), v -> setInnerColor(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(1), v -> setOuterColor(1, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v))
.visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(1))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(1), v -> setRotationSpeed(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(1), v -> setLightningEffect(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(1), v -> setLightningDensity(1, v))
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(1), v -> setLightningRadius(1, v))
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1))
).tab("ability.tab.visual")
));
}
@Override
@SideOnly(Side.CLIENT)
public Entity createPreviewEntity(EntityNPCInterface npc) {
if (npc == null || npc.worldObj == null) return null;
EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj);
orb.setupPreview(npc, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks);
return orb;
}
@Override
public int getPreviewActiveDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
}
@@ -9,6 +9,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
@@ -50,6 +51,11 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
this.showTelegraph = false;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
@@ -62,61 +68,129 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote || target == null) return;
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (isPlayerCaster(caster)) {
// Player: instant hit-scan in look direction
executePlayerProjectile(caster, world);
} else if (target != null) {
// NPC: instant hit-scan on aggro target
executeNpcProjectile(caster, target, world);
}
signalCompletion();
}
/**
* NPC projectile: instant hit-scan damage on the aggro target.
*/
private void executeNpcProjectile(EntityLivingBase caster, EntityLivingBase target, World world) {
if (isPreview()) return;
// Calculate direction to target
double dx = target.posX - caster.posX;
double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight());
double dz = target.posZ - caster.posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) { dx /= len; dy /= len; dz /= len; }
if (len > 0) {
dx /= len;
dy /= len;
dz /= len;
}
// Deal instant damage with scripted event support
// TODO: Use custom EntityAbilityProjectile for actual tracking
applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz);
// Play sound
world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f);
// Handle splash damage for explosive projectiles
if (explosive && explosionRadius > 0) {
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(target,
target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius));
applyExplosionDamage(caster, target, world, target.posX, target.posY, target.posZ);
}
for (Entity entity : entities) {
if (entity instanceof EntityLivingBase && entity != caster) {
EntityLivingBase living = (EntityLivingBase) entity;
float dist = target.getDistanceToEntity(living);
if (dist < explosionRadius) {
float falloff = 1.0f - (dist / explosionRadius);
// Apply splash damage with scripted event support (no knockback)
applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0);
}
}
}
spawnProjectileParticles(world, caster, target);
}
// Explosion particles
world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f);
for (int i = 0; i < 10; i++) {
world.spawnParticle("explode",
target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius,
target.posY + world.rand.nextDouble() * target.height,
target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius,
0, 0.1, 0);
/**
* Player projectile: fires instant hit-scan in look direction.
* Raycasts for the first entity in range and applies damage.
*/
private void executePlayerProjectile(EntityLivingBase caster, World world) {
if (isPreview()) return;
Vec3 look = caster.getLookVec();
double eyeX = caster.posX;
double eyeY = caster.posY + caster.getEyeHeight();
double eyeZ = caster.posZ;
// Raycast: find the closest entity along look direction
EntityLivingBase hitTarget = null;
double closestDist = maxRange;
@SuppressWarnings("unchecked")
List<Entity> candidates = world.getEntitiesWithinAABBExcludingEntity(caster,
caster.boundingBox.expand(maxRange, maxRange, maxRange));
for (Entity entity : candidates) {
if (!(entity instanceof EntityLivingBase)) continue;
if (entity.isDead) continue;
// Check if entity is roughly along look direction
double dx = entity.posX - eyeX;
double dy = (entity.posY + entity.height / 2) - eyeY;
double dz = entity.posZ - eyeZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist > maxRange || dist < 0.5) continue;
// Dot product to check alignment with look vector
double dot = (dx * look.xCoord + dy * look.yCoord + dz * look.zCoord) / dist;
if (dot < 0.95) continue; // Must be within ~18 degree cone
if (dist < closestDist) {
closestDist = dist;
hitTarget = (EntityLivingBase) entity;
}
}
// Spawn projectile particles (visual trail)
spawnProjectileParticles(world, caster, target);
world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f);
// Projectile is instant
signalCompletion();
if (hitTarget != null) {
applyAbilityDamageWithDirection(caster, hitTarget, damage, knockback, look.xCoord, look.zCoord);
if (explosive && explosionRadius > 0) {
applyExplosionDamage(caster, hitTarget, world, hitTarget.posX, hitTarget.posY, hitTarget.posZ);
}
spawnProjectileParticles(world, caster, hitTarget);
} else {
// No hit — spawn particles along look direction anyway
spawnProjectileParticlesInDirection(world, caster, look);
}
}
/**
* Apply explosion splash damage around a point.
*/
private void applyExplosionDamage(EntityLivingBase caster, EntityLivingBase primaryTarget, World world,
double x, double y, double z) {
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(primaryTarget,
primaryTarget.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius));
for (Entity entity : entities) {
if (entity instanceof EntityLivingBase && entity != caster) {
EntityLivingBase living = (EntityLivingBase) entity;
float dist = primaryTarget.getDistanceToEntity(living);
if (dist < explosionRadius) {
float falloff = 1.0f - (dist / explosionRadius);
applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0);
}
}
}
world.playSoundAtEntity(primaryTarget, "random.explode", 0.5f, 1.0f);
for (int i = 0; i < 10; i++) {
world.spawnParticle("explode",
x + (world.rand.nextDouble() - 0.5) * explosionRadius,
y + world.rand.nextDouble() * 2,
z + (world.rand.nextDouble() - 0.5) * explosionRadius,
0, 0.1, 0);
}
}
private void spawnProjectileParticles(World world, EntityLivingBase caster, EntityLivingBase target) {
@@ -150,6 +224,31 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
}
}
/**
* Spawn projectile particles along a direction (player miss — no entity hit).
*/
private void spawnProjectileParticlesInDirection(World world, EntityLivingBase caster, Vec3 look) {
double startX = caster.posX;
double startY = caster.posY + caster.getEyeHeight();
double startZ = caster.posZ;
String particle = "flame";
if (projectileType.equals("arrow")) {
particle = "crit";
} else if (projectileType.equals("magic")) {
particle = "witchMagic";
}
int steps = (int) Math.max(5, maxRange * 2);
for (int i = 0; i < steps; i++) {
double progress = (double) i / steps * maxRange;
double px = startX + look.xCoord * progress;
double py = startY + look.yCoord * progress;
double pz = startZ + look.zCoord * progress;
world.spawnParticle(particle, px, py, pz, 0, 0, 0);
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Projectile is instant, nothing to do per-tick
@@ -35,7 +35,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
this.typeId = "ability.cnpc.shockwave";
this.name = "Shockwave";
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 15.0f;
this.maxRange = 8.0f;
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
this.cooldownTicks = 0;
this.windUpTicks = 25;
@@ -63,48 +63,50 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
// Shockwave is instant - apply effect immediately after windup
if (!isPreview()) {
// Shockwave is instant - apply effect immediately after windup
// Get all entities in radius
AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
// Get all entities in radius
AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
int count = 0;
for (EntityLivingBase entity : entities) {
if (entity == caster) continue;
if (entity.isDead) continue;
int count = 0;
for (EntityLivingBase entity : entities) {
if (entity == caster) continue;
if (entity.isDead) continue;
double dist = caster.getDistanceToEntity(entity);
if (dist > pushRadius) continue;
double dist = caster.getDistanceToEntity(entity);
if (dist > pushRadius) continue;
count++;
if (count > maxTargets) break;
count++;
if (count > maxTargets) break;
// Calculate push direction (away from caster)
double dx = entity.posX - caster.posX;
double dz = entity.posZ - caster.posZ;
double len = Math.sqrt(dx * dx + dz * dz);
// Calculate push direction (away from caster)
double dx = entity.posX - caster.posX;
double dz = entity.posZ - caster.posZ;
double len = Math.sqrt(dx * dx + dz * dz);
if (len > 0) {
dx /= len;
dz /= len;
} else {
// Entity is directly on top of caster, push in random direction
double angle = Math.random() * Math.PI * 2;
dx = Math.cos(angle);
dz = Math.sin(angle);
}
if (len > 0) {
dx /= len;
dz /= len;
} else {
// Entity is directly on top of caster, push in random direction
double angle = Math.random() * Math.PI * 2;
dx = Math.cos(angle);
dz = Math.sin(angle);
}
// Scale knockback by distance (closer = stronger)
float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f;
float finalPush = pushStrength * distFactor;
// Apply damage with custom knockback direction
boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz);
// Scale knockback by distance (closer = stronger)
float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f;
float finalPush = pushStrength * distFactor;
// Apply damage with custom knockback direction
boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz);
// Apply effects if hit connected
if (wasHit) {
applyEffects(entity);
// Apply effects if hit connected
if (wasHit) {
applyEffects(entity);
}
}
}
}
@@ -85,51 +85,74 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Update target position during windup
if (targetingMode == TargetingMode.AOE_SELF) {
// AOE_SELF: slam at caster's current position
if (isPlayerCaster(caster)) {
// Player: slam will launch from current position, no target tracking needed
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
// AOE_TARGET: telegraph follows target via setEntityIdToFollow
targetX = target.posX;
targetY = target.posY;
targetZ = target.posZ;
} else {
// NPC: update target position during windup for telegraph tracking
if (targetingMode == TargetingMode.AOE_SELF) {
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
targetX = target.posX;
targetY = target.posY;
targetZ = target.posZ;
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
// Lock in the destination at moment of launch
hasLaunched = false;
hasLanded = false;
airTicks = 0;
caster.fallDistance = 0;
if (isPlayerCaster(caster)) {
executePlayerSlam(caster);
} else {
executeNpcSlam(caster, target);
}
}
/**
* NPC slam: Lock destination based on targeting mode, then launch in a ballistic arc toward it.
*/
private void executeNpcSlam(EntityLivingBase caster, EntityLivingBase target) {
if (targetingMode == TargetingMode.AOE_SELF) {
// AOE_SELF: slam in place (just jump up)
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
// AOE_TARGET: leap to target's current position
targetX = target.posX;
targetY = target.posY;
targetZ = target.posZ;
} else {
// Fallback: slam in place
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
}
hasLaunched = false;
hasLanded = false;
airTicks = 0;
// Reset fall distance to prevent fall damage during slam
caster.fallDistance = 0;
// Calculate and apply leap velocity
launchTowardTarget(caster);
}
/**
* Player slam: Launch straight up. Player controls horizontal movement via WASD.
* AOE damage triggers wherever the player lands.
*/
private void executePlayerSlam(EntityLivingBase caster) {
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
double vy = calculateLaunchVelocity(Math.max(1.0, leapHeight));
caster.motionY = vy;
hasLaunched = true;
caster.velocityChanged = true;
}
/**
* Calculate the initial vertical velocity needed to reach a target height
* in Minecraft's physics (gravity=0.08, vertical drag=0.98).
@@ -187,11 +210,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
caster.motionZ = 0;
caster.motionY = vy;
hasLaunched = true;
if (caster instanceof EntityNPCInterface) {
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(true);
}
caster.velocityChanged = true;
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
if (!isPreview()) {
caster.velocityChanged = true;
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
}
return;
}
@@ -233,10 +258,12 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
caster.motionY = vy;
hasLaunched = true;
if (caster instanceof EntityNPCInterface) {
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(true);
}
caster.velocityChanged = true;
if (!isPreview()) {
caster.velocityChanged = true;
}
// Face the target
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
@@ -252,7 +279,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
airTicks++;
// Continuously reset fall distance during slam to prevent fall damage
caster.fallDistance = 0;
if (!isPreview()) {
caster.fallDistance = 0;
}
// Check for landing
if (caster.onGround && airTicks > 3) {
@@ -267,12 +296,15 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
return;
}
// While in air, face toward target
double dx = targetX - caster.posX;
double dz = targetZ - caster.posZ;
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
caster.rotationYaw = targetYaw;
caster.rotationYawHead = targetYaw;
// NPC: face toward target destination while in air
// Player: free look — player controls their own camera
if (!isPreview() && !isPlayerCaster(caster)) {
double dx = targetX - caster.posX;
double dz = targetZ - caster.posZ;
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
caster.rotationYaw = targetYaw;
caster.rotationYawHead = targetYaw;
}
}
/**
@@ -280,22 +312,25 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
*/
private void onLanding(EntityLivingBase caster, World world) {
hasLanded = true;
if (caster instanceof EntityNPCInterface) {
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(false);
}
// Signal that the ability has completed its active phase
signalCompletion();
// Reset fall distance to prevent fall damage on landing
caster.fallDistance = 0;
// Stop horizontal momentum
caster.motionX = 0;
caster.motionZ = 0;
caster.velocityChanged = true;
if (world.isRemote) return;
if (!isPreview()) {
// Reset fall distance to prevent fall damage on landing
caster.fallDistance = 0;
caster.velocityChanged = true;
}
if (world.isRemote && !isPreview()) return;
if (isPreview()) return;
// Play slam impact sound on landing
world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f);
@@ -361,7 +396,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
if (caster instanceof EntityNPCInterface) {
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(false);
}
hasLaunched = false;
@@ -371,7 +406,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
if (caster instanceof EntityNPCInterface) {
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(false);
}
hasLaunched = false;
@@ -387,97 +422,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
airTicks = 0;
}
// ==================== PREVIEW MODE ====================
private transient double previewVelX, previewVelY, previewVelZ;
private transient boolean previewLaunched = false;
private transient double previewGroundY;
@Override
@SideOnly(Side.CLIENT)
public void onPreviewExecute(EntityNPCInterface npc) {
previewLaunched = false;
double tx, ty, tz;
if (previewTarget != null && targetingMode == TargetingMode.AOE_TARGET) {
tx = previewTarget.posX;
ty = previewTarget.posY;
tz = previewTarget.posZ;
} else {
tx = npc.posX;
ty = npc.posY;
tz = npc.posZ;
}
previewGroundY = npc.posY;
double dx = tx - npc.posX;
double dz = tz - npc.posZ;
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
// Calculate vertical velocity for exact leapHeight
double arcHeight = Math.max(1.0, leapHeight);
double vy = calculateLaunchVelocity(arcHeight);
if (horizontalDist < 0.5) {
previewVelX = 0;
previewVelZ = 0;
previewVelY = vy;
} else {
if (horizontalDist > maxRange) {
double scale = maxRange / horizontalDist;
dx *= scale;
dz *= scale;
horizontalDist = maxRange;
}
double speedFactor = Math.max(0.1, leapSpeed);
int flightTicks = (int) Math.max(10, Math.min(horizontalDist * 1.5 / speedFactor, 40));
double drag = 0.91;
double dragPowN = Math.pow(drag, flightTicks);
double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN);
double dirX = dx / horizontalDist;
double dirZ = dz / horizontalDist;
previewVelX = dirX * vHorizontal;
previewVelZ = dirZ * vHorizontal;
previewVelY = vy;
}
previewLaunched = true;
}
@Override
@SideOnly(Side.CLIENT)
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
if (!previewLaunched) return;
// Apply velocity
npc.prevPosX = npc.posX;
npc.prevPosY = npc.posY;
npc.prevPosZ = npc.posZ;
npc.posX += previewVelX;
npc.posY += previewVelY;
npc.posZ += previewVelZ;
// Gravity and drag
previewVelY -= 0.08;
previewVelY *= 0.98;
previewVelX *= 0.91;
previewVelZ *= 0.91;
// Ground clamp - stop falling below starting Y
if (npc.posY < previewGroundY && previewVelY < 0) {
npc.posY = previewGroundY;
previewVelY = 0;
previewVelX = 0;
previewVelZ = 0;
previewLaunched = false;
}
}
@Override
public int getPreviewActiveDuration() {
public int getMaxPreviewDuration() {
return 60;
}
@@ -519,11 +465,11 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
// AOE_TARGET: telegraph follows target during windup
if (targetingMode == TargetingMode.AOE_TARGET && target != null) {
// NPC AOE_TARGET: telegraph follows target during windup
// Player: telegraph stays at caster position (no target to follow)
if (!isPlayerCaster(caster) && targetingMode == TargetingMode.AOE_TARGET && target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
// AOE_SELF: telegraph stays at caster position, no follow
return instance;
}
@@ -61,6 +61,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
this.showTelegraph = true;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
@@ -73,19 +78,26 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
// Spawn the entity that handles BOTH visuals AND damage
// Spawn the sweeper entity that handles BOTH visuals AND damage.
// NPC: target is the aggro target — lockOnTarget tracks it during sweep.
// Player: target is null — sweep rotates around caster's facing direction, lockOnTarget has no effect.
activeEntity = new EntityAbilitySweeper(world, caster, target,
beamLength, beamWidth, beamHeight,
colorData,
sweepSpeed, numberOfRotations,
damage, damageInterval, piercing,
lockOnTarget);
world.spawnEntityInWorld(activeEntity);
if (isPreview()) {
activeEntity.setupPreview(caster);
}
spawnAbilityEntity(world, activeEntity);
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
@@ -200,6 +212,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; }
@Override
public int getMaxPreviewDuration() {
return (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10;
}
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
@@ -3,6 +3,7 @@ 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.TelegraphType;
import net.minecraft.block.Block;
import net.minecraft.entity.Entity;
@@ -72,6 +73,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
this.typeId = "ability.cnpc.teleport";
this.name = "Teleport";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.allowedBy = UserType.NPC_ONLY;
this.maxRange = 30.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
@@ -84,11 +86,6 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
this.activeSound = "mob.endermen.portal";
}
@Override
public boolean hasDamage() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
@@ -128,7 +125,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
}
private void performBlink(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote) return;
if (world.isRemote && !isPreview()) return;
double oldX = caster.posX;
double oldY = caster.posY;
@@ -137,36 +134,47 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
Vec3 destination = calculateDestination(caster, target, world);
if (destination == null) return;
boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight;
if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ,
destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) {
destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ,
if (!isPreview()) {
boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight;
if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ,
destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) {
destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ,
destination.xCoord, destination.yCoord, destination.zCoord);
if (destination == null) return;
}
destination = findSafeDestination(world, oldX, oldY, oldZ,
destination.xCoord, destination.yCoord, destination.zCoord);
if (destination == null) return;
// Damage at origin
if (damageAtStart) {
dealDamageAt(caster, world, oldX, oldY, oldZ);
}
// Spawn particles at origin
spawnTeleportParticles(world, oldX, oldY, oldZ);
}
destination = findSafeDestination(world, oldX, oldY, oldZ,
destination.xCoord, destination.yCoord, destination.zCoord);
if (destination == null) return;
// Damage at origin
if (damageAtStart) {
dealDamageAt(caster, world, oldX, oldY, oldZ);
}
// Spawn particles at origin
spawnTeleportParticles(world, oldX, oldY, oldZ);
// Teleport
caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord);
if (isPreview()) {
caster.setPosition(destination.xCoord, destination.yCoord, destination.zCoord);
caster.prevPosX = destination.xCoord;
caster.prevPosY = destination.yCoord;
caster.prevPosZ = destination.zCoord;
} else {
caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord);
}
caster.fallDistance = 0;
// Spawn particles at destination
spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord);
if (!isPreview()) {
// Spawn particles at destination
spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord);
// Damage at destination
if (damageAtEnd) {
dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord);
// Damage at destination
if (damageAtEnd) {
dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord);
}
}
// Face target after teleport
@@ -217,26 +225,8 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
}
}
// No target (player use) - teleport in caster's look direction
Vec3 look = caster.getLookVec();
switch (mode) {
case BEHIND:
// Teleport forward in look direction by behindDistance
newX = caster.posX + look.xCoord * behindDistance;
newZ = caster.posZ + look.zCoord * behindDistance;
newY = caster.posY;
return Vec3.createVectorHelper(newX, newY, newZ);
case SINGLE:
case BLINK:
default:
// Teleport forward in look direction within blinkRadius
double dist = Math.min(blinkRadius, maxRange);
double offset = (RANDOM.nextDouble() * 0.5 + 0.5) * dist; // 50-100% of max distance
newX = caster.posX + look.xCoord * offset;
newZ = caster.posZ + look.zCoord * offset;
newY = caster.posY;
return Vec3.createVectorHelper(newX, newY, newZ);
}
// NPC-only ability — should not reach here
return null;
}
private Vec3 findSafeDestination(World world, double oldX, double oldY, double oldZ,
@@ -446,15 +436,6 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
} catch (Exception e) {
this.mode = TeleportMode.BLINK;
}
} else if (nbt.hasKey("pattern")) {
String legacy = nbt.getString("pattern");
if ("BEHIND_TARGET".equals(legacy)) {
this.mode = TeleportMode.BEHIND;
} else if ("RANDOM".equals(legacy)) {
this.mode = TeleportMode.BLINK;
} else {
this.mode = TeleportMode.SINGLE;
}
} else {
this.mode = TeleportMode.BLINK;
}
@@ -561,6 +542,12 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
this.damageRadius = damageRadius;
}
@Override
public int getMaxPreviewDuration() {
int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1;
return blinkLimit * blinkDelayTicks + 10;
}
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
@@ -1,16 +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.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;
@@ -20,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;
@@ -61,235 +36,76 @@ 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(300, 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.windUpTicks = 30;
this.windUpAnimationName = "Ability_Zone_Windup";
this.activeAnimationName = "Ability_Zone_Active";
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public float getTelegraphRadius() {
public float getZoneRadius() {
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:
// Telegraph follows target during windup, locks on execute
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:
// Use telegraph position with offset
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];
// Update telegraph to show actual trap position
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;
}
return;
}
activeEntities.clear();
ticksSinceLastTrigger++;
if (maxTriggers > 0 && triggerCount >= maxTriggers) {
signalCompletion(); // All triggers used, trap is done
return;
}
if (ticksSinceLastTrigger < triggerCooldown) {
return;
}
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;
}
}
}
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);
}
}
// Use pre-calculated positions from telegraph phase, or generate new ones
List<double[]> positions;
if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) {
positions = new ArrayList<>(preCalculatedPositions);
preCalculatedPositions.clear();
} 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);
positions = new ArrayList<>();
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);
positions.add(pos);
}
}
for (int i = 0; i < zoneCount; i++) {
double[] pos = positions.get(i);
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);
}
spawnAbilityEntity(world, entity);
activeEntities.add(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);
@@ -298,20 +114,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, 300);
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;
@@ -320,168 +127,60 @@ 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) {
addPresetFieldDef(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(
@@ -491,5 +190,7 @@ 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);
}
}
@@ -84,29 +84,32 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
pullComplete = false;
ticksSincePullDamage = 0;
if (aoe) {
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
if (!isPreview()) {
if (aoe) {
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
int count = 0;
for (EntityLivingBase entity : entities) {
if (entity == caster) continue;
if (entity.isDead) continue;
int count = 0;
for (EntityLivingBase entity : entities) {
if (entity == caster) continue;
if (entity.isDead) continue;
double dist = caster.getDistanceToEntity(entity);
if (dist <= pullRadius) {
getPulledEntities().add(entity.getUniqueID());
count++;
if (count >= maxTargets) break;
double dist = caster.getDistanceToEntity(entity);
if (dist <= pullRadius) {
getPulledEntities().add(entity.getUniqueID());
count++;
if (count >= maxTargets) break;
}
}
}
} else {
// Single target mode - still check pullRadius
if (target != null && !target.isDead) {
double dist = caster.getDistanceToEntity(target);
if (dist <= pullRadius) {
getPulledEntities().add(target.getUniqueID());
} else {
// NPC single-target mode: pull the aggro target
// Player: single-target mode has no effect (no target to pull — use AOE mode instead)
if (!isPlayerCaster(caster) && target != null && !target.isDead) {
double dist = caster.getDistanceToEntity(target);
if (dist <= pullRadius) {
getPulledEntities().add(target.getUniqueID());
}
}
}
}
@@ -114,6 +117,12 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (isPreview()) {
// No entities to pull in preview, just run animation for a duration
if (tick >= 60) signalCompletion();
return;
}
if (pullComplete || getPulledEntities().isEmpty()) {
signalCompletion();
return;
@@ -0,0 +1,549 @@
package kamkeel.npcs.controllers.data.ability.type;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
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.Telegraph;
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.SubGuiZonePresetSelector;
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 = 10.0f;
protected int zoneCount = 3;
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 transient List<double[]> preCalculatedPositions = new ArrayList<>();
protected AbilityZone(int defaultDuration, EnergyDisplayData defaultColors) {
this.durationTicks = defaultDuration;
this.colorData = defaultColors;
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.lockMovement = LockMovementType.WINDUP;
this.cooldownTicks = 0;
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};
}
/**
* Returns the actual zone effect radius for telegraph sizing.
* AbilityHazard returns its damage radius, AbilityTrap returns triggerRadius.
*/
public abstract float getZoneRadius();
@Override
public List<TelegraphInstance> createTelegraphs(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || windUpTicks <= 0) return new ArrayList<>();
// Pre-calculate zone positions
preCalculatedPositions.clear();
List<double[]> placedPositions = new ArrayList<>();
float minSeparation = getZoneRadius() * 2.0f;
for (int i = 0; i < zoneCount; i++) {
double[] pos = findSpawnPosition(caster, placedPositions, minSeparation);
placedPositions.add(pos);
preCalculatedPositions.add(pos);
}
// Create a telegraph for each zone position
List<TelegraphInstance> telegraphs = new ArrayList<>();
float zoneRadius = getZoneRadius();
for (double[] pos : preCalculatedPositions) {
Telegraph telegraph;
if (zoneShape == ZoneShape.SQUARE) {
telegraph = Telegraph.square(zoneRadius);
} else {
telegraph = Telegraph.circle(zoneRadius);
}
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
double groundY = findGroundLevel(caster.worldObj, pos[0], caster.posY, pos[1]);
TelegraphInstance instance = new TelegraphInstance(telegraph, pos[0], groundY, pos[1], 0);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(-1);
telegraphs.add(instance);
}
return telegraphs;
}
@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();
preCalculatedPositions.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 style name.
*/
private void applyPresetDefaults(String styleName) {
effects.clear();
particleDir = "";
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; particleDensity = 1.5f; particleScale = 0.8f;
particleMotion = 1; particleGlow = true; particleDir = "mc:mobSpell";
colorData.innerColor = 0x44DD44; colorData.outerColor = 0x116611;
windUpColor = 0x6044DD44; activeColor = 0xC044FF44;
effects.add(new AbilityEffect(AbilityEffect.EffectType.POISON, 100, 0));
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; particleDensity = 2.0f; particleScale = 1.2f;
particleMotion = 0; particleGlow = true; particleDir = "mc:flame";
colorData.innerColor = 0xFF6611; colorData.outerColor = 0xCC2200;
windUpColor = 0x60FF6611; activeColor = 0xC0FF4400;
effects.add(new AbilityEffect(AbilityEffect.EffectType.BURN, 60, 0));
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; particleDensity = 1.0f; particleScale = 1.0f;
particleMotion = 1; particleGlow = true; particleDir = "mc:portal";
colorData.innerColor = 0xAA44FF; colorData.outerColor = 0x6622BB;
windUpColor = 0x60AA44FF; activeColor = 0xC0CC66FF;
effects.add(new AbilityEffect(AbilityEffect.EffectType.WEAKNESS, 100, 0));
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; particleDensity = 0.8f; particleScale = 0.6f;
particleMotion = 2; particleGlow = true; particleDir = "mc:enchantmenttable";
colorData.innerColor = 0x4488FF; colorData.outerColor = 0x2244BB;
windUpColor = 0x604488FF; activeColor = 0xC066AAFF;
effects.add(new AbilityEffect(AbilityEffect.EffectType.MINING_FATIGUE, 80, 1));
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; particleDensity = 1.5f; particleScale = 1.0f;
particleMotion = 1; particleGlow = true; particleDir = "mc:snowshovel";
colorData.innerColor = 0x88CCFF; colorData.outerColor = 0x4488CC;
windUpColor = 0x6088CCFF; activeColor = 0xC0AADDFF;
effects.add(new AbilityEffect(AbilityEffect.EffectType.SLOWNESS, 100, 1));
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; particleDensity = 1.0f; particleScale = 1.0f;
particleMotion = 0; particleGlow = true;
colorData.innerColor = 0xCCCCCC; colorData.outerColor = 0x666666;
windUpColor = 0x60CCCCCC; activeColor = 0xC0FFFFFF;
break;
default:
break;
}
}
/**
* Client-only preset enum for the GUI dropdown.
* Not persisted — presets simply set field values.
*/
@SideOnly(Side.CLIENT)
public enum ZonePreset {
DEFAULT, TOXIC, INFERNO, ARCANE, ELECTRIC, FROST;
@Override
public String toString() {
return "ability.preset." + name().toLowerCase();
}
}
// ═════════════════════════════════════════════════════════════════
// SHARED NBT
// ═════════════════════════════════════════════════════════════════
protected void writeZoneNBT(NBTTagCompound nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setString("zoneShape", zoneShape.name());
nbt.setFloat("spawnRadius", spawnRadius);
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") : 10.0f;
this.zoneCount = nbt.hasKey("zoneCount") ? nbt.getInteger("zoneCount") : 3;
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 layer fields
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 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 addPresetFieldDef(List<FieldDef> defs) {
defs.add(FieldDef.subGuiField("gui.applyPreset",
SubGuiZonePresetSelector::new,
gui -> {
SubGuiZonePresetSelector selector = (SubGuiZonePresetSelector) gui;
if (selector.selectedPreset != null) {
applyPresetDefaults(selector.selectedPreset);
}
}));
}
@SideOnly(Side.CLIENT)
protected void addVisualFieldDefs(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// 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.boolField("gui.enabled", this::isParticles, this::setParticles)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density", this::getParticleDensity, this::setParticleDensity),
FieldDef.floatField("gui.scale", this::getParticleScale, this::setParticleScale)
).tab("ability.tab.visual").visibleWhen(this::isParticles),
FieldDef.row(
FieldDef.enumField("gui.motion", ParticleMotion.class, this::getParticleMotionEnum, this::setParticleMotionEnum),
FieldDef.boolField("gui.glow", this::isParticleGlow, this::setParticleGlow)
).tab("ability.tab.visual").visibleWhen(this::isParticles),
FieldDef.stringField("gui.texture", this::getParticleDir, this::setParticleDir)
.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)
));
}
}
@@ -150,6 +150,12 @@ public class Telegraph {
return new Telegraph("", TelegraphType.POINT);
}
public static Telegraph square(float radius) {
Telegraph t = new Telegraph("", TelegraphType.SQUARE);
t.radius = radius;
return t;
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
@@ -29,6 +29,11 @@ public enum TelegraphType {
*/
POINT,
/**
* Filled square (uses radius as half-size)
*/
SQUARE,
/**
* No telegraph
*/
@@ -222,24 +222,39 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
setCharging(false);
// Update start position to current position
// Origin (tail) stays at the charged position
startX = posX;
startY = posY;
startZ = posZ;
// Reset head offset to origin
headOffsetX = 0;
headOffsetY = 0;
headOffsetZ = 0;
prevHeadOffsetX = 0;
prevHeadOffsetY = 0;
prevHeadOffsetZ = 0;
// Origin no longer follows owner after firing
attachedToOwner = false;
// Sync prev position to prevent visual jump on first frame
prevPosX = posX;
prevPosY = posY;
prevPosZ = posZ;
// Reset trail
trailPoints.clear();
trailPointAges.clear();
trailPoints.add(Vec3.createVectorHelper(0, 0, 0));
// Fire forward based on owner facing direction
Entity owner = getOwnerEntity();
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = 0; // Fire horizontally
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
motionX = -Math.sin(yaw) * getSpeed();
motionY = 0;
motionZ = Math.cos(yaw) * getSpeed();
} else {
motionX = getSpeed();
motionY = 0;
@@ -31,6 +31,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
// Disc shape properties
private float discRadius = 1.0f; // Width of disc
private float discThickness = 0.2f; // Height of disc
private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward)
// Charging state (during windup)
private boolean charging = false;
@@ -38,7 +39,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
private int chargeTick = 0;
private float targetDiscRadius = 1.0f; // Full radius to grow to during charging
private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging
private EnergyAnchorData anchorData = new EnergyAnchorData();
// Data watcher index for charging state (synced to clients)
private static final int DW_CHARGING = 20;
@@ -123,11 +123,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
* The disc will grow from 0 to discRadius over chargeDuration ticks.
* Position follows the owner based on anchor point.
*/
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
public void setupCharging(EnergyAnchorData anchor, int chargeDuration, boolean vertical) {
setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.vertical = vertical;
this.targetDiscRadius = this.discRadius;
this.targetDiscThickness = this.discThickness;
this.discRadius = 0.01f;
@@ -145,7 +146,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
* Follows anchor point and animations like in the real game.
* Can be fired when transitioning to active phase.
*/
public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) {
public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, boolean vertical) {
this.setPreviewMode(true);
this.setPreviewOwner(owner);
@@ -158,6 +159,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.vertical = vertical;
// Store target size and start at 0 for grow effect
this.targetDiscRadius = discRadius;
@@ -542,6 +544,28 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
return returning;
}
public boolean isVertical() {
return vertical;
}
/**
* Get the yaw angle (degrees) from motion vector for vertical disc orientation.
* During charging, uses owner's facing direction.
*/
public float getTravelYaw() {
if (isCharging()) {
Entity owner = previewMode ? previewOwner : getOwnerEntity();
if (owner != null) {
return owner.rotationYaw;
}
}
double speed = Math.sqrt(motionX * motionX + motionZ * motionZ);
if (speed > 0.001) {
return (float) (Math.atan2(-motionX, motionZ) * 180.0 / Math.PI);
}
return 0.0f;
}
// ==================== NBT ====================
@Override
@@ -550,6 +574,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40;
this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f;
this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f;
this.vertical = nbt.hasKey("Vertical") ? nbt.getBoolean("Vertical") : false;
this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning");
// Charging state
boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
@@ -567,6 +592,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
nbt.setInteger("BoomerangDelay", boomerangDelay);
nbt.setFloat("DiscRadius", discRadius);
nbt.setFloat("DiscThickness", discThickness);
nbt.setBoolean("Vertical", vertical);
nbt.setBoolean("Returning", returning);
// Charging state
nbt.setBoolean("Charging", isCharging());
@@ -1,6 +1,5 @@
package kamkeel.npcs.entity;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
@@ -36,6 +35,9 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
// Direction (normalized)
private double dirX, dirY, dirZ;
// Lock vertical direction after firing (only update yaw, keep pitch fixed)
private boolean lockVerticalDirection = false;
// Track hit entities to avoid double-damage
private Set<Integer> hitEntities = new HashSet<>();
@@ -46,7 +48,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
private boolean charging = false;
private int chargeDuration = 40;
private int chargeTick = 0;
private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
private float targetSize = 1.0f; // Full size to grow to during charging
// Data watcher index for charging state (synced to clients)
@@ -167,6 +168,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
return;
}
// Track owner's anchor position and rotation each tick
// so the laser follows the NPC's facing direction
updateLaserOriginAndDirection();
if (!fullyExtended) {
// Expand the laser
currentLength += expansionSpeed;
@@ -189,27 +194,76 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
} else {
// Laser is fully extended, count down linger time
ticksSinceFullExtension++;
// Update end point during linger (origin/direction may still change)
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
if (ticksSinceFullExtension >= lingerTicks) {
this.setDead();
}
}
}
/**
* Update laser origin and direction from the owner's current anchor position and rotation.
* Called each tick so the laser follows the NPC when tracking a target.
*/
private void updateLaserOriginAndDirection() {
Entity owner = getOwnerEntity();
if (owner == null || owner.isDead) return;
if (!(owner instanceof EntityLivingBase)) return;
EntityLivingBase livingOwner = (EntityLivingBase) owner;
// Update origin from anchor point
if (anchorData != null) {
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData);
startX = pos.xCoord;
startY = pos.yCoord;
startZ = pos.zCoord;
} else {
startX = owner.posX;
startY = owner.posY + owner.height * 0.7;
startZ = owner.posZ;
}
// Update direction from owner's current rotation
float yaw = (float) Math.toRadians(owner.rotationYaw);
if (lockVerticalDirection) {
// Only update horizontal direction (yaw), keep vertical (pitch) fixed
double horizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ);
if (horizontalLen < 0.001) horizontalLen = 1.0;
dirX = -Math.sin(yaw) * horizontalLen;
dirZ = Math.cos(yaw) * horizontalLen;
} else {
float pitch = (float) Math.toRadians(owner.rotationPitch);
dirX = -Math.sin(yaw) * Math.cos(pitch);
dirY = -Math.sin(pitch);
dirZ = Math.cos(yaw) * Math.cos(pitch);
}
// Keep entity positioned at origin
prevPosX = startX;
prevPosY = startY;
prevPosZ = startZ;
setPosition(startX, startY, startZ);
}
public void startMoving(EntityLivingBase target) {
if (!isCharging()) return;
setCharging(false);
// Update start position to current position
// Initialize from current anchor position
// updateLaserOriginAndDirection() will keep these updated each tick
startX = posX;
startY = posY;
startZ = posZ;
// Always derive direction from the owner's current yaw/pitch.
// This ensures the laser fires in the direction the NPC is visually facing,
// which matches the telegraph line direction regardless of Lock Movement mode.
// Set initial direction from owner's rotation
Entity owner = getOwnerEntity();
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
@@ -217,7 +271,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
this.dirY = -Math.sin(pitch);
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
} else if (target != null) {
// Fallback: aim at target if owner is somehow unavailable
double dx = target.posX - startX;
double dy = (target.posY + target.getEyeHeight() - 0.4) - startY;
double dz = target.posZ - startZ;
@@ -229,12 +282,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
}
}
setPosition(endX, endY, endZ);
// Initialize end point
this.endX = posX;
this.endY = posY;
this.endZ = posZ;
// Initialize end point at start (will expand from here)
this.endX = startX;
this.endY = startY;
this.endZ = startZ;
}
private void checkBlockCollision() {
@@ -310,14 +361,14 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
}
private void checkEntityCollisionAlongLine() {
// Check collision along the entire laser line
// Use a slightly expanded AABB for the full line
double minX = Math.min(startX, endX) - laserWidth;
double minY = Math.min(startY, endY) - laserWidth;
double minZ = Math.min(startZ, endZ) - laserWidth;
double maxX = Math.max(startX, endX) + laserWidth;
double maxY = Math.max(startY, endY) + laserWidth;
double maxZ = Math.max(startZ, endZ) + laserWidth;
// Expand search AABB by entity sizes to ensure we find all potential targets
double expand = Math.max(laserWidth, 1.0);
double minX = Math.min(startX, endX) - expand;
double minY = Math.min(startY, endY) - expand;
double minZ = Math.min(startZ, endZ) - expand;
double maxX = Math.max(startX, endX) + expand;
double maxY = Math.max(startY, endY) + expand;
double maxZ = Math.max(startZ, endZ) + expand;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ);
@@ -328,7 +379,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
if (shouldIgnoreEntity(entity)) continue;
if (hitEntities.contains(entity.getEntityId())) continue;
// Check if entity is close to the laser line
// Check if entity's bounding box intersects with the laser line
if (isEntityOnLine(entity)) {
hitEntities.add(entity.getEntityId());
applyDamage(entity);
@@ -339,42 +390,64 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
}
/**
* Check if an entity intersects with the laser line.
* Check if an entity's bounding box intersects with the laser line.
* Uses separate XZ (horizontal) and Y (vertical) checks against
* the entity's full bounding box rather than just its center point.
*/
private boolean isEntityOnLine(EntityLivingBase entity) {
// Get entity center
double ex = entity.posX;
double ey = entity.posY + entity.height * 0.5;
double ez = entity.posZ;
// Vector from start to entity
// Project entity XZ position onto the laser line direction
double vx = ex - startX;
double vy = ey - startY;
double vz = ez - startZ;
// Project onto line direction
double dot = vx * dirX + vy * dirY + vz * dirZ;
// Clamp to line segment
// Project using horizontal direction components only
double dirLenXZ = Math.sqrt(dirX * dirX + dirZ * dirZ);
double dot;
if (dirLenXZ > 0.001) {
dot = (vx * dirX + vz * dirZ) / (dirLenXZ * dirLenXZ) * dirLenXZ;
// Remap to full 3D parameter
dot = (vx * dirX + vz * dirZ + (entity.posY + entity.height * 0.5 - startY) * dirY);
} else {
// Laser fires nearly straight up/down - use full 3D projection
dot = vx * dirX + (entity.posY + entity.height * 0.5 - startY) * dirY + vz * dirZ;
}
dot = Math.max(0, Math.min(dot, currentLength));
// Closest point on line
// Closest point on laser line at this parameter
double closestX = startX + dirX * dot;
double closestY = startY + dirY * dot;
double closestZ = startZ + dirZ * dot;
// Distance from entity to closest point
double distSq = (ex - closestX) * (ex - closestX) +
(ey - closestY) * (ey - closestY) +
(ez - closestZ) * (ez - closestZ);
// XZ distance check: entity center vs laser line point
// Use minimum collision width so visually thin lasers still hit reliably
double xzDistSq = (ex - closestX) * (ex - closestX) + (ez - closestZ) * (ez - closestZ);
double effectiveWidth = Math.max(laserWidth, 0.5);
double hitRadiusXZ = effectiveWidth * 0.5 + entity.width * 0.5;
if (xzDistSq > hitRadiusXZ * hitRadiusXZ) return false;
// Hit if within laser width + entity radius
double hitRadius = laserWidth * 0.5 + entity.width * 0.5;
return distSq <= hitRadius * hitRadius;
// Y overlap check: laser point vs entity's full height range
double entityMinY = entity.boundingBox.minY;
double entityMaxY = entity.boundingBox.maxY;
double laserHalfWidth = effectiveWidth * 0.5;
// Check if laser Y is within entity's height range (expanded by laser half-width)
if (closestY < entityMinY - laserHalfWidth) return false;
if (closestY > entityMaxY + laserHalfWidth) return false;
return true;
}
// ==================== GETTERS FOR RENDERER ====================
public void setLockVerticalDirection(boolean lock) {
this.lockVerticalDirection = lock;
}
public boolean isLockVerticalDirection() {
return lockVerticalDirection;
}
public float getLaserWidth() {
return laserWidth;
}
@@ -454,7 +527,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size;
anchorData.readNBT(nbt);
}
@Override
@@ -476,7 +548,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
nbt.setInteger("ChargeDuration", chargeDuration);
nbt.setInteger("ChargeTick", chargeTick);
nbt.setFloat("TargetSize", targetSize);
anchorData.writeNBT(nbt);
}
/**
@@ -60,6 +60,10 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
private int maxTicks = 400;
private long deathWorldTime = -1;
// Preview mode
private boolean previewMode = false;
private EntityLivingBase previewOwner = null;
// Damage state
private transient int ticksSinceDamage = 0;
private transient Set<Integer> hitThisTick = new HashSet<>();
@@ -281,7 +285,16 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
target.velocityChanged = true;
}
/**
* Set up preview mode. Owner is stored directly since world entity lookup won't work.
*/
public void setupPreview(EntityLivingBase owner) {
this.previewMode = true;
this.previewOwner = owner;
}
private Entity getOwner() {
if (previewMode && previewOwner != null) return previewOwner;
if (ownerEntityId == -1) return null;
return worldObj.getEntityByID(ownerEntityId);
}
@@ -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);
}
}
}
}
}
@@ -96,6 +96,7 @@ import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket;
import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket;
import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket;
import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket;
@@ -516,6 +517,7 @@ public class PacketHandler {
REQUEST_PACKET.registerPacket(new CustomAbilityGetPacket());
REQUEST_PACKET.registerPacket(new CustomAbilityRemovePacket());
REQUEST_PACKET.registerPacket(new CustomAbilitySavePacket());
REQUEST_PACKET.registerPacket(new BuiltInAbilityGetPacket());
// Other Packets
REQUEST_PACKET.registerPacket(new IsGuiOpenInform());
@@ -205,6 +205,7 @@ public enum EnumRequestPacket {
CustomAbilityGet,
CustomAbilityRemove,
CustomAbilitySave,
BuiltInAbilityGet,
Hammer,
RecipeScript,
@@ -59,6 +59,16 @@ public final class SpecialKeyStatePacket extends AbstractPacket {
// Trigger ability activation on key press (rising edge)
if (keyDown && !wasDown) {
// Don't activate if player has a GUI open (not the default inventory)
if (player.openContainer != null && !(player.openContainer instanceof net.minecraft.inventory.ContainerPlayer)) {
return;
}
// Don't activate if player is riding an entity (mount)
if (player.ridingEntity != null) {
return;
}
data.abilityData.activateAbility(player);
}
}
@@ -0,0 +1,70 @@
package kamkeel.npcs.network.packets.request.ability;
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.AbilityController;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.PacketUtil;
import kamkeel.npcs.network.enums.EnumItemPacketType;
import kamkeel.npcs.network.enums.EnumRequestPacket;
import kamkeel.npcs.network.packets.data.large.GuiDataPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import java.io.IOException;
/**
* Request packet to get a specific built-in ability by name.
* Built-in abilities are registered presets with fixed configurations.
*/
public final class BuiltInAbilityGetPacket extends AbstractPacket {
public static String packetName = "Request|BuiltInAbilityGet";
private String abilityName;
public BuiltInAbilityGetPacket() {
}
public BuiltInAbilityGetPacket(String abilityName) {
this.abilityName = abilityName;
}
@Override
public Enum getType() {
return EnumRequestPacket.BuiltInAbilityGet;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.REQUEST_PACKET;
}
@SideOnly(Side.CLIENT)
@Override
public void sendData(ByteBuf out) throws IOException {
ByteBufUtils.writeString(out, abilityName);
}
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
if (!(player instanceof EntityPlayerMP))
return;
if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player))
return;
String name = ByteBufUtils.readString(in);
Ability ability = AbilityController.Instance.getAbilityByDisplayName(name);
if (ability != null) {
NBTTagCompound compound = ability.writeNBT();
compound.setBoolean("BuiltIn", true);
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
}
}
}
@@ -47,5 +47,6 @@ public final class CustomAbilitiesGetPacket extends AbstractPacket {
return;
NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player);
NoppesUtilServer.sendBuiltInAbilitiesData((EntityPlayerMP) player);
}
}
@@ -37,6 +37,7 @@ public class AnchorPointHelper {
private static final float ARM_HEIGHT = 0.75f;
private static final float ABOVE_HEAD_HEIGHT = 1.2f;
private static final float CHEST_HEIGHT = 0.65f;
private static final float EYE_HEIGHT = 0.85f;
// Distance in front for FRONT anchor
private static final float DEFAULT_FRONT_DISTANCE = 1.0f;
@@ -98,11 +99,18 @@ public class AnchorPointHelper {
case CHEST:
y += height * CHEST_HEIGHT;
break;
case EYE:
y += entity.getEyeHeight();
break;
}
x += anchorX * scale;
y += anchorY * scale;
z += anchorZ * scale;
// Rotate offsets relative to entity's body yaw
// +X = entity's right, +Y = up, +Z = entity's forward
Vec3 rotatedOffset = rotateOffsetByYaw(anchorX * scale, anchorY * scale, anchorZ * scale, bodyYaw);
x += rotatedOffset.xCoord;
y += rotatedOffset.yCoord;
z += rotatedOffset.zCoord;
return Vec3.createVectorHelper(x, y, z);
}
@@ -246,9 +254,11 @@ public class AnchorPointHelper {
double worldY = entity.posY + shoulderHeight - blockY;
double worldZ = entity.posZ + worldOffsetZ;
worldX += anchor.anchorOffsetX * scale;
worldY += anchor.anchorOffsetY * scale;
worldZ += anchor.anchorOffsetZ * scale;
// Rotate offsets relative to entity's body yaw
Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw);
worldX += rotatedOffset.xCoord;
worldY += rotatedOffset.yCoord;
worldZ += rotatedOffset.zCoord;
return Vec3.createVectorHelper(worldX, worldY, worldZ);
}
@@ -317,9 +327,11 @@ public class AnchorPointHelper {
double y = entity.posY + entity.height * FALLBACK_ARM_HEIGHT + armLengthOffset;
double z = entity.posZ + lateralZ + forwardZ;
x += anchor.anchorOffsetX * scale;
y += anchor.anchorOffsetY * scale;
z += anchor.anchorOffsetZ * scale;
// Rotate offsets relative to entity's body yaw
Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw);
x += rotatedOffset.xCoord;
y += rotatedOffset.yCoord;
z += rotatedOffset.zCoord;
return Vec3.createVectorHelper(x, y, z);
}
@@ -340,8 +352,23 @@ public class AnchorPointHelper {
return ABOVE_HEAD_HEIGHT;
case CHEST:
return CHEST_HEIGHT;
case EYE:
return EYE_HEIGHT;
default:
return FRONT_HEIGHT;
}
}
/**
* Rotate an offset vector by the entity's body yaw so offsets are entity-relative.
* +X = entity's right, +Y = up, +Z = entity's forward.
* @param bodyYawRad body yaw in radians
*/
private static Vec3 rotateOffsetByYaw(double offsetX, double offsetY, double offsetZ, float bodyYawRad) {
double cos = Math.cos(bodyYawRad);
double sin = Math.sin(bodyYawRad);
double worldX = -offsetX * cos - offsetZ * sin;
double worldZ = -offsetX * sin + offsetZ * cos;
return Vec3.createVectorHelper(worldX, offsetY, worldZ);
}
}
+34 -31
View File
@@ -11,7 +11,8 @@ import java.util.function.Supplier;
public class Register<T> {
public static final Map<String, List<String>> REGISTERED_NAMESPACES = new LinkedHashMap<>();
private final String registryKey;
public static final Map<String, String> NAMESPACE_DISPLAY_NAMES = new LinkedHashMap<>();
protected final String registryKey;
protected final String namespace;
protected final Map<String, Supplier<T>> entries = new LinkedHashMap<>();
@@ -28,50 +29,51 @@ public class Register<T> {
public static boolean isEmpty(String registryKey) {
if (REGISTERED_NAMESPACES.isEmpty()) return true;
if (REGISTERED_NAMESPACES.get(registryKey).isEmpty()) return true;
List<String> list = REGISTERED_NAMESPACES.get(registryKey);
if (list == null || list.isEmpty()) return true;
return false;
}
public List<T> catalogue() {
List<T> list = new ArrayList<>();
for (String regKey : REGISTERED_NAMESPACES.keySet()) {
List<String> nameSp = REGISTERED_NAMESPACES.get(regKey);
for (String name : nameSp) {
List<T> nameSpContent = catalogue(regKey, name);
list.addAll(nameSpContent);
}
}
return list;
}
public List<T> catalogue(String namespace) {
return catalogue(registryKey, namespace);
}
public List<T> catalogue(String registryKey, String namespace) {
List<T> list = new ArrayList<>();
for (Map.Entry<String, Supplier<T>> entry : entries.entrySet()) {
if (entry.getKey().startsWith(registryKey + "." + namespace + ":"))
list.add(entry.getValue().get());
}
return list;
}
public static class Abilities extends Register<Ability> {
protected final Map<String, String> uniqueNames = new LinkedHashMap<>();
protected final Set<String> typeOnly = new HashSet<>();
private Abilities(String namespace) {
super("ability", namespace);
}
@Override
public Ability register(String factoryName, Supplier<Ability> factory) {
String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_");
entries.put(name, factory);
uniqueNames.put(name, factoryName);
return factory.get();
}
/**
* Register a type only (shell template, no built-in preset).
* The type will appear in the ability type picker but won't be
* available as a pre-configured ability by name.
*/
public Ability registerType(String factoryName, Supplier<Ability> factory) {
String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_");
entries.put(name, factory);
uniqueNames.put(name, factoryName);
typeOnly.add(name);
return factory.get();
}
public void register() {
for (Map.Entry<String, Supplier<Ability>> entry : entries.entrySet()) {
AbilityController.Instance.registerType(entry.getValue().get().getTypeId(), entry.getValue());
AbilityController.Instance.registerType(entry.getKey(), entry.getValue());
if (!typeOnly.contains(entry.getKey())) {
AbilityController.Instance.registerAbility(uniqueNames.get(entry.getKey()), entry.getValue().get());
}
}
}
public static Register.Abilities create(String namespace) {
public static Register.Abilities create(String namespace, String displayName) {
if (!REGISTERED_NAMESPACES.containsKey("ability"))
REGISTERED_NAMESPACES.put("ability", new ArrayList<>());
@@ -80,6 +82,7 @@ public class Register<T> {
}
REGISTERED_NAMESPACES.get("ability").add(namespace);
NAMESPACE_DISPLAY_NAMES.put(namespace, displayName);
return new Register.Abilities(namespace);
}
@@ -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();
+303 -75
View File
@@ -44,7 +44,7 @@ public class DataAbilities {
/**
* Whether the ability system is enabled for this NPC
*/
public boolean enabled = false;
public boolean enabled = true;
/**
* Minimum cooldown ticks between abilities (global minimum)
@@ -90,6 +90,24 @@ public class DataAbilities {
private transient float lockedRenderYawOffset = 0;
private transient float lockedPitch = 0;
/**
* Locked position values for movement lock.
* Stored when entering a locked phase, applied after super.onLivingUpdate()
* to override any position changes from AI/physics.
*/
private transient boolean positionLocked = false;
private transient double lockedPosX = 0;
private transient double lockedPosY = 0;
private transient double lockedPosZ = 0;
/**
* Hit scan state - forces NPC to face target.
* Set during tickCurrentAbility(), applied after super.onLivingUpdate()
* to override AI look helper rotation.
*/
private transient boolean hitScanActive = false;
private transient EntityLivingBase hitScanTarget = null;
// ═══════════════════════════════════════════════════════════════════
// CONSTRUCTOR
// ═══════════════════════════════════════════════════════════════════
@@ -107,10 +125,33 @@ public class DataAbilities {
* Should be called every tick from onLivingUpdate().
*/
public void tick() {
if (!enabled || npc.worldObj.isRemote || npc.isKilled()) {
if (npc.worldObj.isRemote) {
return;
}
// Force-stop any running ability when NPC is killed or abilities are disabled
if (!enabled || npc.isKilled()) {
if (currentAbility != null) {
removeTelegraph(currentAbility);
stopAbilityAnimation();
releaseRotationControl();
releaseLockedPosition();
currentAbility = null;
lastTarget = null;
} else {
// No ability but locks could be orphaned
if (rotationLocked || hitScanActive) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
}
return;
}
// Safety: release orphaned locks if no ability is actively executing
if (currentAbility == null || !currentAbility.isExecuting()) {
if (rotationLocked || hitScanActive) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
}
// Tick current ability if executing
if (currentAbility != null && currentAbility.isExecuting()) {
tickCurrentAbility();
@@ -133,36 +174,70 @@ public class DataAbilities {
// Handle phase-specific logic
switch (currentAbility.getPhase()) {
case WINDUP:
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
// Burst replay: re-enter windup - set up telegraph, sound, animation, locks
if (currentAbility.isRotationLockedDuringWindup()) {
captureLockedRotation();
}
if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) {
captureLockedPosition();
}
spawnTelegraph(currentAbility, target);
playAbilitySound(currentAbility.getWindUpSound());
playAbilityAnimation(currentAbility.getWindUpAnimation());
}
currentAbility.onWindUpTick(npc, target, npc.worldObj, currentAbility.getCurrentTick());
break;
case ACTIVE:
if (phaseChanged && oldPhase == AbilityPhase.WINDUP) {
// Just entered ACTIVE phase - lock telegraph position if it was following
if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) {
// Just entered ACTIVE phase - lock telegraph positions if they were following
// This commits the ability to its current target position
TelegraphInstance telegraph = currentAbility.getTelegraphInstance();
if (telegraph != null) {
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
telegraph.lockPosition();
}
// Remove telegraph - it has served its purpose
removeTelegraph(currentAbility);
// Remove telegraph unless the ability keeps it during active phase
// (e.g., sweeping attacks where the telegraph shows the ongoing danger zone)
if (!currentAbility.keepTelegraphDuringActive()) {
removeTelegraph(currentAbility);
}
// Handle rotation lock transition from WINDUP to ACTIVE
// Handle rotation control transition from WINDUP to ACTIVE
if (currentAbility.isRotationLockedDuringActive()) {
// ACTIVE or WINDUP_AND_ACTIVE: capture (or keep) rotation lock
if (!rotationLocked) {
captureLockedRotation();
}
} else if (rotationLocked) {
// WINDUP only: release the lock now that windup is over
releaseLockedRotation();
releaseRotationControl();
}
// Handle position lock transition from WINDUP to ACTIVE
if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) {
if (!positionLocked) {
captureLockedPosition();
}
} else if (positionLocked) {
releaseLockedPosition();
}
// Burst re-fire without replay: let entity-spawning abilities spawn+fire in one step
if (oldPhase == AbilityPhase.BURST_DELAY && !currentAbility.isBurstReplayAnimations()) {
currentAbility.onBurstRefire(npc, target, npc.worldObj);
}
// Play active sound and animation
playAbilitySound(currentAbility.getActiveSound());
// Abilities that keep telegraph during active manage their own sound timing
if (!currentAbility.keepTelegraphDuringActive()) {
playAbilitySound(currentAbility.getActiveSound());
}
playAbilityAnimation(currentAbility.getActiveAnimation());
// Snap NPC to face target before execute so startMoving() reads correct rotation
if (currentAbility.isHitScanForCurrentPhase() && target != null) {
faceTarget(target);
}
// Fire execute event (cancelable)
AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent(
npc.wrappedNPC, currentAbility, target);
@@ -187,6 +262,31 @@ public class DataAbilities {
handleAbilityCompletion(target);
return;
}
// Auto-complete for burst overlap mode (entities fly independently)
// This allows entity-spawning abilities to immediately chain next burst
// without each type needing to check burst state in their onActiveTick()
// Wait until all staggered projectiles have been fired before auto-completing
if (currentAbility.isBurstEnabled() && currentAbility.isBurstOverlap()
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
currentAbility.signalCompletion();
}
// Check if ability entered burst delay during onActiveTick or overlap auto-complete
if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) {
// Release all locks - free movement/rotation during delay
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
if (hitScanActive) releaseRotationControl();
}
break;
case BURST_DELAY:
// Free movement and rotation during burst delay
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
break;
case DAZED:
@@ -199,6 +299,15 @@ public class DataAbilities {
break;
}
// Update hit scan state - actual facing is deferred to applyRotationControl()
// which runs AFTER super.onLivingUpdate() to override AI look helper
if (currentAbility != null && currentAbility.isExecuting()
&& currentAbility.isHitScanForCurrentPhase() && target != null) {
enableHitScan(target);
} else if (hitScanActive) {
releaseRotationControl();
}
// Apply movement control if ability is still executing
if (currentAbility != null && currentAbility.isExecuting()) {
applyMovementControl();
@@ -235,6 +344,9 @@ public class DataAbilities {
private void handleAbilityCompletion(EntityLivingBase target) {
if (currentAbility == null) return;
// Remove any remaining telegraphs (for abilities that keep telegraph during active)
removeTelegraph(currentAbility);
// Call onComplete callback
currentAbility.onComplete(npc, target);
@@ -243,8 +355,9 @@ public class DataAbilities {
npc.wrappedNPC, currentAbility, target);
NpcAPI.EVENT_BUS.post(completeEvent);
// Release rotation lock before cleanup
releaseLockedRotation();
// Release locks before cleanup
releaseRotationControl();
releaseLockedPosition();
// Clean up and roll cooldown (this sets currentAbility to null)
onAbilityComplete();
@@ -259,8 +372,9 @@ public class DataAbilities {
// Stop any ability animation
stopAbilityAnimation();
// Release rotation lock
releaseLockedRotation();
// Release locks
releaseRotationControl();
releaseLockedPosition();
// Calculate cooldown: random(min, max) + ability offset
int abilityCooldownOffset = currentAbility.getCooldownTicks();
@@ -349,6 +463,11 @@ public class DataAbilities {
* Check if a specific ability is eligible for use.
*/
private boolean isAbilityEligible(Ability ability, EntityLivingBase target) {
// Check UserType allows NPCs
if (!ability.getAllowedBy().allowsNpc()) {
return false;
}
// Check enabled
if (!ability.isEnabled()) {
return false;
@@ -399,6 +518,9 @@ public class DataAbilities {
if (ability.isRotationLockedDuringWindup()) {
captureLockedRotation();
}
if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) {
captureLockedPosition();
}
spawnTelegraph(ability, target);
playAbilitySound(ability.getWindUpSound());
playAbilityAnimation(ability.getWindUpAnimation());
@@ -416,6 +538,9 @@ public class DataAbilities {
if (ability.isRotationLockedDuringActive()) {
captureLockedRotation();
}
if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) {
captureLockedPosition();
}
// Play active sound and animation
playAbilitySound(ability.getActiveSound());
@@ -486,26 +611,27 @@ public class DataAbilities {
}
/**
* Spawn and send telegraph for an ability.
* Spawn and send telegraphs for an ability.
*/
private void spawnTelegraph(Ability ability, EntityLivingBase target) {
TelegraphInstance telegraph = ability.createTelegraph(npc, target);
if (telegraph != null) {
ability.setTelegraphInstance(telegraph);
// Send to all nearby players
TelegraphSpawnPacket.sendToTracking(telegraph, npc);
List<TelegraphInstance> telegraphs = ability.createTelegraphs(npc, target);
if (!telegraphs.isEmpty()) {
ability.setTelegraphInstances(telegraphs);
for (TelegraphInstance telegraph : telegraphs) {
TelegraphSpawnPacket.sendToTracking(telegraph, npc);
}
}
}
/**
* Remove telegraph when ability ends or is interrupted.
* Remove telegraphs when ability ends or is interrupted.
*/
private void removeTelegraph(Ability ability) {
TelegraphInstance telegraph = ability.getTelegraphInstance();
if (telegraph != null) {
List<TelegraphInstance> telegraphs = ability.getTelegraphInstances();
for (TelegraphInstance telegraph : telegraphs) {
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), npc);
ability.setTelegraphInstance(null);
}
ability.setTelegraphInstances(null);
}
// ═══════════════════════════════════════════════════════════════════
@@ -625,8 +751,9 @@ public class DataAbilities {
// Stop any ability animation
stopAbilityAnimation();
// Release rotation lock
releaseLockedRotation();
// Release locks
releaseRotationControl();
releaseLockedPosition();
// Fire interrupt event
AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent(
@@ -648,7 +775,8 @@ public class DataAbilities {
if (currentAbility != null) {
removeTelegraph(currentAbility);
stopAbilityAnimation();
releaseLockedRotation();
releaseRotationControl();
releaseLockedPosition();
currentAbility.interrupt();
currentAbility = null;
lastTarget = null;
@@ -668,6 +796,11 @@ public class DataAbilities {
return false;
}
// Check UserType allows NPCs
if (!ability.getAllowedBy().allowsNpc()) {
return false;
}
// Stop any currently executing ability
stopCurrentAbility();
@@ -940,15 +1073,15 @@ public class DataAbilities {
}
// ═══════════════════════════════════════════════════════════════════
// ROTATION LOCKING
// ROTATION CONTROL
// ═══════════════════════════════════════════════════════════════════
/** Bit flag for rotation lock in the existing data watcher slot 15 bitfield */
private static final int ROTATION_LOCKED_FLAG = 16;
/** Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 */
private static final int ROTATION_CONTROLLED_FLAG = 16;
/**
* Capture current rotation values to lock NPC's look direction.
* Called when entering a locked phase (WINDUP or ACTIVE depending on LockMovementType).
* Called when entering a LOCKED rotation phase.
*/
private void captureLockedRotation() {
lockedYaw = npc.rotationYaw;
@@ -956,41 +1089,79 @@ public class DataAbilities {
lockedRenderYawOffset = npc.renderYawOffset;
lockedPitch = npc.rotationPitch;
rotationLocked = true;
npc.setBoolFlag(true, ROTATION_LOCKED_FLAG);
npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG);
}
/**
* Release the rotation lock.
* Called when leaving the locked phase or ability completes.
* Release rotation control (both locked and hit scan).
*/
private void releaseLockedRotation() {
private void releaseRotationControl() {
rotationLocked = false;
npc.setBoolFlag(false, ROTATION_LOCKED_FLAG);
hitScanActive = false;
hitScanTarget = null;
npc.setBoolFlag(false, ROTATION_CONTROLLED_FLAG);
}
/**
* Apply locked rotation values to the NPC.
* Called AFTER super.onLivingUpdate() in EntityNPCInterface to override
* any rotation changes made by the look helper, AI tasks, or body smoothing.
* Runs on both client and server.
* Enable hit scan tracking for the given target.
*/
public void applyLockedRotation() {
private void enableHitScan(EntityLivingBase target) {
if (!hitScanActive) {
hitScanActive = true;
npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG);
}
hitScanTarget = target;
}
/**
* Snap the NPC to face the target instantly.
*/
private void faceTarget(EntityLivingBase target) {
double dx = target.posX - npc.posX;
double dz = target.posZ - npc.posZ;
double dy = (target.posY + target.getEyeHeight() * 0.5) - (npc.posY + npc.getEyeHeight());
double distXZ = Math.sqrt(dx * dx + dz * dz);
float yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI);
float pitch = (float) (-(Math.atan2(dy, distXZ)) * 180.0 / Math.PI);
npc.rotationYaw = yaw;
npc.rotationYawHead = yaw;
npc.renderYawOffset = yaw;
npc.rotationPitch = pitch;
npc.prevRotationYaw = yaw;
npc.prevRotationYawHead = yaw;
npc.prevRenderYawOffset = yaw;
npc.prevRotationPitch = pitch;
}
/**
* Apply rotation control after super.onLivingUpdate() and super.onUpdate().
* Handles both LOCKED (freeze at captured values) and TRACK (face target) modes.
*
* Server: computes the correct rotation (locked values or target facing).
* Client: trusts the server-synced rotation values and prevents body smoothing override.
*/
public void applyRotationControl() {
if (npc.worldObj.isRemote) {
// Client-side: check bit 16 in the existing flags data watcher (slot 15)
boolean flagActive = npc.getBoolFlag(ROTATION_LOCKED_FLAG);
if (!flagActive) {
// Client: check single flag for any rotation control
if (!npc.getBoolFlag(ROTATION_CONTROLLED_FLAG)) {
rotationLocked = false;
return;
}
// First tick the flag is active: capture current rotation values
if (!rotationLocked) {
lockedYaw = npc.rotationYaw;
lockedYawHead = npc.rotationYawHead;
lockedRenderYawOffset = npc.renderYawOffset;
lockedPitch = npc.rotationPitch;
rotationLocked = true;
}
// Force all rotation values back to locked state
// Trust server rotation, prevent body smoothing from overriding
npc.prevRotationYaw = npc.rotationYaw;
npc.prevRotationYawHead = npc.rotationYawHead;
npc.renderYawOffset = npc.rotationYawHead;
npc.prevRenderYawOffset = npc.rotationYawHead;
npc.prevRotationPitch = npc.rotationPitch;
return;
}
// Server: apply the appropriate rotation
if (hitScanActive && hitScanTarget != null && !hitScanTarget.isDead) {
faceTarget(hitScanTarget);
} else if (rotationLocked) {
npc.rotationYaw = lockedYaw;
npc.rotationYawHead = lockedYawHead;
npc.renderYawOffset = lockedRenderYawOffset;
@@ -999,32 +1170,89 @@ public class DataAbilities {
npc.prevRotationYawHead = lockedYawHead;
npc.prevRenderYawOffset = lockedRenderYawOffset;
npc.prevRotationPitch = lockedPitch;
}
}
/**
* Check if rotation is currently controlled (locked or tracking target).
* Used by AI tasks to determine if they should try to change NPC look direction.
*/
public boolean isRotationLocked() {
return rotationLocked || hitScanActive;
}
// ═══════════════════════════════════════════════════════════════════
// POSITION LOCKING
// ═══════════════════════════════════════════════════════════════════
/** Bit flag for position lock in data watcher slot 15 */
private static final int POSITION_LOCKED_FLAG = 32;
/**
* Capture current position to lock NPC in place.
* Called when entering a phase with movement lock.
*/
private void captureLockedPosition() {
lockedPosX = npc.posX;
lockedPosY = npc.posY;
lockedPosZ = npc.posZ;
positionLocked = true;
npc.setBoolFlag(true, POSITION_LOCKED_FLAG);
}
/**
* Release the position lock.
* Called when leaving the locked phase or ability completes.
*/
private void releaseLockedPosition() {
positionLocked = false;
npc.setBoolFlag(false, POSITION_LOCKED_FLAG);
}
/**
* Apply locked position values to the NPC.
* Called AFTER super.onLivingUpdate() in EntityNPCInterface to override
* any position changes from AI pathfinding, physics, or entity collisions.
* Runs on both client and server.
*/
public void applyLockedPosition() {
if (npc.worldObj.isRemote) {
// Client-side: check flag via data watcher
boolean flagActive = npc.getBoolFlag(POSITION_LOCKED_FLAG);
if (!flagActive) {
positionLocked = false;
return;
}
// First tick the flag is active: capture current position
if (!positionLocked) {
lockedPosX = npc.posX;
lockedPosY = npc.posY;
lockedPosZ = npc.posZ;
positionLocked = true;
}
// Snap position back and set prev to prevent interpolation jitter
npc.setPosition(lockedPosX, lockedPosY, lockedPosZ);
npc.prevPosX = lockedPosX;
npc.prevPosY = lockedPosY;
npc.prevPosZ = lockedPosZ;
npc.motionX = 0;
npc.motionY = 0;
npc.motionZ = 0;
return;
}
// Server-side: use stored locked values
if (!rotationLocked) {
if (!positionLocked) {
return;
}
// Force rotation back to locked values
npc.rotationYaw = lockedYaw;
npc.rotationYawHead = lockedYawHead;
npc.renderYawOffset = lockedRenderYawOffset;
npc.rotationPitch = lockedPitch;
// Also set prev values to prevent interpolation jitter
npc.prevRotationYaw = lockedYaw;
npc.prevRotationYawHead = lockedYawHead;
npc.prevRenderYawOffset = lockedRenderYawOffset;
npc.prevRotationPitch = lockedPitch;
}
/**
* Check if rotation is currently locked.
*/
public boolean isRotationLocked() {
return rotationLocked;
npc.setPosition(lockedPosX, lockedPosY, lockedPosZ);
npc.prevPosX = lockedPosX;
npc.prevPosY = lockedPosY;
npc.prevPosZ = lockedPosZ;
npc.motionX = 0;
npc.motionY = 0;
npc.motionZ = 0;
}
// ═══════════════════════════════════════════════════════════════════
@@ -2,6 +2,7 @@ package noppes.npcs;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.SyncController;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.packets.data.ParticlePacket;
@@ -780,18 +781,38 @@ public class NoppesUtilServer {
if (controller != null) {
int index = 0;
for (String typeId : controller.getTypes()) {
// Exclude built-in types from the NPC type list
if (controller.isBuiltInType(typeId)) continue;
map.put(typeId, index++);
}
}
sendScrollData(player, map, EnumScrollData.ABILITY_TYPES);
}
public static void sendBuiltInAbilitiesData(EntityPlayerMP player) {
HashMap<String, Integer> map = new HashMap<String, Integer>();
AbilityController controller = AbilityController.Instance;
if (controller != null) {
int index = 0;
for (String name : controller.getAbilityNames()) {
Ability ability = controller.getAbility(name);
if (ability != null) {
map.put(ability.getName(), index++);
}
}
}
sendScrollData(player, map, EnumScrollData.BUILTIN_ABILITIES);
}
public static void sendCustomAbilitiesData(EntityPlayerMP player) {
HashMap<String, Integer> map = new HashMap<String, Integer>();
kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance;
if (controller != null) {
int index = 0;
for (String uuid : controller.getCustomAbilityIds()) {
Ability ability = controller.getCustomAbility(uuid);
if (ability == null || !ability.getAllowedBy().allowsNpc()) continue;
String name = controller.getCustomAbilityName(uuid);
if (name == null || name.isEmpty()) name = uuid;
map.put(name + "\t" + uuid, index++);
@@ -596,6 +596,12 @@ public class ScriptPlayerEventHandler {
}
if (event.entityLiving instanceof EntityPlayerMP) {
// Handle ability damage interactions (Guard counter, damage reduction)
PlayerData pData = PlayerData.get((EntityPlayer) event.entityLiving);
if (pData != null && pData.abilityData != null && pData.abilityData.isExecutingAbility()) {
event.ammount = pData.abilityData.onDamage(event.source, event.ammount);
}
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving);
noppes.npcs.scripted.event.player.PlayerEvent.DamagedEvent pevent = new noppes.npcs.scripted.event.player.PlayerEvent.DamagedEvent((IPlayer) NpcAPI.Instance().getIEntity((EntityPlayer) event.entityLiving), source, event.ammount, event.source);
cancel = EventHooks.onPlayerDamaged(handler, pevent);
@@ -153,12 +153,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;
@@ -272,6 +274,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());
@@ -511,6 +514,8 @@ public class ClientProxy extends CommonProxy {
} else {
abilityNpc = new EntityCustomNpc(Minecraft.getMinecraft().theWorld);
abilityNpc.display.texture = "customnpcs:textures/entity/humanmale/AnimationBody.png";
abilityNpc.height = 1.8f;
abilityNpc.width = 0.6f;
}
return new GuiNpcManageAbilities(abilityNpc, npc != null);
}
@@ -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;
}
}
@@ -198,6 +198,5 @@ public class SubGuiNpcDialog extends SubGuiInterface implements ISubGuiListener,
}
public void save() {
PacketClient.sendClient(new DialogSavePacket(this.dialogCategoryID, dialog.writeToNBT(new NBTTagCompound()), false));
}
}
@@ -521,8 +521,6 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis
public void save() {
GuiNpcTextField.unfocus();
if (dialog.id >= 0)
PacketClient.sendClient(new DialogSavePacket(((SubGuiNpcDialog) parent).dialogCategoryID, dialog.writeToNBT(new NBTTagCompound()), false));
}
private DialogImage getSelectedImage() {
@@ -239,6 +239,5 @@ public class SubGuiNpcQuest extends SubGuiInterface implements ISubGuiListener,
}
public void save() {
PacketClient.sendClient(new QuestSavePacket(this.questCategoryID, quest.writeToNBT(new NBTTagCompound()), false));
}
}
@@ -0,0 +1,72 @@
package noppes.npcs.client.gui;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.util.StatCollector;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.SubGuiInterface;
@SideOnly(Side.CLIENT)
public class SubGuiZonePresetSelector extends SubGuiInterface {
public String selectedPreset = null;
private static final String[] PRESET_NAMES = {"DEFAULT", "TOXIC", "INFERNO", "ARCANE", "ELECTRIC", "FROST"};
private static final String[] PRESET_LABELS = {
"ability.preset.default", "ability.preset.toxic", "ability.preset.inferno",
"ability.preset.arcane", "ability.preset.electric", "ability.preset.frost"
};
private static final int[] PRESET_COLORS = {0xCCCCCC, 0x44DD44, 0xFF6611, 0xAA44FF, 0x4488FF, 0x88CCFF};
public SubGuiZonePresetSelector() {
xSize = 170;
ySize = 135;
setBackground("menubg.png");
}
@Override
public void initGui() {
super.initGui();
int btnW = 72;
int btnH = 20;
int gapY = 4;
int col1 = guiLeft + 7;
int col2 = col1 + btnW + 6;
int startY = guiTop + 22;
for (int i = 0; i < 6; i++) {
int x = (i % 2 == 0) ? col1 : col2;
int y = startY + (i / 2) * (btnH + gapY);
addButton(new GuiNpcButton(i, x, y, btnW, btnH, PRESET_LABELS[i]));
}
int cancelY = startY + 3 * (btnH + gapY) + 4;
addButton(new GuiNpcButton(10, guiLeft + xSize / 2 - 36, cancelY, 72, btnH, "gui.cancel"));
}
@Override
protected void actionPerformed(GuiButton btn) {
super.actionPerformed(btn);
if (btn.id >= 0 && btn.id < 6) {
selectedPreset = PRESET_NAMES[btn.id];
close();
} else if (btn.id == 10) {
close();
}
}
@Override
public void drawScreen(int mouseX, int mouseY, float f) {
super.drawScreen(mouseX, mouseY, f);
drawCenteredString(fontRendererObj, StatCollector.translateToLocal("gui.applyPreset"), guiLeft + xSize / 2, guiTop + 8, 0xFFFFFF);
for (int i = 0; i < 6; i++) {
GuiNpcButton btn = getButton(i);
if (btn != null && btn.visible) {
int x = btn.xPosition + 4;
int y = btn.yPosition + 5;
drawRect(x, y, x + 10, y + 10, 0xFF000000 | PRESET_COLORS[i]);
}
}
}
}
@@ -3,10 +3,12 @@ package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.controllers.data.ability.AbilitySlot;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket;
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket;
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket;
import kamkeel.npcs.util.Register;
import net.minecraft.client.gui.GuiButton;
@@ -57,6 +59,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
private String search = "";
private int selectedAbilityIndex = -1;
// Existing preset names for duplicate checking
private final Set<String> existingPresetNames = new HashSet<>();
// Pending variant selection
private String pendingTypeId = null;
public static int modIndex = 0;
public static ScrollType scrollType = ScrollType.CNPC;
@@ -64,6 +72,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
super(npc);
PacketClient.sendClient(new AbilitiesGetAllPacket());
PacketClient.sendClient(new AbilitiesNpcGetPacket());
PacketClient.sendClient(new CustomAbilitiesGetPacket());
}
public enum ScrollType {
@@ -79,7 +88,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
if (Register.isEmpty("ability"))
return "modded";
return Register.REGISTERED_NAMESPACES.get("ability").get(modIndex);
String namespace = Register.REGISTERED_NAMESPACES.get("ability").get(modIndex);
String displayName = Register.NAMESPACE_DISPLAY_NAMES.get(namespace);
return displayName != null ? displayName : namespace;
case ALL: return "filter.all";
default: return name();
}
@@ -92,38 +103,38 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
int y = guiTop + 10;
// Enabled checkbox
addLabel(new GuiNpcLabel(100, "gui.enabled", guiLeft + 5, y + 5));
addButton(new GuiNpcButton(100, guiLeft + 55, y, 50, 20, new String[]{"gui.no", "gui.yes"}, abilitiesEnabled ? 1 : 0));
addButton(new GuiNpcButton(50, guiLeft + 5, y, 140, 20, scrollType.toString()));
// Cooldown range: Min Cooldown and Max Cooldown (spaced apart more)
addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5));
GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, "" + minCooldown);
addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 210, y + 5));
GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 260, y, 40, 20, "" + minCooldown);
minField.setIntegersOnly();
minField.setMinMaxDefault(0, 10000, 20);
addTextField(minField);
addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 260, y + 5));
GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, "" + maxCooldown);
addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 320, y + 5));
GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 370, y, 40, 20, "" + maxCooldown);
maxField.setIntegersOnly();
maxField.setMinMaxDefault(0, 10000, 60);
addTextField(maxField);
y += 28;
// Enabled checkbox
addButton(new GuiNpcButton(100, guiLeft + 334, y + 145, 76, 20, new String[]{"gui.disabled", "gui.enabled"}, abilitiesEnabled ? 1 : 0));
getButton(100).packedFGColour = abilitiesEnabled ? 0x00FF00 : 0xFF0000;
// Left scroll: available ability types
addLabel(new GuiNpcLabel(1, "ability.availableTypes", guiLeft + 5, y));
if (availableTypesScroll == null) {
availableTypesScroll = new GuiCustomScroll(this, 0);
availableTypesScroll.setSize(140, 103);
availableTypesScroll.setSize(140, 130);
}
availableTypesScroll.guiLeft = guiLeft + 5;
availableTypesScroll.guiTop = y + 12;
availableTypesScroll.setList(getFilteredTypeList());
addScroll(availableTypesScroll);
addButton(new GuiNpcButton(50, guiLeft + 5, y + 120, 140, 20, scrollType.toString()));
// Search bar for types
addTextField(new GuiNpcTextField(4, this, fontRendererObj, guiLeft + 5, y + 145, 140, 18, search));
@@ -131,9 +142,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
addLabel(new GuiNpcLabel(2, "ability.npcAbilities", guiLeft + 210, y));
if (npcAbilitiesScroll == null) {
npcAbilitiesScroll = new GuiCustomScroll(this, 1);
npcAbilitiesScroll.setSize(160, 130);
npcAbilitiesScroll.setSize(200, 130);
} else {
npcAbilitiesScroll.setSize(160, 130);
npcAbilitiesScroll.setSize(200, 130);
}
npcAbilitiesScroll.guiLeft = guiLeft + 210;
npcAbilitiesScroll.guiTop = y + 12;
@@ -142,11 +153,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
// Center buttons: Add/Remove
int centerX = guiLeft + 158;
addButton(new GuiNpcButton(70, centerX, y + 40, 40, 20, ">>>")); // Add
addButton(new GuiNpcButton(71, centerX, y + 62, 40, 20, "<<<")); // Remove
addButton(new GuiNpcButton(70, centerX, y + 15, 40, 20, ">>>")); // Add
addButton(new GuiNpcButton(71, centerX, y + 37, 40, 20, "<<<")); // Remove
getButton(71).setEnabled(selectedAbilityIndex >= 0);
// Load button (under the add/remove arrows)
addButton(new GuiNpcButton(75, centerX, y + 90, 40, 20, "gui.load"));
addButton(new GuiNpcButton(75, centerX, y + 60, 40, 20, "gui.load"));
// On/Off toggle for selected ability (under Load button)
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
@@ -156,9 +168,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
addButton(toggleBtn);
}
// Check if selected ability is built-in (non-editable)
boolean selectedIsBuiltIn = false;
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
Ability sel = npcSlots.get(selectedAbilityIndex).getAbility();
if (sel != null && sel.isBuiltIn()) selectedIsBuiltIn = true;
}
// Right side buttons: Edit and Up/Down carrots with Save button
addButton(new GuiNpcButton(72, guiLeft + 210, y + 145, 55, 20, "gui.edit"));
getButton(72).setEnabled(selectedAbilityIndex >= 0);
getButton(72).setEnabled(selectedAbilityIndex >= 0 && !selectedIsBuiltIn);
// Up/Down carrot buttons
addButton(new GuiNpcButton(73, guiLeft + 270, y + 145, 20, 20, "<"));
@@ -167,8 +186,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1);
// Save button (to the right of carrots, only enabled when ability is selected)
addButton(new GuiNpcButton(76, guiLeft + 320, y + 145, 50, 20, "gui.save"));
getButton(76).setEnabled(selectedAbilityIndex >= 0);
addButton(new GuiNpcButton(76, centerX, y + 82, 40, 20, "gui.save"));
getButton(76).setEnabled(selectedAbilityIndex >= 0 && !selectedIsBuiltIn);
}
@Override
@@ -178,6 +197,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
// Enabled toggle
if (id == 100) {
abilitiesEnabled = ((GuiNpcButton) guibutton).getValue() == 1;
initGui();
save();
return;
}
@@ -188,8 +208,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
String displayName = availableTypesScroll.getSelected();
String typeId = displayNameToTypeId.get(displayName);
if (typeId != null) {
java.util.List<AbilityVariant> variants = AbilityController.Instance.getVariantsForType(typeId);
if (variants.size() > 1) {
pendingTypeId = typeId;
setSubGui(new SubGuiAbilityVariantSelect(variants));
return;
}
Ability newAbility = AbilityController.Instance.create(typeId);
if (newAbility != null) {
if (variants.size() == 1) {
variants.get(0).apply(newAbility);
}
newAbility.setId(UUID.randomUUID().toString());
npcSlots.add(AbilitySlot.inline(newAbility));
selectedAbilityIndex = npcSlots.size() - 1;
@@ -276,7 +305,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) {
setSubGui(abilityToSave.createConfigGui(this));
} else {
setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave));
setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave, null, existingPresetNames));
}
}
}
@@ -301,7 +330,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
if (id == 50) {
if (scrollType != ScrollType.MODDED) {
ScrollType[] values = ScrollType.values();
scrollType = values[(scrollType.ordinal() + 1) % values.length];
ScrollType next = values[(scrollType.ordinal() + 1) % values.length];
// Skip MODDED if no namespaces are registered
if (next == ScrollType.MODDED && Register.isEmpty("ability")) {
next = ScrollType.ALL;
}
scrollType = next;
} else {
List<String> list = Register.REGISTERED_NAMESPACES.get("ability");
@@ -320,8 +354,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
Map<String, Integer> dummyMap = new HashMap<>(allAbilityTypes);
filteredAbilityTypes.clear();
filteredAbilityTypes.putAll(getFilteredData(dummyMap));
}
// Reset scroll position when changing category
if (availableTypesScroll != null) {
availableTypesScroll.resetScroll();
}
}
initGui();
}
@@ -336,6 +374,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
if (availableTypesScroll != null) {
availableTypesScroll.setList(getFilteredTypeList());
}
} else if (type == EnumScrollData.CUSTOM_ABILITIES) {
existingPresetNames.clear();
for (String key : data.keySet()) {
int tabIndex = key.indexOf('\t');
if (tabIndex > 0) {
existingPresetNames.add(key.substring(0, tabIndex));
} else {
existingPresetNames.add(key);
}
}
}
initGui();
}
@@ -423,6 +471,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
String displayName = I18n.format(typeId);
// Search matches either the display name or the typeId
if (search.isEmpty() || displayName.toLowerCase().contains(search) || typeId.toLowerCase().contains(search)) {
// Gray out built-in types to distinguish them from configurable types
if (AbilityController.Instance.isBuiltInType(typeId)) {
displayName = "\u00A77" + displayName;
}
list.add(displayName);
displayNameToTypeId.put(displayName, typeId);
}
@@ -471,6 +523,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
if (slot.isReference()) {
return colorPrefix + (index + 1) + ". \u00A7e> " + nameDisplay + "\u00A7r";
}
if (ability.isBuiltIn()) {
return colorPrefix + (index + 1) + ". \u00A77" + nameDisplay + "\u00A7r";
}
return colorPrefix + (index + 1) + ". " + nameDisplay;
}
@@ -571,10 +626,11 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
Ability ability = slot.getAbility();
if (ability != null && ability.isBuiltIn()) return;
if (slot.isReference()) {
setSubGui(new SubGuiAbilityEditMode());
} else {
Ability ability = slot.getAbility();
if (ability != null) {
setSubGui(ability.createConfigGui(this));
}
@@ -605,7 +661,24 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiAbilityEditMode) {
if (subgui instanceof SubGuiAbilityVariantSelect) {
SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui;
int idx = variantGui.getSelectedIndex();
if (idx >= 0 && pendingTypeId != null) {
Ability newAbility = AbilityController.Instance.create(pendingTypeId);
if (newAbility != null) {
variantGui.getVariants().get(idx).apply(newAbility);
newAbility.setId(UUID.randomUUID().toString());
npcSlots.add(AbilitySlot.inline(newAbility));
selectedAbilityIndex = npcSlots.size() - 1;
updateNpcAbilitiesList();
selectAbilityByIndex(selectedAbilityIndex);
save();
}
}
pendingTypeId = null;
initGui();
} else if (subgui instanceof SubGuiAbilityEditMode) {
int mode = ((SubGuiAbilityEditMode) subgui).getResult();
if (mode < 0) return; // cancelled
@@ -324,7 +324,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
@Override
public void unFocused(GuiNpcTextField textField) {
if (textField.id < DECLARATIVE_ID_START) return;
builder.handleTextFieldEvent(textField.id, textField);
if (builder.handleTextFieldEvent(textField.id, textField)) {
initGui();
}
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -7,24 +7,29 @@ import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.util.ISubGuiListener;
import noppes.npcs.client.gui.util.SubGuiInterface;
import java.util.Set;
/**
* Confirmation dialog for saving an ability preset.
* Uses packet-based communication for proper client-server architecture.
*/
public class SubGuiAbilitySaveConfirm extends SubGuiInterface {
public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGuiListener {
private final Ability ability;
private final IAbilityConfigCallback callback;
private final Set<String> existingNames;
public SubGuiAbilitySaveConfirm(Ability ability) {
this(ability, null);
this(ability, null, null);
}
public SubGuiAbilitySaveConfirm(Ability ability, IAbilityConfigCallback callback) {
public SubGuiAbilitySaveConfirm(Ability ability, IAbilityConfigCallback callback, Set<String> existingNames) {
this.ability = ability;
this.callback = callback;
this.existingNames = existingNames;
setBackground("menubg.png");
xSize = 200;
@@ -52,14 +57,32 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface {
int id = guibutton.id;
if (id == 0) {
// Save the ability via packet
PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
if (callback != null) {
callback.onAbilitySaved(ability);
if (existingNames != null && existingNames.contains(ability.getName())) {
setSubGui(new SubGuiDuplicateNameConfirm());
return;
}
close();
doSave();
} else if (id == 1) {
close();
}
}
private void doSave() {
PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
if (callback != null) {
callback.onAbilitySaved(ability);
}
close();
}
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiDuplicateNameConfirm) {
if (((SubGuiDuplicateNameConfirm) subgui).isConfirmed()) {
doSave();
} else {
close();
}
}
}
}
@@ -5,22 +5,26 @@ import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.resources.I18n;
import noppes.npcs.client.gui.util.GuiCustomScroll;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
import noppes.npcs.client.gui.util.ICustomScrollListener;
import noppes.npcs.client.gui.util.ITextfieldListener;
import noppes.npcs.client.gui.util.SubGuiInterface;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* SubGui for selecting an ability type when creating a new ability.
*/
public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomScrollListener {
public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomScrollListener, ITextfieldListener {
private GuiCustomScroll scroll;
private final HashMap<String, String> displayNameToTypeId = new HashMap<>();
private final HashMap<String, String> allDisplayNameToTypeId = new HashMap<>();
private String selectedTypeId = null;
private String search = "";
public SubGuiAbilityTypeSelect() {
setBackground("menubg.png");
@@ -32,32 +36,44 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS
public void initGui() {
super.initGui();
addLabel(new GuiNpcLabel(0, "ability.selectType", guiLeft + 5, guiTop + 5));
addTextField(new GuiNpcTextField(10, this, fontRendererObj, guiLeft + 5, guiTop + 5, 190, 18, search));
if (scroll == null) {
scroll = new GuiCustomScroll(this, 0);
scroll.setSize(190, 165);
scroll.setSize(190, 145);
}
scroll.guiLeft = guiLeft + 5;
scroll.guiTop = guiTop + 16;
scroll.guiTop = guiTop + 26;
addScroll(scroll);
List<String> list = buildTypeList();
scroll.setList(list);
buildAllTypes();
scroll.setList(getFilteredTypeList());
addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 188, 90, 20, "gui.add"));
getButton(0).setEnabled(scroll.hasSelected());
addButton(new GuiNpcButton(1, guiLeft + 105, guiTop + 188, 90, 20, "gui.cancel"));
}
private List<String> buildTypeList() {
List<String> list = new ArrayList<>();
displayNameToTypeId.clear();
private void buildAllTypes() {
allDisplayNameToTypeId.clear();
String[] types = AbilityController.Instance.getTypes();
for (String typeId : types) {
// Exclude built-in types — they cannot be created as custom presets
if (AbilityController.Instance.isBuiltInType(typeId)) continue;
String displayName = I18n.format(typeId);
list.add(displayName);
displayNameToTypeId.put(displayName, typeId);
allDisplayNameToTypeId.put(displayName, typeId);
}
}
private List<String> getFilteredTypeList() {
List<String> list = new ArrayList<>();
displayNameToTypeId.clear();
for (Map.Entry<String, String> entry : allDisplayNameToTypeId.entrySet()) {
String displayName = entry.getKey();
if (search.isEmpty() || displayName.toLowerCase().contains(search) || entry.getValue().toLowerCase().contains(search)) {
list.add(displayName);
displayNameToTypeId.put(displayName, entry.getValue());
}
}
return list;
}
@@ -89,6 +105,22 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS
}
}
@Override
public void keyTyped(char c, int i) {
super.keyTyped(c, i);
if (getTextField(10) != null && getTextField(10).isFocused()) {
if (!search.equals(getTextField(10).getText())) {
search = getTextField(10).getText().toLowerCase();
scroll.setList(getFilteredTypeList());
scroll.resetScroll();
}
}
}
@Override
public void unFocused(GuiNpcTextField textField) {
}
public String getSelectedTypeId() {
return selectedTypeId;
}
@@ -0,0 +1,154 @@
package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.util.GuiCustomScroll;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.ICustomScrollListener;
import noppes.npcs.client.gui.util.SubGuiInterface;
import java.util.ArrayList;
import java.util.List;
/**
* SubGui for selecting a variant/template when creating a new ability.
* Uses a scroll list with group headers for external addon variants.
*/
public class SubGuiAbilityVariantSelect extends SubGuiInterface implements ICustomScrollListener {
private final List<AbilityVariant> variants;
private int selectedIndex = -1;
// Maps each scroll list index to a variant list index (-1 for non-selectable entries)
private final List<Integer> scrollToVariantIndex = new ArrayList<>();
public SubGuiAbilityVariantSelect(List<AbilityVariant> variants) {
this.variants = variants;
setBackground("menubg.png");
xSize = 200;
ySize = 216;
}
@Override
public void initGui() {
super.initGui();
addLabel(new GuiNpcLabel(0, "ability.selectVariant", guiLeft + 10, guiTop + 8));
// Build the unsorted scroll list with group headers
List<String> scrollList = new ArrayList<>();
scrollToVariantIndex.clear();
boolean hasBaseVariants = false;
boolean hasGroupedVariants = false;
for (AbilityVariant v : variants) {
if (v.getGroup() == null)
hasBaseVariants = true;
else
hasGroupedVariants = true;
}
// Add base (non-grouped) variants first
for (int i = 0; i < variants.size(); i++) {
if (variants.get(i).getGroup() == null) {
scrollList.add(variants.get(i).getDisplayKey());
scrollToVariantIndex.add(i);
}
}
// Blank separator between base and grouped variants
if (hasBaseVariants && hasGroupedVariants) {
scrollList.add("");
scrollToVariantIndex.add(-1);
}
// Add grouped variants with group headers
String lastGroup = null;
for (int i = 0; i < variants.size(); i++) {
AbilityVariant v = variants.get(i);
if (v.getGroup() == null)
continue;
if (!v.getGroup().equals(lastGroup)) {
scrollList.add(v.getGroup());
scrollToVariantIndex.add(-1);
lastGroup = v.getGroup();
}
scrollList.add(v.getDisplayKey());
scrollToVariantIndex.add(i);
}
GuiCustomScroll scroll = new GuiCustomScroll(this, 0);
scroll.guiLeft = guiLeft + 5;
scroll.guiTop = guiTop + 22;
scroll.setUnsortedList(scrollList);
scroll.setSize(190, 155);
// Mark blank separators and group headers as non-interactive colored entries
scroll.colors.put("", 0x000000);
if (lastGroup != null) {
for (int i = 0; i < variants.size(); i++) {
String group = variants.get(i).getGroup();
if (group != null)
scroll.colors.put(group, 0xFFFF00); // Yellow
}
}
// Restore selection in scroll
if (selectedIndex >= 0) {
for (int s = 0; s < scrollToVariantIndex.size(); s++) {
if (scrollToVariantIndex.get(s) == selectedIndex) {
scroll.selected = s;
break;
}
}
}
addScroll(scroll);
// Select / Cancel buttons
addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 188, 90, 20, "gui.select"));
getButton(0).setEnabled(selectedIndex >= 0);
addButton(new GuiNpcButton(1, guiLeft + 105, guiTop + 188, 90, 20, "gui.cancel"));
}
@Override
public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) {
if (scroll.id == 0 && scroll.selected >= 0 && scroll.selected < scrollToVariantIndex.size()) {
int variantIdx = scrollToVariantIndex.get(scroll.selected);
if (variantIdx >= 0) {
selectedIndex = variantIdx;
getButton(0).setEnabled(true);
}
}
}
@Override
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
if (selectedIndex >= 0) {
close();
}
}
@Override
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
if (id == 0 && selectedIndex >= 0) {
close();
} else if (id == 1) {
selectedIndex = -1;
close();
}
}
public int getSelectedIndex() {
return selectedIndex;
}
public List<AbilityVariant> getVariants() {
return variants;
}
}
@@ -0,0 +1,47 @@
package noppes.npcs.client.gui.advanced;
import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.SubGuiInterface;
/**
* Confirmation dialog shown when saving an ability preset with a name that already exists.
*/
public class SubGuiDuplicateNameConfirm extends SubGuiInterface {
private boolean confirmed = false;
public SubGuiDuplicateNameConfirm() {
setBackground("menubg.png");
xSize = 220;
ySize = 80;
}
@Override
public void initGui() {
super.initGui();
int y = guiTop + 10;
addLabel(new GuiNpcLabel(0, "ability.duplicate.name", guiLeft + 10, y));
y += 30;
addButton(new GuiNpcButton(0, guiLeft + 20, y, 80, 20, "gui.continue"));
addButton(new GuiNpcButton(1, guiLeft + 120, y, 80, 20, "gui.cancel"));
}
@Override
public void buttonEvent(GuiButton guibutton) {
if (guibutton.id == 0) {
confirmed = true;
close();
} else if (guibutton.id == 1) {
confirmed = false;
close();
}
}
public boolean isConfirmed() {
return confirmed;
}
}
@@ -154,8 +154,8 @@ public class FieldDef {
public static FieldDef colorSubGui(String label, Supplier<Integer> getter, Consumer<Integer> setter) {
return subGuiField(label,
() -> new SubGuiColorSelector(getter.get()),
gui -> setter.accept(((SubGuiColorSelector) gui).color & 0x00FFFFFF))
() -> new SubGuiColorSelector(getter.get() & 0xFFFFFF),
gui -> setter.accept((((SubGuiColorSelector) gui).color & 0x00FFFFFF) | (getter.get() & 0xFF000000)))
.buttonLabel(() -> String.format("%06X", getter.get() & 0xFFFFFF))
.buttonTextColor(() -> getter.get() & 0xFFFFFF);
}
@@ -120,6 +120,9 @@ public class GuiFieldBuilder {
continue;
}
// Skip invisible fields
if (!def.isVisible()) continue;
// Section header
if (def.getType() == FieldType.SECTION_HEADER) {
y += 3;
@@ -391,7 +394,7 @@ public class GuiFieldBuilder {
/**
* Handles text field unfocus events for declarative fields.
* Call from parent GUI's unFocused(). Returns true if the event was handled.
* Call from parent GUI's unFocused(). Returns true if the value actually changed.
*/
public boolean handleTextFieldEvent(int textFieldId, GuiNpcTextField field) {
FieldDef def = textFieldMap.get(textFieldId);
@@ -399,14 +402,36 @@ public class GuiFieldBuilder {
switch (def.getType()) {
case FLOAT:
try { def.setValue(Float.parseFloat(field.getText())); } catch (NumberFormatException ignored) {}
return true;
try {
float newVal = Float.parseFloat(field.getText());
Object old = def.getValue();
float oldVal = old instanceof Number ? ((Number) old).floatValue() : 0f;
if (newVal != oldVal) {
def.setValue(newVal);
return true;
}
} catch (NumberFormatException ignored) {}
return false;
case INT:
try { def.setValue(Integer.parseInt(field.getText())); } catch (NumberFormatException ignored) {}
return true;
try {
int newVal = Integer.parseInt(field.getText());
Object old = def.getValue();
int oldVal = old instanceof Number ? ((Number) old).intValue() : 0;
if (newVal != oldVal) {
def.setValue(newVal);
return true;
}
} catch (NumberFormatException ignored) {}
return false;
case STRING:
def.setValue(field.getText());
return true;
String newVal = field.getText();
Object old = def.getValue();
String oldVal = old != null ? old.toString() : "";
if (!newVal.equals(oldVal)) {
def.setValue(newVal);
return true;
}
return false;
default:
return false;
}
@@ -65,16 +65,17 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol
public void initGui() {
super.initGui();
// Toggle button - added FIRST to maintain stable buttonList index
// (prevents double-toggle when initGui() is called from actionPerformed)
String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom";
this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel));
// Add/Remove buttons - only show for custom animations
if (!showingBuiltIn) {
this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add"));
this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove"));
}
// Toggle button for Custom/Built-in - under Remove button
String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom";
this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel));
if (scrollAnimations == null) {
scrollAnimations = new GuiCustomScroll(this, 0, 0);
scrollAnimations.setSize(143, 185);
@@ -200,11 +201,15 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol
// Toggle button
if (button.id == 10) {
showingBuiltIn = !showingBuiltIn;
// Clear selection when switching views
// Clear selection and search when switching views
selected = null;
search = "";
animation = new Animation();
currentIsBuiltIn = false;
playingAnimation = false;
scrollAnimations.clear();
// Re-request data to ensure both lists are populated
PacketClient.sendClient(new AnimationsGetPacket());
initGui();
return;
}
@@ -85,11 +85,13 @@ public class GuiNPCManageFactions extends GuiNPCInterface2 implements IScrollDat
addLabel(new GuiNpcLabel(3, "faction.hidden", guiLeft + 8, y + 5));
this.addButton(new GuiNpcButton(3, guiLeft + 100, y, 45, 20, new String[]{"gui.no", "gui.yes"}, faction.hideFaction ? 1 : 0));
getButton(3).setHoverText("faction.hidden.hover");
y += 23;
addLabel(new GuiNpcLabel(4, "faction.attacked", guiLeft + 8, y + 5));
this.addButton(new GuiNpcButton(4, guiLeft + 100, y, 45, 20, new String[]{"gui.no", "gui.yes"}, faction.getsAttacked ? 1 : 0));
getButton(4).setHoverText("faction.attacked.hover");
y += 23;
@@ -6,6 +6,7 @@ import kamkeel.npcs.controllers.data.ability.AbilityController;
import java.util.UUID;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket;
@@ -16,7 +17,11 @@ import net.minecraft.client.gui.GuiYesNoCallback;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
import noppes.npcs.client.NoppesUtil;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.SubGuiAbilityTypeSelect;
import noppes.npcs.client.gui.advanced.SubGuiAbilityVariantSelect;
import noppes.npcs.client.gui.advanced.SubGuiDuplicateNameConfirm;
import noppes.npcs.client.gui.util.AbilityPreviewExecutor;
import noppes.npcs.client.gui.util.GuiAbilityInterface;
import noppes.npcs.client.gui.util.GuiCustomScroll;
@@ -47,6 +52,7 @@ import java.util.Vector;
* - Left side: 3D preview of NPC with ability effects
* - Right side: Ability list with search
* - Play/Pause/Stop buttons to preview abilities
* - Toggle between Custom and Built-in ability views
*/
public class GuiNpcManageAbilities extends GuiAbilityInterface
implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback,
@@ -54,7 +60,8 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
// ==================== DATA ====================
private GuiCustomScroll scroll;
private HashMap<String, Integer> abilityData = new HashMap<>();
private HashMap<String, Integer> customData = new HashMap<>();
private HashMap<String, Integer> builtInData = new HashMap<>();
/** Maps display name -> UUID for custom abilities */
private final HashMap<String, String> displayToUuid = new HashMap<>();
private String selected = null;
@@ -62,6 +69,17 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
private String search = "";
private Ability selectedAbility = null;
// ==================== BUILT-IN TOGGLE ====================
private boolean showingBuiltIn = false;
private boolean currentIsBuiltIn = false;
// ==================== PENDING SAVE ====================
private Ability pendingSaveAbility = null;
private boolean pendingNewCreation = false;
// ==================== PENDING VARIANT ====================
private String pendingTypeId = null;
// ==================== PREVIEW ====================
private AbilityPreviewExecutor previewExecutor;
private long prevTick = 0;
@@ -77,7 +95,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
previewExecutor = new AbilityPreviewExecutor();
previewExecutor.setParentGui(this);
// Request ability data from server
// Request ability data from server (sends both custom and built-in)
PacketClient.sendClient(new CustomAbilitiesGetPacket());
// Enable animation data
@@ -89,18 +107,26 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
public void initGui() {
super.initGui();
// Add button
addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 8, 45, 20, "gui.add"));
// Toggle button — added FIRST to maintain stable buttonList index
String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom";
addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 52, 45, 20, toggleLabel));
// Remove button
addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove"));
getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected));
if (!showingBuiltIn) {
// Add button — only for custom view
addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 8, 45, 20, "gui.add"));
// Edit button
addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 52, 45, 20, "gui.edit"));
getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null);
// Remove button — only for custom view
addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove"));
getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected));
}
// Scroll list of saved abilities (right side)
// Edit button — hidden when viewing built-in
if (!showingBuiltIn && !currentIsBuiltIn) {
addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 74, 45, 20, "gui.edit"));
getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null);
}
// Scroll list of abilities (right side)
if (scroll == null) {
scroll = new GuiCustomScroll(this, 0);
scroll.setSize(143, 185);
@@ -111,7 +137,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
// Update scroll list
scroll.setList(getSearchList());
if (selected != null && displayToUuid.containsKey(selected)) {
if (selected != null) {
scroll.setSelected(selected);
}
@@ -123,9 +149,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
return;
}
// ID/Name label (below search bar)
addLabel(new GuiNpcLabel(10, "ID: " + selectedAbility.getId(), guiLeft + 220, guiTop + 215));
// Play/Pause/Stop buttons (below model preview area)
String animTexture = "customnpcs:textures/gui/animation.png";
int playButtonOffsetX = 60;
@@ -150,17 +173,34 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
}
}
private HashMap<String, Integer> getCurrentData() {
return showingBuiltIn ? builtInData : customData;
}
private List<String> getSearchList() {
if (search.isEmpty()) {
return new ArrayList<>(displayToUuid.keySet());
}
List<String> list = new ArrayList<>();
for (String name : displayToUuid.keySet()) {
if (name.toLowerCase().contains(search.toLowerCase())) {
list.add(name);
if (showingBuiltIn) {
if (search.isEmpty()) {
return new ArrayList<>(builtInData.keySet());
}
List<String> list = new ArrayList<>();
for (String name : builtInData.keySet()) {
if (name.toLowerCase().contains(search.toLowerCase())) {
list.add(name);
}
}
return list;
} else {
if (search.isEmpty()) {
return new ArrayList<>(displayToUuid.keySet());
}
List<String> list = new ArrayList<>();
for (String name : displayToUuid.keySet()) {
if (name.toLowerCase().contains(search.toLowerCase())) {
list.add(name);
}
}
return list;
}
return list;
}
@Override
@@ -216,13 +256,30 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
int id = guibutton.id;
if (id == 2) {
// Toggle between custom and built-in views
if (id == 10) {
showingBuiltIn = !showingBuiltIn;
selected = null;
selectedUuid = null;
selectedAbility = null;
currentIsBuiltIn = false;
search = "";
previewExecutor.stop();
if (scroll != null) {
scroll.clear();
}
PacketClient.sendClient(new CustomAbilitiesGetPacket());
initGui();
return;
}
if (id == 2 && !showingBuiltIn) {
// Add — open type selection
setSubGui(new SubGuiAbilityTypeSelect());
} else if (id == 1 && selected != null && selectedUuid != null) {
} else if (id == 1 && !showingBuiltIn && selected != null && selectedUuid != null) {
GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1);
displayGuiScreen(guiyesno);
} else if (id == 100 && selectedAbility != null) {
} else if (id == 100 && !showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) {
// Edit
previewExecutor.stop();
setSubGui(selectedAbility.createConfigGui(this));
@@ -265,11 +322,20 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
String newSelection = scroll.getSelected();
if (newSelection != null && !newSelection.equals(selected)) {
previewExecutor.stop();
selected = newSelection;
selectedUuid = displayToUuid.get(selected);
if (selectedUuid != null) {
PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid));
if (showingBuiltIn) {
// Fetch built-in ability by display name
selectedUuid = null;
currentIsBuiltIn = true;
PacketClient.sendClient(new BuiltInAbilityGetPacket(selected));
} else {
// Fetch custom ability by UUID
selectedUuid = displayToUuid.get(selected);
currentIsBuiltIn = false;
if (selectedUuid != null) {
PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid));
}
}
}
}
@@ -278,8 +344,8 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
@Override
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
if (scroll.id == 0 && selection != null && !selection.isEmpty()) {
// Double-click to edit
if (selectedAbility != null) {
// Double-click to edit — only for custom, non-built-in abilities
if (!showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) {
previewExecutor.stop();
setSubGui(selectedAbility.createConfigGui(this));
}
@@ -290,7 +356,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
public void setData(Vector<String> list, HashMap<String, Integer> data, EnumScrollData type) {
if (type == EnumScrollData.CUSTOM_ABILITIES) {
String prevSelected = scroll != null ? scroll.getSelected() : null;
this.abilityData = data;
this.customData = data;
displayToUuid.clear();
for (String key : data.keySet()) {
// key format: "displayName\tUUID"
@@ -303,18 +369,30 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
displayToUuid.put(key, key);
}
}
if (scroll != null) {
if (!showingBuiltIn && scroll != null) {
scroll.setList(getSearchList());
if (prevSelected != null && displayToUuid.containsKey(prevSelected)) {
scroll.setSelected(prevSelected);
}
}
initGui();
} else if (type == EnumScrollData.BUILTIN_ABILITIES) {
this.builtInData = data;
if (showingBuiltIn && scroll != null) {
scroll.setList(getSearchList());
if (selected != null && builtInData.containsKey(selected)) {
scroll.setSelected(selected);
}
}
initGui();
}
}
@Override
public void setGuiData(NBTTagCompound compound) {
// Check if this is a built-in ability response
currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn");
// Received ability data from server
selectedAbility = AbilityController.Instance.fromNBT(compound);
if (selectedAbility != null) {
@@ -337,27 +415,111 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiAbilityTypeSelect) {
String typeId = ((SubGuiAbilityTypeSelect) subgui).getSelectedTypeId();
if (typeId != null) {
Ability newAbility = AbilityController.Instance.create(typeId);
if (subgui instanceof SubGuiAbilityVariantSelect) {
SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui;
int idx = variantGui.getSelectedIndex();
if (idx >= 0 && pendingTypeId != null) {
Ability newAbility = AbilityController.Instance.create(pendingTypeId);
if (newAbility != null) {
variantGui.getVariants().get(idx).apply(newAbility);
newAbility.setId(UUID.randomUUID().toString());
newAbility.setName(newAbility.getTypeId());
// Save to server, then open config GUI
PacketClient.sendClient(new CustomAbilitySavePacket(newAbility.writeNBT()));
selectedAbility = newAbility;
selected = newAbility.getName();
selectedUuid = newAbility.getId();
previewExecutor.stop();
setSubGui(selectedAbility.createConfigGui(this));
if (hasDuplicateName(newAbility)) {
pendingSaveAbility = newAbility;
pendingNewCreation = true;
pendingTypeId = null;
setSubGui(new SubGuiDuplicateNameConfirm());
return;
}
pendingTypeId = null;
openConfig(newAbility);
return;
}
}
pendingTypeId = null;
} else if (subgui instanceof SubGuiAbilityTypeSelect) {
String typeId = ((SubGuiAbilityTypeSelect) subgui).getSelectedTypeId();
if (typeId != null) {
java.util.List<AbilityVariant> variants = AbilityController.Instance.getVariantsForType(typeId);
if (variants.size() > 1) {
pendingTypeId = typeId;
setSubGui(new SubGuiAbilityVariantSelect(variants));
return;
}
Ability newAbility = AbilityController.Instance.create(typeId);
if (newAbility != null) {
if (variants.size() == 1) {
variants.get(0).apply(newAbility);
}
newAbility.setId(UUID.randomUUID().toString());
if (hasDuplicateName(newAbility)) {
pendingSaveAbility = newAbility;
pendingNewCreation = true;
setSubGui(new SubGuiDuplicateNameConfirm());
return;
}
openConfig(newAbility);
return;
}
}
} else if (subgui instanceof SubGuiDuplicateNameConfirm) {
SubGuiDuplicateNameConfirm confirm = (SubGuiDuplicateNameConfirm) subgui;
if (confirm.isConfirmed() && pendingSaveAbility != null) {
if (pendingNewCreation) {
openConfig(pendingSaveAbility);
pendingSaveAbility = null;
pendingNewCreation = false;
return;
} else {
PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
PacketClient.sendClient(new CustomAbilitiesGetPacket());
pendingSaveAbility = null;
}
} else if (pendingSaveAbility != null) {
if (pendingNewCreation) {
// Cancel new creation - discard
pendingSaveAbility = null;
pendingNewCreation = false;
} else {
// Cancel edit save - re-open config GUI to change name
Ability ability = pendingSaveAbility;
pendingSaveAbility = null;
setSubGui(ability.createConfigGui(this));
return;
}
}
} else if (pendingSaveAbility != null) {
// Config GUI closed with a pending save (set by onAbilitySaved)
if (hasDuplicateName(pendingSaveAbility)) {
pendingNewCreation = false;
setSubGui(new SubGuiDuplicateNameConfirm());
return;
}
PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
PacketClient.sendClient(new CustomAbilitiesGetPacket());
pendingSaveAbility = null;
}
initGui();
}
private void openConfig(Ability ability) {
selectedAbility = ability;
selected = ability.getName();
selectedUuid = ability.getId();
previewExecutor.stop();
setSubGui(ability.createConfigGui(this));
}
private boolean hasDuplicateName(Ability ability) {
String name = ability.getName();
if (name == null || name.isEmpty()) return false;
for (java.util.Map.Entry<String, String> entry : displayToUuid.entrySet()) {
if (entry.getKey().equals(name) && !entry.getValue().equals(ability.getId())) {
return true;
}
}
return false;
}
@Override
public void confirmClicked(boolean result, int id) {
NoppesUtil.openGUI(player, this);
@@ -375,7 +537,8 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
@Override
public void onAbilitySaved(Ability ability) {
PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
// Defer save until subGuiClosed where we can check for duplicate names
pendingSaveAbility = ability;
}
@Override
@@ -4,15 +4,10 @@ import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
import kamkeel.npcs.controllers.data.ability.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityProjectile;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.world.World;
import noppes.npcs.controllers.data.Animation;
import noppes.npcs.controllers.data.AnimationData;
import noppes.npcs.entity.EntityCustomNpc;
@@ -25,38 +20,39 @@ import java.util.List;
/**
* Client-side executor for previewing abilities in the GUI.
*
* Lifecycle:
* 1. WINDUP phase - plays windup animation, entities spawn and charge
* 2. ACTIVE phase - plays active animation, entities fire and move
* 3. EXTENDED phase - continues for +100 ticks after animations to see results
* 4. IDLE - preview complete
* Uses unified execution: calls the REAL ability methods (onWindUpTick, onExecute, onActiveTick)
* with previewMode=true. Abilities skip damage/effects/sounds when isPreview() returns true.
*
* Features:
* - Invisible fake target NPC placed in front of the preview NPC for targeting
* - NPC movement tracking for abilities that move the NPC (e.g. slam)
* Lifecycle:
* 1. WINDUP phase - real onWindUpTick() called, telegraph shown, entities may spawn
* 2. ACTIVE phase - real onExecute() then onActiveTick() called, physics simulated
* 3. EXTENDED phase - 100 extra ticks to observe results (entities continue updating)
* 4. IDLE - preview complete
*/
@SideOnly(Side.CLIENT)
public class AbilityPreviewExecutor {
public class AbilityPreviewExecutor implements PreviewEntityHandler {
// ==================== PREVIEW STATE ====================
private Ability previewAbility;
private EntityNPCInterface previewNpc;
private AbilityPhase phase = AbilityPhase.IDLE;
/** Executor's own phase tracking (separate from ability.getPhase()) */
private enum ExecutorPhase { IDLE, WINDUP, ACTIVE, EXTENDED }
private ExecutorPhase executorPhase = ExecutorPhase.IDLE;
private int currentTick = 0;
private boolean playing = false;
private boolean paused = false;
// ==================== TIMING ====================
private static final int EXTENDED_DURATION = 100;
private int activeDuration = 40;
private int maxPreviewDuration = 200;
// ==================== FAKE TARGET ====================
/** Invisible fake target NPC for ability targeting (placed 5 blocks in front of preview NPC) */
private EntityNPCInterface fakeTarget;
private static final double TARGET_DISTANCE = 5.0;
// ==================== NPC START POSITION ====================
/** Saved NPC position for restoring after movement abilities */
private double npcStartX, npcStartY, npcStartZ;
// ==================== PREVIEW CONTENT ====================
@@ -74,7 +70,8 @@ public class AbilityPreviewExecutor {
}
/**
* Start previewing an ability.
* Start previewing an ability using unified execution.
* Sets previewMode=true on the ability, then calls its real methods.
*/
public void startPreview(Ability ability, EntityNPCInterface npc) {
stop();
@@ -84,31 +81,35 @@ public class AbilityPreviewExecutor {
this.currentTick = 0;
this.playing = true;
this.paused = false;
this.activeDuration = ability.getPreviewActiveDuration();
this.maxPreviewDuration = ability.getMaxPreviewDuration();
// Save NPC start position for restoring later
this.npcStartX = npc.posX;
this.npcStartY = npc.posY;
this.npcStartZ = npc.posZ;
// Enable preview mode on the ability
ability.setPreviewMode(true);
ability.setPreviewEntityHandler(this);
// Create invisible fake target NPC for targeting
createFakeTarget(npc);
// Set preview target on the ability
ability.setPreviewTarget(fakeTarget);
// Start tracking NPC movement
if (parentGui != null) {
parentGui.startTrackingMovement();
}
// Use the ability's real start() method with the fake target
ability.start(fakeTarget);
if (ability.getWindUpTicks() <= 0) {
// No windup — go straight to active
this.phase = AbilityPhase.ACTIVE;
this.executorPhase = ExecutorPhase.ACTIVE;
transitionToActive();
} else {
// Normal windup flow
this.phase = AbilityPhase.WINDUP;
this.executorPhase = ExecutorPhase.WINDUP;
// Create telegraph for preview
previewTelegraph = ability.createTelegraph(npc, fakeTarget);
@@ -121,24 +122,17 @@ public class AbilityPreviewExecutor {
data.setAnimation(windUpAnim);
data.animation.paused = false;
}
// Spawn preview entity on first tick if ability spawns during windup
if (ability.spawnPreviewDuringWindup()) {
spawnPreviewEntity();
}
}
}
/**
* Create an invisible fake target NPC placed in front of the preview NPC.
* This entity is NOT rendered - it only exists for ability targeting logic.
*/
private void createFakeTarget(EntityNPCInterface npc) {
if (npc.worldObj == null) return;
try {
fakeTarget = new EntityCustomNpc(npc.worldObj);
// Position target in front of NPC based on its facing direction
float facingYaw = GuiAbilityInterface.NPC_FACING_YAW;
double yawRad = Math.toRadians(facingYaw);
double targetX = npc.posX - Math.sin(yawRad) * TARGET_DISTANCE;
@@ -153,15 +147,12 @@ public class AbilityPreviewExecutor {
}
}
/**
* Get the fake target entity for abilities that need a target.
*/
public EntityLivingBase getFakeTarget() {
return fakeTarget;
}
public void play() {
if (previewAbility == null || phase == AbilityPhase.IDLE) {
if (previewAbility == null || executorPhase == ExecutorPhase.IDLE) {
return;
}
playing = true;
@@ -178,6 +169,9 @@ public class AbilityPreviewExecutor {
public void pause() {
paused = true;
// Immediately sync prevPos to pos for NPC and all entities to prevent interpolation jitter
syncPrevPositions();
if (previewNpc != null) {
AnimationData data = previewNpc.display.animationData;
if (data.animation != null) {
@@ -186,10 +180,37 @@ public class AbilityPreviewExecutor {
}
}
/**
* Sync prevPos to pos for the NPC and all preview entities.
* Prevents rendering interpolation jitter when standing still or paused.
*/
private void syncPrevPositions() {
if (previewNpc != null) {
previewNpc.prevPosX = previewNpc.posX;
previewNpc.prevPosY = previewNpc.posY;
previewNpc.prevPosZ = previewNpc.posZ;
}
for (Entity entity : previewEntities) {
if (entity != null && !entity.isDead) {
entity.prevPosX = entity.posX;
entity.prevPosY = entity.posY;
entity.prevPosZ = entity.posZ;
}
}
}
public void stop() {
// Clean up the ability
if (previewAbility != null) {
previewAbility.cleanup();
previewAbility.setPreviewMode(false);
previewAbility.setPreviewEntityHandler(null);
previewAbility.reset();
}
playing = false;
paused = false;
phase = AbilityPhase.IDLE;
executorPhase = ExecutorPhase.IDLE;
currentTick = 0;
previewTelegraph = null;
@@ -211,9 +232,11 @@ public class AbilityPreviewExecutor {
previewNpc.prevPosX = npcStartX;
previewNpc.prevPosY = npcStartY;
previewNpc.prevPosZ = npcStartZ;
previewNpc.motionX = 0;
previewNpc.motionY = 0;
previewNpc.motionZ = 0;
}
// Remove fake target
fakeTarget = null;
if (previewNpc != null) {
@@ -224,13 +247,24 @@ public class AbilityPreviewExecutor {
}
/**
* Tick the preview forward.
* Tick the preview forward. Calls real ability methods.
*/
public void tick() {
if (!playing || paused || previewAbility == null || previewNpc == null) {
if (!playing || previewAbility == null || previewNpc == null) {
return;
}
if (paused) {
// Sync prevPos to pos for NPC and all entities to prevent interpolation jitter while paused
syncPrevPositions();
return;
}
// Sync NPC's prevPos to current pos BEFORE any updates to prevent jitter when standing still
previewNpc.prevPosX = previewNpc.posX;
previewNpc.prevPosY = previewNpc.posY;
previewNpc.prevPosZ = previewNpc.posZ;
currentTick++;
if (previewTelegraph != null) {
@@ -239,14 +273,14 @@ public class AbilityPreviewExecutor {
tickEntities();
switch (phase) {
switch (executorPhase) {
case WINDUP:
tickWindup();
break;
case ACTIVE:
tickActive();
break;
case IDLE:
case EXTENDED:
tickExtended();
break;
default:
@@ -255,7 +289,8 @@ public class AbilityPreviewExecutor {
}
private void tickWindup() {
previewAbility.onPreviewWindUpTick(previewNpc, currentTick);
// Call the REAL windup tick method
previewAbility.onWindUpTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick);
if (currentTick >= previewAbility.getWindUpTicks()) {
transitionToActive();
@@ -263,35 +298,41 @@ public class AbilityPreviewExecutor {
}
private void tickActive() {
previewAbility.onPreviewActiveTick(previewNpc, currentTick);
// Call the REAL active tick method
previewAbility.onActiveTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick);
if (currentTick == 1 && !previewAbility.spawnPreviewDuringWindup()) {
spawnPreviewEntity();
fireEntities();
// Simulate physics for movement abilities
simulatePhysics();
// Check if the ability signaled completion (phase went to IDLE)
if (previewAbility.getPhase() == AbilityPhase.IDLE) {
transitionToExtended();
return;
}
if (currentTick >= activeDuration) {
// Safety timeout
if (currentTick >= maxPreviewDuration) {
transitionToExtended();
}
}
private void tickExtended() {
// Continue ticking entities so they can finish their animations
if (currentTick >= EXTENDED_DURATION) {
playing = false;
}
}
private void transitionToActive() {
phase = AbilityPhase.ACTIVE;
executorPhase = ExecutorPhase.ACTIVE;
currentTick = 0;
previewTelegraph = null;
// Notify ability of execution (for movement abilities like Slam/Charge/Dash)
previewAbility.onPreviewExecute(previewNpc);
fireEntities();
// Call the REAL onExecute method (fires entities, initiates movement, etc.)
previewAbility.onExecute(previewNpc, fakeTarget, previewNpc.worldObj);
// Start active animation
Animation activeAnim = previewAbility.getActiveAnimation();
if (activeAnim != null && previewNpc != null) {
AnimationData data = previewNpc.display.animationData;
@@ -301,7 +342,7 @@ public class AbilityPreviewExecutor {
}
private void transitionToExtended() {
phase = AbilityPhase.IDLE;
executorPhase = ExecutorPhase.EXTENDED;
currentTick = 0;
if (previewNpc != null) {
@@ -310,26 +351,40 @@ public class AbilityPreviewExecutor {
}
}
private void spawnPreviewEntity() {
Entity entity = previewAbility.createPreviewEntity(previewNpc);
if (entity != null) {
previewEntities.add(entity);
if (parentGui != null) {
parentGui.addPreviewEntity(entity);
}
}
}
/**
* Simulate basic physics for movement abilities (Slam, Charge, Dash).
* Applies gravity, drag, and ground clamping to NPC position.
*/
private void simulatePhysics() {
if (!previewAbility.hasAbilityMovement()) return;
if (previewNpc == null) return;
private void fireEntities() {
for (Entity entity : previewEntities) {
if (entity instanceof EntityAbilityOrb) {
((EntityAbilityOrb) entity).startPreviewFiring();
} else if (entity instanceof EntityAbilityBeam) {
((EntityAbilityBeam) entity).startPreviewFiring();
} else if (entity instanceof EntityAbilityDisc) {
((EntityAbilityDisc) entity).startPreviewFiring();
}
// Save previous position for interpolation
previewNpc.prevPosX = previewNpc.posX;
previewNpc.prevPosY = previewNpc.posY;
previewNpc.prevPosZ = previewNpc.posZ;
// Apply velocity
previewNpc.posX += previewNpc.motionX;
previewNpc.posY += previewNpc.motionY;
previewNpc.posZ += previewNpc.motionZ;
// Apply gravity
previewNpc.motionY -= 0.08;
previewNpc.motionY *= 0.98;
// Apply horizontal drag
previewNpc.motionX *= 0.91;
previewNpc.motionZ *= 0.91;
// Ground clamp: if below start position and falling, stop
if (previewNpc.posY < npcStartY && previewNpc.motionY < 0) {
previewNpc.posY = npcStartY;
previewNpc.motionY = 0;
previewNpc.onGround = true;
}
previewNpc.fallDistance = 0;
}
private void tickEntities() {
@@ -347,12 +402,38 @@ public class AbilityPreviewExecutor {
}
}
// ==================== PreviewEntityHandler ====================
@Override
public void onEntitySpawned(Entity entity) {
if (entity == null) return;
previewEntities.add(entity);
if (parentGui != null) {
parentGui.addPreviewEntity(entity);
}
}
@Override
public void onEntityRemoved(Entity entity) {
if (entity == null) return;
previewEntities.remove(entity);
if (parentGui != null) {
parentGui.removePreviewEntity(entity);
}
}
// ==================== GETTERS ====================
public boolean isPlaying() { return playing; }
public boolean isPaused() { return paused; }
public boolean isActive() { return playing || paused; }
public AbilityPhase getPhase() { return phase; }
public AbilityPhase getPhase() {
switch (executorPhase) {
case WINDUP: return AbilityPhase.WINDUP;
case ACTIVE: return AbilityPhase.ACTIVE;
default: return AbilityPhase.IDLE;
}
}
public int getCurrentTick() { return currentTick; }
public TelegraphInstance getTelegraph() { return previewTelegraph; }
public List<Entity> getPreviewEntities() { return previewEntities; }
@@ -364,11 +445,11 @@ public class AbilityPreviewExecutor {
return "Stopped";
} else if (paused) {
return "Paused";
} else if (phase == AbilityPhase.WINDUP) {
} else if (executorPhase == ExecutorPhase.WINDUP) {
return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0);
} else if (phase == AbilityPhase.ACTIVE) {
return "Active: " + currentTick + "/" + activeDuration;
} else if (phase == AbilityPhase.IDLE && playing) {
} else if (executorPhase == ExecutorPhase.ACTIVE) {
return "Active: " + currentTick + "/" + maxPreviewDuration;
} else if (executorPhase == ExecutorPhase.EXTENDED) {
return "Extended: " + currentTick + "/" + EXTENDED_DURATION;
}
return "";
@@ -11,10 +11,7 @@ import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import noppes.npcs.client.ClientEventHandler;
import noppes.npcs.client.EntityUtil;
import noppes.npcs.entity.EntityCustomNpc;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.entity.data.ModelData;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
@@ -50,7 +47,6 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
private GuiNpcButton btnLeft, btnRight;
// ==================== NPC STATE ====================
public ModelData playerdata;
public EntityNPCInterface npc;
/** NPC starting position (saved when preview starts, used to track movement) */
@@ -85,7 +81,6 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
public GuiAbilityInterface(EntityNPCInterface npc, boolean hasMenuNpc) {
super(hasMenuNpc ? npc : null);
this.npc = npc;
this.playerdata = ((EntityCustomNpc) npc).modelData;
this.drawDefaultBackground = false;
CAMERA_ZOOM = 30f;
@@ -135,20 +130,17 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
* Draw the 3D preview with NPC and entities.
*/
protected void drawPreview(int mouseX, int mouseY, float partialTicks) {
if (hasSubGui())
return;
GL11.glColor4f(1, 1, 1, 1);
// Enable scissor to clip rendering to preview area
GL11.glEnable(GL11.GL_SCISSOR_TEST);
setScissorClip(previewX, previewY, previewWidth, previewHeight);
// Get the entity to render (could be transformed player model)
EntityLivingBase renderEntity = playerdata.getEntity(npc);
if (renderEntity == null) {
renderEntity = this.npc;
}
// Sync NPC properties to render entity
EntityUtil.Copy(npc, renderEntity);
// Always use the standard NPC animation renderer for ability preview
EntityLivingBase renderEntity = this.npc;
// Set NPC facing direction (affects anchor points and visual facing)
npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW;
@@ -204,11 +196,7 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
ClientEventHandler.renderingEntityInGUI = true;
// Render the NPC at its position delta (for movement like slam jumps)
try {
RenderManager.instance.renderEntityWithPosYaw(renderEntity, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks);
} catch (Exception e) {
playerdata.setEntityClass(null);
}
RenderManager.instance.renderEntityWithPosYaw(renderEntity, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks);
// Render preview entities at their positions relative to NPC start position
renderPreviewEntities(partialTicks);
@@ -200,7 +200,8 @@ public class GuiCustomScroll extends GuiScreen {
int k = (14 * i + 4) - scrollY;
if (k >= 4 && k + 12 < ySize) {
int xOffset = scrollHeight < ySize - 8 ? 0 : 10;
String displayString = StatCollector.translateToLocal(list.get(i));
String rawEntry = list.get(i);
String displayString = StatCollector.translateToLocal(rawEntry);
String text = "";
float maxWidth = (xSize + xOffset - 8) * 0.8f;
@@ -215,7 +216,13 @@ public class GuiCustomScroll extends GuiScreen {
text += "...";
} else
text = displayString;
if ((multipleSelection && selectedList.contains(text)) || (!multipleSelection && selected == i)) {
// Entries with a custom color are non-interactive (headers/separators)
Integer customColor = colors.get(rawEntry);
if (customColor != null) {
if (!text.isEmpty())
fontRendererObj.drawString(text, j, k, customColor);
} else if ((multipleSelection && selectedList.contains(text)) || (!multipleSelection && selected == i)) {
drawVerticalLine(j - 2, k - 4, k + 10, 0xffffffff);
drawVerticalLine(j + xSize - 18 + xOffset, k - 4, k + 10, 0xffffffff);
drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k - 3, 0xffffffff);
@@ -239,13 +246,17 @@ public class GuiCustomScroll extends GuiScreen {
private int getMouseOver(int i, int j) {
i -= guiLeft;
j -= guiTop;
//fontRenderer.drawString(".", i , j, 0xffffff);
if (i >= 4 && i < xSize - 4 && j >= 4 && j < ySize) {
for (int j1 = 0; j1 < list.size(); j1++) {
if (!mouseInOption(i, j, j1)) {
continue;
}
// Skip non-interactive entries (colored headers/separators)
if (colors.containsKey(list.get(j1))) {
continue;
}
return j1;
}
@@ -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));
@@ -8,6 +8,7 @@ public enum EnumScrollData {
QUEST_GROUP,
ABILITY_TYPES,
CUSTOM_ABILITIES,
BUILTIN_ABILITIES,
ANIMATIONS,
BUILTIN_ANIMATIONS
}
@@ -4,6 +4,7 @@ import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
import kamkeel.npcs.controllers.data.ability.UserType;
import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket;
@@ -118,9 +119,8 @@ public class PlayerAbilityData implements IPlayerAbilityData {
case ACTIVE:
if (phaseChanged && oldPhase == AbilityPhase.WINDUP) {
// Just entered ACTIVE phase
TelegraphInstance telegraph = currentAbility.getTelegraphInstance();
if (telegraph != null) {
// Just entered ACTIVE phase - lock all telegraph positions
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
telegraph.lockPosition();
}
removeTelegraph(currentAbility, player);
@@ -418,6 +418,53 @@ public class PlayerAbilityData implements IPlayerAbilityData {
}
}
/**
* Handle damage taken while an ability is executing.
* Called from LivingHurtEvent BEFORE damage is applied to health.
* Triggers Guard counter detection and handles ability interruption.
*
* @param source The damage source
* @param amount The damage amount (post-armor)
* @return The modified damage amount (reduced by guard, or 0 if counter absorbs)
*/
public float onDamage(DamageSource source, float amount) {
if (currentAbility == null || !currentAbility.isExecuting()) {
return amount;
}
net.minecraft.entity.Entity sourceEntity = source.getEntity();
EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null;
// Track counter state before calling onDamageTaken
boolean wasCounterTriggered = false;
if (currentAbility instanceof AbilityGuard) {
wasCounterTriggered = ((AbilityGuard) currentAbility).isCounterTriggered();
}
currentAbility.onDamageTaken(playerData.player, attacker, source, amount);
// Guard: handle damage reduction and counter absorption
if (currentAbility instanceof AbilityGuard) {
AbilityGuard guard = (AbilityGuard) currentAbility;
if (guard.isGuarding()) {
// Counter just triggered — absorb full damage
if (!wasCounterTriggered && guard.isCounterTriggered()) {
return 0;
}
// Normal guard — apply flat damage reduction
float reduction = guard.getDamageReductionFactor();
return Math.max(0, amount - reduction);
}
}
// Check for ability interruption
if (currentAbility != null && currentAbility.canInterrupt(source)) {
interruptCurrentAbility();
}
return amount;
}
@Override
public boolean isOnCooldown() {
EntityPlayer player = playerData.player;
@@ -451,23 +498,25 @@ public class PlayerAbilityData implements IPlayerAbilityData {
// ═══════════════════════════════════════════════════════════════════
private void spawnTelegraph(Ability ability, EntityPlayer player, EntityLivingBase target) {
TelegraphInstance telegraph = ability.createTelegraph(player, target);
if (telegraph != null) {
ability.setTelegraphInstance(telegraph);
List<TelegraphInstance> telegraphs = ability.createTelegraphs(player, target);
if (!telegraphs.isEmpty()) {
ability.setTelegraphInstances(telegraphs);
if (player instanceof EntityPlayerMP) {
TelegraphSpawnPacket.sendToTracking(telegraph, player);
for (TelegraphInstance telegraph : telegraphs) {
TelegraphSpawnPacket.sendToTracking(telegraph, player);
}
}
}
}
private void removeTelegraph(Ability ability, EntityPlayer player) {
TelegraphInstance telegraph = ability.getTelegraphInstance();
if (telegraph != null) {
List<TelegraphInstance> telegraphs = ability.getTelegraphInstances();
for (TelegraphInstance telegraph : telegraphs) {
if (player instanceof EntityPlayerMP) {
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player);
}
ability.setTelegraphInstance(null);
}
ability.setTelegraphInstances(null);
}
// ═══════════════════════════════════════════════════════════════════
@@ -308,6 +308,13 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
@Override
public void onUpdate() {
super.onUpdate();
// Re-apply locks after super.onUpdate() to override body rotation smoothing
// in EntityLivingBase.onUpdate() (func_110146_f) which runs AFTER onLivingUpdate()
// and overwrites renderYawOffset, causing a visible twitch each frame.
abilities.applyRotationControl();
abilities.applyLockedPosition();
if (!isRemote()) {
if (this.ticksExisted % 10 == 0) {
EventHooks.onNPCUpdate(this);
@@ -481,8 +488,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
super.onLivingUpdate();
// Apply locked rotation after super.onLivingUpdate() to override look helper changes
abilities.applyLockedRotation();
// Apply locks after super.onLivingUpdate() to override AI/physics changes
abilities.applyRotationControl();
abilities.applyLockedPosition();
handleMountRiderState();
@@ -670,6 +678,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
if (entity instanceof EntityLivingBase)
attackingEntity = (EntityLivingBase) entity;
if (faction.isPassive)
return false;
if (attackingEntity != null && attackingEntity == getOwner())
return false;
else if (attackingEntity instanceof EntityNPCInterface) {
@@ -42,12 +42,25 @@ public class ScriptDataAbilities implements IDataAbilities {
@Override
public void addAbility(IAbility ability) {
if (ability instanceof Ability) {
data.addAbility((Ability) ability);
Ability a = (Ability) ability;
// Validate ability allows NPCs
if (!a.getAllowedBy().allowsNpc()) {
return; // Ability is PLAYER_ONLY
}
data.addAbility(a);
}
}
@Override
public void addAbilityReference(String key) {
// Validate ability exists and allows NPCs
Ability ability = AbilityController.Instance.resolveAbility(key);
if (ability == null) {
return; // Ability doesn't exist
}
if (!ability.getAllowedBy().allowsNpc()) {
return; // Ability is PLAYER_ONLY
}
data.addAbilityReference(key);
}
@@ -158,6 +171,14 @@ public class ScriptDataAbilities implements IDataAbilities {
@Override
public boolean executeAbility(String key, Object target) {
// Validate ability exists and allows NPCs
Ability ability = AbilityController.Instance.resolveAbility(key);
if (ability == null) {
return false; // Ability doesn't exist
}
if (!ability.getAllowedBy().allowsNpc()) {
return false; // Ability is PLAYER_ONLY
}
EntityLivingBase targetEntity = resolveTarget(target);
return data.executeAbility(key, targetEntity);
}
@@ -1,5 +1,7 @@
package noppes.npcs.scripted.wrapper;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import net.minecraft.entity.player.EntityPlayer;
import noppes.npcs.api.ability.IAbility;
import noppes.npcs.api.ability.IPlayerAbilityData;
@@ -26,7 +28,17 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData {
@Override
public void unlockAbility(String key) {
data.unlockAbility(key);
// Validate ability exists and allows players
Ability ability = AbilityController.Instance.resolveAbility(key);
if (ability == null) {
return; // Ability doesn't exist
}
if (!ability.getAllowedBy().allowsPlayer()) {
return; // Ability is NPC-only
}
// Use canonical key (registry key for built-in, UUID for custom)
String canonicalKey = ability.getId() != null ? ability.getId() : key;
data.unlockAbility(canonicalKey);
}
@Override
@@ -0,0 +1,59 @@
{
"Loop": 0,
"Speed": 1.0f,
"Frames": [
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 3.0f,
"FrameParts": [
{
"Pivot2": -6.0f,
"Part": "RIGHT_ARM",
"Rotation1": -50.0f,
"Rotation0": -75.0f,
"Pivot0": 0.0f,
"Pivot1": 3.0f,
"Rotation2": -45.0f
},
{
"Pivot2": -6.0f,
"Part": "LEFT_ARM",
"Rotation1": 50.0f,
"Rotation0": -75.0f,
"Pivot0": 0.0f,
"Pivot1": 3.0f,
"Rotation2": 45.0f
},
{
"Pivot2": -6.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -6.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 31.0f,
"Pivot0": 0.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 2
}
],
"CurrentFrameTime": 0,
"CurrentFrame": 0,
"Smooth": 0b,
"ID": 28,
"WhileAttacking": 1b,
"WhileWalking": 1b,
"Name": "Ability_Charge_Active",
"WhileStanding": 1b
}
@@ -0,0 +1,122 @@
{
"Loop": -1,
"Speed": 1.0f,
"Frames": [
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 0.8f,
"FrameParts": [
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 0.0f,
"Rotation0": 69.0f,
"Pivot0": 0.0f,
"Pivot1": 5.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -8.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 12.0f,
"Rotation2": 18.0f
},
{
"Pivot2": -6.0f,
"Part": "RIGHT_LEG",
"Rotation1": 0.0f,
"Rotation0": 44.0f,
"Pivot0": 0.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -8.0f,
"Part": "LEFT_ARM",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 12.0f,
"Rotation2": -18.0f
},
{
"Pivot2": -10.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 18.0f,
"Pivot0": 0.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -10.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 75.0f,
"Pivot0": 0.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 18
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 3.0f,
"FrameParts": [
{
"Pivot2": -6.0f,
"Part": "RIGHT_ARM",
"Rotation1": -50.0f,
"Rotation0": -75.0f,
"Pivot0": 0.0f,
"Pivot1": 3.0f,
"Rotation2": -45.0f
},
{
"Pivot2": -6.0f,
"Part": "LEFT_ARM",
"Rotation1": 50.0f,
"Rotation0": -75.0f,
"Pivot0": 0.0f,
"Pivot1": 3.0f,
"Rotation2": 45.0f
},
{
"Pivot2": -6.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -6.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 31.0f,
"Pivot0": 0.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 2
}
],
"CurrentFrameTime": 0,
"CurrentFrame": 0,
"Smooth": 0b,
"ID": 27,
"WhileAttacking": 1b,
"WhileWalking": 1b,
"Name": "Ability_Charge_Windup",
"WhileStanding": 1b
}
@@ -1,17 +1,17 @@
{
"Loop": 5,
"Loop": -1,
"Speed": 1.0f,
"Frames": [
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 2.0f,
"Speed": 3.0f,
"FrameParts": [
{
"Pivot2": -5.0f,
"Part": "BODY",
"Rotation1": -37.0f,
"Rotation0": 37.0f,
"Pivot2": -6.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 18.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
@@ -25,15 +25,6 @@
"Pivot1": 2.0f,
"Rotation2": 56.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -37.0f,
"Rotation0": 31.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -25.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_ARM",
@@ -43,33 +34,6 @@
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -1.0f,
"Part": "RIGHT_LEG",
"Rotation1": -18.0f,
"Rotation0": 12.0f,
"Pivot0": -1.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -6.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 18.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 2
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 1.5f,
"FrameParts": [
{
"Pivot2": -5.0f,
"Part": "BODY",
@@ -79,33 +43,6 @@
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -7.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": 25.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 56.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -37.0f,
"Rotation0": 31.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -25.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_ARM",
"Rotation1": 0.0f,
"Rotation0": 120.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -1.0f,
"Part": "RIGHT_LEG",
@@ -115,42 +52,6 @@
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -6.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 18.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 4
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 0.5f,
"FrameParts": [
{
"Pivot2": -5.0f,
"Part": "BODY",
"Rotation1": -37.0f,
"Rotation0": 37.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -7.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": 25.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 56.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
@@ -159,37 +60,10 @@
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -25.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_ARM",
"Rotation1": 0.0f,
"Rotation0": 120.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -1.0f,
"Part": "RIGHT_LEG",
"Rotation1": -18.0f,
"Rotation0": 12.0f,
"Pivot0": -1.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -6.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 18.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 4
"Duration": 3
},
{
"ColorMarker": 16777215,
@@ -198,8 +72,8 @@
"FrameParts": [
{
"Pivot2": 2.0f,
"Part": "BODY",
"Rotation1": -25.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -12.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
@@ -214,15 +88,6 @@
"Pivot1": 0.0f,
"Rotation2": 75.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -25.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 2.0f,
"Part": "LEFT_ARM",
@@ -232,6 +97,15 @@
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 2.0f,
"Part": "BODY",
"Rotation1": -25.0f,
"Rotation0": -12.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
@@ -242,17 +116,17 @@
"Rotation2": 6.0f
},
{
"Pivot2": 2.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -12.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -25.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
}
],
"Smooth": 0b,
"Duration": 2
"Duration": 3
},
{
"ColorMarker": 16777215,
@@ -261,8 +135,8 @@
"FrameParts": [
{
"Pivot2": 2.0f,
"Part": "BODY",
"Rotation1": -25.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -12.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
@@ -277,15 +151,6 @@
"Pivot1": 0.0f,
"Rotation2": 75.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -25.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 2.0f,
"Part": "LEFT_ARM",
@@ -295,6 +160,15 @@
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 2.0f,
"Part": "BODY",
"Rotation1": -25.0f,
"Rotation0": -12.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
@@ -305,27 +179,27 @@
"Rotation2": 6.0f
},
{
"Pivot2": 2.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -12.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -25.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
}
],
"Smooth": 0b,
"Duration": 4
"Duration": 3
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 0.5f,
"Speed": 0.25f,
"FrameParts": [
{
"Pivot2": 2.0f,
"Part": "BODY",
"Rotation1": -25.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -12.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
@@ -340,15 +214,6 @@
"Pivot1": 0.0f,
"Rotation2": 75.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -25.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 2.0f,
"Part": "LEFT_ARM",
@@ -358,6 +223,15 @@
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 2.0f,
"Part": "BODY",
"Rotation1": -25.0f,
"Rotation0": -12.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
@@ -368,17 +242,17 @@
"Rotation2": 6.0f
},
{
"Pivot2": 2.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -12.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -25.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
}
],
"Smooth": 0b,
"Duration": 4
"Duration": 3
}
],
"CurrentFrameTime": 0,
@@ -7,51 +7,6 @@
"Comment": "",
"Speed": 0.5f,
"FrameParts": [
{
"Pivot2": -8.0f,
"Part": "BODY",
"Rotation1": 25.0f,
"Rotation0": 31.0f,
"Pivot0": -2.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -5.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": 50.0f,
"Pivot0": 0.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -2.0f,
"Part": "LEFT_LEG",
"Rotation1": 0.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -31.0f
},
{
"Pivot2": -8.0f,
"Part": "LEFT_ARM",
"Rotation1": 31.0f,
"Rotation0": -56.0f,
"Pivot0": -2.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -3.0f,
"Part": "RIGHT_LEG",
"Rotation1": 25.0f,
"Rotation0": 18.0f,
"Pivot0": -1.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "FULL_MODEL",
@@ -69,6 +24,51 @@
"Pivot0": -2.0f,
"Pivot1": 2.0f,
"Rotation2": -12.0f
},
{
"Pivot2": -5.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": 50.0f,
"Pivot0": 0.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -8.0f,
"Part": "LEFT_ARM",
"Rotation1": 31.0f,
"Rotation0": -56.0f,
"Pivot0": -2.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -8.0f,
"Part": "BODY",
"Rotation1": 25.0f,
"Rotation0": 31.0f,
"Pivot0": -2.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -3.0f,
"Part": "RIGHT_LEG",
"Rotation1": 25.0f,
"Rotation0": 18.0f,
"Pivot0": -1.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -2.0f,
"Part": "LEFT_LEG",
"Rotation1": 0.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -31.0f
}
],
"Smooth": 0b,
@@ -79,51 +79,6 @@
"Comment": "",
"Speed": 0.35f,
"FrameParts": [
{
"Pivot2": 0.0f,
"Part": "BODY",
"Rotation1": 50.0f,
"Rotation0": -6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 5.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": 50.0f,
"Pivot0": 2.0f,
"Pivot1": 0.0f,
"Rotation2": 170.0f
},
{
"Pivot2": -3.0f,
"Part": "LEFT_ARM",
"Rotation1": 75.0f,
"Rotation0": -107.0f,
"Pivot0": -6.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -4.0f,
"Part": "LEFT_LEG",
"Rotation1": -6.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": 50.0f,
"Rotation0": 18.0f,
"Pivot0": -3.0f,
"Pivot1": -4.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "FULL_MODEL",
@@ -141,6 +96,51 @@
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 5.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": 50.0f,
"Pivot0": 2.0f,
"Pivot1": 0.0f,
"Rotation2": 170.0f
},
{
"Pivot2": -3.0f,
"Part": "LEFT_ARM",
"Rotation1": 75.0f,
"Rotation0": -107.0f,
"Pivot0": -6.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "BODY",
"Rotation1": 50.0f,
"Rotation0": -6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": 50.0f,
"Rotation0": 18.0f,
"Pivot0": -3.0f,
"Pivot1": -4.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -4.0f,
"Part": "LEFT_LEG",
"Rotation1": -6.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
@@ -153,9 +153,18 @@
"FrameParts": [
{
"Pivot2": 0.0f,
"Part": "BODY",
"Rotation1": 50.0f,
"Rotation0": -6.0f,
"Part": "FULL_MODEL",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
@@ -179,10 +188,10 @@
"Rotation2": 0.0f
},
{
"Pivot2": -4.0f,
"Part": "LEFT_LEG",
"Rotation1": -6.0f,
"Rotation0": 12.0f,
"Pivot2": 0.0f,
"Part": "BODY",
"Rotation1": 50.0f,
"Rotation0": -6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
@@ -197,19 +206,10 @@
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "FULL_MODEL",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot2": -4.0f,
"Part": "LEFT_LEG",
"Rotation1": -6.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
@@ -221,13 +221,13 @@
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 2.0f,
"Speed": 3.0f,
"FrameParts": [
{
"Pivot2": -5.0f,
"Part": "BODY",
"Rotation1": -37.0f,
"Rotation0": 37.0f,
"Pivot2": -6.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 18.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
@@ -241,15 +241,6 @@
"Pivot1": 2.0f,
"Rotation2": 56.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -37.0f,
"Rotation0": 31.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -25.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_ARM",
@@ -259,6 +250,15 @@
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -5.0f,
"Part": "BODY",
"Rotation1": -37.0f,
"Rotation0": 37.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -1.0f,
"Part": "RIGHT_LEG",
@@ -269,13 +269,13 @@
"Rotation2": 0.0f
},
{
"Pivot2": -6.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 18.0f,
"Pivot0": 3.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -37.0f,
"Rotation0": 31.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -25.0f
}
],
"Smooth": 0b,
@@ -283,7 +283,7 @@
}
],
"CurrentFrameTime": 0,
"CurrentFrame": 1,
"CurrentFrame": 3,
"Smooth": 0b,
"ID": 17,
"WhileAttacking": 1b,
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,203 @@
{
"Loop": 1,
"Speed": 1.0f,
"Frames": [
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 3.0f,
"FrameParts": [
{
"Pivot2": -7.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 18.0f
},
{
"Pivot2": -7.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": -90.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -18.0f
},
{
"Pivot2": -6.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -1.0f,
"Part": "LEFT_ARM",
"Rotation1": -25.0f,
"Rotation0": -90.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
"Rotation2": -25.0f
}
],
"Smooth": 0b,
"Duration": 2
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 3.0f,
"FrameParts": [
{
"Pivot2": -7.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 18.0f
},
{
"Pivot2": -1.0f,
"Part": "RIGHT_ARM",
"Rotation1": 25.0f,
"Rotation0": -90.0f,
"Pivot0": 1.0f,
"Pivot1": 1.0f,
"Rotation2": 25.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -18.0f
},
{
"Pivot2": -6.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -7.0f,
"Part": "LEFT_ARM",
"Rotation1": 0.0f,
"Rotation0": -90.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 5
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 3.0f,
"FrameParts": [
{
"Pivot2": -7.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 18.0f
},
{
"Pivot2": -7.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": -90.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -18.0f
},
{
"Pivot2": -6.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -1.0f,
"Part": "LEFT_ARM",
"Rotation1": -25.0f,
"Rotation0": -90.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
"Rotation2": -25.0f
}
],
"Smooth": 0b,
"Duration": 5
}
],
"CurrentFrameTime": 0,
"CurrentFrame": 0,
"Smooth": 0b,
"ID": 26,
"WhileAttacking": 1b,
"WhileWalking": 1b,
"Name": "Ability_OrbBarrage_Active",
"WhileStanding": 1b
}
@@ -0,0 +1,201 @@
{
"Loop": -1,
"Speed": 1.0f,
"Frames": [
{
"ColorMarker": 16777215,
"Comment": "",
"FrameParts": [
{
"Pivot2": 3.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": 31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 18.0f
},
{
"Pivot2": 3.0f,
"Part": "RIGHT_ARM",
"Rotation1": 56.0f,
"Rotation0": -126.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -18.0f
},
{
"Pivot2": 3.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 3.0f,
"Part": "LEFT_ARM",
"Rotation1": -56.0f,
"Rotation0": -126.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 12.0f
}
],
"Duration": 8
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 0.25f,
"FrameParts": [
{
"Pivot2": 3.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": 31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 18.0f
},
{
"Pivot2": 3.0f,
"Part": "RIGHT_ARM",
"Rotation1": 56.0f,
"Rotation0": -126.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": -31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -18.0f
},
{
"Pivot2": 3.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 3.0f,
"Part": "LEFT_ARM",
"Rotation1": -56.0f,
"Rotation0": -126.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 12.0f
}
],
"Smooth": 0b,
"Duration": 19
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 3.0f,
"FrameParts": [
{
"Pivot2": -7.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 2.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 18.0f
},
{
"Pivot2": -7.0f,
"Part": "RIGHT_ARM",
"Rotation1": 0.0f,
"Rotation0": -90.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 31.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -18.0f
},
{
"Pivot2": -6.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -1.0f,
"Part": "LEFT_ARM",
"Rotation1": -25.0f,
"Rotation0": -90.0f,
"Pivot0": -1.0f,
"Pivot1": 1.0f,
"Rotation2": -25.0f
}
],
"Smooth": 0b,
"Duration": 3
}
],
"CurrentFrameTime": 0,
"CurrentFrame": 2,
"Smooth": 0b,
"ID": 25,
"WhileAttacking": 1b,
"WhileWalking": 1b,
"Name": "Ability_OrbBarrage_Windup",
"WhileStanding": 1b
}
@@ -0,0 +1,266 @@
{
"Loop": 3,
"Speed": 1.0f,
"Frames": [
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 1.0f,
"FrameParts": [
{
"Pivot2": -12.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 82.0f,
"Pivot0": 0.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 25.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": -12.0f
},
{
"Pivot2": -10.0f,
"Part": "LEFT_ARM",
"Rotation1": 6.0f,
"Rotation0": 0.0f,
"Pivot0": -1.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -11.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 101.0f,
"Pivot0": 0.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -25.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 12.0f
},
{
"Pivot2": -10.0f,
"Part": "RIGHT_ARM",
"Rotation1": -6.0f,
"Rotation0": 0.0f,
"Pivot0": 1.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 10
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 2.0f,
"FrameParts": [
{
"Pivot2": 3.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 3.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 4.0f,
"Part": "LEFT_ARM",
"Rotation1": -56.0f,
"Rotation0": -132.0f,
"Pivot0": -2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 12.0f
},
{
"Pivot2": 4.0f,
"Part": "RIGHT_ARM",
"Rotation1": 56.0f,
"Rotation0": -132.0f,
"Pivot0": 2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 3
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 0.5f,
"FrameParts": [
{
"Pivot2": 3.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 3.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 4.0f,
"Part": "LEFT_ARM",
"Rotation1": -56.0f,
"Rotation0": -132.0f,
"Pivot0": -2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 12.0f
},
{
"Pivot2": 4.0f,
"Part": "RIGHT_ARM",
"Rotation1": 56.0f,
"Rotation0": -132.0f,
"Pivot0": 2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 6
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 0.5f,
"FrameParts": [
{
"Pivot2": 1.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": -6.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 1.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 0.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 3.0f,
"Part": "LEFT_ARM",
"Rotation1": -56.0f,
"Rotation0": -37.0f,
"Pivot0": -2.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 12.0f
},
{
"Pivot2": 3.0f,
"Part": "RIGHT_ARM",
"Rotation1": 56.0f,
"Rotation0": -37.0f,
"Pivot0": 2.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 5
}
],
"CurrentFrameTime": 0,
"CurrentFrame": 0,
"Smooth": 0b,
"ID": 24,
"WhileAttacking": 1b,
"WhileWalking": 1b,
"Name": "Ability_Zone_Active",
"WhileStanding": 1b
}
@@ -0,0 +1,392 @@
{
"Loop": -1,
"Speed": 1.0f,
"Frames": [
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 2.0f,
"FrameParts": [
{
"Pivot2": -3.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 12.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 12.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -6.0f
},
{
"Pivot2": -3.0f,
"Part": "LEFT_ARM",
"Rotation1": 37.0f,
"Rotation0": -37.0f,
"Pivot0": -1.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -3.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 31.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -12.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 6.0f
},
{
"Pivot2": -3.0f,
"Part": "RIGHT_ARM",
"Rotation1": -37.0f,
"Rotation0": -37.0f,
"Pivot0": 1.0f,
"Pivot1": 0.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 6
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 0.25f,
"FrameParts": [
{
"Pivot2": 3.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 3.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 4.0f,
"Part": "LEFT_ARM",
"Rotation1": -56.0f,
"Rotation0": -132.0f,
"Pivot0": -2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 12.0f
},
{
"Pivot2": 4.0f,
"Part": "RIGHT_ARM",
"Rotation1": 56.0f,
"Rotation0": -132.0f,
"Pivot0": 2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 6
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 0.5f,
"FrameParts": [
{
"Pivot2": 3.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 3.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 4.0f,
"Part": "LEFT_ARM",
"Rotation1": -56.0f,
"Rotation0": -132.0f,
"Pivot0": -2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 12.0f
},
{
"Pivot2": 4.0f,
"Part": "RIGHT_ARM",
"Rotation1": 56.0f,
"Rotation0": -132.0f,
"Pivot0": 2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 6
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 0.75f,
"FrameParts": [
{
"Pivot2": 3.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 3.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 4.0f,
"Part": "LEFT_ARM",
"Rotation1": -56.0f,
"Rotation0": -132.0f,
"Pivot0": -2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 12.0f
},
{
"Pivot2": 4.0f,
"Part": "RIGHT_ARM",
"Rotation1": 56.0f,
"Rotation0": -132.0f,
"Pivot0": 2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 6
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 1.0f,
"FrameParts": [
{
"Pivot2": 3.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": -12.0f
},
{
"Pivot2": 3.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": -18.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 4.0f,
"Part": "LEFT_ARM",
"Rotation1": -56.0f,
"Rotation0": -132.0f,
"Pivot0": -2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -18.0f,
"Rotation0": 6.0f,
"Pivot0": 0.0f,
"Pivot1": 0.0f,
"Rotation2": 12.0f
},
{
"Pivot2": 4.0f,
"Part": "RIGHT_ARM",
"Rotation1": 56.0f,
"Rotation0": -132.0f,
"Pivot0": 2.0f,
"Pivot1": 3.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 3
},
{
"ColorMarker": 16777215,
"Comment": "",
"Speed": 3.0f,
"FrameParts": [
{
"Pivot2": -12.0f,
"Part": "BODY",
"Rotation1": 0.0f,
"Rotation0": 82.0f,
"Pivot0": 0.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "LEFT_LEG",
"Rotation1": 25.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": -12.0f
},
{
"Pivot2": -10.0f,
"Part": "LEFT_ARM",
"Rotation1": 6.0f,
"Rotation0": 0.0f,
"Pivot0": -1.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
},
{
"Pivot2": -11.0f,
"Part": "HEAD",
"Rotation1": 0.0f,
"Rotation0": 101.0f,
"Pivot0": 0.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
},
{
"Pivot2": 0.0f,
"Part": "RIGHT_LEG",
"Rotation1": -25.0f,
"Rotation0": 25.0f,
"Pivot0": 0.0f,
"Pivot1": 1.0f,
"Rotation2": 12.0f
},
{
"Pivot2": -10.0f,
"Part": "RIGHT_ARM",
"Rotation1": -6.0f,
"Rotation0": 0.0f,
"Pivot0": 1.0f,
"Pivot1": 12.0f,
"Rotation2": 0.0f
}
],
"Smooth": 0b,
"Duration": 3
}
],
"CurrentFrameTime": 0,
"CurrentFrame": 5,
"Smooth": 0b,
"ID": 23,
"WhileAttacking": 1b,
"WhileWalking": 1b,
"Name": "Ability_Zone_Windup",
"WhileStanding": 1b
}
@@ -164,6 +164,7 @@ gui.playerMap=Regen PlayerMap
gui.unused=Unused
gui.apply=Apply
gui.cancel=Cancel
gui.continue=Continue
gui.yes=Yes
gui.lineColor1=Upper line color
@@ -638,6 +639,7 @@ faction.friendly=Friendly
faction.points=Points
faction.ondeath=On death
faction.hidden=Hidden
faction.hidden.hover=Hides this faction from the player's faction standings UI
faction.default=Default
faction.attackHostile=Attack hostile factions
faction.defend=Defence faction members
@@ -646,8 +648,9 @@ faction.options=Faction Options
faction.increasepoints=Faction %s increased by %s points
faction.decreasepoints=Faction %s decreased by %s points
faction.attacked=Attacked by mobs
faction.attacked.hover=Whether vanilla mobs will target and attack NPCs in this faction
faction.passive=Is Passive
faction.passive.hover=Passive factions will not queue any attacking tasks [Good for Performance]
faction.passive.hover=Passive faction NPCs cannot be damaged and will not queue any attacking tasks [Good for Performance]
menu.inventory=Inventory
menu.display=Display
@@ -1546,6 +1549,7 @@ ability.anchor.rightHand=Right Hand
ability.anchor.leftHand=Left Hand
ability.anchor.aboveHead=Above Head
ability.anchor.chest=Chest
ability.anchor.eye=Eye
# Sweep Mode
ability.sweep.swipe=Swipe
@@ -1559,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
@@ -1604,11 +1644,8 @@ ability.cnpc.slam=Slam
ability.cnpc.heavy_hit=Heavy Hit
ability.cnpc.cutter=Cutter
ability.cnpc.beam=Beam
ability.cnpc.beam_dual=Beam Dual
ability.cnpc.projectile=Projectile
ability.cnpc.orb=Orb
ability.cnpc.orb_dual=Orb Dual
ability.cnpc.orb_barrage=Orb Barrage
ability.cnpc.charge=Charge
ability.cnpc.dash=Dash
ability.cnpc.teleport=Teleport
@@ -1619,7 +1656,6 @@ ability.cnpc.heal=Heal
ability.cnpc.hazard=Hazard
ability.cnpc.trap=Trap
ability.cnpc.disc=Disc
ability.cnpc.disc_dual=Disc Dual
ability.cnpc.laser_shot=Laser Shot
ability.cnpc.sweeper=Sweeper
@@ -1631,11 +1667,12 @@ ability.cnpc.sweeper=Sweeper
ability.weight=Weight
ability.windUpTicks=Wind Up
ability.cooldownTicks=Cooldown
ability.lockPosition=Lock Position
ability.lockType=Lock Type
ability.lockType.movement=Movement
ability.lockType.rotation=Rotation
ability.lockType.both=Both
ability.lockMovement=Lock Movement
ability.rotationMode=Rotation
ability.rotationPhase=Phase
ability.rotation.free=Free
ability.rotation.locked=Locked
ability.rotation.track=Track
ability.interruptible=Interruptible
ability.dazedTicks=Dazed Ticks
ability.targetingMode=Targeting
@@ -1661,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
@@ -1672,14 +1710,16 @@ ability.section.boomerang=Boomerang
ability.section.explosive=Explosive
ability.section.colors=Colors
ability.section.effects=Effects
ability.section.dual=Dual
ability.section.beam1=Beam 1
ability.section.beam2=Beam 2
ability.section.disc1=Disc 1
ability.section.disc2=Disc 2
ability.section.orb1=Orb 1
ability.section.orb2=Orb 2
ability.section.barrage=Barrage
ability.section.burst=Burst
ability.section.visual=Visual
ability.section.projectile1=Projectile 1
ability.section.projectile2=Projectile 2
ability.section.projectile3=Projectile 3
ability.section.projectile4=Projectile 4
ability.section.projectile5=Projectile 5
ability.section.projectile6=Projectile 6
ability.section.projectile7=Projectile 7
ability.section.projectile8=Projectile 8
ability.section.counter=Counter
ability.section.area=Area
ability.section.damage=Damage
@@ -1708,6 +1748,20 @@ 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
gui.applyPreset=Apply Preset
# Common Type Settings
ability.radius=Radius
@@ -1720,9 +1774,14 @@ ability.stunTicks=Stun Ticks
ability.hitWidth=Hit Width
ability.hitLength=Hit Length
ability.effects=Effects
ability.effect.burn=Burn
ability.mode=Mode
ability.duration=Duration
ability.anchorPoint=Anchor Point
ability.anchor.offsetX=Offset X
ability.anchor.offsetY=Offset Y
ability.anchor.offsetZ=Offset Z
ability.vertical=Vertical
ability.rotationSpeed=Rotation Speed
# Slam
@@ -1786,7 +1845,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
@@ -1795,8 +1853,6 @@ ability.affectsCaster=Affects Caster
# Trap
ability.armTime=Arm Time
ability.maxTriggers=Max Triggers
ability.triggerCooldown=Trigger Cooldown
# Sweeper
ability.sweepSpeed=Sweep Speed
@@ -1819,16 +1875,27 @@ ability.outerEnabled=Outer Glow
ability.outerWidth=Outer Width
ability.outerAlpha=Outer Alpha
ability.lightning=Lightning
ability.lightningDensity=Density
ability.lightningRadius=Radius
# Dual Energy Projectile Settings
ability.dualFire=Dual Fire
# Projectile Count / Settings
ability.projectileCount=Count
ability.dualFireDelay=Fire Delay
ability.fireDelay=Fire Delay
ability.colorOverride=Color Override
# Burst System
ability.burstEnabled=Burst
ability.burstAmount=Amount
ability.burstDelay=Delay
ability.burstReplayAnimations=Replay Animations
ability.burstOverlap=Overlap
ability.hover.burstOverlap=When enabled, projectiles from previous bursts keep flying while new ones fire
ability.hover.burstReplay=Replay windup and active animations for each burst iteration
# Ability Save/Load
ability.save.confirm=Save ability preset
ability.save.overwrite=Overwrite existing preset
ability.duplicate.name=A preset with this name already exists
ability.load.select=Select ability to load:
ability.load.mode=How should this ability be loaded?
ability.clone=Clone
@@ -1841,6 +1908,12 @@ ability.edit.mode=How should this ability be edited?
ability.cloneModify=Clone & Edit
ability.modifyParent=Edit Parent
# Ability Variant Selection
ability.selectVariant=Select Variant
ability.variant.single=Single
ability.variant.dual=Dual
ability.variant.barrage=Barrage
# Ability Type Selection
ability.selectType=Select Ability Type:
@@ -1862,8 +1935,9 @@ ability.noTypeSettings=No type-specific settings
# ═══════════════════════════════════════════════════════════════
ability.hover.weight=Higher weight = more likely to be chosen when multiple abilities available
ability.hover.lockPosition=Prevents NPC from moving or rotating while ability is charging or active
ability.hover.lockType=What to lock: Movement (pathfinding), Rotation (facing direction), or Both
ability.hover.lockMovement=Prevents NPC pathfinding during the selected phases (Windup, Active, or Both)
ability.hover.rotationMode=Free: normal AI rotation. Locked: freeze facing direction. Track: force face target for precise aiming.
ability.hover.rotationPhase=When the rotation mode applies (Windup, Active, or Both)
ability.hover.interruptible=If enabled, ability can be interrupted by taking damage
ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second)
ability.hover.windup=Charge-up time before ability activates (20 ticks = 1 second)
@@ -1884,6 +1958,7 @@ ability.hover.piercing=Hits multiple targets instead of stopping on first hit
ability.hover.lightning=Add electrical effect to visuals
ability.hover.outerGlow=Enable outer glow layer around projectile
ability.hover.anchorPoint=Where on the NPC the ability spawns from
ability.hover.vertical=Disc flies standing up with thin edge facing target (like a saw blade)
ability.hover.dualCharge=Whether or not should create two anchor points
ability.hover.damage=Base damage dealt to targets
ability.hover.speed=Movement speed of projectile
@@ -1903,7 +1978,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