diff --git a/build.gradle b/build.gradle
index 35dede377..16b20f9fa 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ plugins {
id "dts.typescript-generator"
}
-version = "1.11-beta3"
+version = "1.11-beta4"
group= "kamkeel.customnpc-plus"
archivesBaseName = "CustomNPC-Plus"
diff --git a/src/api b/src/api
index 22bce4b2a..2c2f0c0d8 160000
--- a/src/api
+++ b/src/api
@@ -1 +1 @@
-Subproject commit 22bce4b2acc9cf9a120a7fdddbb62cc4e567c796
+Subproject commit 2c2f0c0d8f21f0ab3a3bc6bea168b3d3b85aa806
diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java
index 725e440e5..d1b969921 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java
@@ -14,12 +14,12 @@ import java.util.List;
/**
* Renders the AbilityBeam entity with a head, tail orb, and curving trail.
* The trail shows the path the head has traveled using laser-style rectangular segments.
- *
+ *
* IMPORTANT: Trail points are stored RELATIVE to the origin position.
* We render by translating to the origin first, then drawing all points relative to that.
- *
+ *
* Trail segments now share vertices at connection points to prevent gaps during curves.
- *
+ *
* Design inspired by LouisXIV's energy rendering system.
*/
@SideOnly(Side.CLIENT)
@@ -182,7 +182,7 @@ public class RenderAbilityBeam extends RenderEnergyAbility {
* Uses averaged perpendicular vectors at connection points to prevent gaps.
*/
private void renderSmoothTrail(List trail, float width, int innerColor, int outerColor,
- boolean outerColorEnabled, float outerColorWidth) {
+ boolean outerColorEnabled, float outerColorWidth) {
if (trail.size() < 2) return;
int trailSize = trail.size();
@@ -213,7 +213,7 @@ public class RenderAbilityBeam extends RenderEnergyAbility {
* Older trail points fade out based on their age.
*/
private void renderFadingTrail(EntityAbilityBeam beam, List trail, float width, int innerColor, int outerColor,
- boolean outerColorEnabled, float outerColorWidth) {
+ boolean outerColorEnabled, float outerColorWidth) {
if (trail.size() < 2) return;
List ages = beam.getTrailPointAges();
@@ -242,7 +242,7 @@ public class RenderAbilityBeam extends RenderEnergyAbility {
* Render trail tube with age-based fading.
*/
private void renderFadingTrailTube(List trail, List perpFrames, List ages,
- int fadeTime, float width, int color, float baseAlpha) {
+ int fadeTime, float width, int color, float baseAlpha) {
float[] rgb = extractRGB(color);
float r = rgb[0], g = rgb[1], b = rgb[2];
float halfWidth = width * 0.5f;
@@ -392,13 +392,17 @@ public class RenderAbilityBeam extends RenderEnergyAbility {
dy /= len;
dz /= len;
} else {
- dx = 0; dy = 0; dz = 1;
+ dx = 0;
+ dy = 0;
+ dz = 1;
}
// Calculate perpendicular vectors using consistent up reference
double upX = 0, upY = 1, upZ = 0;
if (Math.abs(dy) > 0.9) {
- upX = 1; upY = 0; upZ = 0;
+ upX = 1;
+ upY = 0;
+ upZ = 0;
}
// Cross product for horizontal perpendicular
@@ -417,8 +421,12 @@ public class RenderAbilityBeam extends RenderEnergyAbility {
double vertY = dz * horzX - dx * horzZ;
double vertZ = dx * horzY - dy * horzX;
- frame[0] = horzX; frame[1] = horzY; frame[2] = horzZ;
- frame[3] = vertX; frame[4] = vertY; frame[5] = vertZ;
+ frame[0] = horzX;
+ frame[1] = horzY;
+ frame[2] = horzZ;
+ frame[3] = vertX;
+ frame[4] = vertY;
+ frame[5] = vertZ;
frames.add(frame);
}
@@ -429,7 +437,7 @@ public class RenderAbilityBeam extends RenderEnergyAbility {
* Render the trail as a continuous tube using pre-computed perpendicular frames.
*/
private void renderTrailTube(List trail, List perpFrames,
- float width, int color, float baseAlpha, boolean fadeFromStart) {
+ float width, int color, float baseAlpha, boolean fadeFromStart) {
float[] rgb = extractRGB(color);
float r = rgb[0], g = rgb[1], b = rgb[2];
float halfWidth = width * 0.5f;
diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java
index 76ffc6cfb..7f1a2e12c 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java
@@ -9,7 +9,7 @@ import org.lwjgl.opengl.GL11;
/**
* Renders the AbilityDisc entity as a flat spinning square.
* Rotates only on Y axis (flat spin like a saw blade).
- *
+ *
* Design inspired by LouisXIV's energy rendering system.
*/
@SideOnly(Side.CLIENT)
diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java
index e26f33489..543dba44c 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java
@@ -10,7 +10,7 @@ import org.lwjgl.opengl.GL11;
/**
* Renders the AbilityLaser entity as a rectangular beam with clear inner/outer colors.
- *
+ *
* Design inspired by LouisXIV's energy rendering system.
*/
@SideOnly(Side.CLIENT)
@@ -69,7 +69,9 @@ public class RenderAbilityLaser extends RenderEnergyAbility {
// Calculate perpendicular vectors (one horizontal, one vertical relative to beam)
double upX = 0, upY = 1, upZ = 0;
if (Math.abs(dy) > 0.9) {
- upX = 1; upY = 0; upZ = 0;
+ upX = 1;
+ upY = 0;
+ upZ = 0;
}
// Cross product for horizontal perpendicular
@@ -98,29 +100,29 @@ public class RenderAbilityLaser extends RenderEnergyAbility {
float outerWidth = innerWidth + laser.getOuterColorWidth() * width;
GL11.glDepthMask(false);
renderBeamRectangle(renderStartX, renderStartY, renderStartZ,
- renderEndX, renderEndY, renderEndZ,
- horzX, horzY, horzZ, vertX, vertY, vertZ,
- outerWidth, laser.getOuterColor(), alpha * 0.4f);
+ renderEndX, renderEndY, renderEndZ,
+ horzX, horzY, horzZ, vertX, vertY, vertZ,
+ outerWidth, laser.getOuterColor(), alpha * 0.4f);
// Render middle layer (halfway between inner and outer)
float midWidth = innerWidth + (outerWidth - innerWidth) * 0.5f;
renderBeamRectangle(renderStartX, renderStartY, renderStartZ,
- renderEndX, renderEndY, renderEndZ,
- horzX, horzY, horzZ, vertX, vertY, vertZ,
- midWidth, laser.getOuterColor(), alpha * 0.7f);
+ renderEndX, renderEndY, renderEndZ,
+ horzX, horzY, horzZ, vertX, vertY, vertZ,
+ midWidth, laser.getOuterColor(), alpha * 0.7f);
GL11.glDepthMask(true);
}
// Render inner core (solid)
renderBeamRectangle(renderStartX, renderStartY, renderStartZ,
- renderEndX, renderEndY, renderEndZ,
- horzX, horzY, horzZ, vertX, vertY, vertZ,
- innerWidth, laser.getInnerColor(), alpha);
+ renderEndX, renderEndY, renderEndZ,
+ horzX, horzY, horzZ, vertX, vertY, vertZ,
+ innerWidth, laser.getInnerColor(), alpha);
// Render lightning along the laser beam
if (laser.hasLightningEffect() && laser.getCurrentLength() > 0.1f) {
renderLaserLightning(laser, renderStartX, renderStartY, renderStartZ,
- renderEndX, renderEndY, renderEndZ, width);
+ renderEndX, renderEndY, renderEndZ, width);
}
restoreRenderState();
@@ -179,8 +181,8 @@ public class RenderAbilityLaser extends RenderEnergyAbility {
* Render lightning effects along the laser beam at multiple points.
*/
private void renderLaserLightning(EntityAbilityLaser laser,
- double startX, double startY, double startZ,
- double endX, double endY, double endZ, float width) {
+ double startX, double startY, double startZ,
+ double endX, double endY, double endZ, float width) {
AttachedLightningRenderer.LightningState state = getLightningState(laser);
float density = laser.getLightningDensity();
@@ -229,10 +231,10 @@ public class RenderAbilityLaser extends RenderEnergyAbility {
* Render a beam as a 3D rectangular prism (box along a line).
*/
private void renderBeamRectangle(double x1, double y1, double z1,
- double x2, double y2, double z2,
- double horzX, double horzY, double horzZ,
- double vertX, double vertY, double vertZ,
- float width, int color, float alpha) {
+ double x2, double y2, double z2,
+ double horzX, double horzY, double horzZ,
+ double vertX, double vertY, double vertZ,
+ float width, int color, float alpha) {
float[] rgb = extractRGB(color);
float r = rgb[0], g = rgb[1], b = rgb[2];
diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java
index 10705d762..2dbeac3c0 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java
@@ -104,8 +104,8 @@ public class RenderAbilitySweeper extends Render {
* Uses the same approach as RenderAbilityBeam.renderBeamRectangle.
*/
private void renderBeamSegment(double x1, double y1, double z1,
- double x2, double y2, double z2,
- float width, float r, float g, float b, float a) {
+ double x2, double y2, double z2,
+ float width, float r, float g, float b, float a) {
// Calculate direction
double dx = x2 - x1;
double dy = y2 - y1;
@@ -121,7 +121,9 @@ public class RenderAbilitySweeper extends Render {
// Calculate perpendicular vectors
double upX = 0, upY = 1, upZ = 0;
if (Math.abs(dy) > 0.9) {
- upX = 1; upY = 0; upZ = 0;
+ upX = 1;
+ upY = 0;
+ upZ = 0;
}
// Cross product for horizontal perpendicular
diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java
index e9775c542..ec21a268a 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java
@@ -8,13 +8,13 @@ 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.CustomFX;
import noppes.npcs.client.fx.ZoneParticleFX;
import org.lwjgl.opengl.GL11;
@@ -373,11 +373,21 @@ public class RenderAbilityZone extends Render {
}
}
- 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 float ringR(Ctx c) {
+ return c.outerEnabled ? c.outerR : c.ir;
+ }
- private static float sin(double v) { return (float) Math.sin(v); }
+ 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);
+ }
private void renderSwayingAccents(Ctx c, float r, float g, float b, float a, float height) {
if (height <= 0.1f) return;
@@ -413,10 +423,22 @@ public class RenderAbilityZone extends Render {
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;
+ 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);
@@ -467,10 +489,22 @@ public class RenderAbilityZone extends Render {
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;
+ 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;
@@ -506,10 +540,14 @@ public class RenderAbilityZone extends Render {
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};
+ 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;
@@ -519,9 +557,9 @@ public class RenderAbilityZone extends Render {
}
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) {
+ 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,
@@ -587,7 +625,7 @@ public class RenderAbilityZone extends Render {
}
private void renderRingLine(ZoneShape shape, float radius,
- float r, float g, float b, float a, float yOffset) {
+ float r, float g, float b, float a, float yOffset) {
switch (shape) {
case CIRCLE:
renderCircleRingLine(radius, r, g, b, a, yOffset);
@@ -621,8 +659,8 @@ public class RenderAbilityZone extends Render {
}
private void renderRotatingBorder(ZoneShape shape, float radius,
- float r, float g, float b, float a,
- float rotationDeg, float yOffset) {
+ float r, float g, float b, float a,
+ float rotationDeg, float yOffset) {
int dashCount = 16;
if (shape == ZoneShape.SQUARE) {
@@ -688,8 +726,8 @@ public class RenderAbilityZone extends Render {
}
private void renderVerticalAccents(ZoneShape shape, float radius,
- float r, float g, float b, float a,
- float height, float rotationDeg) {
+ float r, float g, float b, float a,
+ float height, float rotationDeg) {
if (height <= 0.1f) return;
Tessellator tess = Tessellator.instance;
@@ -701,10 +739,22 @@ public class RenderAbilityZone extends Render {
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;
+ 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);
diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java
index f4aa9c318..9496f06a5 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java
@@ -15,7 +15,7 @@ import org.lwjgl.opengl.GL12;
/**
* Base renderer for ability projectiles.
* Provides common GL state setup and rendering utilities.
- *
+ *
* Design inspired by LouisXIV's energy rendering system.
*/
@SideOnly(Side.CLIENT)
@@ -212,8 +212,8 @@ public abstract class RenderEnergyAbility extends Render {
* Render a line segment as a quad billboard facing the camera.
*/
protected void renderLineSegment(double x1, double y1, double z1,
- double x2, double y2, double z2,
- float width, int color, float alpha) {
+ double x2, double y2, double z2,
+ float width, int color, float alpha) {
float[] rgb = extractRGB(color);
float r = rgb[0], g = rgb[1], b = rgb[2];
@@ -270,9 +270,10 @@ public abstract class RenderEnergyAbility extends Render {
/**
* Render attached lightning arcs around an energy entity.
- * @param entity the energy entity
+ *
+ * @param entity the energy entity
* @param innerScale multiplier for inner radius (e.g. 0.3f for projectiles, 0.95f for dome)
- * @param baseSize the entity's current render size/scale
+ * @param baseSize the entity's current render size/scale
*/
protected void renderAttachedLightning(EntityEnergyAbility entity, float innerScale, float baseSize) {
AttachedLightningRenderer.LightningState state = getLightningState(entity);
diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyDome.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyDome.java
index ecdba16eb..8653bb27d 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyDome.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyDome.java
@@ -95,7 +95,11 @@ public class RenderEnergyDome extends RenderEnergyBarrier {
float ny = (y1 + y1 + y2 + y2) * 0.25f;
float nz = (z1 + z2 + z3 + z4) * 0.25f;
float nLen = (float) Math.sqrt(nx * nx + ny * ny + nz * nz);
- if (nLen > 0) { nx /= nLen; ny /= nLen; nz /= nLen; }
+ if (nLen > 0) {
+ nx /= nLen;
+ ny /= nLen;
+ nz /= nLen;
+ }
tess.setNormal(nx, ny, nz);
tess.addVertex(x1, y1, z1);
diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java
index 78ba58aed..9bcf473ac 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java
@@ -299,8 +299,8 @@ public class TelegraphRenderer {
* @param partialTicks Partial ticks for interpolation
*/
public void renderTelegraphInGUI(TelegraphInstance instance,
- double centerX, double centerY, double centerZ,
- float scale, float partialTicks) {
+ double centerX, double centerY, double centerZ,
+ float scale, float partialTicks) {
if (instance == null) return;
Telegraph telegraph = instance.getTelegraph();
diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java
index 2910a9566..5e9e2342b 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java
@@ -165,8 +165,8 @@ public class AttachedLightningRenderer {
* Generate a jagged lightning path between two points.
*/
private static List generateLightningPath(double x1, double y1, double z1,
- double x2, double y2, double z2,
- int segments, float displacement) {
+ double x2, double y2, double z2,
+ int segments, float displacement) {
List points = new ArrayList<>();
points.add(new double[]{x1, y1, z1});
@@ -257,8 +257,8 @@ 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) {
+ double x2, double y2, double z2,
+ float displacement, int outerColor, int innerColor, int maxAge) {
int segments = 4 + rand.nextInt(3);
List points = generateLightningPath(x1, y1, z1, x2, y2, z2, segments, displacement);
return new LightningArc(points, maxAge, outerColor, innerColor);
diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java
index 6cf6e7d3a..e077189ab 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java
@@ -243,7 +243,7 @@ public class LightningBolt {
public Segment(Vec3d start, Vec3d end) {
this(new BoltPoint(start, new Vec3d(0, 0, 0)),
- new BoltPoint(end, new Vec3d(0, 0, 0)), 1, 0, 0);
+ new BoltPoint(end, new Vec3d(0, 0, 0)), 1, 0, 0);
}
public void calcDiff() {
diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java
index 1951666e4..d3f9aeedd 100644
--- a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java
+++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java
@@ -5,7 +5,6 @@ import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.client.renderer.lightning.LightningBolt.Segment;
import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.ActiveRenderInfo;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.entity.Entity;
@@ -19,7 +18,7 @@ import java.awt.Color;
/**
* Handles rendering of lightning bolts in the world.
* Adapted from Botania's LightningHandler by Vazkii/ChickenBones.
- *
+ *
* Register this handler with: MinecraftForge.EVENT_BUS.register(new LightningHandler());
*/
@SideOnly(Side.CLIENT)
@@ -168,13 +167,13 @@ public class LightningHandler {
/**
* Spawn a lightning bolt between two points.
*
- * @param world The world
- * @param start Start position
- * @param end End position
+ * @param world The world
+ * @param start Start position
+ * @param end End position
* @param ticksPerMeter Speed of bolt expansion
- * @param seed Random seed for consistent appearance
- * @param colorOuter Outer glow color (ARGB)
- * @param colorInner Inner core color (ARGB)
+ * @param seed Random seed for consistent appearance
+ * @param colorOuter Outer glow color (ARGB)
+ * @param colorInner Inner core color (ARGB)
*/
public static void spawnLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, long seed, int colorOuter, int colorInner) {
LightningBolt bolt = new LightningBolt(world, start, end, ticksPerMeter, seed, colorOuter, colorInner);
diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java
index 10df3cdbe..9be809697 100644
--- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java
+++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java
@@ -1,12 +1,14 @@
package kamkeel.npcs.command;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
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.PlayerAbilityData;
import noppes.npcs.controllers.data.PlayerData;
import java.util.ArrayList;
@@ -77,7 +79,7 @@ public class AbilityCommand extends CommandKamkeelBase {
)
public void reload(ICommandSender sender, String[] args) {
AbilityController.Instance.load();
- sendResult(sender, "Abilities reloaded from disk.");
+ sendResult(sender, "Abilities and chained abilities reloaded from disk.");
}
@SubCommand(
@@ -110,14 +112,15 @@ public class AbilityCommand extends CommandKamkeelBase {
return;
}
- String key = args[0];
+ String key = joinArgs(args, 0);
Ability ability = AbilityController.Instance.resolveAbility(key);
if (ability == null) {
sendError(sender, "No ability found for: " + key);
return;
}
- sendResult(sender, "Ability: \u00A7b" + ability.getName());
+ sendResult(sender, "Ability: \u00A7b" + ability.getDisplayName());
+ sendResult(sender, " Name: \u00A77" + ability.getName());
sendResult(sender, " Type: \u00A7d" + ability.getTypeId());
sendResult(sender, " Cooldown: \u00A7e" + ability.getCooldownTicks() + " ticks");
sendResult(sender, " Wind Up: \u00A7e" + ability.getWindUpTicks() + " ticks");
@@ -162,46 +165,42 @@ public class AbilityCommand extends CommandKamkeelBase {
}
@SubCommand(
- desc = "Unlock an ability for a player",
+ desc = "Give an ability to a player",
usage = " "
)
- public void unlock(ICommandSender sender, String[] args) {
+ public void give(ICommandSender sender, String[] args) {
if (args.length < 2) {
- sendError(sender, "Usage: /kam ability unlock ");
+ sendError(sender, "Usage: /kam ability give ");
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, name for custom)
+ // 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
+ canonicalKey = abilityKey;
}
- String displayName = ability.getName() != null ? ability.getName() : canonicalKey;
+ String displayName = ability.getDisplayName();
- // 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);
@@ -209,39 +208,36 @@ public class AbilityCommand extends CommandKamkeelBase {
}
data.abilityData.unlockAbility(canonicalKey);
- sendResult(sender, "Unlocked ability '\u00A7b" + displayName + "\u00A77' for player \u00A7a" + playerName);
+ sendResult(sender, "Gave ability '\u00A7b" + displayName + "\u00A77' to player \u00A7a" + playerName);
}
@SubCommand(
- desc = "Lock (revoke) an ability from a player",
+ desc = "Remove an ability from a player",
usage = " "
)
- public void lock(ICommandSender sender, String[] args) {
+ public void remove(ICommandSender sender, String[] args) {
if (args.length < 2) {
- sendError(sender, "Usage: /kam ability lock ");
+ sendError(sender, "Usage: /kam ability remove ");
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;
+ displayName = ability.getDisplayName();
}
- // Get player data and lock
PlayerData data = PlayerDataController.Instance.getPlayerData(player);
if (!data.abilityData.hasUnlockedAbility(canonicalKey)) {
sendError(sender, "Player doesn't have ability: " + displayName);
@@ -249,7 +245,98 @@ public class AbilityCommand extends CommandKamkeelBase {
}
data.abilityData.lockAbility(canonicalKey);
- sendResult(sender, "Locked ability '\u00A7b" + displayName + "\u00A77' from player \u00A7a" + playerName);
+ sendResult(sender, "Removed ability '\u00A7b" + displayName + "\u00A77' from player \u00A7a" + playerName);
+ }
+
+ @SubCommand(
+ desc = "Give a chained ability to a player",
+ usage = " "
+ )
+ public void giveChain(ICommandSender sender, String[] args) {
+ if (args.length < 2) {
+ sendError(sender, "Usage: /kam ability giveChain ");
+ return;
+ }
+
+ String playerName = args[0];
+ String chainKey = joinArgs(args, 1);
+
+ EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName);
+ if (player == null) {
+ sendError(sender, "Player not found: " + playerName);
+ return;
+ }
+
+ ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(chainKey);
+ if (chain == null) {
+ sendError(sender, "Chained ability not found: " + chainKey);
+ return;
+ }
+
+ if (!chain.getAllowedBy().allowsPlayer()) {
+ sendError(sender, "Chained ability '\u00A7b" + chain.getDisplayName() + "\u00A7c' is NPC-only and cannot be given to players.");
+ return;
+ }
+
+ String chainId = chain.getId();
+ if (chainId == null || chainId.isEmpty()) {
+ sendError(sender, "Chained ability has no ID: " + chain.getDisplayName());
+ return;
+ }
+
+ // Store by UUID: chain:
+ String storageKey = PlayerAbilityData.CHAIN_PREFIX + chainId;
+ PlayerData data = PlayerDataController.Instance.getPlayerData(player);
+ if (data.abilityData.hasUnlockedAbility(storageKey)) {
+ sendError(sender, "Player already has chained ability: " + chain.getDisplayName());
+ return;
+ }
+
+ data.abilityData.unlockAbility(storageKey);
+ sendResult(sender, "Gave chained ability '\u00A7b" + chain.getDisplayName() + "\u00A77' to player \u00A7a" + playerName);
+ }
+
+ @SubCommand(
+ desc = "Remove a chained ability from a player",
+ usage = " "
+ )
+ public void removeChain(ICommandSender sender, String[] args) {
+ if (args.length < 2) {
+ sendError(sender, "Usage: /kam ability removeChain ");
+ return;
+ }
+
+ String playerName = args[0];
+ String chainKey = joinArgs(args, 1);
+
+ EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName);
+ if (player == null) {
+ sendError(sender, "Player not found: " + playerName);
+ return;
+ }
+
+ ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(chainKey);
+ if (chain == null) {
+ sendError(sender, "Chained ability not found: " + chainKey);
+ return;
+ }
+
+ String chainId = chain.getId();
+ if (chainId == null || chainId.isEmpty()) {
+ sendError(sender, "Chained ability has no ID: " + chain.getDisplayName());
+ return;
+ }
+
+ // Storage uses UUID: chain:
+ String storageKey = PlayerAbilityData.CHAIN_PREFIX + chainId;
+ PlayerData data = PlayerDataController.Instance.getPlayerData(player);
+ if (!data.abilityData.hasUnlockedAbility(storageKey)) {
+ sendError(sender, "Player doesn't have chained ability: " + chain.getDisplayName());
+ return;
+ }
+
+ data.abilityData.lockAbility(storageKey);
+ sendResult(sender, "Removed chained ability '\u00A7b" + chain.getDisplayName() + "\u00A77' from player \u00A7a" + playerName);
}
@SubCommand(
@@ -283,14 +370,23 @@ public class AbilityCommand extends CommandKamkeelBase {
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 + "]");
+
+ if (key.startsWith(PlayerAbilityData.CHAIN_PREFIX)) {
+ // Chained ability
+ String chainName = key.substring(PlayerAbilityData.CHAIN_PREFIX.length());
+ ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(chainName);
+ String displayName = chain != null ? chain.getDisplayName() : chainName;
+ sendResult(sender, prefix + "\u00A7d[Chain] \u00A7b" + displayName);
} else {
- sendResult(sender, prefix + "\u00A7b" + displayName);
+ // Regular ability
+ Ability ability = AbilityController.Instance.resolveAbility(key);
+ String displayName = ability != null ? ability.getDisplayName() : key;
+ if (!displayName.equals(key)) {
+ sendResult(sender, prefix + "\u00A7b" + displayName + " \u00A78[" + key + "]");
+ } else {
+ sendResult(sender, prefix + "\u00A7b" + displayName);
+ }
}
}
}
@@ -306,12 +402,33 @@ public class AbilityCommand extends CommandKamkeelBase {
/**
* Get list of player-usable ability names (for tab completion).
- * Uses registry keys for built-in abilities and display names for custom abilities.
- * Used by CommandKamkeel for usage token.
+ * Resolves UUIDs to names so tab completion shows human-readable names.
+ * Used by CommandKamkeel for {@code } usage token.
*/
public static List getPlayerAbilityNames() {
- List keys = new ArrayList<>(AbilityController.Instance.getPlayerAbilityKeys());
- Collections.sort(keys);
- return keys;
+ Set keys = AbilityController.Instance.getPlayerAbilityKeys();
+ List names = new ArrayList<>();
+ for (String key : keys) {
+ Ability a = AbilityController.Instance.resolveAbility(key);
+ names.add(a != null ? a.getName() : key);
+ }
+ Collections.sort(names);
+ return names;
+ }
+
+ /**
+ * Get list of player-usable chained ability names (for tab completion).
+ * Used by CommandKamkeel for {@code } usage token.
+ */
+ public static List getPlayerChainNames() {
+ List names = new ArrayList<>();
+ for (String chainName : AbilityController.Instance.getChainedAbilityNamesSet()) {
+ ChainedAbility chain = AbilityController.Instance.getChainedAbility(chainName);
+ if (chain != null && chain.getAllowedBy().allowsPlayer()) {
+ names.add(chainName);
+ }
+ }
+ Collections.sort(names);
+ return names;
}
}
diff --git a/src/main/java/kamkeel/npcs/command/AuctionCommand.java b/src/main/java/kamkeel/npcs/command/AuctionCommand.java
index 87d7e4385..a56dce671 100644
--- a/src/main/java/kamkeel/npcs/command/AuctionCommand.java
+++ b/src/main/java/kamkeel/npcs/command/AuctionCommand.java
@@ -20,6 +20,7 @@ import static kamkeel.npcs.util.ColorUtil.sendMessage;
import static kamkeel.npcs.util.ColorUtil.sendResult;
public class AuctionCommand extends CommandKamkeelBase {
+ private static final int COMMAND_PAGE_SIZE = 10;
public AuctionCommand() {
super();
@@ -62,9 +63,10 @@ public class AuctionCommand extends CommandKamkeelBase {
}
}
- List listings = controller.getActiveListings(null, page - 1, 10);
+ page = Math.max(1, page);
+ List listings = controller.getActiveListings(null, page - 1, COMMAND_PAGE_SIZE);
int total = controller.getTotalActiveListings(null);
- int totalPages = Math.max(1, (int) Math.ceil((double) total / 10));
+ int totalPages = Math.max(1, (int) Math.ceil((double) total / COMMAND_PAGE_SIZE));
if (listings.isEmpty()) {
sendMessage(sender, "\u00A77No active auctions found");
@@ -98,6 +100,10 @@ public class AuctionCommand extends CommandKamkeelBase {
return;
}
+ if (args.length < 1) {
+ throw new CommandException("Usage: /auction view ");
+ }
+
String playername = args[0];
List dataList = PlayerDataController.Instance.getPlayersData(sender, playername);
diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java
index 91356c825..0f0e0dbbf 100644
--- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java
+++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java
@@ -21,6 +21,7 @@ import java.util.List;
import java.util.Map;
import static kamkeel.npcs.command.AbilityCommand.getPlayerAbilityNames;
+import static kamkeel.npcs.command.AbilityCommand.getPlayerChainNames;
import static kamkeel.npcs.command.EffectCommand.getSortedEffectNames;
public class CommandKamkeel extends CommandBase {
@@ -139,6 +140,9 @@ public class CommandKamkeel extends CommandBase {
} else if (usage.equals("")) {
List keys = getPlayerAbilityNames();
return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()]));
+ } else if (usage.equals("")) {
+ List keys = getPlayerChainNames();
+ return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()]));
}
}
}
diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java
index df4d34998..c6ffe7ec1 100644
--- a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java
+++ b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java
@@ -11,7 +11,13 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
public abstract class CommandKamkeelBase extends CommandBase {
public Map subcommands = new HashMap();
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java
similarity index 57%
rename from src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java
rename to src/main/java/kamkeel/npcs/controllers/AbilityController.java
index 406d66c12..9c73edc7c 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java
+++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java
@@ -1,7 +1,27 @@
-package kamkeel.npcs.controllers.data.ability;
+package kamkeel.npcs.controllers;
-import kamkeel.npcs.controllers.data.ability.type.*;
-import kamkeel.npcs.controllers.SyncController;
+import kamkeel.npcs.controllers.data.ability.Ability;
+import kamkeel.npcs.controllers.data.ability.AbilityPhase;
+import kamkeel.npcs.controllers.data.ability.AbilityVariant;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
+import kamkeel.npcs.controllers.data.ability.IAbilityAction;
+import kamkeel.npcs.controllers.data.ability.IAbilityExtender;
+import kamkeel.npcs.controllers.data.ability.IAbilityFieldProvider;
+import kamkeel.npcs.controllers.data.ability.IChainedAbilityFieldProvider;
+import kamkeel.npcs.controllers.data.ability.UserType;
+import kamkeel.npcs.controllers.data.ability.type.AbilityCharge;
+import kamkeel.npcs.controllers.data.ability.type.AbilityCutter;
+import kamkeel.npcs.controllers.data.ability.type.AbilityDash;
+import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
+import kamkeel.npcs.controllers.data.ability.type.AbilityHazard;
+import kamkeel.npcs.controllers.data.ability.type.AbilityHeal;
+import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit;
+import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile;
+import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave;
+import kamkeel.npcs.controllers.data.ability.type.AbilitySlam;
+import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport;
+import kamkeel.npcs.controllers.data.ability.type.AbilityTrap;
+import kamkeel.npcs.controllers.data.ability.type.AbilityVortex;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityBeam;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDisc;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDome;
@@ -12,16 +32,22 @@ import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySlicer;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySweeper;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityWall;
import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.CustomNpcs;
import noppes.npcs.LogWriter;
import noppes.npcs.api.handler.IAbilityHandler;
import noppes.npcs.util.NBTJsonUtil;
-import net.minecraft.entity.player.EntityPlayer;
-
import java.io.File;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Pattern;
@@ -33,11 +59,13 @@ public class AbilityController implements IAbilityHandler {
// ── Core Registries ──────────────────────────────────────────────────────
private final Map> abilityTypes = new LinkedHashMap<>();
private final Map builtAbilities = new LinkedHashMap<>();
- private final Map customAbilities = new LinkedHashMap<>();
+ private final Map customAbilities = new LinkedHashMap<>(); // name → Ability
+ private final Map customAbilitiesById = new LinkedHashMap<>(); // UUID → Ability
// ── Extension Points ─────────────────────────────────────────────────────
private final Map> externalVariants = new LinkedHashMap<>();
private final List fieldProviders = new ArrayList<>();
+ private final List chainedFieldProviders = new ArrayList<>();
private final List extenders = new ArrayList<>();
private final List> flightCheckers = new ArrayList<>();
@@ -46,6 +74,11 @@ public class AbilityController implements IAbilityHandler {
Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
Pattern.CASE_INSENSITIVE);
+ // ── Chained Abilities ──────────────────────────────────────────────────
+ private final Map chainedAbilities = new LinkedHashMap<>(); // name → ChainedAbility
+ private final Map chainedAbilitiesById = new LinkedHashMap<>(); // UUID → ChainedAbility
+ private int chainedAbilityRevision = 0;
+
// ── Derived State ────────────────────────────────────────────────────────
private final Set builtInTypeIds = new HashSet<>();
@@ -162,6 +195,7 @@ public class AbilityController implements IAbilityHandler {
public void load() {
customAbilities.clear();
+ customAbilitiesById.clear();
int migrated = 0;
File dir = getDir();
@@ -177,6 +211,13 @@ public class AbilityController implements IAbilityHandler {
Ability ability = fromNBT(nbt);
if (ability == null) continue;
+ // Skip built-in abilities that were erroneously saved as custom files
+ if (ability.isBuiltIn()) {
+ file.delete();
+ LogWriter.info("Removed stale built-in ability file: " + filename);
+ continue;
+ }
+
// Legacy migration: UUID-named files get renamed to name-based
if (UUID_PATTERN.matcher(key).matches()) {
String name = ability.getName();
@@ -202,8 +243,24 @@ public class AbilityController implements IAbilityHandler {
migrated++;
}
- ability.setId(key);
+ // Ensure ability has a UUID; generate one for legacy abilities
+ String uuid = ability.getId();
+ if (uuid == null || uuid.isEmpty() || uuid.equals(key)) {
+ // Legacy: id was the name or empty — generate a real UUID
+ uuid = UUID.randomUUID().toString();
+ ability.setId(uuid);
+ // Re-save file with the new UUID
+ try {
+ NBTJsonUtil.SaveFile(file, ability.writeNBT());
+ } catch (Exception ex) {
+ LogWriter.error("Failed to save UUID for ability: " + key, ex);
+ }
+ migrated++;
+ }
+
+ ability.setName(key);
customAbilities.put(key, ability);
+ customAbilitiesById.put(uuid, ability);
} catch (Exception e) {
LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e);
}
@@ -212,9 +269,11 @@ public class AbilityController implements IAbilityHandler {
customAbilityRevision++;
if (migrated > 0) {
- LogWriter.info("Migrated " + migrated + " UUID-named abilities to name-based files");
+ LogWriter.info("Migrated " + migrated + " abilities (UUID-named files or missing UUIDs)");
}
LogWriter.info("Loaded " + customAbilities.size() + " custom abilities");
+
+ loadChainedAbilities();
}
private boolean nameFileExists(File dir, String name) {
@@ -223,42 +282,50 @@ public class AbilityController implements IAbilityHandler {
public boolean saveCustomAbility(Ability ability) {
if (ability == null) return false;
+ if (ability.isBuiltIn()) return false;
String name = ability.getName();
if (name == null || name.isEmpty()) return false;
- // Ensure unique name for new abilities (no existing id)
- String oldName = ability.getId();
- boolean isNew = oldName == null || oldName.isEmpty();
+ String uuid = ability.getId();
+ boolean isNew = (uuid == null || uuid.isEmpty());
+
if (isNew) {
+ // Generate UUID for new ability
+ uuid = UUID.randomUUID().toString();
+ ability.setId(uuid);
+
+ // Ensure unique name
while (hasCustomAbilityName(name)) {
name = name + "_";
}
ability.setName(name);
+ } else {
+ // Existing ability — check for rename via UUID lookup
+ Ability existing = customAbilitiesById.get(uuid);
+ if (existing != null) {
+ String oldName = existing.getName();
+ if (!oldName.equals(name)) {
+ // Name changed! Ensure new name is unique (excluding self)
+ String testName = name;
+ while (customAbilities.containsKey(testName) && !testName.equals(oldName)) {
+ testName = testName + "_";
+ }
+ if (!testName.equals(name)) {
+ name = testName;
+ ability.setName(name);
+ }
+
+ // Delete old file and remove old map entry
+ File oldFile = new File(getDir(), oldName + ".json");
+ if (oldFile.exists()) {
+ oldFile.delete();
+ }
+ customAbilities.remove(oldName);
+ }
+ }
}
- // Handle rename: old id (previous name) differs from current name
- if (!isNew && !oldName.equals(name)) {
- // Check uniqueness of new name (excluding the entry being renamed)
- String testName = name;
- while (customAbilities.containsKey(testName) && !testName.equals(oldName)) {
- testName = testName + "_";
- }
- if (!testName.equals(name)) {
- name = testName;
- ability.setName(name);
- }
-
- // Delete old file and remove old map entry
- File oldFile = new File(getDir(), oldName + ".json");
- if (oldFile.exists()) {
- oldFile.delete();
- }
- customAbilities.remove(oldName);
- }
-
- ability.setId(name);
-
File dir = getDir();
File fileNew = new File(dir, name + ".json_new");
File fileCurrent = new File(dir, name + ".json");
@@ -276,8 +343,9 @@ public class AbilityController implements IAbilityHandler {
}
customAbilities.put(name, ability);
+ customAbilitiesById.put(uuid, ability);
customAbilityRevision++;
- LogWriter.info("Saved custom ability: " + name);
+ LogWriter.info("Saved custom ability: " + name + " [" + uuid + "]");
SyncController.syncAllCustomAbilities();
return true;
} catch (Exception e) {
@@ -292,6 +360,12 @@ public class AbilityController implements IAbilityHandler {
Ability removed = customAbilities.remove(name);
if (removed == null) return false;
+ // Also remove from UUID index
+ String uuid = removed.getId();
+ if (uuid != null && !uuid.isEmpty()) {
+ customAbilitiesById.remove(uuid);
+ }
+
File dir = getDir();
File file = new File(dir, name + ".json");
if (file.exists()) {
@@ -318,7 +392,14 @@ public class AbilityController implements IAbilityHandler {
public void setCustomAbilities(Map synced) {
customAbilities.clear();
+ customAbilitiesById.clear();
customAbilities.putAll(synced);
+ for (Ability ability : synced.values()) {
+ String uuid = ability.getId();
+ if (uuid != null && !uuid.isEmpty()) {
+ customAbilitiesById.put(uuid, ability);
+ }
+ }
customAbilityRevision++;
}
@@ -334,6 +415,229 @@ public class AbilityController implements IAbilityHandler {
return dir;
}
+ // ═══════════════════════════════════════════════════════════════════════════
+ // CHAINED ABILITIES
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ private void loadChainedAbilities() {
+ chainedAbilities.clear();
+ chainedAbilitiesById.clear();
+
+ File dir = getChainedDir();
+ File[] files = dir.exists() ? dir.listFiles() : null;
+ if (files != null) {
+ for (File file : files) {
+ if (!file.isFile() || !file.getName().endsWith(".json")) continue;
+ try {
+ String filename = file.getName();
+ String key = filename.substring(0, filename.length() - 5);
+
+ NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
+ ChainedAbility chain = new ChainedAbility();
+ chain.readNBT(nbt);
+
+ chain.setName(key);
+ chainedAbilities.put(key, chain);
+
+ String uuid = chain.getId();
+ if (uuid != null && !uuid.isEmpty()) {
+ chainedAbilitiesById.put(uuid, chain);
+ }
+ } catch (Exception e) {
+ LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e);
+ }
+ }
+ }
+
+ chainedAbilityRevision++;
+ LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities");
+ }
+
+ public boolean saveChainedAbility(ChainedAbility chain) {
+ if (chain == null) return false;
+
+ String name = chain.getName();
+ if (name == null || name.isEmpty()) return false;
+
+ String uuid = chain.getId();
+ boolean isNew = (uuid == null || uuid.isEmpty());
+
+ if (isNew) {
+ uuid = UUID.randomUUID().toString();
+ chain.setId(uuid);
+
+ while (hasChainedAbilityName(name)) {
+ name = name + "_";
+ }
+ chain.setName(name);
+ } else {
+ ChainedAbility existing = chainedAbilitiesById.get(uuid);
+ if (existing != null) {
+ String oldName = existing.getName();
+ if (!oldName.equals(name)) {
+ String testName = name;
+ while (chainedAbilities.containsKey(testName) && !testName.equals(oldName)) {
+ testName = testName + "_";
+ }
+ if (!testName.equals(name)) {
+ name = testName;
+ chain.setName(name);
+ }
+
+ File oldFile = new File(getChainedDir(), oldName + ".json");
+ if (oldFile.exists()) {
+ oldFile.delete();
+ }
+ chainedAbilities.remove(oldName);
+ }
+ }
+ }
+
+ File dir = getChainedDir();
+ File fileNew = new File(dir, name + ".json_new");
+ File fileCurrent = new File(dir, name + ".json");
+
+ try {
+ NBTTagCompound nbt = chain.writeNBT();
+ NBTJsonUtil.SaveFile(fileNew, nbt);
+
+ if (fileCurrent.exists()) {
+ fileCurrent.delete();
+ }
+ fileNew.renameTo(fileCurrent);
+ if (fileNew.exists()) {
+ fileNew.delete();
+ }
+
+ chainedAbilities.put(name, chain);
+ chainedAbilitiesById.put(uuid, chain);
+ chainedAbilityRevision++;
+ LogWriter.info("Saved chained ability: " + name + " [" + uuid + "]");
+ SyncController.syncAllChainedAbilities();
+ return true;
+ } catch (Exception e) {
+ LogWriter.error("Error saving chained ability: " + name, e);
+ return false;
+ }
+ }
+
+ public boolean deleteChainedAbility(String name) {
+ if (name == null || name.isEmpty()) return false;
+
+ ChainedAbility removed = chainedAbilities.remove(name);
+ if (removed == null) return false;
+
+ String uuid = removed.getId();
+ if (uuid != null && !uuid.isEmpty()) {
+ chainedAbilitiesById.remove(uuid);
+ }
+
+ File dir = getChainedDir();
+ File file = new File(dir, name + ".json");
+ if (file.exists()) {
+ file.delete();
+ }
+
+ chainedAbilityRevision++;
+ LogWriter.info("Deleted chained ability: " + name);
+ SyncController.syncAllChainedAbilities();
+ return true;
+ }
+
+ public ChainedAbility resolveChainedAbility(String key) {
+ if (key == null || key.isEmpty()) return null;
+
+ ChainedAbility byId = chainedAbilitiesById.get(key);
+ if (byId != null) return byId.deepCopy();
+
+ ChainedAbility chain = chainedAbilities.get(key);
+ if (chain != null) return chain.deepCopy();
+
+ for (Map.Entry entry : chainedAbilities.entrySet()) {
+ if (entry.getKey().equalsIgnoreCase(key)) {
+ return entry.getValue().deepCopy();
+ }
+ }
+
+ return null;
+ }
+
+ public boolean canResolveChainedAbility(String key) {
+ if (key == null || key.isEmpty()) return false;
+ if (chainedAbilitiesById.containsKey(key)) return true;
+ if (chainedAbilities.containsKey(key)) return true;
+ for (String name : chainedAbilities.keySet()) {
+ if (name.equalsIgnoreCase(key)) return true;
+ }
+ return false;
+ }
+
+ @Override
+ public ChainedAbility getChainedAbility(String name) {
+ ChainedAbility chain = chainedAbilities.get(name);
+ return chain != null ? chain.deepCopy() : null;
+ }
+
+ public ChainedAbility getChainedAbilityByUUID(String uuid) {
+ return chainedAbilitiesById.get(uuid);
+ }
+
+ public Set getChainedAbilityNamesSet() {
+ return new LinkedHashSet<>(chainedAbilities.keySet());
+ }
+
+ public Map getChainedAbilities() {
+ return chainedAbilities;
+ }
+
+ @Override
+ public String[] getChainedAbilityNames() {
+ return chainedAbilities.keySet().toArray(new String[0]);
+ }
+
+ @Override
+ public boolean hasChainedAbilityName(String name) {
+ return chainedAbilities.containsKey(name);
+ }
+
+ @Override
+ public boolean deleteChainedAbilityByName(String name) {
+ return deleteChainedAbility(name);
+ }
+
+ @Override
+ public boolean saveChainedAbility(noppes.npcs.api.ability.IChainedAbility chain) {
+ if (chain instanceof ChainedAbility) {
+ return saveChainedAbility((ChainedAbility) chain);
+ }
+ return false;
+ }
+
+ public int getChainedAbilityRevision() {
+ return chainedAbilityRevision;
+ }
+
+ public void setChainedAbilities(Map synced) {
+ chainedAbilities.clear();
+ chainedAbilitiesById.clear();
+ chainedAbilities.putAll(synced);
+ for (ChainedAbility chain : synced.values()) {
+ String uuid = chain.getId();
+ if (uuid != null && !uuid.isEmpty()) {
+ chainedAbilitiesById.put(uuid, chain);
+ }
+ }
+ chainedAbilityRevision++;
+ }
+
+ private File getChainedDir() {
+ File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities" + File.separator + "chained");
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+ return dir;
+ }
+
// ═══════════════════════════════════════════════════════════════════════════
// RESOLUTION
// ═══════════════════════════════════════════════════════════════════════════
@@ -341,17 +645,28 @@ public class AbilityController implements IAbilityHandler {
public Ability resolveAbility(String key) {
if (key == null || key.isEmpty()) return null;
+ // Custom: UUID lookup (most common for persistent references)
+ Ability byUuid = customAbilitiesById.get(key);
+ if (byUuid != null) return byUuid.deepCopy();
+
// Built-in: exact name
Ability builtIn = builtAbilities.get(key);
if (builtIn != null) return builtIn.deepCopy();
- // Built-in: case-insensitive name
+ // Built-in: case-insensitive map key
for (Map.Entry entry : builtAbilities.entrySet()) {
if (entry.getKey().equalsIgnoreCase(key)) {
return entry.getValue().deepCopy();
}
}
+ // Built-in: by ability name (name may differ from map key, e.g. "Ki Fist" vs "ki_fist")
+ for (Ability ability : builtAbilities.values()) {
+ if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) {
+ return ability.deepCopy();
+ }
+ }
+
// Built-in: registry key / ID
for (Ability ability : builtAbilities.values()) {
if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) {
@@ -359,7 +674,7 @@ public class AbilityController implements IAbilityHandler {
}
}
- // Custom: exact name
+ // Custom: exact name (fallback for legacy references)
Ability custom = customAbilities.get(key);
if (custom != null) return custom.deepCopy();
@@ -381,11 +696,11 @@ public class AbilityController implements IAbilityHandler {
keys.add(id);
}
}
- // Custom abilities keyed by name
+ // Custom abilities keyed by UUID (stable references)
for (Ability ability : customAbilities.values()) {
- String name = ability.getName();
- if (name != null && !name.isEmpty()) {
- keys.add(name);
+ String id = ability.getId();
+ if (id != null && !id.isEmpty()) {
+ keys.add(id);
}
}
return keys;
@@ -401,12 +716,12 @@ public class AbilityController implements IAbilityHandler {
}
}
}
- // Custom abilities keyed by name
+ // Custom abilities keyed by UUID (stable references)
for (Ability ability : customAbilities.values()) {
if (ability.getAllowedBy().allowsPlayer()) {
- String name = ability.getName();
- if (name != null && !name.isEmpty()) {
- keys.add(name);
+ String id = ability.getId();
+ if (id != null && !id.isEmpty()) {
+ keys.add(id);
}
}
}
@@ -414,7 +729,16 @@ public class AbilityController implements IAbilityHandler {
}
public boolean hasAbility(String key) {
- return builtAbilities.containsKey(key) || customAbilities.containsKey(key);
+ return builtAbilities.containsKey(key) || customAbilities.containsKey(key)
+ || customAbilitiesById.containsKey(key);
+ }
+
+ public Ability getCustomAbilityByUUID(String uuid) {
+ return customAbilitiesById.get(uuid);
+ }
+
+ public Ability getCustomAbilityByName(String name) {
+ return customAbilities.get(name);
}
/**
@@ -424,6 +748,9 @@ public class AbilityController implements IAbilityHandler {
public boolean canResolveAbility(String key) {
if (key == null || key.isEmpty()) return false;
+ // Custom: UUID lookup (most common for persistent references)
+ if (customAbilitiesById.containsKey(key)) return true;
+
// Built-in: exact name
if (builtAbilities.containsKey(key)) return true;
@@ -437,7 +764,7 @@ public class AbilityController implements IAbilityHandler {
if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) return true;
}
- // Custom: exact name
+ // Custom: exact name (fallback for legacy references)
if (customAbilities.containsKey(key)) return true;
// Custom: case-insensitive name
@@ -448,6 +775,31 @@ public class AbilityController implements IAbilityHandler {
return false;
}
+ // ═══════════════════════════════════════════════════════════════════════════
+ // UNIFIED ACTION RESOLUTION
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ /**
+ * Resolve any action key to an IAbilityAction.
+ * Checks abilities first, then chained abilities.
+ */
+ public IAbilityAction resolveAction(String key) {
+ if (key == null || key.isEmpty()) return null;
+
+ Ability ability = resolveAbility(key);
+ if (ability != null) return ability;
+
+ return resolveChainedAbility(key);
+ }
+
+ /**
+ * Check if a key can be resolved as any action (ability or chain).
+ */
+ public boolean canResolveAction(String key) {
+ if (canResolveAbility(key)) return true;
+ return canResolveChainedAbility(key);
+ }
+
// ═══════════════════════════════════════════════════════════════════════════
// EXTENSION POINTS
// ═══════════════════════════════════════════════════════════════════════════
@@ -467,7 +819,8 @@ public class AbilityController implements IAbilityHandler {
// If no built-in variants exist but external ones do,
// inject a "Base" variant so the user always gets a choice
if (result.isEmpty()) {
- result.add(new AbilityVariant("ability.variant.base", a -> {}));
+ result.add(new AbilityVariant("ability.variant.base", a -> {
+ }));
}
result.addAll(ext);
}
@@ -482,6 +835,14 @@ public class AbilityController implements IAbilityHandler {
return fieldProviders;
}
+ public void registerChainedFieldProvider(IChainedAbilityFieldProvider provider) {
+ chainedFieldProviders.add(provider);
+ }
+
+ public List getChainedFieldProviders() {
+ return chainedFieldProviders;
+ }
+
public void registerExtender(IAbilityExtender extender) {
extenders.add(extender);
}
@@ -545,7 +906,7 @@ public class AbilityController implements IAbilityHandler {
double knockbackDirX, double knockbackDirZ) {
for (IAbilityExtender ext : extenders) {
if (ext.onAbilityDamage(ability, caster, target, damage, knockback, knockbackUp,
- knockbackDirX, knockbackDirZ)) {
+ knockbackDirX, knockbackDirZ)) {
return true;
}
}
@@ -621,4 +982,5 @@ public class AbilityController implements IAbilityHandler {
public boolean deleteCustomAbilityByName(String name) {
return deleteCustomAbility(name);
}
+
}
diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java
index b6010ad86..af7366a3f 100644
--- a/src/main/java/kamkeel/npcs/controllers/SyncController.java
+++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java
@@ -5,6 +5,8 @@ import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import kamkeel.npcs.addon.DBCAddon;
+import kamkeel.npcs.controllers.data.ability.Ability;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
import kamkeel.npcs.network.LargeAbstractPacket;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumSyncAction;
@@ -39,8 +41,6 @@ import noppes.npcs.controllers.data.Quest;
import noppes.npcs.controllers.data.QuestCategory;
import noppes.npcs.controllers.data.RecipeAnvil;
import noppes.npcs.controllers.data.RecipeCarpentry;
-import kamkeel.npcs.controllers.data.ability.Ability;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
import java.io.IOException;
import java.util.EnumMap;
@@ -76,7 +76,8 @@ public class SyncController {
EnumSyncType.CUSTOM_EFFECTS,
EnumSyncType.MAGIC,
EnumSyncType.MAGIC_CYCLE,
- EnumSyncType.CUSTOM_ABILITY
+ EnumSyncType.CUSTOM_ABILITY,
+ EnumSyncType.CHAINED_ABILITY
};
public static void load() {
@@ -93,6 +94,7 @@ public class SyncController {
registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT);
registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT);
registerCache(EnumSyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT);
+ registerCache(EnumSyncType.CHAINED_ABILITY, SyncController::chainedAbilitiesNBT);
}
public static void syncPlayer(EntityPlayerMP player) {
@@ -349,6 +351,27 @@ public class SyncController {
updateAllPlayerRevisions(EnumSyncType.CUSTOM_ABILITY, payload.getRevision());
}
+ public static NBTTagCompound chainedAbilitiesNBT() {
+ NBTTagList list = new NBTTagList();
+ NBTTagCompound compound = new NBTTagCompound();
+ for (Map.Entry entry : AbilityController.Instance.getChainedAbilities().entrySet()) {
+ NBTTagCompound chainNBT = entry.getValue().writeNBT();
+ chainNBT.setString("ChainedAbilityId", entry.getKey());
+ list.appendTag(chainNBT);
+ }
+ compound.setTag("Data", list);
+ return compound;
+ }
+
+ public static void syncAllChainedAbilities() {
+ CachedSyncPayload payload = rebuildNow(EnumSyncType.CHAINED_ABILITY);
+ if (payload == null) {
+ return;
+ }
+ PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CHAINED_ABILITY, payload));
+ updateAllPlayerRevisions(EnumSyncType.CHAINED_ABILITY, payload.getRevision());
+ }
+
public static void syncPlayerData(EntityPlayerMP player, boolean update) {
PlayerData data = PlayerData.get(player);
if (data != null) {
@@ -626,16 +649,30 @@ public class SyncController {
LinkedHashMap sync = new LinkedHashMap<>();
for (int i = 0; i < list.tagCount(); i++) {
NBTTagCompound nbt = list.getCompoundTagAt(i);
- String uuid = nbt.getString("CustomAbilityId");
+ String name = nbt.getString("CustomAbilityId");
Ability ability = AbilityController.Instance.fromNBT(nbt);
if (ability != null) {
- ability.setId(uuid);
- sync.put(uuid, ability);
+ ability.setName(name);
+ sync.put(name, ability);
}
}
AbilityController.Instance.setCustomAbilities(sync);
break;
}
+ case CHAINED_ABILITY: {
+ NBTTagList list = fullCompound.getTagList("Data", 10);
+ LinkedHashMap sync = new LinkedHashMap<>();
+ for (int i = 0; i < list.tagCount(); i++) {
+ NBTTagCompound nbt = list.getCompoundTagAt(i);
+ String id = nbt.getString("ChainedAbilityId");
+ ChainedAbility chain = new ChainedAbility();
+ chain.readNBT(nbt);
+ chain.setName(id);
+ sync.put(id, chain);
+ }
+ AbilityController.Instance.setChainedAbilities(sync);
+ break;
+ }
}
ClientCacheHandler.updateClientRevision(enumSyncType, revision);
@@ -886,6 +923,8 @@ public class SyncController {
return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS);
case CUSTOM_ABILITY:
return EnumSet.of(EnumSyncType.CUSTOM_ABILITY);
+ case CHAINED_ABILITY:
+ return EnumSet.of(EnumSyncType.CHAINED_ABILITY);
default:
return EnumSet.noneOf(EnumSyncType.class);
}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java
index 891558b96..9f332e12d 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java
@@ -2,51 +2,53 @@ package kamkeel.npcs.controllers.data.ability;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.block.Block;
+import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.util.AxisAlignedBB;
+import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
+import net.minecraft.nbt.NBTTagString;
+import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
+import net.minecraft.util.StatCollector;
import net.minecraft.world.World;
import noppes.npcs.DataAbilities;
import noppes.npcs.NpcDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.ability.IAbility;
+import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
+import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.controllers.AnimationController;
-import noppes.npcs.controllers.data.Animation;
-import net.minecraft.entity.player.EntityPlayer;
-import noppes.npcs.controllers.data.Frame;
-import noppes.npcs.entity.EntityNPCInterface;
-import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.controllers.ScriptController;
+import noppes.npcs.controllers.data.Animation;
+import noppes.npcs.controllers.data.Frame;
import noppes.npcs.controllers.data.PlayerDataScript;
+import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.scripted.NpcAPI;
import noppes.npcs.scripted.event.AbilityEvent;
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;
-
-public abstract class Ability implements IAbility {
+public abstract class Ability implements IAbility, IAbilityAction {
// ═══════════════════════════════════════════════════════════════════
// CONFIGURATION (saved to NBT)
// ═══════════════════════════════════════════════════════════════════
- protected String id;
- protected String name;
- protected String typeId; // e.g., "ability.cnpc.slam" (also used as lang key)
+ protected String id = ""; // UUID for custom, registry key for built-in
+ protected String name = ""; // Unique file key / identifier
+ protected String displayName = ""; // Cosmetic name (falls back to name when empty)
+ protected String typeId = ""; // e.g., "ability.cnpc.slam" (also used as lang key)
// Selection
protected int weight = 10;
@@ -102,6 +104,35 @@ public abstract class Ability implements IAbility {
// Cooldown override
protected boolean ignoreCooldown = false;
+ // ═══════════════════════════════════════════════════════════════════
+ // BUILT-IN (set in constructor via configureAsBuiltIn, NOT persisted)
+ // ═══════════════════════════════════════════════════════════════════
+
+ protected transient boolean builtIn = false;
+ protected transient String registryKey;
+
+ // ═══════════════════════════════════════════════════════════════════
+ // TOGGLE CONFIGURATION (saved to NBT for custom abilities)
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Number of toggle states. 0 = not toggleable, 1 = binary on/off, 2+ = multi-state cycling.
+ * Cycling order: Off(0) -> State 1 -> State 2 -> ... -> State N -> Off(0)
+ */
+ protected int toggleStates = 0;
+
+ /**
+ * Whether the toggled-ON state ticks each game tick (e.g., Kaioken drains ki per tick)
+ */
+ protected boolean hasActiveToggle = false;
+
+ /**
+ * Optional display labels for each toggle state (1-indexed).
+ * Used in ability wheel/hotbar to show current state name.
+ * Null means no labels. Array length should match toggleStates.
+ */
+ protected String[] toggleStateLabels;
+
// Configurable potion effects
protected List effects = new ArrayList<>();
@@ -205,9 +236,50 @@ public abstract class Ability implements IAbility {
return false;
}
+ // ═══════════════════════════════════════════════════════════════════
+ // TOGGLE CALLBACKS (optional overrides for toggleable abilities)
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Called when this toggle ability is first activated (state goes from 0 to any active state).
+ * Override to apply effects (e.g., set a DBC flag, apply a buff).
+ */
+ public void onToggleOn(EntityLivingBase caster) {
+ }
+
+ /**
+ * Called when this toggle ability is fully deactivated (state goes to 0).
+ * Override to remove effects (e.g., clear a DBC flag, remove a buff).
+ */
+ public void onToggleOff(EntityLivingBase caster) {
+ }
+
+ /**
+ * Called whenever the toggle state changes (including on/off transitions and mid-cycling).
+ * For multi-state toggles, this fires on every state transition.
+ *
+ * @param caster The entity
+ * @param oldState Previous state (0 = was off)
+ * @param newState New state (0 = now off, 1+ = active state)
+ */
+ public void onToggleStateChanged(EntityLivingBase caster, int oldState, int newState) {
+ }
+
+ /**
+ * Called every tick while this toggle is active (only if hasActiveToggle is true).
+ * Override for per-tick effects (e.g., ki drain for Kaioken).
+ *
+ * @param state Current toggle state (1-based)
+ * @return true to keep the toggle active, false to auto-deactivate
+ */
+ public boolean onToggleTick(EntityLivingBase caster, int tickCount, int state) {
+ return true;
+ }
+
/**
* 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) {
@@ -231,6 +303,17 @@ public abstract class Ability implements IAbility {
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
+ /**
+ * Called when the ability enters BURST_DELAY between burst iterations.
+ * Override in subclasses to reset transient state that should not carry
+ * across burst repetitions (e.g., movement direction, hit entity lists).
+ *
+ * Unlike {@link #cleanup()}, this does NOT kill spawned entities (they may
+ * still be in flight for overlap mode).
+ */
+ public void resetForBurst() {
+ }
+
public void onDamageTaken(EntityLivingBase caster, EntityLivingBase attacker, DamageSource source, float damage) {
}
@@ -483,6 +566,12 @@ public abstract class Ability implements IAbility {
// ── General tab ──────────────────────────────────────────────
defs.add(FieldDef.stringField("gui.name", this::getName, this::setName)
.tab("General"));
+ defs.add(FieldDef.stringField("gui.displayName", this::getRawDisplayName, this::setDisplayName)
+ .tab("General"));
+ defs.add(FieldDef.labelField("ability.validFor", () ->
+ StatCollector.translateToLocal("ability.validFor") + ": \u00A7e"
+ + StatCollector.translateToLocal("ability.userType." + getAllowedBy().name()))
+ .tab("General"));
defs.add(FieldDef.row(
FieldDef.intField("ability.weight", this::getWeight, this::setWeight).range(1, 1000),
FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled)
@@ -508,27 +597,36 @@ public abstract class Ability implements IAbility {
() -> this.getLockMovement().getDisplayKey(),
v -> {
for (LockMovementType t : LockMovementType.values()) {
- if (t.getDisplayKey().equals(v)) { this.setLockMovement(t); break; }
+ if (t.getDisplayKey().equals(v)) {
+ this.setLockMovement(t);
+ break;
+ }
}
})
- .hover("ability.hover.lockMovement")
- .tab("General"));
+ .hover("ability.hover.lockMovement")
+ .tab("General"));
defs.add(FieldDef.row(
FieldDef.stringEnumField("ability.rotationMode", RotationMode.getDisplayKeys(),
- () -> this.getRotationMode().getDisplayKey(),
- v -> {
- for (RotationMode m : RotationMode.values()) {
- if (m.getDisplayKey().equals(v)) { this.setRotationMode(m); break; }
- }
- })
+ () -> this.getRotationMode().getDisplayKey(),
+ v -> {
+ for (RotationMode m : RotationMode.values()) {
+ if (m.getDisplayKey().equals(v)) {
+ this.setRotationMode(m);
+ break;
+ }
+ }
+ })
.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; }
- }
- })
+ () -> 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"));
@@ -571,15 +669,17 @@ public abstract class Ability implements IAbility {
allowedKeys[i] = allowed[i].name();
}
defs.add(FieldDef.stringEnumField("ability.targetingMode", allowedKeys,
- () -> this.getTargetingMode().name(),
- v -> {
- try { this.setTargetingMode(TargetingMode.valueOf(v)); }
- catch (Exception ignored) {}
- })
+ () -> this.getTargetingMode().name(),
+ v -> {
+ try {
+ this.setTargetingMode(TargetingMode.valueOf(v));
+ } catch (Exception ignored) {
+ }
+ })
.tab("Target").hover("ability.hover.targeting"));
} else {
defs.add(FieldDef.enumField("ability.targetingMode", TargetingMode.class,
- this::getTargetingMode, this::setTargetingMode)
+ this::getTargetingMode, this::setTargetingMode)
.tab("Target").hover("ability.hover.targeting"));
}
}
@@ -592,12 +692,12 @@ public abstract class Ability implements IAbility {
.tab("Effects"));
defs.add(FieldDef.section("ability.section.animations").tab("Effects"));
defs.add(FieldDef.animSubGui("ability.windUpAnimation",
- this::getWindUpAnimationId, this::setWindUpAnimationId,
- this::getWindUpAnimationName, this::setWindUpAnimationName)
+ this::getWindUpAnimationId, this::setWindUpAnimationId,
+ this::getWindUpAnimationName, this::setWindUpAnimationName)
.tab("Effects"));
defs.add(FieldDef.animSubGui("ability.activeAnimation",
- this::getActiveAnimationId, this::setActiveAnimationId,
- this::getActiveAnimationName, this::setActiveAnimationName)
+ this::getActiveAnimationId, this::setActiveAnimationId,
+ this::getActiveAnimationName, this::setActiveAnimationName)
.tab("Effects"));
defs.add(FieldDef.animSubGui("ability.dazedAnimation",
this::getDazedAnimationId, this::setDazedAnimationId,
@@ -653,7 +753,7 @@ public abstract class Ability implements IAbility {
* Create a telegraph instance for this ability.
* Override for custom telegraph shapes.
*
- * @param caster The caster
+ * @param caster The caster
* @param target The target (for position calculation)
* @return The telegraph instance, or null if no telegraph
*/
@@ -916,9 +1016,9 @@ public abstract class Ability implements IAbility {
if (phase == AbilityPhase.ACTIVE) {
// Check if more burst iterations remain
if (burstEnabled && burstAmount > 0 && burstIndex < burstAmount) {
- // Don't cleanup here - entities may still be in flight
- // For non-overlap: cleaned up when next burst starts (in onExecute)
- // For overlap: entities continue flying indefinitely
+ // Reset transient state for next burst iteration (e.g., movement direction, hit lists)
+ // Does NOT kill entities - they may still be in flight (overlap mode)
+ resetForBurst();
burstIndex++;
phase = AbilityPhase.BURST_DELAY;
currentTick = 0;
@@ -1119,6 +1219,7 @@ public abstract class Ability implements IAbility {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setString("id", id);
nbt.setString("name", name);
+ nbt.setString("displayName", displayName);
nbt.setString("typeId", typeId);
nbt.setInteger("weight", weight);
nbt.setBoolean("enabled", enabled);
@@ -1150,6 +1251,17 @@ public abstract class Ability implements IAbility {
nbt.setInteger("allowedBy", allowedBy.ordinal());
nbt.setBoolean("ignoreCooldown", ignoreCooldown);
+ // Toggle
+ nbt.setInteger("toggleStates", toggleStates);
+ nbt.setBoolean("hasActiveToggle", hasActiveToggle);
+ if (toggleStateLabels != null && toggleStateLabels.length > 0) {
+ NBTTagList labelList = new NBTTagList();
+ for (String label : toggleStateLabels) {
+ labelList.appendTag(new NBTTagString(label != null ? label : ""));
+ }
+ nbt.setTag("toggleStateLabels", labelList);
+ }
+
// Burst
nbt.setBoolean("burstEnabled", burstEnabled);
nbt.setInteger("burstAmount", burstAmount);
@@ -1180,17 +1292,22 @@ public abstract class Ability implements IAbility {
public void readNBT(NBTTagCompound nbt) {
id = nbt.getString("id");
name = nbt.getString("name");
+ displayName = nbt.getString("displayName");
typeId = nbt.getString("typeId");
- weight = nbt.getInteger("weight");
- enabled = nbt.getBoolean("enabled");
- targetingMode = TargetingMode.valueOf(nbt.getString("targetingMode"));
+ weight = nbt.hasKey("weight") ? nbt.getInteger("weight") : 10;
+ enabled = nbt.hasKey("enabled") ? nbt.getBoolean("enabled") : true;
+ try {
+ targetingMode = TargetingMode.valueOf(nbt.getString("targetingMode"));
+ } catch (Exception e) {
+ targetingMode = TargetingMode.AGGRO_TARGET;
+ }
minRange = nbt.getFloat("minRange");
maxRange = nbt.getFloat("maxRange");
cooldownTicks = nbt.getInteger("cooldown");
windUpTicks = nbt.getInteger("windUp");
- syncWindupWithAnimation = nbt.getBoolean("syncWindup");
- dazedTicks = nbt.getInteger("recovery");
- interruptible = nbt.getBoolean("interruptible");
+ syncWindupWithAnimation = nbt.hasKey("syncWindup") ? nbt.getBoolean("syncWindup") : true;
+ dazedTicks = nbt.hasKey("recovery") ? nbt.getInteger("recovery") : 80;
+ interruptible = nbt.hasKey("interruptible") ? nbt.getBoolean("interruptible") : true;
lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement"));
rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode"));
rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase"));
@@ -1204,7 +1321,7 @@ public abstract class Ability implements IAbility {
windUpAnimationName = nbt.getString("windUpAnimationName");
activeAnimationName = nbt.getString("activeAnimationName");
dazedAnimationName = nbt.getString("dazedAnimationName");
- showTelegraph = nbt.getBoolean("showTelegraph");
+ showTelegraph = nbt.hasKey("showTelegraph") ? nbt.getBoolean("showTelegraph") : true;
try {
telegraphType = TelegraphType.valueOf(nbt.getString("telegraphType"));
} catch (Exception e) {
@@ -1215,6 +1332,20 @@ public abstract class Ability implements IAbility {
allowedBy = UserType.fromOrdinal(nbt.getInteger("allowedBy"));
ignoreCooldown = nbt.getBoolean("ignoreCooldown");
+ // Toggle
+ toggleStates = nbt.getInteger("toggleStates");
+ hasActiveToggle = nbt.getBoolean("hasActiveToggle");
+ if (nbt.hasKey("toggleStateLabels")) {
+ NBTTagList labelList = nbt.getTagList("toggleStateLabels", 8);
+ toggleStateLabels = new String[labelList.tagCount()];
+ for (int i = 0; i < labelList.tagCount(); i++) {
+ String label = labelList.getStringTagAt(i);
+ toggleStateLabels[i] = (label != null && !label.isEmpty()) ? label : null;
+ }
+ } else {
+ toggleStateLabels = null;
+ }
+
// Burst
burstEnabled = nbt.getBoolean("burstEnabled");
burstAmount = nbt.getInteger("burstAmount");
@@ -1242,7 +1373,10 @@ public abstract class Ability implements IAbility {
}
}
- readTypeNBT(nbt.getCompoundTag("typeData"));
+ // Type-specific
+ if (nbt.hasKey("typeData")) {
+ readTypeNBT(nbt.getCompoundTag("typeData"));
+ }
}
// ═══════════════════════════════════════════════════════════════════
@@ -1265,18 +1399,70 @@ public abstract class Ability implements IAbility {
this.name = name;
}
+ /**
+ * Get the display name for this ability.
+ * Returns displayName if set, otherwise falls back to name.
+ * Converts & color codes to § for rendering.
+ */
+ public String getDisplayName() {
+ String result = (displayName != null && !displayName.isEmpty()) ? displayName : name;
+ return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : "";
+ }
+
+ /**
+ * Get the raw display name (may be empty).
+ */
+ public String getRawDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName != null ? displayName : "";
+ }
+
public String getTypeId() {
return typeId;
}
public boolean isBuiltIn() {
- return false;
+ return builtIn;
+ }
+
+ /**
+ * Get the registry key for built-in abilities.
+ * Returns null for custom abilities.
+ */
+ public String getRegistryKey() {
+ return registryKey;
+ }
+
+ /**
+ * Configure this ability as a built-in ability with fixed config.
+ * Sets builtIn flag, assigns registry key, id, name, and derives typeId.
+ * Call this in the constructor of any ability that should be non-customizable.
+ */
+ protected void configureAsBuiltIn(String registryKey) {
+ this.builtIn = true;
+ this.registryKey = registryKey;
+ this.id = registryKey;
+ this.name = registryKey;
+ this.typeId = "ability." + registryKey.replace(':', '.');
}
public Ability deepCopy() {
return AbilityController.Instance.fromNBT(this.writeNBT());
}
+ @Override
+ public boolean isChain() {
+ return false;
+ }
+
+ @Override
+ public IAbilityAction deepCopyAction() {
+ return deepCopy();
+ }
+
public boolean isNpcInlineEdit() {
return npcInlineEdit;
}
@@ -1399,6 +1585,7 @@ public abstract class Ability implements IAbility {
/**
* API method: Get lock movement type as integer.
+ *
* @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
*/
@Override
@@ -1408,6 +1595,7 @@ public abstract class Ability implements IAbility {
/**
* API method: Set lock movement type from integer.
+ *
* @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
*/
@Override
@@ -1417,6 +1605,7 @@ public abstract class Ability implements IAbility {
/**
* API method: Get rotation mode as integer.
+ *
* @return 0=FREE, 1=LOCKED, 2=TRACK
*/
@Override
@@ -1426,6 +1615,7 @@ public abstract class Ability implements IAbility {
/**
* API method: Set rotation mode from integer.
+ *
* @param type 0=FREE, 1=LOCKED, 2=TRACK
*/
@Override
@@ -1435,6 +1625,7 @@ public abstract class Ability implements IAbility {
/**
* API method: Get rotation phase as integer.
+ *
* @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
*/
@Override
@@ -1444,6 +1635,7 @@ public abstract class Ability implements IAbility {
/**
* API method: Set rotation phase from integer.
+ *
* @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
*/
@Override
@@ -1727,6 +1919,46 @@ public abstract class Ability implements IAbility {
this.ignoreCooldown = ignoreCooldown;
}
+ public boolean isToggleable() {
+ return toggleStates > 0;
+ }
+
+ public int getToggleStates() {
+ return toggleStates;
+ }
+
+ public void setToggleStates(int toggleStates) {
+ this.toggleStates = Math.max(0, toggleStates);
+ }
+
+ public boolean hasActiveToggle() {
+ return hasActiveToggle;
+ }
+
+ public void setHasActiveToggle(boolean hasActiveToggle) {
+ this.hasActiveToggle = hasActiveToggle;
+ }
+
+ /**
+ * Get the display label for a specific toggle state.
+ * @param state 1-based state index
+ * @return the label, or null if none set
+ */
+ public String getToggleStateLabel(int state) {
+ if (toggleStateLabels != null && state >= 1 && state <= toggleStateLabels.length) {
+ return toggleStateLabels[state - 1];
+ }
+ return null;
+ }
+
+ /**
+ * Set display labels for toggle states (1-indexed).
+ * @param labels one label per state, null entries allowed
+ */
+ public void setToggleStateLabels(String... labels) {
+ this.toggleStateLabels = (labels != null && labels.length > 0) ? labels : null;
+ }
+
public List getEffects() {
return effects;
}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java
new file mode 100644
index 000000000..830d5d0ec
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java
@@ -0,0 +1,309 @@
+package kamkeel.npcs.controllers.data.ability;
+
+import kamkeel.npcs.controllers.AbilityController;
+import net.minecraft.nbt.NBTTagCompound;
+
+/**
+ * Unified slot for storing any combat action: inline ability, ability reference, or chain reference.
+ * Replaces {@link AbilitySlot} with support for all {@link IAbilityAction} types.
+ *
+ * References are lazy-resolved via their respective controllers and cached until
+ * the controller revision changes.
+ */
+public class AbilityAction {
+
+ public enum SlotType {
+ INLINE_ABILITY,
+ ABILITY_REFERENCE,
+ CHAIN_REFERENCE,
+ INLINE_CHAIN
+ }
+
+ private SlotType slotType;
+ private Ability inlineAbility;
+ private ChainedAbility inlineChain;
+ private String referenceId;
+
+ /**
+ * Per-slot enabled override for reference slots (null = use master's default).
+ */
+ private Boolean enabledOverride;
+
+ // Cache
+ private transient IAbilityAction cachedAction;
+ private transient int cachedRevision = -1;
+
+ private AbilityAction() {
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // FACTORY METHODS
+ // ═══════════════════════════════════════════════════════════════════
+
+ public static AbilityAction inline(Ability ability) {
+ AbilityAction slot = new AbilityAction();
+ slot.slotType = SlotType.INLINE_ABILITY;
+ slot.inlineAbility = ability;
+ return slot;
+ }
+
+ public static AbilityAction abilityReference(String referenceId) {
+ AbilityAction slot = new AbilityAction();
+ slot.slotType = SlotType.ABILITY_REFERENCE;
+ slot.referenceId = referenceId;
+ return slot;
+ }
+
+ public static AbilityAction chainReference(String referenceId) {
+ AbilityAction slot = new AbilityAction();
+ slot.slotType = SlotType.CHAIN_REFERENCE;
+ slot.referenceId = referenceId;
+ return slot;
+ }
+
+ public static AbilityAction inlineChain(ChainedAbility chain) {
+ AbilityAction slot = new AbilityAction();
+ slot.slotType = SlotType.INLINE_CHAIN;
+ slot.inlineChain = chain;
+ return slot;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // QUERIES
+ // ═══════════════════════════════════════════════════════════════════
+
+ public SlotType getSlotType() {
+ return slotType;
+ }
+
+ public boolean isReference() {
+ return slotType == SlotType.ABILITY_REFERENCE || slotType == SlotType.CHAIN_REFERENCE;
+ }
+
+ public boolean isChainReference() {
+ return slotType == SlotType.CHAIN_REFERENCE;
+ }
+
+ public boolean isInlineChain() {
+ return slotType == SlotType.INLINE_CHAIN;
+ }
+
+ public boolean isChain() {
+ return slotType == SlotType.CHAIN_REFERENCE || slotType == SlotType.INLINE_CHAIN;
+ }
+
+ public boolean isAbilityReference() {
+ return slotType == SlotType.ABILITY_REFERENCE;
+ }
+
+ public String getReferenceId() {
+ return referenceId;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // RESOLUTION
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Resolve the action. Inline returns directly; references resolve
+ * from the appropriate controller with version-based caching.
+ */
+ public IAbilityAction getAction() {
+ switch (slotType) {
+ case INLINE_ABILITY:
+ return inlineAbility;
+
+ case INLINE_CHAIN:
+ return inlineChain;
+
+ case ABILITY_REFERENCE: {
+ AbilityController ctrl = AbilityController.Instance;
+ if (ctrl == null) return null;
+ int rev = ctrl.getCustomAbilityRevision();
+ if (cachedAction != null && cachedRevision == rev) {
+ return cachedAction;
+ }
+ Ability resolved = ctrl.resolveAbility(referenceId);
+ if (resolved != null && enabledOverride != null) {
+ resolved.setEnabled(enabledOverride);
+ }
+ cachedAction = resolved;
+ cachedRevision = rev;
+ return cachedAction;
+ }
+
+ case CHAIN_REFERENCE: {
+ AbilityController ctrl = AbilityController.Instance;
+ if (ctrl == null) return null;
+ int rev = ctrl.getChainedAbilityRevision();
+ if (cachedAction != null && cachedRevision == rev) {
+ return cachedAction;
+ }
+ ChainedAbility resolved = ctrl.resolveChainedAbility(referenceId);
+ if (resolved != null && enabledOverride != null) {
+ resolved.setEnabled(enabledOverride);
+ }
+ cachedAction = resolved;
+ cachedRevision = rev;
+ return cachedAction;
+ }
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Get as Ability (returns null if this is a chain slot or resolution fails).
+ */
+ public Ability getAbility() {
+ IAbilityAction action = getAction();
+ return (action instanceof Ability) ? (Ability) action : null;
+ }
+
+ /**
+ * Get as ChainedAbility (returns null if this is an ability slot or resolution fails).
+ */
+ public ChainedAbility getChainedAbility() {
+ IAbilityAction action = getAction();
+ return (action instanceof ChainedAbility) ? (ChainedAbility) action : null;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // ENABLED OVERRIDE
+ // ═══════════════════════════════════════════════════════════════════
+
+ public void setEnabled(boolean enabled) {
+ if (slotType == SlotType.INLINE_ABILITY) {
+ if (inlineAbility != null) {
+ inlineAbility.setEnabled(enabled);
+ }
+ } else if (slotType == SlotType.INLINE_CHAIN) {
+ if (inlineChain != null) {
+ inlineChain.setEnabled(enabled);
+ }
+ } else {
+ this.enabledOverride = enabled;
+ if (cachedAction != null) {
+ cachedAction.setEnabled(enabled);
+ }
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // INLINE CONVERSION (ability references only)
+ // ═══════════════════════════════════════════════════════════════════
+
+ public Ability getInlineAbility() {
+ return inlineAbility;
+ }
+
+ public ChainedAbility getInlineChain() {
+ return inlineChain;
+ }
+
+ /**
+ * Convert this reference slot to an inline slot by copying the resolved data.
+ * Works for ability references (→ INLINE_ABILITY) and chain references (→ INLINE_CHAIN).
+ * Chain entries are kept as-is (not auto-converted to inline).
+ */
+ public boolean convertToInline() {
+ if (slotType == SlotType.ABILITY_REFERENCE) {
+ Ability resolved = getAbility();
+ if (resolved == null) return false;
+
+ inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT());
+ slotType = SlotType.INLINE_ABILITY;
+ referenceId = null;
+ cachedAction = null;
+ cachedRevision = -1;
+ return true;
+ }
+
+ if (slotType == SlotType.CHAIN_REFERENCE) {
+ ChainedAbility resolved = getChainedAbility();
+ if (resolved == null) return false;
+
+ inlineChain = resolved.deepCopy();
+ slotType = SlotType.INLINE_CHAIN;
+ referenceId = null;
+ cachedAction = null;
+ cachedRevision = -1;
+ return true;
+ }
+
+ return false;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // NBT
+ // ═══════════════════════════════════════════════════════════════════
+
+ public NBTTagCompound writeNBT() {
+ switch (slotType) {
+ case INLINE_CHAIN: {
+ NBTTagCompound nbt = new NBTTagCompound();
+ if (inlineChain != null) {
+ nbt.setTag("InlineChain", inlineChain.writeNBT());
+ }
+ return nbt;
+ }
+
+ case CHAIN_REFERENCE: {
+ NBTTagCompound nbt = new NBTTagCompound();
+ nbt.setString("ChainReference", referenceId);
+ if (enabledOverride != null) {
+ nbt.setBoolean("RefEnabled", enabledOverride);
+ }
+ return nbt;
+ }
+
+ case ABILITY_REFERENCE: {
+ NBTTagCompound nbt = new NBTTagCompound();
+ nbt.setString("Reference", referenceId);
+ if (enabledOverride != null) {
+ nbt.setBoolean("RefEnabled", enabledOverride);
+ }
+ return nbt;
+ }
+
+ case INLINE_ABILITY:
+ default:
+ return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound();
+ }
+ }
+
+ public static AbilityAction fromNBT(NBTTagCompound nbt) {
+ if (nbt == null) return null;
+
+ // Inline chain (new format)
+ if (nbt.hasKey("InlineChain")) {
+ ChainedAbility chain = new ChainedAbility();
+ chain.readNBT(nbt.getCompoundTag("InlineChain"));
+ return inlineChain(chain);
+ }
+
+ // Chain reference
+ if (nbt.hasKey("ChainReference")) {
+ AbilityAction slot = chainReference(nbt.getString("ChainReference"));
+ if (nbt.hasKey("RefEnabled")) {
+ slot.enabledOverride = nbt.getBoolean("RefEnabled");
+ }
+ return slot;
+ }
+
+ // Ability reference
+ if (nbt.hasKey("Reference")) {
+ AbilityAction slot = abilityReference(nbt.getString("Reference"));
+ if (nbt.hasKey("RefEnabled")) {
+ slot.enabledOverride = nbt.getBoolean("RefEnabled");
+ }
+ return slot;
+ }
+
+ // Inline ability
+ Ability ability = AbilityController.Instance.fromNBT(nbt);
+ if (ability == null) return null;
+ return inline(ability);
+ }
+}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java
index ab8446114..1afbf0906 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java
@@ -1,5 +1,6 @@
package kamkeel.npcs.controllers.data.ability;
+import kamkeel.npcs.controllers.AbilityController;
import net.minecraft.nbt.NBTTagCompound;
/**
@@ -8,19 +9,25 @@ import net.minecraft.nbt.NBTTagCompound;
*
* References are lazy-resolved via {@link AbilityController#resolveAbility(String)}
* and cached until the controller version changes.
+ *
+ * @deprecated Use {@link AbilityAction} instead, which supports abilities and chained abilities.
*/
+@Deprecated
public class AbilitySlot {
private Ability inlineAbility;
private String referenceId;
- /** Per-slot enabled override for reference slots (null = use master's default). */
+ /**
+ * Per-slot enabled override for reference slots (null = use master's default).
+ */
private Boolean enabledOverride;
private transient Ability cachedAbility;
private transient int cachedVersion = -1;
- private AbilitySlot() {}
+ private AbilitySlot() {
+ }
/**
* Create a slot with an inline (embedded) ability.
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java
index 797e6b20b..0ef3b9692 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java
@@ -6,7 +6,7 @@ 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.
*/
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java
index 58e54b5aa..959aa2cf4 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java
@@ -50,14 +50,22 @@ public enum AnchorPoint {
@Override
public String toString() {
switch (this) {
- case FRONT: return "ability.anchor.front";
- case CENTER: return "ability.anchor.center";
- case RIGHT_HAND: return "ability.anchor.rightHand";
- 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();
+ case FRONT:
+ return "ability.anchor.front";
+ case CENTER:
+ return "ability.anchor.center";
+ case RIGHT_HAND:
+ return "ability.anchor.rightHand";
+ 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();
}
}
}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java
deleted file mode 100644
index 36344e9e5..000000000
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package kamkeel.npcs.controllers.data.ability;
-
-import net.minecraft.nbt.NBTTagCompound;
-
-/**
- * Abstract base class for built-in (non-customizable) abilities.
- * Built-in abilities are registered by key, have fixed configurations,
- * and cannot be modified through the preset GUI system.
- *
- * Examples: simple utility abilities like Jump, Sprint, etc.
- * Third-party mods can extend this class to create their own built-in abilities.
- */
-public abstract class BuiltInAbility extends Ability {
-
- /**
- * The registry key used to look up this ability (e.g., "cnpc:jump", "dbc:ki_blast").
- */
- private final String registryKey;
-
- public BuiltInAbility(String registryKey) {
- this.registryKey = registryKey;
- this.id = registryKey;
- this.name = registryKey;
- // Convert "namespace:name" to "ability.namespace.name" for GUI filter compatibility
- this.typeId = "ability." + registryKey.replace(':', '.');
- }
-
- @Override
- public boolean isBuiltIn() {
- return true;
- }
-
- /**
- * Get the registry key for this built-in ability.
- */
- public String getRegistryKey() {
- return registryKey;
- }
-
- /**
- * Built-in abilities do not write type-specific NBT (they have fixed configs).
- */
- @Override
- public void writeTypeNBT(NBTTagCompound nbt) {
- // No-op: built-in abilities have fixed configurations
- }
-
- /**
- * Built-in abilities do not read type-specific NBT.
- */
- @Override
- public void readTypeNBT(NBTTagCompound nbt) {
- // No-op: built-in abilities have fixed configurations
- }
-
- /**
- * Get the duration of the active phase in ticks.
- * Built-in abilities must define their own active duration.
- */
- public abstract int getActiveDurationTicks();
-}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbility.java
new file mode 100644
index 000000000..5de37416e
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbility.java
@@ -0,0 +1,438 @@
+package kamkeel.npcs.controllers.data.ability;
+
+import kamkeel.npcs.controllers.AbilityController;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.nbt.NBTTagList;
+import noppes.npcs.api.ability.IChainedAbility;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A chained ability is an ordered sequence of ability references that execute
+ * one after another. It has its own conditions, cooldown, weight, and per-entry
+ * delay configuration. Individual ability conditions are skipped during chain execution.
+ *
+ * All entries must be ability references (by name/key). Inline abilities are not supported.
+ * When saving, all references are validated against {@link AbilityController}.
+ */
+public class ChainedAbility implements IChainedAbility, IAbilityAction {
+
+ // ═══════════════════════════════════════════════════════════════════
+ // CONFIGURATION (saved to NBT)
+ // ═══════════════════════════════════════════════════════════════════
+
+ private String id = ""; // UUID (stable persistent reference)
+ private String name = ""; // Unique file key / identifier
+ private String displayName = ""; // Cosmetic name (falls back to name when empty)
+
+ private boolean enabled = true;
+
+ /**
+ * Weight for NPC weighted random selection.
+ */
+ private int weight = 10;
+
+ /**
+ * Whether each ability in the chain uses its own windup phase.
+ */
+ private boolean windUpAll = true;
+
+ /**
+ * Chain-level cooldown in ticks (applied after entire chain completes).
+ */
+ private int cooldownTicks = 100;
+
+ /**
+ * Minimum range for NPC eligibility check.
+ */
+ private float minRange = 0;
+
+ /**
+ * Maximum range for NPC eligibility check.
+ */
+ private float maxRange = 20;
+
+ /**
+ * Chain-level conditions (individual ability conditions are ignored).
+ */
+ private List conditions = new ArrayList<>();
+
+ /**
+ * Ordered list of ability entries to execute.
+ */
+ private List entries = new ArrayList<>();
+
+ /**
+ * Extension data for external mods (e.g., icons, DBC stats).
+ */
+ private NBTTagCompound customData = new NBTTagCompound();
+
+ public ChainedAbility() {
+ }
+
+ public ChainedAbility(String name) {
+ this.name = name != null ? name : "";
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // GETTERS / SETTERS
+ // ═══════════════════════════════════════════════════════════════════
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id != null ? id : "";
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name != null ? name : "";
+ }
+
+ /**
+ * Get the display name for this chained ability.
+ * Returns displayName if set, otherwise falls back to name.
+ * Converts & color codes to § for rendering.
+ */
+ public String getDisplayName() {
+ String result = (displayName != null && !displayName.isEmpty()) ? displayName : name;
+ return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : "";
+ }
+
+ public String getRawDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName != null ? displayName : "";
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public void setWeight(int weight) {
+ this.weight = Math.max(1, weight);
+ }
+
+ public boolean isWindUpAll() {
+ return windUpAll;
+ }
+
+ public void setWindUpAll(boolean windUpAll) {
+ this.windUpAll = windUpAll;
+ }
+
+ public int getCooldownTicks() {
+ return cooldownTicks;
+ }
+
+ public void setCooldownTicks(int cooldownTicks) {
+ this.cooldownTicks = Math.max(0, cooldownTicks);
+ }
+
+ public float getMinRange() {
+ return minRange;
+ }
+
+ public void setMinRange(float minRange) {
+ this.minRange = Math.max(0, minRange);
+ }
+
+ public float getMaxRange() {
+ return maxRange;
+ }
+
+ public void setMaxRange(float maxRange) {
+ this.maxRange = Math.max(0, maxRange);
+ }
+
+ /**
+ * Compute the allowed user type from child abilities.
+ * A chain allows players if ALL resolved entries allow players.
+ * A chain allows NPCs if ALL resolved entries allow NPCs.
+ * Unresolvable entries are skipped (don't restrict).
+ */
+ public UserType getAllowedBy() {
+ if (entries.isEmpty()) return UserType.BOTH;
+ boolean allAllowPlayer = true;
+ boolean allAllowNpc = true;
+ for (ChainedAbilityEntry entry : entries) {
+ Ability a = entry.resolve();
+ if (a == null) continue;
+ UserType ut = a.getAllowedBy();
+ if (!ut.allowsPlayer()) allAllowPlayer = false;
+ if (!ut.allowsNpc()) allAllowNpc = false;
+ }
+ if (allAllowPlayer && allAllowNpc) return UserType.BOTH;
+ if (allAllowPlayer) return UserType.PLAYER_ONLY;
+ if (allAllowNpc) return UserType.NPC_ONLY;
+ return UserType.NPC_ONLY;
+ }
+
+ public List getConditions() {
+ return conditions;
+ }
+
+ public void setConditions(List conditions) {
+ this.conditions = conditions != null ? conditions : new ArrayList<>();
+ }
+
+ public List getEntries() {
+ return entries;
+ }
+
+ public void setEntries(List entries) {
+ this.entries = entries != null ? entries : new ArrayList<>();
+ }
+
+ public NBTTagCompound getCustomData() {
+ return customData;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // IChainedAbility INTERFACE
+ // ═══════════════════════════════════════════════════════════════════
+
+ @Override
+ public int getEntryCount() {
+ return entries.size();
+ }
+
+ @Override
+ public String getEntryReference(int index) {
+ if (index < 0 || index >= entries.size()) return null;
+ ChainedAbilityEntry entry = entries.get(index);
+ if (entry.isInline()) {
+ Ability a = entry.getInlineAbility();
+ return a != null ? a.getName() : "";
+ }
+ return entry.getAbilityReference();
+ }
+
+ @Override
+ public boolean isEntryInline(int index) {
+ if (index < 0 || index >= entries.size()) return false;
+ return entries.get(index).isInline();
+ }
+
+ @Override
+ public int getEntryDelay(int index) {
+ if (index < 0 || index >= entries.size()) return 0;
+ return entries.get(index).getDelayTicks();
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // ENTRY MANAGEMENT
+ // ═══════════════════════════════════════════════════════════════════
+
+ public void addEntry(ChainedAbilityEntry entry) {
+ if (entry != null) {
+ entries.add(entry);
+ }
+ }
+
+ public void addEntry(String abilityReference, int delayTicks) {
+ entries.add(new ChainedAbilityEntry(abilityReference, delayTicks));
+ }
+
+ public void removeEntry(int index) {
+ if (index >= 0 && index < entries.size()) {
+ entries.remove(index);
+ }
+ }
+
+ public void moveEntryUp(int index) {
+ if (index > 0 && index < entries.size()) {
+ ChainedAbilityEntry entry = entries.remove(index);
+ entries.add(index - 1, entry);
+ }
+ }
+
+ public void moveEntryDown(int index) {
+ if (index >= 0 && index < entries.size() - 1) {
+ ChainedAbilityEntry entry = entries.remove(index);
+ entries.add(index + 1, entry);
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // CONDITIONS
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Check all chain-level conditions against the caster and target.
+ */
+ public boolean checkConditions(EntityLivingBase caster, EntityLivingBase target) {
+ for (Condition c : conditions) {
+ if (!c.check(caster, target)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check conditions for player use (skip target-requiring conditions).
+ */
+ public boolean checkConditionsForPlayer(EntityLivingBase caster) {
+ for (Condition c : conditions) {
+ if (c.requiresTarget()) continue;
+ if (!c.check(caster, null)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // VALIDATION
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Validate this chained ability. Returns a list of error messages.
+ * An empty list means valid.
+ */
+ public List validate() {
+ List errors = new ArrayList<>();
+
+ if (name == null || name.isEmpty()) {
+ errors.add("Name is required");
+ }
+
+ if (entries.isEmpty()) {
+ errors.add("At least one ability entry is required");
+ }
+
+ AbilityController controller = AbilityController.Instance;
+ if (controller != null) {
+ for (int i = 0; i < entries.size(); i++) {
+ ChainedAbilityEntry entry = entries.get(i);
+ if (entry.isInline()) {
+ if (entry.getInlineAbility() == null) {
+ errors.add("Entry " + (i + 1) + ": inline ability is null");
+ }
+ } else {
+ String ref = entry.getAbilityReference();
+ if (ref == null || ref.isEmpty()) {
+ errors.add("Entry " + (i + 1) + ": ability reference is empty");
+ } else if (!controller.canResolveAbility(ref)) {
+ errors.add("Entry " + (i + 1) + ": cannot resolve ability '" + ref + "'");
+ }
+ }
+ }
+ }
+
+ return errors;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DEEP COPY
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Create a deep copy via NBT round-trip.
+ */
+ public ChainedAbility deepCopy() {
+ ChainedAbility copy = new ChainedAbility();
+ copy.readNBT(this.writeNBT());
+ return copy;
+ }
+
+ @Override
+ public boolean isChain() {
+ return true;
+ }
+
+ @Override
+ public IAbilityAction deepCopyAction() {
+ return deepCopy();
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // NBT SERIALIZATION
+ // ═══════════════════════════════════════════════════════════════════
+
+ public NBTTagCompound writeNBT() {
+ NBTTagCompound nbt = new NBTTagCompound();
+ nbt.setString("Id", id);
+ nbt.setString("Name", name);
+ nbt.setString("DisplayName", displayName);
+ nbt.setBoolean("Enabled", enabled);
+ nbt.setInteger("Weight", weight);
+ nbt.setBoolean("WindUpAll", windUpAll);
+ nbt.setInteger("CooldownTicks", cooldownTicks);
+ nbt.setFloat("MinRange", minRange);
+ nbt.setFloat("MaxRange", maxRange);
+
+ // Conditions
+ NBTTagList condList = new NBTTagList();
+ for (Condition c : conditions) {
+ condList.appendTag(c.writeNBT());
+ }
+ nbt.setTag("Conditions", condList);
+
+ // Entries
+ NBTTagList entryList = new NBTTagList();
+ for (ChainedAbilityEntry entry : entries) {
+ entryList.appendTag(entry.writeNBT());
+ }
+ nbt.setTag("Entries", entryList);
+
+ // Custom data (for external mods)
+ nbt.setTag("customData", (NBTTagCompound) customData.copy());
+
+ return nbt;
+ }
+
+ public void readNBT(NBTTagCompound nbt) {
+ id = nbt.getString("Id");
+ name = nbt.getString("Name");
+ displayName = nbt.getString("DisplayName");
+ enabled = !nbt.hasKey("Enabled") || nbt.getBoolean("Enabled");
+ weight = nbt.hasKey("Weight") ? nbt.getInteger("Weight") : 10;
+ windUpAll = !nbt.hasKey("WindUpAll") || nbt.getBoolean("WindUpAll");
+ cooldownTicks = nbt.hasKey("CooldownTicks") ? nbt.getInteger("CooldownTicks") : 100;
+ minRange = nbt.getFloat("MinRange");
+ maxRange = nbt.hasKey("MaxRange") ? nbt.getFloat("MaxRange") : 20;
+ // AllowedBy is computed from child abilities — old NBT data silently ignored
+
+ // Conditions
+ conditions.clear();
+ NBTTagList condList = nbt.getTagList("Conditions", 10);
+ for (int i = 0; i < condList.tagCount(); i++) {
+ Condition c = Condition.fromNBT(condList.getCompoundTagAt(i));
+ if (c != null) {
+ conditions.add(c);
+ }
+ }
+
+ // Entries
+ entries.clear();
+ NBTTagList entryList = nbt.getTagList("Entries", 10);
+ for (int i = 0; i < entryList.tagCount(); i++) {
+ ChainedAbilityEntry entry = ChainedAbilityEntry.fromNBT(entryList.getCompoundTagAt(i));
+ if (entry != null) {
+ entries.add(entry);
+ }
+ }
+
+ // Custom data (for external mods)
+ customData = nbt.getCompoundTag("customData");
+ }
+}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityEntry.java b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityEntry.java
new file mode 100644
index 000000000..be8528ed0
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/ChainedAbilityEntry.java
@@ -0,0 +1,202 @@
+package kamkeel.npcs.controllers.data.ability;
+
+import kamkeel.npcs.controllers.AbilityController;
+import net.minecraft.nbt.NBTTagCompound;
+
+/**
+ * A single entry in a {@link ChainedAbility}, representing one ability
+ * to execute in sequence along with its delay configuration.
+ *
+ * Entries can be either a reference to a registered ability (by name/key)
+ * or an inline ability that is owned directly by this entry.
+ */
+public class ChainedAbilityEntry {
+
+ public enum EntryType {REFERENCE, INLINE}
+
+ private EntryType entryType = EntryType.REFERENCE;
+
+ /**
+ * Reference key (built-in name or custom ability name). Used when entryType == REFERENCE.
+ */
+ private String abilityReference = "";
+
+ /**
+ * Inline ability data. Used when entryType == INLINE.
+ */
+ private Ability inlineAbility = null;
+
+ /**
+ * Delay in ticks BEFORE this ability starts (after previous entry completes).
+ */
+ private int delayTicks = 0;
+
+ public ChainedAbilityEntry() {
+ }
+
+ public ChainedAbilityEntry(String abilityReference, int delayTicks) {
+ this.entryType = EntryType.REFERENCE;
+ this.abilityReference = abilityReference != null ? abilityReference : "";
+ this.delayTicks = Math.max(0, delayTicks);
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // FACTORY METHODS
+ // ═══════════════════════════════════════════════════════════════════
+
+ public static ChainedAbilityEntry reference(String ref, int delay) {
+ ChainedAbilityEntry entry = new ChainedAbilityEntry();
+ entry.entryType = EntryType.REFERENCE;
+ entry.abilityReference = ref != null ? ref : "";
+ entry.delayTicks = Math.max(0, delay);
+ return entry;
+ }
+
+ public static ChainedAbilityEntry inline(Ability ability, int delay) {
+ ChainedAbilityEntry entry = new ChainedAbilityEntry();
+ entry.entryType = EntryType.INLINE;
+ entry.inlineAbility = ability;
+ entry.delayTicks = Math.max(0, delay);
+ return entry;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // TYPE QUERIES
+ // ═══════════════════════════════════════════════════════════════════
+
+ public EntryType getEntryType() {
+ return entryType;
+ }
+
+ public boolean isInline() {
+ return entryType == EntryType.INLINE;
+ }
+
+ public boolean isReference() {
+ return entryType == EntryType.REFERENCE;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // GETTERS / SETTERS
+ // ═══════════════════════════════════════════════════════════════════
+
+ public String getAbilityReference() {
+ return abilityReference;
+ }
+
+ public void setAbilityReference(String abilityReference) {
+ this.abilityReference = abilityReference != null ? abilityReference : "";
+ }
+
+ public Ability getInlineAbility() {
+ return inlineAbility;
+ }
+
+ public void setInlineAbility(Ability ability) {
+ this.inlineAbility = ability;
+ }
+
+ public int getDelayTicks() {
+ return delayTicks;
+ }
+
+ public void setDelayTicks(int delayTicks) {
+ this.delayTicks = Math.max(0, delayTicks);
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // RESOLUTION
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Resolve this entry to an Ability instance.
+ * Inline entries return their ability directly.
+ * Reference entries resolve via AbilityController.
+ */
+ public Ability resolve() {
+ if (entryType == EntryType.INLINE) {
+ return inlineAbility;
+ }
+ AbilityController ctrl = AbilityController.Instance;
+ if (ctrl == null) return null;
+ return ctrl.resolveAbility(abilityReference);
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // CONVERSION
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Convert a reference entry to inline by cloning the resolved ability.
+ * Returns false if already inline or the reference cannot be resolved.
+ */
+ public boolean convertToInline() {
+ if (entryType != EntryType.REFERENCE) return false;
+
+ AbilityController ctrl = AbilityController.Instance;
+ if (ctrl == null) return false;
+
+ Ability resolved = ctrl.resolveAbility(abilityReference);
+ if (resolved == null) return false;
+
+ inlineAbility = ctrl.fromNBT(resolved.writeNBT());
+ entryType = EntryType.INLINE;
+ abilityReference = "";
+ return true;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // DEEP COPY
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Create a deep copy of this entry. Inline abilities are copied via NBT round-trip.
+ */
+ public ChainedAbilityEntry deepCopy() {
+ ChainedAbilityEntry copy = new ChainedAbilityEntry();
+ copy.entryType = this.entryType;
+ copy.abilityReference = this.abilityReference;
+ copy.delayTicks = this.delayTicks;
+ if (this.inlineAbility != null) {
+ copy.inlineAbility = AbilityController.Instance.fromNBT(this.inlineAbility.writeNBT());
+ }
+ return copy;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // NBT (backwards compatible)
+ // ═══════════════════════════════════════════════════════════════════
+
+ public NBTTagCompound writeNBT() {
+ NBTTagCompound nbt = new NBTTagCompound();
+ nbt.setInteger("Delay", delayTicks);
+
+ if (entryType == EntryType.INLINE && inlineAbility != null) {
+ nbt.setTag("InlineAbility", inlineAbility.writeNBT());
+ } else {
+ nbt.setString("Reference", abilityReference);
+ }
+ return nbt;
+ }
+
+ public static ChainedAbilityEntry fromNBT(NBTTagCompound nbt) {
+ if (nbt == null) return null;
+
+ ChainedAbilityEntry entry = new ChainedAbilityEntry();
+ entry.delayTicks = Math.max(0, nbt.getInteger("Delay"));
+
+ // Inline entry (new format)
+ if (nbt.hasKey("InlineAbility")) {
+ NBTTagCompound abilityNBT = nbt.getCompoundTag("InlineAbility");
+ entry.entryType = EntryType.INLINE;
+ entry.inlineAbility = AbilityController.Instance != null
+ ? AbilityController.Instance.fromNBT(abilityNBT) : null;
+ } else {
+ // Reference entry (old + new format)
+ entry.entryType = EntryType.REFERENCE;
+ entry.abilityReference = nbt.getString("Reference");
+ }
+
+ return entry;
+ }
+}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityAction.java b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityAction.java
new file mode 100644
index 000000000..a1f4ac777
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityAction.java
@@ -0,0 +1,50 @@
+package kamkeel.npcs.controllers.data.ability;
+
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.nbt.NBTTagCompound;
+
+import java.util.List;
+
+/**
+ * Common interface for all combat actions that can be assigned to NPCs or players.
+ * Implemented by {@link Ability} (individual ability) and {@link ChainedAbility} (sequence).
+ *
+ * This enables unified storage via {@link AbilityAction}, unified weighted random selection,
+ * and unified eligibility checking without instanceof or separate codepaths.
+ */
+public interface IAbilityAction {
+
+ String getName();
+
+ boolean isEnabled();
+
+ void setEnabled(boolean enabled);
+
+ int getWeight();
+
+ int getCooldownTicks();
+
+ float getMinRange();
+
+ float getMaxRange();
+
+ UserType getAllowedBy();
+
+ List getConditions();
+
+ boolean checkConditions(EntityLivingBase caster, EntityLivingBase target);
+
+ boolean checkConditionsForPlayer(EntityLivingBase caster);
+
+ /**
+ * Whether this action is a chained ability (sequence of abilities) vs an individual ability.
+ */
+ boolean isChain();
+
+ /**
+ * Create a deep copy of this action.
+ */
+ IAbilityAction deepCopyAction();
+
+ NBTTagCompound writeNBT();
+}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/IChainedAbilityFieldProvider.java b/src/main/java/kamkeel/npcs/controllers/data/ability/IChainedAbilityFieldProvider.java
new file mode 100644
index 000000000..95e8b70e0
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/IChainedAbilityFieldProvider.java
@@ -0,0 +1,25 @@
+package kamkeel.npcs.controllers.data.ability;
+
+import noppes.npcs.client.gui.builder.FieldDef;
+
+import java.util.List;
+
+/**
+ * External field definition provider for chained abilities.
+ * Called during GUI initialization to allow mods to inject custom tabs/fields
+ * into the chained ability configuration GUI.
+ *
+ * Note: Although this interface references FieldDef (client-only), it is safe to
+ * register on both sides because the generic type is erased at bytecode level.
+ * The addFieldDefinitions method is only ever called from client-side GUI code.
+ */
+public interface IChainedAbilityFieldProvider {
+ /**
+ * Add field definitions to the chained ability's GUI.
+ * Fields added here should use .tab() to assign them to a custom tab.
+ *
+ * @param chain The chained ability being configured
+ * @param defs The mutable list of field definitions to add to
+ */
+ void addFieldDefinitions(ChainedAbility chain, List defs);
+}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java b/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java
index 77fae9774..13c8bc526 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java
@@ -9,5 +9,6 @@ import net.minecraft.entity.Entity;
*/
public interface PreviewEntityHandler {
void onEntitySpawned(Entity entity);
+
void onEntityRemoved(Entity entity);
}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java
index 9bdbdac6f..7e84c6a14 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java
@@ -27,11 +27,16 @@ public enum TargetingMode {
@Override
public String toString() {
switch (this) {
- case AGGRO_TARGET: return "ability.target.aggro_target";
- case SELF: return "ability.target.self";
- case AOE_SELF: return "ability.target.aoe_self";
- case AOE_TARGET: return "ability.target.aoe_target";
- default: return name();
+ case AGGRO_TARGET:
+ return "ability.target.aggro_target";
+ case SELF:
+ return "ability.target.self";
+ case AOE_SELF:
+ return "ability.target.aoe_self";
+ case AOE_TARGET:
+ return "ability.target.aoe_target";
+ default:
+ return name();
}
}
}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java b/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java
new file mode 100644
index 000000000..4a758bead
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/ToggleEntry.java
@@ -0,0 +1,40 @@
+package kamkeel.npcs.controllers.data.ability;
+
+/**
+ * Tracks the state of a single active toggle for an entity.
+ * Stored in AbstractDataAbilities.activeToggles map.
+ *
+ * State is 1-based: state 1 = first ON state, state 2 = second, etc.
+ * State 0 means OFF (entry should not exist in the map).
+ */
+public class ToggleEntry {
+ private final Ability ability;
+ private int tickCount;
+ private int state;
+
+ public ToggleEntry(Ability ability, int state) {
+ this.ability = ability;
+ this.tickCount = 0;
+ this.state = Math.max(1, state);
+ }
+
+ public Ability getAbility() {
+ return ability;
+ }
+
+ public int getTickCount() {
+ return tickCount;
+ }
+
+ public void incrementTick() {
+ tickCount++;
+ }
+
+ public int getState() {
+ return state;
+ }
+
+ public void setState(int state) {
+ this.state = Math.max(1, state);
+ }
+}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyBarrierData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyBarrierData.java
index 2d65b6228..f31466329 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyBarrierData.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyBarrierData.java
@@ -22,7 +22,9 @@ public class EnergyBarrierData {
// Knockback settings
public boolean knockbackEnabled = false;
public float knockbackStrength = 1.0f;
- /** 0=Both, 1=Player only, 2=NPC only */
+ /**
+ * 0=Both, 1=Player only, 2=NPC only
+ */
public int knockbackTarget = 0;
// Melee settings
@@ -36,7 +38,8 @@ public class EnergyBarrierData {
*/
private Map damageMultipliers = new HashMap<>();
- public EnergyBarrierData() {}
+ public EnergyBarrierData() {
+ }
public EnergyBarrierData(float maxHealth, boolean useHealth, int durationTicks, boolean useDuration) {
this.maxHealth = maxHealth;
@@ -70,23 +73,50 @@ public class EnergyBarrierData {
// ==================== GETTERS & SETTERS ====================
- public float getMaxHealth() { return maxHealth; }
- public void setMaxHealth(float maxHealth) { this.maxHealth = Math.max(0, maxHealth); }
+ public float getMaxHealth() {
+ return maxHealth;
+ }
- public boolean isUseHealth() { return useHealth; }
- public void setUseHealth(boolean useHealth) { this.useHealth = useHealth; }
+ public void setMaxHealth(float maxHealth) {
+ this.maxHealth = Math.max(0, maxHealth);
+ }
- public int getDurationTicks() { return durationTicks; }
- public void setDurationTicks(int durationTicks) { this.durationTicks = Math.max(1, durationTicks); }
+ public boolean isUseHealth() {
+ return useHealth;
+ }
- public boolean isUseDuration() { return useDuration; }
- public void setUseDuration(boolean useDuration) { this.useDuration = useDuration; }
+ public void setUseHealth(boolean useHealth) {
+ this.useHealth = useHealth;
+ }
- public float getDefaultMultiplier() { return defaultMultiplier; }
- public void setDefaultMultiplier(float defaultMultiplier) { this.defaultMultiplier = defaultMultiplier; }
+ public int getDurationTicks() {
+ return durationTicks;
+ }
+
+ public void setDurationTicks(int durationTicks) {
+ this.durationTicks = Math.max(1, durationTicks);
+ }
+
+ public boolean isUseDuration() {
+ return useDuration;
+ }
+
+ public void setUseDuration(boolean useDuration) {
+ this.useDuration = useDuration;
+ }
+
+ public float getDefaultMultiplier() {
+ return defaultMultiplier;
+ }
+
+ public void setDefaultMultiplier(float defaultMultiplier) {
+ this.defaultMultiplier = defaultMultiplier;
+ }
// Knockback target key helpers
- public static String[] getKnockbackTargetKeys() { return KNOCKBACK_TARGET_KEYS; }
+ public static String[] getKnockbackTargetKeys() {
+ return KNOCKBACK_TARGET_KEYS;
+ }
public String getKnockbackTargetKey() {
return KNOCKBACK_TARGET_KEYS[Math.max(0, Math.min(knockbackTarget, 2))];
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java
index e6ccf15a1..2ecbf7cc5 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java
@@ -15,7 +15,8 @@ public class EnergyCombatData implements IEnergyCombatData {
public float explosionRadius = 3.0f;
public float explosionDamageFalloff = 0.5f;
- public EnergyCombatData() {}
+ public EnergyCombatData() {
+ }
public EnergyCombatData(float damage, float knockback, float knockbackUp,
boolean explosive, float explosionRadius, float explosionDamageFalloff) {
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java
index 415c5796c..3b0998602 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java
@@ -20,7 +20,8 @@ public class EnergyDisplayData implements IEnergyDisplayData {
// public boolean skyboxEnabled = false;
// public String skyboxTexture = "";
- public EnergyDisplayData() {}
+ public EnergyDisplayData() {
+ }
public EnergyDisplayData(int innerColor, int outerColor) {
this.innerColor = innerColor;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java
index d385c06e7..a03e70525 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java
@@ -14,7 +14,8 @@ public class EnergyHomingData implements IEnergyHomingData {
public float homingStrength = 0.15f;
public float homingRange = 20.0f;
- public EnergyHomingData() {}
+ public EnergyHomingData() {
+ }
public EnergyHomingData(float speed, boolean homing, float homingStrength, float homingRange) {
this.speed = speed;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java
index f320acdb0..cce2a8283 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java
@@ -8,10 +8,11 @@ import noppes.npcs.api.ability.data.IEnergyLifespanData;
* Used as a parameter object for entity constructors and ability configuration.
*/
public class EnergyLifespanData implements IEnergyLifespanData {
- public float maxDistance = 30.0f;
+ public float maxDistance = 150.0f;
public int maxLifetime = 200;
- public EnergyLifespanData() {}
+ public EnergyLifespanData() {
+ }
public EnergyLifespanData(float maxDistance, int maxLifetime) {
this.maxDistance = maxDistance;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java
index 66786a14a..1eb2b001a 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java
@@ -13,7 +13,8 @@ public class EnergyLightningData implements IEnergyLightningData {
public float lightningRadius = 0.5f;
public int lightningFadeTime = 6;
- public EnergyLightningData() {}
+ public EnergyLightningData() {
+ }
public EnergyLightningData(boolean lightningEffect, float lightningDensity,
float lightningRadius, int lightningFadeTime) {
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyPanelData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyPanelData.java
index 1fa8586fe..3a5206f21 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyPanelData.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyPanelData.java
@@ -17,7 +17,8 @@ public class EnergyPanelData {
public float launchDamage = 8.0f;
public float launchKnockback = 2.0f;
- public EnergyPanelData() {}
+ public EnergyPanelData() {
+ }
public EnergyPanelData(float panelWidth, float panelHeight, float heightOffset) {
this.panelWidth = panelWidth;
@@ -27,26 +28,61 @@ public class EnergyPanelData {
// ==================== GETTERS & SETTERS ====================
- public float getPanelWidth() { return panelWidth; }
- public void setPanelWidth(float panelWidth) { this.panelWidth = Math.max(0.5f, panelWidth); }
+ public float getPanelWidth() {
+ return panelWidth;
+ }
- public float getPanelHeight() { return panelHeight; }
- public void setPanelHeight(float panelHeight) { this.panelHeight = Math.max(0.5f, panelHeight); }
+ public void setPanelWidth(float panelWidth) {
+ this.panelWidth = Math.max(0.5f, panelWidth);
+ }
- public float getHeightOffset() { return heightOffset; }
- public void setHeightOffset(float heightOffset) { this.heightOffset = heightOffset; }
+ public float getPanelHeight() {
+ return panelHeight;
+ }
- public boolean isLaunching() { return launching; }
- public void setLaunching(boolean launching) { this.launching = launching; }
+ public void setPanelHeight(float panelHeight) {
+ this.panelHeight = Math.max(0.5f, panelHeight);
+ }
- public float getLaunchSpeed() { return launchSpeed; }
- public void setLaunchSpeed(float launchSpeed) { this.launchSpeed = Math.max(0.1f, launchSpeed); }
+ public float getHeightOffset() {
+ return heightOffset;
+ }
- public float getLaunchDamage() { return launchDamage; }
- public void setLaunchDamage(float launchDamage) { this.launchDamage = Math.max(0, launchDamage); }
+ public void setHeightOffset(float heightOffset) {
+ this.heightOffset = heightOffset;
+ }
- public float getLaunchKnockback() { return launchKnockback; }
- public void setLaunchKnockback(float launchKnockback) { this.launchKnockback = Math.max(0, launchKnockback); }
+ public boolean isLaunching() {
+ return launching;
+ }
+
+ public void setLaunching(boolean launching) {
+ this.launching = launching;
+ }
+
+ public float getLaunchSpeed() {
+ return launchSpeed;
+ }
+
+ public void setLaunchSpeed(float launchSpeed) {
+ this.launchSpeed = Math.max(0.1f, launchSpeed);
+ }
+
+ public float getLaunchDamage() {
+ return launchDamage;
+ }
+
+ public void setLaunchDamage(float launchDamage) {
+ this.launchDamage = Math.max(0, launchDamage);
+ }
+
+ public float getLaunchKnockback() {
+ return launchKnockback;
+ }
+
+ public void setLaunchKnockback(float launchKnockback) {
+ this.launchKnockback = Math.max(0, launchKnockback);
+ }
// ==================== NBT ====================
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyTrajectoryData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyTrajectoryData.java
index 15adb3062..ea7735e11 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyTrajectoryData.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyTrajectoryData.java
@@ -15,7 +15,8 @@ public class EnergyTrajectoryData implements IEnergyTrajectoryData {
public int currentPath = 0;
private final List trajectory = new ArrayList<>();
- public EnergyTrajectoryData() {}
+ public EnergyTrajectoryData() {
+ }
public EnergyTrajectoryData(int startingPath, Path... paths) {
trajectory.addAll(Arrays.asList(paths));
@@ -85,6 +86,7 @@ public class EnergyTrajectoryData implements IEnergyTrajectoryData {
IPath p = getPath(path);
return p == null ? 0 : p.getZ();
}
+
@Override
public void setX(int path, double x) {
IPath p = getPath(path);
@@ -126,8 +128,8 @@ public class EnergyTrajectoryData implements IEnergyTrajectoryData {
@Override
public void setPath(int path, double x, double y, double z, int delay) {
Path newPath = new Path(
- NpcAPI.Instance().getIPos(x, y, z),
- delay
+ NpcAPI.Instance().getIPos(x, y, z),
+ delay
);
while (trajectory.size() <= path) {
@@ -196,7 +198,7 @@ public class EnergyTrajectoryData implements IEnergyTrajectoryData {
}
}
- public static class Path implements IEnergyTrajectoryData.IPath{
+ public static class Path implements IEnergyTrajectoryData.IPath {
public IPos pos;
public int delayTicks;
public boolean concluded;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java
index d41418d91..2456b161d 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java
@@ -199,5 +199,7 @@ public class AbilityFieldBuilder extends GuiFieldBuilder {
return super.handleTextFieldEvent(textFieldId, field);
}
- public Map getEffectWidgetMeta() { return effectWidgetMeta; }
+ public Map getEffectWidgetMeta() {
+ return effectWidgetMeta;
+ }
}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java
index 780ddc050..120f6190d 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java
@@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
+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;
@@ -11,10 +12,8 @@ import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
-
-import noppes.npcs.client.gui.builder.FieldDef;
-import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import noppes.npcs.api.ability.type.IAbilityCharge;
+import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.HashSet;
@@ -172,6 +171,12 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge {
hitEntities.clear();
}
+ @Override
+ public void resetForBurst() {
+ super.resetForBurst();
+ hitEntities.clear();
+ }
+
@Override
public float getTelegraphLength() {
return maxRange;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java
index 58c92009f..bac7cc234 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java
@@ -1,8 +1,11 @@
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.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
+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;
@@ -11,15 +14,8 @@ 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.IAbilityCutter;
-
import noppes.npcs.client.gui.builder.FieldDef;
-import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
-
-import cpw.mods.fml.relauncher.Side;
-import cpw.mods.fml.relauncher.SideOnly;
import java.util.Arrays;
import java.util.HashSet;
@@ -40,9 +36,12 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
@Override
public String toString() {
switch (this) {
- case SWIPE: return "ability.sweep.swipe";
- case SPIN: return "ability.sweep.spin";
- default: return name();
+ case SWIPE:
+ return "ability.sweep.swipe";
+ case SPIN:
+ return "ability.sweep.spin";
+ default:
+ return name();
}
}
}
@@ -232,7 +231,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
* For SWIPE mode, this covers the continuous arc from startAngle to endAngle.
*/
private void performSweepDamageRange(EntityLivingBase caster, World world, float minDist, float maxDist,
- float startAngle, float endAngle) {
+ float startAngle, float endAngle) {
// Calculate the angular range covered this tick
float minAngle = Math.min(startAngle, endAngle);
float maxAngle = Math.max(startAngle, endAngle);
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java
index 41c5faaa1..fffbf6ca5 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java
@@ -8,9 +8,8 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
-
-import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.api.ability.type.IAbilityDash;
+import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.List;
@@ -32,9 +31,12 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
@Override
public String toString() {
switch (this) {
- case AGGRESSIVE: return "ability.dash.aggressive";
- case DEFENSIVE: return "ability.dash.defensive";
- default: return name();
+ case AGGRESSIVE:
+ return "ability.dash.aggressive";
+ case DEFENSIVE:
+ return "ability.dash.defensive";
+ default:
+ return name();
}
}
}
@@ -194,6 +196,12 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
chosenDirection = null;
}
+ @Override
+ public void resetForBurst() {
+ super.resetForBurst();
+ chosenDirection = null;
+ }
+
@Override
public int getMaxPreviewDuration() {
return (int) Math.ceil(dashDistance / dashSpeed) + 5;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java
index 331ed0de4..24489b9eb 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java
@@ -1,25 +1,26 @@
package kamkeel.npcs.controllers.data.ability.type;
-import kamkeel.npcs.controllers.data.ability.*;
+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.LockMovementType;
+import kamkeel.npcs.controllers.data.ability.TargetingMode;
+import kamkeel.npcs.controllers.data.ability.UserType;
+import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
+import noppes.npcs.api.ability.type.IAbilityGuard;
import noppes.npcs.api.handler.data.IAnimation;
+import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.controllers.AnimationController;
import noppes.npcs.controllers.data.Animation;
import noppes.npcs.controllers.data.PlayerData;
import noppes.npcs.entity.EntityNPCInterface;
-
-import noppes.npcs.api.ability.type.IAbilityGuard;
-
-import noppes.npcs.client.gui.builder.FieldDef;
-import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
-
-import cpw.mods.fml.relauncher.Side;
-import cpw.mods.fml.relauncher.SideOnly;
import noppes.npcs.util.ValueUtil;
import java.util.Arrays;
@@ -77,9 +78,12 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
@Override
public String toString() {
switch (this) {
- case FLAT: return "ability.counter.flat";
- case PERCENT: return "ability.counter.percent";
- default: return name();
+ case FLAT:
+ return "ability.counter.flat";
+ case PERCENT:
+ return "ability.counter.percent";
+ default:
+ return name();
}
}
}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java
index 7ca81c95a..a23e38963 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java
@@ -1,19 +1,15 @@
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.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
-
-
import noppes.npcs.api.ability.type.IAbilityHazard;
-
import noppes.npcs.client.gui.builder.FieldDef;
-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;
@@ -106,20 +102,45 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard {
this.affectsCaster = nbt.getBoolean("affectsCaster");
}
- 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 getDamagePerSecond() {
+ return damagePerSecond;
+ }
- public boolean isIgnoreInvulnFrames() { return ignoreInvulnFrames; }
- public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { this.ignoreInvulnFrames = ignoreInvulnFrames; }
+ public void setDamagePerSecond(float damagePerSecond) {
+ this.damagePerSecond = damagePerSecond;
+ }
- public boolean isAffectsCaster() { return affectsCaster; }
- public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; }
+ 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 boolean isAffectsCaster() {
+ return affectsCaster;
+ }
+
+ public void setAffectsCaster(boolean affectsCaster) {
+ this.affectsCaster = affectsCaster;
+ }
@SideOnly(Side.CLIENT)
@Override
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java
index 6119e1340..89cfd2dbf 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java
@@ -1,5 +1,7 @@
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.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
@@ -9,13 +11,9 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.world.World;
-import noppes.npcs.entity.EntityNPCInterface;
-
-import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.api.ability.type.IAbilityHeal;
-
-import cpw.mods.fml.relauncher.Side;
-import cpw.mods.fml.relauncher.SideOnly;
+import noppes.npcs.client.gui.builder.FieldDef;
+import noppes.npcs.entity.EntityNPCInterface;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java
index 7c85826a2..d7314b707 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java
@@ -5,13 +5,13 @@ import cpw.mods.fml.relauncher.SideOnly;
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.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
-import noppes.npcs.client.gui.builder.FieldDef;
-import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import noppes.npcs.api.ability.type.IAbilityHeavyHit;
+import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.List;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java
index de2090cc3..d5b8d8c52 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java
@@ -53,6 +53,12 @@ public abstract class AbilityMovement extends Ability {
maxActiveTicks = 0;
}
+ @Override
+ public void resetForBurst() {
+ movementDirection = null;
+ maxActiveTicks = 0;
+ }
+
// ═══════════════════════════════════════════════════════════════════
// DIRECTION HELPERS
// ═══════════════════════════════════════════════════════════════════
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java
index 46242fc71..1aba9ff3c 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java
@@ -5,18 +5,15 @@ import cpw.mods.fml.relauncher.SideOnly;
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.gui.AbilityFieldDefs;
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;
-
import noppes.npcs.api.ability.type.IAbilityProjectile;
-
import noppes.npcs.client.gui.builder.FieldDef;
-import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import java.util.Arrays;
import java.util.List;
@@ -94,7 +91,11 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
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;
+ }
applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz);
world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f);
@@ -167,7 +168,7 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
* Apply explosion splash damage around a point.
*/
private void applyExplosionDamage(EntityLivingBase caster, EntityLivingBase primaryTarget, World world,
- double x, double y, double z) {
+ double x, double y, double z) {
@SuppressWarnings("unchecked")
List entities = world.getEntitiesWithinAABBExcludingEntity(primaryTarget,
primaryTarget.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius));
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java
index 1eebdab99..4cca57508 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java
@@ -5,16 +5,13 @@ import cpw.mods.fml.relauncher.SideOnly;
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.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
-import net.minecraft.util.DamageSource;
-import noppes.npcs.entity.EntityNPCInterface;
-
-import noppes.npcs.client.gui.builder.FieldDef;
-import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import noppes.npcs.api.ability.type.IAbilityShockwave;
+import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.List;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java
index f8a6559ca..1e844271f 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java
@@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly;
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.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
@@ -13,13 +14,10 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
+import noppes.npcs.api.ability.type.IAbilitySlam;
+import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.entity.EntityNPCInterface;
-import noppes.npcs.api.ability.type.IAbilitySlam;
-import somehussar.gui.annotationHandling.GuiEditable;
-
-import noppes.npcs.client.gui.builder.FieldDef;
-import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import java.util.Arrays;
import java.util.List;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java
index 70228652e..cc2deb4ea 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java
@@ -1,9 +1,12 @@
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.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.UserType;
+import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.block.Block;
import net.minecraft.entity.Entity;
@@ -13,15 +16,8 @@ import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MathHelper;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
-import noppes.npcs.entity.EntityNPCInterface;
-
import noppes.npcs.api.ability.type.IAbilityTeleport;
-
import noppes.npcs.client.gui.builder.FieldDef;
-import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
-
-import cpw.mods.fml.relauncher.Side;
-import cpw.mods.fml.relauncher.SideOnly;
import java.util.Arrays;
import java.util.List;
@@ -43,10 +39,14 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
@Override
public String toString() {
switch (this) {
- case BLINK: return "ability.teleport.blink";
- case BEHIND: return "ability.teleport.behind";
- case SINGLE: return "ability.teleport.single";
- default: return name();
+ case BLINK:
+ return "ability.teleport.blink";
+ case BEHIND:
+ return "ability.teleport.behind";
+ case SINGLE:
+ return "ability.teleport.single";
+ default:
+ return name();
}
}
}
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java
index a8782122f..d875353f6 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java
@@ -1,5 +1,7 @@
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.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
@@ -12,9 +14,6 @@ import noppes.npcs.client.gui.SubGuiTrapPresetSelector;
import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.constants.EnumPotionType;
-import cpw.mods.fml.relauncher.Side;
-import cpw.mods.fml.relauncher.SideOnly;
-
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -115,51 +114,80 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
switch (preset) {
case "VENOM":
groundAlpha = 0.04f;
- particles = true; particleDensity = 0.3f; particleScale = 0.5f;
- particleMotion = 1; particleDir = "mc:mobSpell";
- colorData.innerColor = 0x44DD44; colorData.outerColor = 0x116611;
- windUpColor = 0x6044DD44; activeColor = 0xC044FF44;
+ particles = true;
+ particleDensity = 0.3f;
+ particleScale = 0.5f;
+ particleMotion = 1;
+ particleDir = "mc:mobSpell";
+ colorData.innerColor = 0x44DD44;
+ colorData.outerColor = 0x116611;
+ windUpColor = 0x6044DD44;
+ activeColor = 0xC044FF44;
effects.add(new AbilityPotionEffect(EnumPotionType.Poison, 100, 0));
break;
case "EXPLOSIVE":
groundAlpha = 0.04f;
- particles = true; particleDensity = 0.3f; particleScale = 0.6f;
- particleMotion = 0; particleDir = "mc:smoke";
- colorData.innerColor = 0xFF6611; colorData.outerColor = 0xCC2200;
- windUpColor = 0x60FF6611; activeColor = 0xC0FF4400;
+ particles = true;
+ particleDensity = 0.3f;
+ particleScale = 0.6f;
+ particleMotion = 0;
+ particleDir = "mc:smoke";
+ colorData.innerColor = 0xFF6611;
+ colorData.outerColor = 0xCC2200;
+ windUpColor = 0x60FF6611;
+ activeColor = 0xC0FF4400;
effects.add(new AbilityPotionEffect(EnumPotionType.Fire, 60, 0));
break;
case "CURSED":
groundAlpha = 0.04f;
- particles = true; particleDensity = 0.3f; particleScale = 0.5f;
- particleMotion = 1; particleDir = "mc:portal";
- colorData.innerColor = 0xAA44FF; colorData.outerColor = 0x6622BB;
- windUpColor = 0x60AA44FF; activeColor = 0xC0CC66FF;
+ particles = true;
+ particleDensity = 0.3f;
+ particleScale = 0.5f;
+ particleMotion = 1;
+ particleDir = "mc:portal";
+ colorData.innerColor = 0xAA44FF;
+ colorData.outerColor = 0x6622BB;
+ windUpColor = 0x60AA44FF;
+ activeColor = 0xC0CC66FF;
effects.add(new AbilityPotionEffect(EnumPotionType.Weakness, 100, 0));
break;
case "SHOCK":
groundAlpha = 0.04f;
- particles = true; particleDensity = 0.3f; particleScale = 0.4f;
- particleMotion = 2; particleDir = "mc:enchantmenttable";
- colorData.innerColor = 0x4488FF; colorData.outerColor = 0x2244BB;
- windUpColor = 0x604488FF; activeColor = 0xC066AAFF;
+ particles = true;
+ particleDensity = 0.3f;
+ particleScale = 0.4f;
+ particleMotion = 2;
+ particleDir = "mc:enchantmenttable";
+ colorData.innerColor = 0x4488FF;
+ colorData.outerColor = 0x2244BB;
+ windUpColor = 0x604488FF;
+ activeColor = 0xC066AAFF;
effects.add(new AbilityPotionEffect(EnumPotionType.MiningFatigue, 80, 1));
break;
case "SNARE":
groundAlpha = 0.04f;
- particles = true; particleDensity = 0.3f; particleScale = 0.5f;
- particleMotion = 1; particleDir = "mc:snowshovel";
- colorData.innerColor = 0x88CCFF; colorData.outerColor = 0x4488CC;
- windUpColor = 0x6088CCFF; activeColor = 0xC0AADDFF;
+ particles = true;
+ particleDensity = 0.3f;
+ particleScale = 0.5f;
+ particleMotion = 1;
+ particleDir = "mc:snowshovel";
+ colorData.innerColor = 0x88CCFF;
+ colorData.outerColor = 0x4488CC;
+ windUpColor = 0x6088CCFF;
+ activeColor = 0xC0AADDFF;
effects.add(new AbilityPotionEffect(EnumPotionType.Slowness, 100, 1));
break;
case "HIDDEN":
default:
groundAlpha = 0.03f;
- particles = false; particleDensity = 0.0f; particleScale = 1.0f;
+ particles = false;
+ particleDensity = 0.0f;
+ particleScale = 1.0f;
particleMotion = 0;
- colorData.innerColor = 0x888888; colorData.outerColor = 0x444444;
- windUpColor = 0x60888888; activeColor = 0xC0AAAAAA;
+ colorData.innerColor = 0x888888;
+ colorData.outerColor = 0x444444;
+ windUpColor = 0x60888888;
+ activeColor = 0xC0AAAAAA;
break;
}
}
@@ -190,22 +218,69 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
this.visible = nbt.getBoolean("visible");
}
- 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 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;
+ }
@SideOnly(Side.CLIENT)
@Override
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java
index f969c27cd..d5f68e013 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java
@@ -1,21 +1,18 @@
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.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
+import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.world.World;
-import noppes.npcs.entity.EntityNPCInterface;
-
-import noppes.npcs.client.gui.builder.FieldDef;
-import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import noppes.npcs.api.ability.type.IAbilityVortex;
-
-import cpw.mods.fml.relauncher.Side;
-import cpw.mods.fml.relauncher.SideOnly;
+import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.HashSet;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java
index b75e41c3e..0b233a637 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java
@@ -1,5 +1,7 @@
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.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
@@ -14,15 +16,10 @@ import kamkeel.npcs.entity.EntityAbilityZone.ParticleMotion;
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
-import noppes.npcs.constants.EnumPotionType;
import net.minecraft.util.DamageSource;
-
-
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 noppes.npcs.constants.EnumPotionType;
import java.util.ArrayList;
import java.util.Arrays;
@@ -213,57 +210,134 @@ public abstract class AbilityZone extends Ability {
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;
+ 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 AbilityPotionEffect(EnumPotionType.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;
+ 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 AbilityPotionEffect(EnumPotionType.Fire, 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;
+ 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 AbilityPotionEffect(EnumPotionType.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;
+ 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 AbilityPotionEffect(EnumPotionType.MiningFatigue, 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;
+ 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 AbilityPotionEffect(EnumPotionType.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;
+ 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;
@@ -310,8 +384,11 @@ public abstract class AbilityZone extends Ability {
protected void readZoneNBT(NBTTagCompound nbt, int defaultDuration) {
this.durationTicks = nbt.getInteger("durationTicks");
- try { this.zoneShape = ZoneShape.valueOf(nbt.getString("zoneShape")); }
- catch (Exception e) { this.zoneShape = ZoneShape.CIRCLE; }
+ try {
+ this.zoneShape = ZoneShape.valueOf(nbt.getString("zoneShape"));
+ } catch (Exception e) {
+ this.zoneShape = ZoneShape.CIRCLE;
+ }
this.spawnRadius = nbt.getFloat("spawnRadius");
this.zoneCount = nbt.getInteger("zoneCount");
this.zoneHeight = nbt.getFloat("zoneHeight");
@@ -337,73 +414,238 @@ public abstract class AbilityZone extends Ability {
this.particleGlow = nbt.getBoolean("particleGlow");
}
- public int getDurationTicks() { return durationTicks; }
- public void setDurationTicks(int durationTicks) { this.durationTicks = Math.max(1, durationTicks); }
+ public int getDurationTicks() {
+ return durationTicks;
+ }
- public ZoneShape getZoneShapeEnum() { return zoneShape; }
- public void setZoneShapeEnum(ZoneShape shape) { this.zoneShape = shape; }
+ 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 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 getSpawnRadius() {
+ return spawnRadius;
+ }
- 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)); }
+ public void setSpawnRadius(float spawnRadius) {
+ this.spawnRadius = Math.max(0, spawnRadius);
+ }
- 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; }
+ public int getZoneCount() {
+ return zoneCount;
+ }
- 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; }
+ 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));
+ }
+
+ 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;
+ }
+
+ 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;
+ }
@SideOnly(Side.CLIENT)
protected void addPresetFieldDef(List defs) {
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java
index 66e5ef4ac..ca1e1ac47 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java
@@ -6,7 +6,11 @@ import kamkeel.npcs.controllers.data.ability.AbilityVariant;
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.ability.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityBeam;
@@ -34,7 +38,7 @@ public class AbilityBeam extends AbilityEnergyProjectile impl
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f),
new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f),
new EnergyHomingData(0.4f, true, 0.1f, 15.0f),
- new EnergyLifespanData(25.0f, 200)
+ new EnergyLifespanData(150.0f, 200)
);
this.typeId = "ability.cnpc.beam";
this.name = "Beam";
@@ -56,7 +60,7 @@ public class AbilityBeam extends AbilityEnergyProjectile impl
@Override
protected EntityAbilityBeam createEntity(EntityLivingBase caster, EntityLivingBase target,
- Vec3 spawnPos, EnergyDisplayData resolved, int index) {
+ Vec3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityAbilityBeam(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
@@ -81,7 +85,7 @@ public class AbilityBeam extends AbilityEnergyProjectile impl
@Override
protected void setupEntityPreview(EntityAbilityBeam beam, EntityLivingBase caster,
- EnergyDisplayData resolved, ProjectileData projData, int index) {
+ EnergyDisplayData resolved, ProjectileData projData, int index) {
beam.setupPreview(caster, beamWidth, headSize, resolved, lightningData, projData.anchor, windUpTicks, 1.0f);
}
@@ -139,14 +143,29 @@ public class AbilityBeam extends AbilityEnergyProjectile impl
// ==================== TYPE-SPECIFIC GETTERS ====================
- public float getSpeed() { return homingData.speed; }
- public void setSpeed(float speed) { homingData.speed = speed; }
+ public float getSpeed() {
+ return homingData.speed;
+ }
- public float getBeamWidth() { return beamWidth; }
- public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
+ public void setSpeed(float speed) {
+ homingData.speed = speed;
+ }
- public float getHeadSize() { return headSize; }
- public void setHeadSize(float headSize) { this.headSize = headSize; }
+ 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;
+ }
// ==================== TYPE-SPECIFIC GUI ====================
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java
index cd36eeab3..0fd8b494c 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java
@@ -5,7 +5,11 @@ import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
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.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityDisc;
@@ -36,7 +40,7 @@ public class AbilityDisc extends AbilityEnergyProjectile impl
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f),
new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f),
new EnergyHomingData(0.6f, true, 0.12f, 18.0f),
- new EnergyLifespanData(35.0f, 200)
+ new EnergyLifespanData(150.0f, 200)
);
this.typeId = "ability.cnpc.disc";
this.name = "Disc";
@@ -56,7 +60,7 @@ public class AbilityDisc extends AbilityEnergyProjectile impl
@Override
protected EntityAbilityDisc createEntity(EntityLivingBase caster, EntityLivingBase target,
- Vec3 spawnPos, EnergyDisplayData resolved, int index) {
+ Vec3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityAbilityDisc(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
@@ -81,7 +85,7 @@ public class AbilityDisc extends AbilityEnergyProjectile impl
@Override
protected void setupEntityPreview(EntityAbilityDisc disc, EntityLivingBase caster,
- EnergyDisplayData resolved, ProjectileData projData, int index) {
+ EnergyDisplayData resolved, ProjectileData projData, int index) {
disc.setupPreview(caster, discRadius, discThickness, resolved, lightningData, projData.anchor, windUpTicks, vertical);
}
@@ -139,23 +143,53 @@ public class AbilityDisc extends AbilityEnergyProjectile impl
// ==================== TYPE-SPECIFIC GETTERS ====================
- public float getSpeed() { return homingData.speed; }
- public void setSpeed(float speed) { homingData.speed = speed; }
+ public float getSpeed() {
+ return homingData.speed;
+ }
- public float getDiscRadius() { return discRadius; }
- public void setDiscRadius(float discRadius) { this.discRadius = discRadius; }
+ public void setSpeed(float speed) {
+ homingData.speed = speed;
+ }
- public float getDiscThickness() { return discThickness; }
- public void setDiscThickness(float discThickness) { this.discThickness = discThickness; }
+ public float getDiscRadius() {
+ return discRadius;
+ }
- public boolean isVertical() { return vertical; }
- public void setVertical(boolean vertical) { this.vertical = vertical; }
+ public void setDiscRadius(float discRadius) {
+ this.discRadius = discRadius;
+ }
- public boolean isBoomerang() { return boomerang; }
- public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; }
+ public float getDiscThickness() {
+ return discThickness;
+ }
- public int getBoomerangDelay() { return boomerangDelay; }
- public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; }
+ public void setDiscThickness(float discThickness) {
+ this.discThickness = discThickness;
+ }
+
+ 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;
+ }
// ==================== TYPE-SPECIFIC GUI ====================
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java
index b6bb57e96..2fe0c4572 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java
@@ -10,7 +10,6 @@ import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyDome;
-import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -111,11 +110,21 @@ public class AbilityDome extends AbilityEnergyBarrier {
// ==================== GETTERS & SETTERS ====================
- public float getDomeRadius() { return domeRadius; }
- public void setDomeRadius(float radius) { this.domeRadius = Math.max(1.0f, radius); }
+ public float getDomeRadius() {
+ return domeRadius;
+ }
- public boolean isFollowCaster() { return followCaster; }
- public void setFollowCaster(boolean follow) { this.followCaster = follow; }
+ public void setDomeRadius(float radius) {
+ this.domeRadius = Math.max(1.0f, radius);
+ }
+
+ public boolean isFollowCaster() {
+ return followCaster;
+ }
+
+ public void setFollowCaster(boolean follow) {
+ this.followCaster = follow;
+ }
// ==================== TYPE-SPECIFIC GUI ====================
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java
index 3e0747160..59917889e 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java
@@ -26,40 +26,95 @@ public abstract class AbilityEnergy extends Ability {
// ==================== DISPLAY DATA ====================
- public int getInnerColor() { return displayData.innerColor; }
- public void setInnerColor(int color) { displayData.innerColor = color; }
+ public int getInnerColor() {
+ return displayData.innerColor;
+ }
- public float getInnerAlpha() { return displayData.innerAlpha; }
- public void setInnerAlpha(float alpha) { displayData.innerAlpha = alpha; }
+ public void setInnerColor(int color) {
+ displayData.innerColor = color;
+ }
- public int getOuterColor() { return displayData.outerColor; }
- public void setOuterColor(int color) { displayData.outerColor = color; }
+ public float getInnerAlpha() {
+ return displayData.innerAlpha;
+ }
- public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
- public void setOuterColorEnabled(boolean enabled) { displayData.outerColorEnabled = enabled; }
+ public void setInnerAlpha(float alpha) {
+ displayData.innerAlpha = alpha;
+ }
- public float getOuterColorWidth() { return displayData.outerColorWidth; }
- public void setOuterColorWidth(float width) { displayData.outerColorWidth = width; }
+ public int getOuterColor() {
+ return displayData.outerColor;
+ }
- public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
- public void setOuterColorAlpha(float alpha) { displayData.outerColorAlpha = alpha; }
+ public void setOuterColor(int color) {
+ displayData.outerColor = color;
+ }
- public float getRotationSpeed() { return displayData.rotationSpeed; }
- public void setRotationSpeed(float speed) { displayData.rotationSpeed = speed; }
+ public boolean isOuterColorEnabled() {
+ return displayData.outerColorEnabled;
+ }
+
+ public void setOuterColorEnabled(boolean enabled) {
+ displayData.outerColorEnabled = enabled;
+ }
+
+ public float getOuterColorWidth() {
+ return displayData.outerColorWidth;
+ }
+
+ public void setOuterColorWidth(float width) {
+ displayData.outerColorWidth = width;
+ }
+
+ public float getOuterColorAlpha() {
+ return displayData.outerColorAlpha;
+ }
+
+ public void setOuterColorAlpha(float alpha) {
+ displayData.outerColorAlpha = alpha;
+ }
+
+ public float getRotationSpeed() {
+ return displayData.rotationSpeed;
+ }
+
+ public void setRotationSpeed(float speed) {
+ displayData.rotationSpeed = speed;
+ }
// ==================== LIGHTNING DATA ====================
- public boolean hasLightningEffect() { return lightningData.lightningEffect; }
- public void setLightningEffect(boolean enabled) { lightningData.lightningEffect = enabled; }
+ public boolean hasLightningEffect() {
+ return lightningData.lightningEffect;
+ }
- public float getLightningDensity() { return lightningData.lightningDensity; }
- public void setLightningDensity(float density) { lightningData.lightningDensity = density; }
+ public void setLightningEffect(boolean enabled) {
+ lightningData.lightningEffect = enabled;
+ }
- public float getLightningRadius() { return lightningData.lightningRadius; }
- public void setLightningRadius(float radius) { lightningData.lightningRadius = radius; }
+ public float getLightningDensity() {
+ return lightningData.lightningDensity;
+ }
- public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
- public void setLightningFadeTime(int fadeTime) { lightningData.lightningFadeTime = fadeTime; }
+ public void setLightningDensity(float density) {
+ lightningData.lightningDensity = density;
+ }
+
+ public float getLightningRadius() {
+ return lightningData.lightningRadius;
+ }
+
+ public void setLightningRadius(float radius) {
+ lightningData.lightningRadius = radius;
+ }
+
+ public int getLightningFadeTime() {
+ return lightningData.lightningFadeTime;
+ }
+
+ public void setLightningFadeTime(int fadeTime) {
+ lightningData.lightningFadeTime = fadeTime;
+ }
// ==================== NBT HELPERS ====================
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyBarrier.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyBarrier.java
index 7909f5a56..3319b7dc6 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyBarrier.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyBarrier.java
@@ -142,37 +142,95 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
// Display and lightning data inherited from AbilityEnergy
// Barrier data
- public float getBarrierMaxHealth() { return barrierData.maxHealth; }
- public void setBarrierMaxHealth(float maxHealth) { barrierData.setMaxHealth(maxHealth); }
+ public float getBarrierMaxHealth() {
+ return barrierData.maxHealth;
+ }
- public boolean isUseHealth() { return barrierData.useHealth; }
- public void setUseHealth(boolean useHealth) { barrierData.useHealth = useHealth; }
+ public void setBarrierMaxHealth(float maxHealth) {
+ barrierData.setMaxHealth(maxHealth);
+ }
- public int getBarrierDuration() { return barrierData.durationTicks; }
- public void setBarrierDuration(int ticks) { barrierData.setDurationTicks(ticks); }
+ public boolean isUseHealth() {
+ return barrierData.useHealth;
+ }
- public boolean isUseDuration() { return barrierData.useDuration; }
- public void setUseDuration(boolean useDuration) { barrierData.useDuration = useDuration; }
+ public void setUseHealth(boolean useHealth) {
+ barrierData.useHealth = useHealth;
+ }
- public float getDefaultMultiplier() { return barrierData.defaultMultiplier; }
- public void setDefaultMultiplier(float mult) { barrierData.defaultMultiplier = mult; }
+ public int getBarrierDuration() {
+ return barrierData.durationTicks;
+ }
- public void setDamageMultiplier(String typeId, float mult) { barrierData.setMultiplier(typeId, mult); }
- public float getDamageMultiplier(String typeId) { return barrierData.getMultiplier(typeId); }
+ public void setBarrierDuration(int ticks) {
+ barrierData.setDurationTicks(ticks);
+ }
+
+ public boolean isUseDuration() {
+ return barrierData.useDuration;
+ }
+
+ public void setUseDuration(boolean useDuration) {
+ barrierData.useDuration = useDuration;
+ }
+
+ public float getDefaultMultiplier() {
+ return barrierData.defaultMultiplier;
+ }
+
+ public void setDefaultMultiplier(float mult) {
+ barrierData.defaultMultiplier = mult;
+ }
+
+ public void setDamageMultiplier(String typeId, float mult) {
+ barrierData.setMultiplier(typeId, mult);
+ }
+
+ public float getDamageMultiplier(String typeId) {
+ return barrierData.getMultiplier(typeId);
+ }
// Knockback data
- public boolean isKnockbackEnabled() { return barrierData.knockbackEnabled; }
- public void setKnockbackEnabled(boolean enabled) { barrierData.knockbackEnabled = enabled; }
- public float getKnockbackStrength() { return barrierData.knockbackStrength; }
- public void setKnockbackStrength(float strength) { barrierData.knockbackStrength = strength; }
- public String getKnockbackTargetKey() { return barrierData.getKnockbackTargetKey(); }
- public void setKnockbackTargetKey(String key) { barrierData.setKnockbackTargetFromKey(key); }
+ public boolean isKnockbackEnabled() {
+ return barrierData.knockbackEnabled;
+ }
+
+ public void setKnockbackEnabled(boolean enabled) {
+ barrierData.knockbackEnabled = enabled;
+ }
+
+ public float getKnockbackStrength() {
+ return barrierData.knockbackStrength;
+ }
+
+ public void setKnockbackStrength(float strength) {
+ barrierData.knockbackStrength = strength;
+ }
+
+ public String getKnockbackTargetKey() {
+ return barrierData.getKnockbackTargetKey();
+ }
+
+ public void setKnockbackTargetKey(String key) {
+ barrierData.setKnockbackTargetFromKey(key);
+ }
// Melee data
- public boolean isMeleeEnabled() { return barrierData.meleeEnabled; }
- public void setMeleeEnabled(boolean enabled) { barrierData.meleeEnabled = enabled; }
- public float getMeleeDamageMultiplier() { return barrierData.meleeDamageMultiplier; }
- public void setMeleeDamageMultiplier(float mult) { barrierData.meleeDamageMultiplier = mult; }
+ public boolean isMeleeEnabled() {
+ return barrierData.meleeEnabled;
+ }
+
+ public void setMeleeEnabled(boolean enabled) {
+ barrierData.meleeEnabled = enabled;
+ }
+
+ public float getMeleeDamageMultiplier() {
+ return barrierData.meleeDamageMultiplier;
+ }
+
+ public void setMeleeDamageMultiplier(float mult) {
+ barrierData.meleeDamageMultiplier = mult;
+ }
// ==================== GUI ====================
@@ -199,7 +257,7 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
FieldDef.floatField("ability.knockbackStrength", this::getKnockbackStrength, this::setKnockbackStrength)
.range(0, 10).visibleWhen(this::isKnockbackEnabled),
FieldDef.stringEnumField("ability.knockbackTarget", EnergyBarrierData.getKnockbackTargetKeys(),
- this::getKnockbackTargetKey, this::setKnockbackTargetKey)
+ this::getKnockbackTargetKey, this::setKnockbackTargetKey)
.visibleWhen(this::isKnockbackEnabled)
));
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java
index d9ab67646..2286919d6 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java
@@ -2,10 +2,14 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
-import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
-import kamkeel.npcs.controllers.data.ability.data.*;
+import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
+import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
@@ -24,7 +28,7 @@ import java.util.List;
* Abstract base for energy projectile abilities (Orb, Disc, Beam, LaserShot).
* Handles shared lifecycle (windup → execute → active → completion), multi-projectile
* support with staggered firing, telegraph creation, NBT persistence, and GUI definitions.
- *
+ *
* Subclasses provide entity creation/firing/charging logic and type-specific fields.
*
* @param The entity type this ability spawns
@@ -64,6 +68,7 @@ public abstract class AbilityEnergyProjectile
this.homingData = homingData;
this.lifespanData = lifespanData;
this.trajectoryData = new EnergyTrajectoryData();
+ this.burstOverlap = true;
initProjectiles(1);
}
@@ -74,7 +79,7 @@ public abstract class AbilityEnergyProjectile
* Called during windup (tick 1) and burst refire (onExecute when entities are null).
*/
protected abstract E createEntity(EntityLivingBase caster, EntityLivingBase target,
- Vec3 spawnPos, EnergyDisplayData resolved, int index);
+ Vec3 spawnPos, EnergyDisplayData resolved, int index);
/**
* Fire an entity (start moving toward target). Entity is guaranteed non-null and alive.
@@ -90,7 +95,7 @@ public abstract class AbilityEnergyProjectile
* Setup entity for preview mode during windup.
*/
protected abstract void setupEntityPreview(E entity, EntityLivingBase caster,
- EnergyDisplayData resolved, ProjectileData projData, int index);
+ EnergyDisplayData resolved, ProjectileData projData, int index);
/**
* Create typed array (Java generics can't create generic arrays).
@@ -129,9 +134,12 @@ public abstract class AbilityEnergyProjectile
*/
protected AnchorPoint getDefaultAnchor(int index) {
switch (index) {
- case 0: return AnchorPoint.RIGHT_HAND;
- case 1: return AnchorPoint.LEFT_HAND;
- default: return AnchorPoint.FRONT;
+ case 0:
+ return AnchorPoint.RIGHT_HAND;
+ case 1:
+ return AnchorPoint.LEFT_HAND;
+ default:
+ return AnchorPoint.FRONT;
}
}
@@ -168,15 +176,31 @@ public abstract class AbilityEnergyProjectile
}
}
- public int getProjectileCount() { return projectileCount; }
+ public int getProjectileCount() {
+ return projectileCount;
+ }
- public int getFireDelay() { return fireDelay; }
- public void setFireDelay(int delay) { this.fireDelay = Math.max(0, delay); }
+ public int getFireDelay() {
+ return fireDelay;
+ }
- /** @deprecated Use {@link #getFireDelay()} */
- public int getDualFireDelay() { return fireDelay; }
- /** @deprecated Use {@link #setFireDelay(int)} */
- public void setDualFireDelay(int delay) { setFireDelay(delay); }
+ 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);
+ }
protected int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
@@ -317,7 +341,7 @@ public abstract class AbilityEnergyProjectile
// Check if entity was removed from world (chunk unload, etc.)
// Skip this check for preview entities - they're not registered in the world
if (!isPreview() && tick > 5 && entity.worldObj != null
- && entity.worldObj.getEntityByID(entity.getEntityId()) != entity) {
+ && entity.worldObj.getEntityByID(entity.getEntityId()) != entity) {
entity.setDead();
continue;
}
@@ -449,64 +473,163 @@ public abstract class AbilityEnergyProjectile
// ==================== API GETTERS & SETTERS ====================
// Combat data
- @Override public float getDamage() { return combatData.damage; }
- @Override public void setDamage(float damage) { combatData.damage = damage; }
+ @Override
+ public float getDamage() {
+ return combatData.damage;
+ }
- @Override public float getKnockback() { return combatData.knockback; }
- @Override public void setKnockback(float knockback) { combatData.knockback = knockback; }
+ @Override
+ public void setDamage(float damage) {
+ combatData.damage = damage;
+ }
- @Override public float getKnockbackUp() { return combatData.knockbackUp; }
- @Override public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; }
+ @Override
+ public float getKnockback() {
+ return combatData.knockback;
+ }
- @Override public boolean isExplosive() { return combatData.explosive; }
- @Override public void setExplosive(boolean explosive) { combatData.explosive = explosive; }
+ @Override
+ public void setKnockback(float knockback) {
+ combatData.knockback = knockback;
+ }
- @Override public float getExplosionRadius() { return combatData.explosionRadius; }
- @Override public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; }
+ @Override
+ public float getKnockbackUp() {
+ return combatData.knockbackUp;
+ }
- @Override public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
- @Override public void setExplosionDamageFalloff(float falloff) { combatData.explosionDamageFalloff = falloff; }
+ @Override
+ public void setKnockbackUp(float knockbackUp) {
+ combatData.knockbackUp = knockbackUp;
+ }
+
+ @Override
+ public boolean isExplosive() {
+ return combatData.explosive;
+ }
+
+ @Override
+ public void setExplosive(boolean explosive) {
+ combatData.explosive = explosive;
+ }
+
+ @Override
+ public float getExplosionRadius() {
+ return combatData.explosionRadius;
+ }
+
+ @Override
+ public void setExplosionRadius(float explosionRadius) {
+ combatData.explosionRadius = explosionRadius;
+ }
+
+ @Override
+ public float getExplosionDamageFalloff() {
+ return combatData.explosionDamageFalloff;
+ }
+
+ @Override
+ public void setExplosionDamageFalloff(float falloff) {
+ combatData.explosionDamageFalloff = falloff;
+ }
// Lifespan data
- @Override public float getMaxDistance() { return lifespanData.maxDistance; }
- @Override public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
+ @Override
+ public float getMaxDistance() {
+ return lifespanData.maxDistance;
+ }
- @Override public int getMaxLifetime() { return lifespanData.maxLifetime; }
- @Override public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; }
+ @Override
+ public void setMaxDistance(float maxDistance) {
+ lifespanData.maxDistance = maxDistance;
+ }
+
+ @Override
+ public int getMaxLifetime() {
+ return lifespanData.maxLifetime;
+ }
+
+ @Override
+ public void setMaxLifetime(int maxLifetime) {
+ lifespanData.maxLifetime = maxLifetime;
+ }
// Homing data
- public boolean isHoming() { return homingData.homing; }
- public void setHoming(boolean homing) { homingData.homing = homing; }
+ public boolean isHoming() {
+ return homingData.homing;
+ }
- public float getHomingStrength() { return homingData.homingStrength; }
- public void setHomingStrength(float strength) { homingData.homingStrength = strength; }
+ public void setHoming(boolean homing) {
+ homingData.homing = homing;
+ }
- public float getHomingRange() { return homingData.homingRange; }
- public void setHomingRange(float range) { homingData.homingRange = range; }
+ public float getHomingStrength() {
+ return homingData.homingStrength;
+ }
+
+ public void setHomingStrength(float strength) {
+ homingData.homingStrength = strength;
+ }
+
+ public float getHomingRange() {
+ return homingData.homingRange;
+ }
+
+ public void setHomingRange(float range) {
+ homingData.homingRange = range;
+ }
// Display and lightning data inherited from AbilityEnergy
// Anchor - default to projectile 0
- public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
- public void setAnchorPointEnum(AnchorPoint point) { projectiles[0].anchor.anchorPoint = point; }
+ public AnchorPoint getAnchorPointEnum() {
+ return projectiles[0].anchor.anchorPoint;
+ }
- public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
- public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
+ public void setAnchorPointEnum(AnchorPoint point) {
+ projectiles[0].anchor.anchorPoint = point;
+ }
- public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
- public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
+ public float getAnchorOffsetX() {
+ return projectiles[0].anchor.anchorOffsetX;
+ }
- public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
- public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
+ public void setAnchorOffsetX(float x) {
+ projectiles[0].anchor.anchorOffsetX = x;
+ }
- @Override public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
- @Override public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
+ 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 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;
@@ -517,17 +640,32 @@ public abstract class AbilityEnergyProjectile
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; }
+ 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;
+ }
// ==================== GUI FIELD DEFINITIONS ====================
@@ -555,7 +693,7 @@ public abstract class AbilityEnergyProjectile
.visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
- () -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
+ () -> 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)
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java
index c62c886e7..4639901d4 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java
@@ -6,7 +6,11 @@ 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.ability.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
@@ -30,13 +34,14 @@ public class AbilityLaserShot extends AbilityEnergyProjectile implem
@Override
protected EntityAbilityOrb createEntity(EntityLivingBase caster, EntityLivingBase target,
- Vec3 spawnPos, EnergyDisplayData resolved, int index) {
+ Vec3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityAbilityOrb(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
@@ -74,7 +78,7 @@ public class AbilityOrb extends AbilityEnergyProjectile implem
@Override
protected void setupEntityPreview(EntityAbilityOrb orb, EntityLivingBase caster,
- EnergyDisplayData resolved, ProjectileData projData, int index) {
+ EnergyDisplayData resolved, ProjectileData projData, int index) {
orb.setupPreview(caster, orbSize, resolved, lightningData, projData.anchor, windUpTicks);
}
@@ -137,11 +141,21 @@ public class AbilityOrb extends AbilityEnergyProjectile implem
// ==================== TYPE-SPECIFIC GETTERS ====================
- public float getOrbSpeed() { return homingData.speed; }
- public void setOrbSpeed(float speed) { homingData.speed = speed; }
+ public float getOrbSpeed() {
+ return homingData.speed;
+ }
- public float getOrbSize() { return orbSize; }
- public void setOrbSize(float size) { this.orbSize = size; }
+ public void setOrbSpeed(float speed) {
+ homingData.speed = speed;
+ }
+
+ public float getOrbSize() {
+ return orbSize;
+ }
+
+ public void setOrbSize(float size) {
+ this.orbSize = size;
+ }
// ==================== TYPE-SPECIFIC GUI ====================
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java
index 3cb407ef3..5d78e8c1d 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java
@@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.ability.data.EnergyPanelData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
-import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -124,11 +123,21 @@ public class AbilityShield extends AbilityEnergyBarrier {
// ==================== GETTERS & SETTERS ====================
- public float getShieldWidth() { return shieldWidth; }
- public void setShieldWidth(float width) { this.shieldWidth = Math.max(0.5f, width); }
+ public float getShieldWidth() {
+ return shieldWidth;
+ }
- public float getShieldHeight() { return shieldHeight; }
- public void setShieldHeight(float height) { this.shieldHeight = Math.max(0.5f, height); }
+ public void setShieldWidth(float width) {
+ this.shieldWidth = Math.max(0.5f, width);
+ }
+
+ public float getShieldHeight() {
+ return shieldHeight;
+ }
+
+ public void setShieldHeight(float height) {
+ this.shieldHeight = Math.max(0.5f, height);
+ }
// ==================== TYPE-SPECIFIC GUI ====================
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java
index c79822f52..b6e08e83b 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java
@@ -5,7 +5,11 @@ import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
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.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityEnergySlicer;
@@ -33,7 +37,7 @@ public class AbilitySlicer extends AbilityEnergyProjectile {
new EnergyDisplayData(0xFF4444, 0xFF0000, true, 0.2f, 0.6f, 0.0f),
new EnergyCombatData(12.0f, 0.5f, 0.0f, false, 3.0f, 0.5f),
new EnergyHomingData(0.8f, false, 0.0f, 0.0f),
- new EnergyLifespanData(40.0f, 100)
+ new EnergyLifespanData(150.0f, 100)
);
this.typeId = "ability.cnpc.slicer";
this.name = "Slicer";
@@ -53,7 +57,7 @@ public class AbilitySlicer extends AbilityEnergyProjectile {
@Override
protected EntityEnergySlicer createEntity(EntityLivingBase caster, EntityLivingBase target,
- Vec3 spawnPos, EnergyDisplayData resolved, int index) {
+ Vec3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityEnergySlicer(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
@@ -79,7 +83,7 @@ public class AbilitySlicer extends AbilityEnergyProjectile {
@Override
protected void setupEntityPreview(EntityEnergySlicer slicer, EntityLivingBase caster,
- EnergyDisplayData resolved, ProjectileData projData, int index) {
+ EnergyDisplayData resolved, ProjectileData projData, int index) {
slicer.setupPreview(caster, sliceWidth, sliceThickness, resolved, lightningData, projData.anchor, windUpTicks);
}
@@ -130,17 +134,37 @@ public class AbilitySlicer extends AbilityEnergyProjectile {
// ==================== TYPE-SPECIFIC GETTERS ====================
- public float getSlicerSpeed() { return homingData.speed; }
- public void setSlicerSpeed(float speed) { homingData.speed = speed; }
+ public float getSlicerSpeed() {
+ return homingData.speed;
+ }
- public float getSliceWidth() { return sliceWidth; }
- public void setSliceWidth(float width) { this.sliceWidth = Math.max(0.5f, width); }
+ public void setSlicerSpeed(float speed) {
+ homingData.speed = speed;
+ }
- public float getSliceThickness() { return sliceThickness; }
- public void setSliceThickness(float thickness) { this.sliceThickness = Math.max(0.05f, thickness); }
+ public float getSliceWidth() {
+ return sliceWidth;
+ }
- public boolean isPiercing() { return piercing; }
- public void setPiercing(boolean piercing) { this.piercing = piercing; }
+ public void setSliceWidth(float width) {
+ this.sliceWidth = Math.max(0.5f, width);
+ }
+
+ public float getSliceThickness() {
+ return sliceThickness;
+ }
+
+ public void setSliceThickness(float thickness) {
+ this.sliceThickness = Math.max(0.05f, thickness);
+ }
+
+ public boolean isPiercing() {
+ return piercing;
+ }
+
+ public void setPiercing(boolean piercing) {
+ this.piercing = piercing;
+ }
// ==================== TYPE-SPECIFIC GUI ====================
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java
index 67f9643d3..1e0302522 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java
@@ -22,7 +22,7 @@ import java.util.List;
* Sweeper ability: A low sweeping beam that rotates around the NPC.
* Players can jump over it. The beam completes a set number of rotations before ending.
* Telegraph shows a large circle on the ground indicating range.
- *
+ *
* All damage logic is handled by the EntityAbilitySweeper entity to ensure
* visual and damage hitbox alignment.
*/
@@ -173,24 +173,77 @@ public class AbilitySweeper extends AbilityEnergy implements IAbilitySweeper {
}
// Getters & Setters (type-specific only; color/lightning inherited from AbilityEnergy)
- public float getBeamLength() { return beamLength; }
- public void setBeamLength(float beamLength) { this.beamLength = beamLength; }
- public float getBeamWidth() { return beamWidth; }
- public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
- public float getBeamHeight() { return beamHeight; }
- public void setBeamHeight(float beamHeight) { this.beamHeight = beamHeight; }
- public float getDamage() { return damage; }
- public void setDamage(float damage) { this.damage = damage; }
- public int getDamageInterval() { return damageInterval; }
- public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; }
- public boolean isPiercing() { return piercing; }
- public void setPiercing(boolean piercing) { this.piercing = piercing; }
- public float getSweepSpeed() { return sweepSpeed; }
- public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; }
- public int getNumberOfRotations() { return numberOfRotations; }
- public void setNumberOfRotations(int numberOfRotations) { this.numberOfRotations = numberOfRotations; }
- public boolean isLockOnTarget() { return lockOnTarget; }
- public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; }
+ public float getBeamLength() {
+ return beamLength;
+ }
+
+ public void setBeamLength(float beamLength) {
+ this.beamLength = beamLength;
+ }
+
+ public float getBeamWidth() {
+ return beamWidth;
+ }
+
+ public void setBeamWidth(float beamWidth) {
+ this.beamWidth = beamWidth;
+ }
+
+ public float getBeamHeight() {
+ return beamHeight;
+ }
+
+ public void setBeamHeight(float beamHeight) {
+ this.beamHeight = beamHeight;
+ }
+
+ public float getDamage() {
+ return damage;
+ }
+
+ public void setDamage(float damage) {
+ this.damage = damage;
+ }
+
+ public int getDamageInterval() {
+ return damageInterval;
+ }
+
+ public void setDamageInterval(int damageInterval) {
+ this.damageInterval = damageInterval;
+ }
+
+ public boolean isPiercing() {
+ return piercing;
+ }
+
+ public void setPiercing(boolean piercing) {
+ this.piercing = piercing;
+ }
+
+ public float getSweepSpeed() {
+ return sweepSpeed;
+ }
+
+ public void setSweepSpeed(float sweepSpeed) {
+ this.sweepSpeed = sweepSpeed;
+ }
+
+ public int getNumberOfRotations() {
+ return numberOfRotations;
+ }
+
+ public void setNumberOfRotations(int numberOfRotations) {
+ this.numberOfRotations = numberOfRotations;
+ }
+
+ public boolean isLockOnTarget() {
+ return lockOnTarget;
+ }
+
+ public void setLockOnTarget(boolean lockOnTarget) {
+ this.lockOnTarget = lockOnTarget;
+ }
@Override
public int getMaxPreviewDuration() {
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java
index f6bc037a4..60680888f 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java
@@ -11,7 +11,6 @@ import kamkeel.npcs.controllers.data.ability.data.EnergyPanelData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
-import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -139,28 +138,65 @@ public class AbilityWall extends AbilityEnergyBarrier {
// ==================== GETTERS & SETTERS ====================
- public EnergyPanelData getPanelData() { return panelData; }
+ public EnergyPanelData getPanelData() {
+ return panelData;
+ }
- public float getPanelWidth() { return panelData.panelWidth; }
- public void setPanelWidth(float width) { panelData.setPanelWidth(width); }
+ public float getPanelWidth() {
+ return panelData.panelWidth;
+ }
- public float getPanelHeight() { return panelData.panelHeight; }
- public void setPanelHeight(float height) { panelData.setPanelHeight(height); }
+ public void setPanelWidth(float width) {
+ panelData.setPanelWidth(width);
+ }
- public float getHeightOffset() { return panelData.heightOffset; }
- public void setHeightOffset(float offset) { panelData.heightOffset = offset; }
+ public float getPanelHeight() {
+ return panelData.panelHeight;
+ }
- public boolean isLaunching() { return panelData.launching; }
- public void setLaunching(boolean launching) { panelData.launching = launching; }
+ public void setPanelHeight(float height) {
+ panelData.setPanelHeight(height);
+ }
- public float getLaunchSpeed() { return panelData.launchSpeed; }
- public void setLaunchSpeed(float speed) { panelData.setLaunchSpeed(speed); }
+ public float getHeightOffset() {
+ return panelData.heightOffset;
+ }
- public float getLaunchDamage() { return panelData.launchDamage; }
- public void setLaunchDamage(float damage) { panelData.setLaunchDamage(damage); }
+ public void setHeightOffset(float offset) {
+ panelData.heightOffset = offset;
+ }
- public float getLaunchKnockback() { return panelData.launchKnockback; }
- public void setLaunchKnockback(float kb) { panelData.setLaunchKnockback(kb); }
+ public boolean isLaunching() {
+ return panelData.launching;
+ }
+
+ public void setLaunching(boolean launching) {
+ panelData.launching = launching;
+ }
+
+ public float getLaunchSpeed() {
+ return panelData.launchSpeed;
+ }
+
+ public void setLaunchSpeed(float speed) {
+ panelData.setLaunchSpeed(speed);
+ }
+
+ public float getLaunchDamage() {
+ return panelData.launchDamage;
+ }
+
+ public void setLaunchDamage(float damage) {
+ panelData.setLaunchDamage(damage);
+ }
+
+ public float getLaunchKnockback() {
+ return panelData.launchKnockback;
+ }
+
+ public void setLaunchKnockback(float kb) {
+ panelData.setLaunchKnockback(kb);
+ }
// ==================== TYPE-SPECIFIC GUI ====================
diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java
index 4b6b6e18d..87cddc415 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java
@@ -1,9 +1,14 @@
package kamkeel.npcs.entity;
-import kamkeel.npcs.controllers.data.ability.data.*;
+import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
-import noppes.npcs.EventHooks;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
@@ -11,6 +16,7 @@ import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
+import noppes.npcs.EventHooks;
import noppes.npcs.LogWriter;
import java.util.ArrayList;
@@ -19,10 +25,10 @@ import java.util.List;
/**
* Beam projectile - head with trailing path that curves with homing.
* Stays attached to origin point, trail shows path of head.
- *
+ *
* IMPORTANT: Trail points are stored RELATIVE to startX/Y/Z (origin).
* This ensures rendering is stable regardless of entity position interpolation.
- *
+ *
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityBeam extends EntityEnergyProjectile {
@@ -63,6 +69,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
/**
* Full constructor with all parameters using data classes.
+ *
* @param anchoredMode If true, origin follows owner and tail orb is rendered.
* If false, beam is free-moving with trailing length (no tail orb).
*/
@@ -214,7 +221,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
/**
* Start the beam firing (exit charging mode).
* Called by ability when windup ends.
- *
+ *
* For anchored beams: origin follows owner, head starts at charged position
* For non-anchored beams: origin fixed at charged position, head starts there
*/
@@ -281,8 +288,8 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
// Check distance using head offset (distance from origin)
double distFromOrigin = Math.sqrt(
headOffsetX * headOffsetX +
- headOffsetY * headOffsetY +
- headOffsetZ * headOffsetZ
+ headOffsetY * headOffsetY +
+ headOffsetZ * headOffsetZ
);
boolean exceeded = distFromOrigin >= getMaxDistance();
if (exceeded && !worldObj.isRemote && DEBUG_LOGGING) {
@@ -440,8 +447,8 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
Vec3 lastPoint = trailPoints.get(trailPoints.size() - 1);
double dist = Math.sqrt(
(headOffsetX - lastPoint.xCoord) * (headOffsetX - lastPoint.xCoord) +
- (headOffsetY - lastPoint.yCoord) * (headOffsetY - lastPoint.yCoord) +
- (headOffsetZ - lastPoint.zCoord) * (headOffsetZ - lastPoint.zCoord)
+ (headOffsetY - lastPoint.yCoord) * (headOffsetY - lastPoint.yCoord) +
+ (headOffsetZ - lastPoint.zCoord) * (headOffsetZ - lastPoint.zCoord)
);
if (dist >= MIN_POINT_DISTANCE) {
diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java
index 72ebe29ae..2b8a99d07 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java
@@ -1,22 +1,28 @@
package kamkeel.npcs.entity;
-import kamkeel.npcs.controllers.data.ability.data.*;
+import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
-import noppes.npcs.EventHooks;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
+import noppes.npcs.EventHooks;
import java.util.List;
/**
* Disc projectile - flat spinning disc with optional boomerang behavior.
* Has a wider, thinner hitbox compared to Orb.
- *
+ *
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityDisc extends EntityEnergyProjectile {
@@ -252,8 +258,8 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
protected boolean checkMaxDistance() {
double distTraveled = Math.sqrt(
(posX - startX) * (posX - startX) +
- (posY - startY) * (posY - startY) +
- (posZ - startZ) * (posZ - startZ)
+ (posY - startY) * (posY - startY) +
+ (posZ - startZ) * (posZ - startZ)
);
if (boomerang) {
@@ -270,8 +276,8 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
if (owner != null) {
double distToOwner = Math.sqrt(
(posX - owner.posX) * (posX - owner.posX) +
- (posY - owner.posY) * (posY - owner.posY) +
- (posZ - owner.posZ) * (posZ - owner.posZ)
+ (posY - owner.posY) * (posY - owner.posY) +
+ (posZ - owner.posZ) * (posZ - owner.posZ)
);
return distToOwner < 1.5;
}
diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java
index 391d9466c..22880c1ec 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java
@@ -1,15 +1,19 @@
package kamkeel.npcs.entity;
-import kamkeel.npcs.controllers.data.ability.data.*;
+import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
-import noppes.npcs.EventHooks;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
+import noppes.npcs.EventHooks;
import java.util.HashSet;
import java.util.List;
@@ -18,7 +22,7 @@ import java.util.Set;
/**
* Laser projectile - fast expanding thin line that pierces through multiple targets.
* No homing, travels in a straight line from origin to max distance.
- *
+ *
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityLaser extends EntityEnergyProjectile {
@@ -39,6 +43,9 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
// Lock vertical direction after firing (only update yaw, keep pitch fixed)
private boolean lockVerticalDirection = false;
+ // Whether to die on first entity impact (hit-scan mode)
+ private boolean dieOnImpact = false;
+
// Track hit entities to avoid double-damage
private Set hitEntities = new HashSet<>();
@@ -246,8 +253,8 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
// Laser stops at block
currentLength = (float) Math.sqrt(
(blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) +
- (blockHit.hitVec.yCoord - startY) * (blockHit.hitVec.yCoord - startY) +
- (blockHit.hitVec.zCoord - startZ) * (blockHit.hitVec.zCoord - startZ)
+ (blockHit.hitVec.yCoord - startY) * (blockHit.hitVec.yCoord - startY) +
+ (blockHit.hitVec.zCoord - startZ) * (blockHit.hitVec.zCoord - startZ)
);
endX = blockHit.hitVec.xCoord;
endY = blockHit.hitVec.yCoord;
@@ -294,6 +301,20 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
hitEntities.add(entity.getEntityId());
applyDamage(entity);
+ if (dieOnImpact) {
+ // Stop at impact point and begin linger/fade
+ double dx = entity.posX - startX;
+ double dy = (entity.posY + entity.height * 0.5) - startY;
+ double dz = entity.posZ - startZ;
+ float impactDist = (float) Math.sqrt(dx * dx + dy * dy + dz * dz);
+ currentLength = Math.min(impactDist, currentLength);
+ endX = startX + dirX * currentLength;
+ endY = startY + dirY * currentLength;
+ endZ = startZ + dirZ * currentLength;
+ fullyExtended = true;
+ return; // Stop checking further entities
+ }
+
// Piercing - don't stop, continue to next entity
}
}
@@ -382,6 +403,14 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
this.lingerTicks = ticks;
}
+ public boolean isDieOnImpact() {
+ return dieOnImpact;
+ }
+
+ public void setDieOnImpact(boolean dieOnImpact) {
+ this.dieOnImpact = dieOnImpact;
+ }
+
public void setDirection(double x, double y, double z) {
this.dirX = x;
this.dirY = y;
@@ -447,6 +476,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
this.laserWidth = nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f;
this.expansionSpeed = nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f;
this.lingerTicks = nbt.hasKey("LingerTicks") ? nbt.getInteger("LingerTicks") : 10;
+ this.dieOnImpact = nbt.getBoolean("DieOnImpact");
this.dirX = nbt.getDouble("DirX");
this.dirY = nbt.getDouble("DirY");
this.dirZ = nbt.getDouble("DirZ");
@@ -464,6 +494,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
nbt.setFloat("LaserWidth", laserWidth);
nbt.setFloat("ExpansionSpeed", expansionSpeed);
nbt.setInteger("LingerTicks", lingerTicks);
+ nbt.setBoolean("DieOnImpact", dieOnImpact);
nbt.setDouble("DirX", dirX);
nbt.setDouble("DirY", dirY);
nbt.setDouble("DirZ", dirZ);
diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java
index 61cbe7a0b..82447edd2 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java
@@ -1,22 +1,28 @@
package kamkeel.npcs.entity;
-import kamkeel.npcs.controllers.data.ability.data.*;
+import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
-import noppes.npcs.EventHooks;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
+import noppes.npcs.EventHooks;
import java.util.List;
/**
* Orb projectile - spherical homing energy ball.
* Extends EntityEnergyAbility for shared functionality.
- *
+ *
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityOrb extends EntityEnergyProjectile {
diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java
index f8b0aaac1..f215ca6d6 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java
@@ -23,7 +23,7 @@ import java.util.Set;
* Unlike other projectiles, this handles its own damage logic since it's
* attached to and rotates around the caster.
* Extends EntityEnergyAbility for shared visual/owner/charging state.
- *
+ *
* Design: Similar to Beam trail visuals but rotating around origin.
*/
public class EntityAbilitySweeper extends EntityEnergyAbility {
@@ -65,11 +65,11 @@ public class EntityAbilitySweeper extends EntityEnergyAbility {
}
public EntityAbilitySweeper(World world, EntityLivingBase owner, EntityLivingBase target,
- float beamLength, float beamWidth, float beamHeight,
- EnergyDisplayData displayData,
- float sweepSpeed, int numberOfRotations,
- float damage, int damageInterval, boolean piercing,
- boolean lockOnTarget) {
+ float beamLength, float beamWidth, float beamHeight,
+ EnergyDisplayData displayData,
+ float sweepSpeed, int numberOfRotations,
+ float damage, int damageInterval, boolean piercing,
+ boolean lockOnTarget) {
this(world);
this.ownerEntityId = owner.getEntityId();
diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java
index 11eaade79..ac42b3b1d 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java
@@ -34,16 +34,20 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
// ENUMS
// ═══════════════════════════════════════════════════════════════════
- public enum ZoneType { TRAP, HAZARD }
+ 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();
+ case CIRCLE:
+ return "ability.shape.circle";
+ case SQUARE:
+ return "ability.shape.square";
+ default:
+ return name();
}
}
}
@@ -54,10 +58,14 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
@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();
+ case STATIC:
+ return "ability.accentStyle.static";
+ case SWAYING:
+ return "ability.accentStyle.swaying";
+ case FLICKERING:
+ return "ability.accentStyle.flickering";
+ default:
+ return name();
}
}
}
@@ -68,10 +76,14 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
@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();
+ case RISING:
+ return "ability.particleMotion.rising";
+ case DRIFTING:
+ return "ability.particleMotion.drifting";
+ case SPARKS:
+ return "ability.particleMotion.sparks";
+ default:
+ return name();
}
}
}
@@ -174,7 +186,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
}
private EntityAbilityZone(World world, ZoneType type, EntityLivingBase owner,
- double x, double y, double z) {
+ double x, double y, double z) {
this(world);
this.zoneType = type;
this.ownerEntityId = owner.getEntityId();
@@ -184,17 +196,17 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
}
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,
- boolean visible,
- List effects) {
+ 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,
+ boolean visible,
+ List effects) {
EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.TRAP, owner, x, y, z);
zone.shape = shape;
zone.radius = triggerRadius;
@@ -226,17 +238,17 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
}
public static EntityAbilityZone createHazard(World world, EntityLivingBase owner,
- double x, double y, double z,
- ZoneShape shape,
- float radius,
- float damagePerSecond, int damageInterval,
- boolean ignoreInvulnFrames, boolean affectsCaster,
- int durationTicks,
- int innerColor, int outerColor, boolean outerColorEnabled,
- float zoneHeight,
- float particleDensity, float particleScale,
- float animSpeed, float lightningDensity,
- List effects) {
+ double x, double y, double z,
+ ZoneShape shape,
+ float radius,
+ float damagePerSecond, int damageInterval,
+ boolean ignoreInvulnFrames, boolean affectsCaster,
+ int durationTicks,
+ int innerColor, int outerColor, boolean outerColorEnabled,
+ float zoneHeight,
+ float particleDensity, float particleScale,
+ float animSpeed, float lightningDensity,
+ List effects) {
EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.HAZARD, owner, x, y, z);
zone.shape = shape;
zone.radius = radius;
@@ -268,7 +280,8 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
// ═══════════════════════════════════════════════════════════════════
@Override
- protected void entityInit() {}
+ protected void entityInit() {
+ }
@Override
public void handleHealthUpdate(byte id) {
@@ -549,45 +562,133 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
// 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 boolean isVisible() { return visible; }
- 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; }
+ public ZoneType getZoneType() {
+ return zoneType;
+ }
- 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; }
+ 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 boolean isVisible() {
+ return visible;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
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) {
+ 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;
diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java
index c98fae96a..9721fb60e 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java
@@ -30,7 +30,9 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit
protected int ownerEntityId = -1;
// ==================== STATE ====================
- /** The ability that spawned this entity. Transient, not saved to NBT. */
+ /**
+ * The ability that spawned this entity. Transient, not saved to NBT.
+ */
protected transient Ability sourceAbility = null;
protected boolean previewMode = false;
protected EntityLivingBase previewOwner = null;
@@ -98,59 +100,137 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit
return worldObj.getEntityByID(ownerEntityId);
}
- public int getOwnerEntityId() { return ownerEntityId; }
- public void setOwnerEntityId(int id) { this.ownerEntityId = id; }
+ public int getOwnerEntityId() {
+ return ownerEntityId;
+ }
+
+ public void setOwnerEntityId(int id) {
+ this.ownerEntityId = id;
+ }
// ==================== SOURCE ABILITY ====================
- public Ability getSourceAbility() { return sourceAbility; }
- public void setSourceAbility(Ability ability) { this.sourceAbility = ability; }
+ public Ability getSourceAbility() {
+ return sourceAbility;
+ }
+
+ public void setSourceAbility(Ability ability) {
+ this.sourceAbility = ability;
+ }
// ==================== PREVIEW MODE ====================
- public boolean isPreviewMode() { return previewMode; }
- public void setPreviewMode(boolean preview) { this.previewMode = preview; }
- public void setPreviewOwner(EntityLivingBase owner) { this.previewOwner = owner; }
+ public boolean isPreviewMode() {
+ return previewMode;
+ }
+
+ public void setPreviewMode(boolean preview) {
+ this.previewMode = preview;
+ }
+
+ public void setPreviewOwner(EntityLivingBase owner) {
+ this.previewOwner = owner;
+ }
// ==================== DISPLAY GETTERS & SETTERS ====================
- public EnergyDisplayData getDisplayData() { return displayData; }
- public EnergyLightningData getLightningData() { return lightningData; }
+ public EnergyDisplayData getDisplayData() {
+ return displayData;
+ }
- public int getInnerColor() { return displayData.getInnerColor(); }
- public void setInnerColor(int color) { displayData.setInnerColor(color); }
+ public EnergyLightningData getLightningData() {
+ return lightningData;
+ }
- public float getInnerAlpha() { return displayData.getInnerAlpha(); }
- public void setInnerAlpha(float alpha) { displayData.setInnerAlpha(alpha); }
+ public int getInnerColor() {
+ return displayData.getInnerColor();
+ }
- public int getOuterColor() { return displayData.getOuterColor(); }
- public void setOuterColor(int color) { displayData.setOuterColor(color); }
+ public void setInnerColor(int color) {
+ displayData.setInnerColor(color);
+ }
- public boolean isOuterColorEnabled() { return displayData.isOuterColorEnabled(); }
- public void setOuterColorEnabled(boolean enabled) { displayData.setOuterColorEnabled(enabled); }
+ public float getInnerAlpha() {
+ return displayData.getInnerAlpha();
+ }
- public float getOuterColorWidth() { return displayData.getOuterColorWidth(); }
- public void setOuterColorWidth(float width) { displayData.setOuterColorWidth(width); }
+ public void setInnerAlpha(float alpha) {
+ displayData.setInnerAlpha(alpha);
+ }
- public float getOuterColorAlpha() { return displayData.getOuterColorAlpha(); }
- public void setOuterColorAlpha(float alpha) { displayData.setOuterColorAlpha(alpha); }
+ public int getOuterColor() {
+ return displayData.getOuterColor();
+ }
- public float getRotationSpeed() { return displayData.getRotationSpeed(); }
- public void setRotationSpeed(float speed) { displayData.setRotationSpeed(speed); }
+ public void setOuterColor(int color) {
+ displayData.setOuterColor(color);
+ }
+
+ public boolean isOuterColorEnabled() {
+ return displayData.isOuterColorEnabled();
+ }
+
+ public void setOuterColorEnabled(boolean enabled) {
+ displayData.setOuterColorEnabled(enabled);
+ }
+
+ public float getOuterColorWidth() {
+ return displayData.getOuterColorWidth();
+ }
+
+ public void setOuterColorWidth(float width) {
+ displayData.setOuterColorWidth(width);
+ }
+
+ public float getOuterColorAlpha() {
+ return displayData.getOuterColorAlpha();
+ }
+
+ public void setOuterColorAlpha(float alpha) {
+ displayData.setOuterColorAlpha(alpha);
+ }
+
+ public float getRotationSpeed() {
+ return displayData.getRotationSpeed();
+ }
+
+ public void setRotationSpeed(float speed) {
+ displayData.setRotationSpeed(speed);
+ }
// ==================== LIGHTNING GETTERS & SETTERS ====================
- public boolean hasLightningEffect() { return lightningData.isLightningEffect(); }
- public void setLightningEffect(boolean enabled) { lightningData.setLightningEffect(enabled); }
+ public boolean hasLightningEffect() {
+ return lightningData.isLightningEffect();
+ }
- public float getLightningDensity() { return lightningData.getLightningDensity(); }
- public void setLightningDensity(float density) { lightningData.setLightningDensity(density); }
+ public void setLightningEffect(boolean enabled) {
+ lightningData.setLightningEffect(enabled);
+ }
- public float getLightningRadius() { return lightningData.getLightningRadius(); }
- public void setLightningRadius(float radius) { lightningData.setLightningRadius(radius); }
+ public float getLightningDensity() {
+ return lightningData.getLightningDensity();
+ }
- public int getLightningFadeTime() { return lightningData.getLightningFadeTime(); }
- public void setLightningFadeTime(int ticks) { lightningData.setLightningFadeTime(ticks); }
+ public void setLightningDensity(float density) {
+ lightningData.setLightningDensity(density);
+ }
+
+ public float getLightningRadius() {
+ return lightningData.getLightningRadius();
+ }
+
+ public void setLightningRadius(float radius) {
+ lightningData.setLightningRadius(radius);
+ }
+
+ public int getLightningFadeTime() {
+ return lightningData.getLightningFadeTime();
+ }
+
+ public void setLightningFadeTime(int ticks) {
+ lightningData.setLightningFadeTime(ticks);
+ }
// ==================== BRIGHTNESS ====================
diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java
index c295f586d..7ff4b04c2 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java
@@ -1,10 +1,6 @@
package kamkeel.npcs.entity;
-import cpw.mods.fml.relauncher.Side;
-import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.data.EnergyBarrierData;
-import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
-import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
@@ -144,6 +140,7 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
* Shared barrier tick logic for onUpdate(). Handles spawned event, owner death,
* duration check, hit flash decay, and tick event.
* Call this after incrementing ticksAlive in subclass onUpdate().
+ *
* @return true if barrier died this tick (caller should return early)
*/
protected boolean updateBarrierTick() {
@@ -223,18 +220,33 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
protected boolean isKnockbackTarget(EntityLivingBase entity) {
switch (barrierData.knockbackTarget) {
- case 1: return entity instanceof EntityPlayer;
- case 2: return entity instanceof EntityNPCInterface;
- default: return entity instanceof EntityPlayer || entity instanceof EntityNPCInterface;
+ case 1:
+ return entity instanceof EntityPlayer;
+ case 2:
+ return entity instanceof EntityNPCInterface;
+ default:
+ return entity instanceof EntityPlayer || entity instanceof EntityNPCInterface;
}
}
// ==================== GETTERS ====================
- public EnergyBarrierData getBarrierData() { return barrierData; }
- public float getCurrentHealth() { return currentHealth; }
- public void setCurrentHealth(float health) { this.currentHealth = health; syncHealthPercent(); }
- public int getTicksAlive() { return ticksAlive; }
+ public EnergyBarrierData getBarrierData() {
+ return barrierData;
+ }
+
+ public float getCurrentHealth() {
+ return currentHealth;
+ }
+
+ public void setCurrentHealth(float health) {
+ this.currentHealth = health;
+ syncHealthPercent();
+ }
+
+ public int getTicksAlive() {
+ return ticksAlive;
+ }
// ==================== NBT HELPERS ====================
diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java
index d0a2a8a96..6ba436914 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java
@@ -195,10 +195,21 @@ public class EntityEnergyDome extends EntityEnergyBarrier {
// ==================== GETTERS ====================
- public float getDomeRadius() { return domeRadius; }
- public void setDomeRadius(float radius) { this.domeRadius = Math.max(0.1f, radius); }
- public boolean isFollowCaster() { return followCaster; }
- public void setFollowCaster(boolean follow) { this.followCaster = follow; }
+ public float getDomeRadius() {
+ return domeRadius;
+ }
+
+ public void setDomeRadius(float radius) {
+ this.domeRadius = Math.max(0.1f, radius);
+ }
+
+ public boolean isFollowCaster() {
+ return followCaster;
+ }
+
+ public void setFollowCaster(boolean follow) {
+ this.followCaster = follow;
+ }
// ==================== NBT ====================
diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java
index 909e0dda8..98145836e 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java
@@ -343,9 +343,17 @@ public class EntityEnergyPanel extends EntityEnergyBarrier {
// ==================== GETTERS ====================
- public PanelMode getMode() { return mode; }
- public float getPanelYaw() { return panelYaw; }
- public EnergyPanelData getPanelData() { return panelData; }
+ public PanelMode getMode() {
+ return mode;
+ }
+
+ public float getPanelYaw() {
+ return panelYaw;
+ }
+
+ public EnergyPanelData getPanelData() {
+ return panelData;
+ }
// ==================== NBT ====================
diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java
index 8f58fada7..b65439f83 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java
@@ -2,10 +2,16 @@ package kamkeel.npcs.entity;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
-import kamkeel.npcs.controllers.data.ability.data.*;
+import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
@@ -37,7 +43,7 @@ import java.util.Map;
* Base class for all ability projectiles (Orb, Disc, Beam, Laser, Slicer).
* Provides common functionality for combat, effects, homing, and interpolation.
* Extends EntityEnergyAbility for shared visual/owner/charging state.
- *
+ *
* Design inspired by LouisXIV's energy attack system.
*/
public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
@@ -56,6 +62,10 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
refs.add(new WeakReference<>(projectile));
}
+ public static void clearAllProjectiles() {
+ activeProjectiles.clear();
+ }
+
public static List getActiveProjectiles(int ownerEntityId) {
List> refs = activeProjectiles.get(ownerEntityId);
if (refs == null) return Collections.emptyList();
@@ -270,13 +280,14 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
/**
* Check if projectile has exceeded max distance.
+ *
* @return true if should die
*/
protected boolean checkMaxDistance() {
double distTraveled = Math.sqrt(
(posX - startX) * (posX - startX) +
- (posY - startY) * (posY - startY) +
- (posZ - startZ) * (posZ - startZ)
+ (posY - startY) * (posY - startY) +
+ (posZ - startZ) * (posZ - startZ)
);
return distTraveled >= getMaxDistance();
}
@@ -291,6 +302,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
/**
* Check for collision with energy barrier entities.
* Barriers only block incoming projectiles from enemies.
+ *
* @return true if projectile was absorbed (caller should stop processing)
*/
protected boolean checkBarrierCollision() {
@@ -451,12 +463,13 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
double dist = Math.sqrt(
Math.pow(target.posX - posX, 2) +
- Math.pow(target.posY - posY, 2) +
- Math.pow(target.posZ - posZ, 2)
+ Math.pow(target.posY - posY, 2) +
+ Math.pow(target.posZ - posZ, 2)
);
- if (dist <= getExplosionRadius()) {
- float falloff = 1.0f - (float) (dist / getExplosionRadius()) * getExplosionDamageFalloff();
+ float radius = getExplosionRadius();
+ if (radius > 0 && dist <= radius) {
+ float falloff = 1.0f - (float) (dist / radius) * getExplosionDamageFalloff();
applyDamage(target, getDamage() * falloff, getKnockback() * falloff);
}
}
@@ -513,7 +526,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
* Update during charging state - grow size and follow anchor.
* Default implementation grows {@code size} from 0 to {@code targetSize}.
* Override in subclasses for type-specific charging (Disc, Beam).
- *
+ *
* Note: Owner death checks are handled by onUpdate() before this is called.
*/
protected void updateCharging() {
@@ -575,7 +588,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
* Call from subclass constructors after initProjectile().
*/
protected void calculateInitialVelocity(EntityLivingBase owner, EntityLivingBase target,
- double x, double y, double z) {
+ double x, double y, double z) {
if (target != null) {
double dx = target.posX - x;
double dy = (target.posY + target.getEyeHeight()) - y;
@@ -696,7 +709,10 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// ==================== VISUAL GETTERS & SETTERS ====================
- public float getSize() { return size; }
+ public float getSize() {
+ return size;
+ }
+
public void setProjectileSize(float size) {
this.size = size;
this.renderCurrentSize = size;
@@ -733,64 +749,147 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// ==================== LIFESPAN GETTERS & SETTERS ====================
- public float getMaxDistance() { return lifespanData.getMaxDistance(); }
- public void setMaxDistance(float distance) { lifespanData.setMaxDistance(distance); }
+ public float getMaxDistance() {
+ return lifespanData.getMaxDistance();
+ }
- public int getMaxLifetime() { return lifespanData.getMaxLifetime(); }
- public void setMaxLifetime(int ticks) { lifespanData.setMaxLifetime(ticks); }
+ public void setMaxDistance(float distance) {
+ lifespanData.setMaxDistance(distance);
+ }
+
+ public int getMaxLifetime() {
+ return lifespanData.getMaxLifetime();
+ }
+
+ public void setMaxLifetime(int ticks) {
+ lifespanData.setMaxLifetime(ticks);
+ }
// ==================== COMBAT GETTERS & SETTERS ====================
- public float getDamage() { return combatData.getDamage(); }
- public void setCombatDamage(float damage) { combatData.setDamage(damage); }
+ public float getDamage() {
+ return combatData.getDamage();
+ }
- public float getKnockback() { return combatData.knockback; }
- public void setCombatKnockback(float knockback) { combatData.setKnockback(knockback); }
+ public void setCombatDamage(float damage) {
+ combatData.setDamage(damage);
+ }
- public float getKnockbackUp() { return combatData.knockbackUp; }
- public void setCombatKnockbackUp(float knockbackUp) { combatData.setKnockbackUp(knockbackUp); }
+ public float getKnockback() {
+ return combatData.knockback;
+ }
- public boolean isExplosive() { return combatData.isExplosive(); }
- public void setExplosive(boolean explosive) { combatData.setExplosive(explosive); }
+ public void setCombatKnockback(float knockback) {
+ combatData.setKnockback(knockback);
+ }
- public float getExplosionRadius() { return combatData.explosionRadius; }
- public void setExplosionRadius(float radius) { combatData.setExplosionRadius(radius); }
+ public float getKnockbackUp() {
+ return combatData.knockbackUp;
+ }
- public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
- public void setExplosionDamageFalloff(float falloff) { combatData.setExplosionDamageFalloff(falloff); }
+ public void setCombatKnockbackUp(float knockbackUp) {
+ combatData.setKnockbackUp(knockbackUp);
+ }
+
+ public boolean isExplosive() {
+ return combatData.isExplosive();
+ }
+
+ public void setExplosive(boolean explosive) {
+ combatData.setExplosive(explosive);
+ }
+
+ public float getExplosionRadius() {
+ return combatData.explosionRadius;
+ }
+
+ public void setExplosionRadius(float radius) {
+ combatData.setExplosionRadius(radius);
+ }
+
+ public float getExplosionDamageFalloff() {
+ return combatData.explosionDamageFalloff;
+ }
+
+ public void setExplosionDamageFalloff(float falloff) {
+ combatData.setExplosionDamageFalloff(falloff);
+ }
// ==================== HOMING GETTERS & SETTERS ====================
- public boolean isHoming() { return homingData.isHoming(); }
- public void setHomingEnabled(boolean homing) { homingData.setHoming(homing); }
+ public boolean isHoming() {
+ return homingData.isHoming();
+ }
- public float getHomingStrength() { return homingData.getHomingStrength(); }
- public void setHomingStrength(float strength) { homingData.setHomingStrength(strength); }
+ public void setHomingEnabled(boolean homing) {
+ homingData.setHoming(homing);
+ }
- public float getHomingRange() { return homingData.getHomingRange(); }
- public void setHomingRange(float range) { homingData.setHomingRange(range); }
+ public float getHomingStrength() {
+ return homingData.getHomingStrength();
+ }
+
+ public void setHomingStrength(float strength) {
+ homingData.setHomingStrength(strength);
+ }
+
+ public float getHomingRange() {
+ return homingData.getHomingRange();
+ }
+
+ public void setHomingRange(float range) {
+ homingData.setHomingRange(range);
+ }
// ==================== SPEED & ANCHOR GETTERS & SETTERS ====================
- public float getSpeed() { return homingData.getSpeed(); }
- public void setSpeed(float speed) { homingData.setSpeed(speed); }
+ public float getSpeed() {
+ return homingData.getSpeed();
+ }
- public AnchorPoint getAnchorPoint() { return anchorData.getAnchorPoint(); }
+ public void setSpeed(float speed) {
+ homingData.setSpeed(speed);
+ }
- public int getAnchor() { return anchorData.getAnchor(); }
- public float getAnchorOffsetX() { return anchorData.getAnchorOffsetX(); }
- public float getAnchorOffsetY() { return anchorData.getAnchorOffsetY(); }
- public float getAnchorOffsetZ() { return anchorData.getAnchorOffsetZ(); }
+ public AnchorPoint getAnchorPoint() {
+ return anchorData.getAnchorPoint();
+ }
+
+ public int getAnchor() {
+ return anchorData.getAnchor();
+ }
+
+ public float getAnchorOffsetX() {
+ return anchorData.getAnchorOffsetX();
+ }
+
+ public float getAnchorOffsetY() {
+ return anchorData.getAnchorOffsetY();
+ }
+
+ public float getAnchorOffsetZ() {
+ return anchorData.getAnchorOffsetZ();
+ }
// ==================== MOVEMENT GETTERS ====================
- public double getStartX() { return startX; }
- public double getStartY() { return startY; }
- public double getStartZ() { return startZ; }
+ public double getStartX() {
+ return startX;
+ }
+
+ public double getStartY() {
+ return startY;
+ }
+
+ public double getStartZ() {
+ return startZ;
+ }
// ==================== STATE GETTERS ====================
- public boolean getHasHit() { return hasHit; }
+ public boolean getHasHit() {
+ return hasHit;
+ }
// ==================== NBT ====================
diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java
index c76fc7bad..34fc986e6 100644
--- a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java
+++ b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java
@@ -1,6 +1,12 @@
package kamkeel.npcs.entity;
-import kamkeel.npcs.controllers.data.ability.data.*;
+import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
+import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
@@ -293,14 +299,29 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
// ==================== GETTERS ====================
- public float getSliceWidth() { return sliceWidth; }
- public void setSliceWidth(float width) { this.sliceWidth = width; }
+ public float getSliceWidth() {
+ return sliceWidth;
+ }
- public float getSliceThickness() { return sliceThickness; }
- public void setSliceThickness(float thickness) { this.sliceThickness = thickness; }
+ public void setSliceWidth(float width) {
+ this.sliceWidth = width;
+ }
- public boolean isPiercing() { return piercing; }
- public void setPiercing(boolean piercing) { this.piercing = piercing; }
+ public float getSliceThickness() {
+ return sliceThickness;
+ }
+
+ public void setSliceThickness(float thickness) {
+ this.sliceThickness = thickness;
+ }
+
+ public boolean isPiercing() {
+ return piercing;
+ }
+
+ public void setPiercing(boolean piercing) {
+ this.piercing = piercing;
+ }
// ==================== NBT ====================
diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java
index dee4a3a94..c3c555d48 100644
--- a/src/main/java/kamkeel/npcs/network/PacketHandler.java
+++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java
@@ -28,8 +28,6 @@ import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket;
import kamkeel.npcs.network.packets.data.VillagerListPacket;
import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket;
import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket;
-import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
-import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket;
import kamkeel.npcs.network.packets.data.gui.GuiClosePacket;
import kamkeel.npcs.network.packets.data.gui.GuiErrorPacket;
import kamkeel.npcs.network.packets.data.gui.GuiOpenBookPacket;
@@ -53,6 +51,9 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket;
import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket;
import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket;
import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket;
+import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
+import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket;
+import kamkeel.npcs.network.packets.player.AuctionActionPacket;
import kamkeel.npcs.network.packets.player.BankActionPacket;
import kamkeel.npcs.network.packets.player.CheckPlayerValue;
import kamkeel.npcs.network.packets.player.CompanionActionPacket;
@@ -61,7 +62,6 @@ import kamkeel.npcs.network.packets.player.FollowerPacket;
import kamkeel.npcs.network.packets.player.GetNPCRole;
import kamkeel.npcs.network.packets.player.GetTraderData;
import kamkeel.npcs.network.packets.player.InputDevicePacket;
-import kamkeel.npcs.network.packets.player.AuctionActionPacket;
import kamkeel.npcs.network.packets.player.MailActionPacket;
import kamkeel.npcs.network.packets.player.SaveBookPacket;
import kamkeel.npcs.network.packets.player.SaveSignPacket;
@@ -94,10 +94,14 @@ import kamkeel.npcs.network.packets.request.TraderMarketSavePacket;
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.BuiltInAbilityGetPacket;
+import kamkeel.npcs.network.packets.request.ability.ChainedAbilitiesGetPacket;
+import kamkeel.npcs.network.packets.request.ability.ChainedAbilityGetPacket;
+import kamkeel.npcs.network.packets.request.ability.ChainedAbilityRemovePacket;
+import kamkeel.npcs.network.packets.request.ability.ChainedAbilitySavePacket;
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;
@@ -112,8 +116,8 @@ import kamkeel.npcs.network.packets.request.clone.CloneAllTagsPacket;
import kamkeel.npcs.network.packets.request.clone.CloneAllTagsShortPacket;
import kamkeel.npcs.network.packets.request.clone.CloneFolderCrudPacket;
import kamkeel.npcs.network.packets.request.clone.CloneFolderListPacket;
-import kamkeel.npcs.network.packets.request.clone.CloneMovePacket;
import kamkeel.npcs.network.packets.request.clone.CloneListPacket;
+import kamkeel.npcs.network.packets.request.clone.CloneMovePacket;
import kamkeel.npcs.network.packets.request.clone.ClonePreSavePacket;
import kamkeel.npcs.network.packets.request.clone.CloneRemovePacket;
import kamkeel.npcs.network.packets.request.clone.CloneSavePacket;
@@ -519,6 +523,10 @@ public class PacketHandler {
REQUEST_PACKET.registerPacket(new CustomAbilityRemovePacket());
REQUEST_PACKET.registerPacket(new CustomAbilitySavePacket());
REQUEST_PACKET.registerPacket(new BuiltInAbilityGetPacket());
+ REQUEST_PACKET.registerPacket(new ChainedAbilitiesGetPacket());
+ REQUEST_PACKET.registerPacket(new ChainedAbilityGetPacket());
+ REQUEST_PACKET.registerPacket(new ChainedAbilitySavePacket());
+ REQUEST_PACKET.registerPacket(new ChainedAbilityRemovePacket());
// Other Packets
REQUEST_PACKET.registerPacket(new IsGuiOpenInform());
diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java
index 25ba2bba6..e0f3451a1 100644
--- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java
+++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java
@@ -206,6 +206,10 @@ public enum EnumRequestPacket {
CustomAbilityRemove,
CustomAbilitySave,
BuiltInAbilityGet,
+ ChainedAbilitiesGet,
+ ChainedAbilityGet,
+ ChainedAbilitySave,
+ ChainedAbilityRemove,
Hammer,
RecipeScript,
diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java
index 1ac0ec828..ee6cf352f 100644
--- a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java
+++ b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java
@@ -13,5 +13,6 @@ public enum EnumSyncType {
CARPENTRY_RECIPES,
ANVIL_RECIPES,
CUSTOM_EFFECTS,
- CUSTOM_ABILITY
+ CUSTOM_ABILITY,
+ CHAINED_ABILITY
}
diff --git a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java
index 75e0c40ae..da9eb9aa2 100644
--- a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java
@@ -15,8 +15,8 @@ import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.AuctionClientConfig;
import noppes.npcs.client.ClientCacheHandler;
-import noppes.npcs.config.ConfigMain;
import noppes.npcs.client.ScriptClientConfig;
+import noppes.npcs.config.ConfigMain;
import noppes.npcs.controllers.AuctionConfigSync;
import noppes.npcs.controllers.ScriptConfigSync;
diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java
index 03c0b82b4..6de87cb22 100644
--- a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java
@@ -16,7 +16,7 @@ import java.io.IOException;
/**
* Lightweight packet syncing ability lock state from server to client.
* Sent every tick during ability execution when flags change, and once when ability ends.
- *
+ *
* Flags byte layout:
* bit 0: movement locked (suppress WASD, zero motion)
* bit 1: rotation locked (freeze yaw/pitch)
diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java
index 3ce74eb4c..5eed2aa93 100644
--- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java
@@ -17,6 +17,7 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import noppes.npcs.NoppesUtilServer;
import noppes.npcs.config.ConfigMarket;
+import noppes.npcs.constants.EnumAuctionPage;
import noppes.npcs.constants.EnumGuiType;
import noppes.npcs.constants.EnumRoleType;
import noppes.npcs.controllers.AuctionController;
@@ -39,7 +40,8 @@ public class AuctionActionPacket extends AbstractPacket {
private long amount;
private NBTTagCompound data;
- public AuctionActionPacket() {}
+ public AuctionActionPacket() {
+ }
private AuctionActionPacket(Action action) {
this.action = action;
@@ -57,7 +59,9 @@ public class AuctionActionPacket extends AbstractPacket {
}
@Override
- public boolean needsNPC() { return false; } // Allow opening via command without NPC
+ public boolean needsNPC() {
+ return false;
+ } // Allow opening via command without NPC
// =========================================
// Client-side static methods
@@ -116,7 +120,9 @@ public class AuctionActionPacket extends AbstractPacket {
PacketClient.sendClient(packet);
}
- /** Request listings from server with filter and page */
+ /**
+ * Request listings from server with filter and page
+ */
@SideOnly(Side.CLIENT)
public static void requestListings(AuctionFilter filter, int page) {
AuctionActionPacket packet = new AuctionActionPacket(Action.RequestListings);
@@ -130,10 +136,14 @@ public class AuctionActionPacket extends AbstractPacket {
// =========================================
@Override
- public Enum getType() { return EnumPlayerPacket.AuctionAction; }
+ public Enum getType() {
+ return EnumPlayerPacket.AuctionAction;
+ }
@Override
- public PacketChannel getChannel() { return PacketHandler.PLAYER_PACKET; }
+ public PacketChannel getChannel() {
+ return PacketHandler.PLAYER_PACKET;
+ }
@Override
@SideOnly(Side.CLIENT)
@@ -290,7 +300,10 @@ public class AuctionActionPacket extends AbstractPacket {
long startingPrice = in.readLong();
String buyoutStr = ByteBufUtils.readString(in);
long buyoutPrice = 0;
- try { buyoutPrice = Long.parseLong(buyoutStr); } catch (NumberFormatException ignored) {}
+ try {
+ buyoutPrice = Long.parseLong(buyoutStr);
+ } catch (NumberFormatException ignored) {
+ }
NBTTagCompound itemNBT = ByteBufUtils.readNBT(in);
if (itemNBT == null) return "No item data received.";
@@ -315,13 +328,8 @@ public class AuctionActionPacket extends AbstractPacket {
private void handleOpenPage(ByteBuf in, EntityPlayerMP player) {
int page = in.readInt();
- EnumGuiType guiType;
- switch (page) {
- case 0: guiType = EnumGuiType.PlayerAuction; break;
- case 1: guiType = EnumGuiType.PlayerAuctionSell; break;
- case 2: guiType = EnumGuiType.PlayerAuctionTrades; break;
- default: guiType = EnumGuiType.PlayerAuction; break;
- }
+ EnumAuctionPage auctionPage = EnumAuctionPage.fromOrdinal(page);
+ EnumGuiType guiType = auctionPage.getGuiType();
NoppesUtilServer.sendOpenGui(player, guiType, npc);
// Send trades data when opening the trades page
@@ -333,7 +341,9 @@ public class AuctionActionPacket extends AbstractPacket {
}
}
- /** Send full trades data to player (listings, bids, claims) */
+ /**
+ * Send full trades data to player (listings, bids, claims)
+ */
private void sendTradesData(EntityPlayerMP player, AuctionController controller) {
NBTTagCompound response = new NBTTagCompound();
response.setBoolean("TradesData", true);
@@ -481,6 +491,9 @@ public class AuctionActionPacket extends AbstractPacket {
} else {
// Need only part of stack
stack.splitStack(needed);
+ if (stack.stackSize <= 0) {
+ player.inventory.setInventorySlotContents(i, null);
+ }
needed = 0;
}
}
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java
index 6e565f3de..67a8f8241 100644
--- a/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java
@@ -3,8 +3,8 @@ 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.AbilityController;
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;
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitiesGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitiesGetPacket.java
new file mode 100644
index 000000000..97ccfa52d
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitiesGetPacket.java
@@ -0,0 +1,51 @@
+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.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 net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.EntityPlayerMP;
+import noppes.npcs.NoppesUtilServer;
+
+import java.io.IOException;
+
+/**
+ * Request packet to get list of all chained ability names.
+ */
+public final class ChainedAbilitiesGetPacket extends AbstractPacket {
+ public static String packetName = "Request|ChainedAbilitiesGet";
+
+ public ChainedAbilitiesGetPacket() {
+ }
+
+ @Override
+ public Enum getType() {
+ return EnumRequestPacket.ChainedAbilitiesGet;
+ }
+
+ @Override
+ public PacketChannel getChannel() {
+ return PacketHandler.REQUEST_PACKET;
+ }
+
+ @SideOnly(Side.CLIENT)
+ @Override
+ public void sendData(ByteBuf out) throws IOException {
+ }
+
+ @Override
+ public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
+ if (!(player instanceof EntityPlayerMP))
+ return;
+ if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player))
+ return;
+
+ NoppesUtilServer.sendChainedAbilitiesData((EntityPlayerMP) player);
+ }
+}
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java
new file mode 100644
index 000000000..da8ca9d9f
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java
@@ -0,0 +1,68 @@
+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.AbilityController;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
+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 chained ability by name.
+ */
+public final class ChainedAbilityGetPacket extends AbstractPacket {
+ public static String packetName = "Request|ChainedAbilityGet";
+
+ private String name;
+
+ public ChainedAbilityGetPacket() {
+ }
+
+ public ChainedAbilityGetPacket(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Enum getType() {
+ return EnumRequestPacket.ChainedAbilityGet;
+ }
+
+ @Override
+ public PacketChannel getChannel() {
+ return PacketHandler.REQUEST_PACKET;
+ }
+
+ @SideOnly(Side.CLIENT)
+ @Override
+ public void sendData(ByteBuf out) throws IOException {
+ ByteBufUtils.writeString(out, name);
+ }
+
+ @Override
+ public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
+ if (!(player instanceof EntityPlayerMP))
+ return;
+
+ if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player))
+ return;
+
+ String chainName = ByteBufUtils.readString(in);
+ ChainedAbility chain = AbilityController.Instance.getChainedAbility(chainName);
+ if (chain != null) {
+ NBTTagCompound compound = chain.writeNBT();
+ GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
+ }
+ }
+}
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityRemovePacket.java
new file mode 100644
index 000000000..b373d6331
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityRemovePacket.java
@@ -0,0 +1,69 @@
+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.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.util.ByteBufUtils;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.entity.player.EntityPlayerMP;
+import noppes.npcs.CustomNpcsPermissions;
+import noppes.npcs.NoppesUtilServer;
+
+import java.io.IOException;
+
+/**
+ * Request packet to remove a chained ability by name.
+ */
+public final class ChainedAbilityRemovePacket extends AbstractPacket {
+ public static String packetName = "Request|ChainedAbilityRemove";
+
+ private String name;
+
+ public ChainedAbilityRemovePacket() {
+ }
+
+ public ChainedAbilityRemovePacket(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Enum getType() {
+ return EnumRequestPacket.ChainedAbilityRemove;
+ }
+
+ @Override
+ public PacketChannel getChannel() {
+ return PacketHandler.REQUEST_PACKET;
+ }
+
+ @Override
+ public CustomNpcsPermissions.Permission getPermission() {
+ return CustomNpcsPermissions.GLOBAL_ABILITY;
+ }
+
+ @SideOnly(Side.CLIENT)
+ @Override
+ public void sendData(ByteBuf out) throws IOException {
+ ByteBufUtils.writeString(out, name);
+ }
+
+ @Override
+ public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
+ if (!(player instanceof EntityPlayerMP))
+ return;
+
+ if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player))
+ return;
+
+ String chainName = ByteBufUtils.readString(in);
+ AbilityController.Instance.deleteChainedAbility(chainName);
+ NoppesUtilServer.sendChainedAbilitiesData((EntityPlayerMP) player);
+ }
+}
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java
new file mode 100644
index 000000000..ca9925bbf
--- /dev/null
+++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java
@@ -0,0 +1,79 @@
+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.AbilityController;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
+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 noppes.npcs.CustomNpcsPermissions;
+import noppes.npcs.NoppesUtilServer;
+
+import java.io.IOException;
+
+/**
+ * Request packet to save a chained ability.
+ */
+public final class ChainedAbilitySavePacket extends AbstractPacket {
+ public static String packetName = "Request|ChainedAbilitySave";
+
+ private NBTTagCompound chainNBT;
+
+ public ChainedAbilitySavePacket() {
+ }
+
+ public ChainedAbilitySavePacket(NBTTagCompound chainNBT) {
+ this.chainNBT = chainNBT;
+ }
+
+ @Override
+ public Enum getType() {
+ return EnumRequestPacket.ChainedAbilitySave;
+ }
+
+ @Override
+ public PacketChannel getChannel() {
+ return PacketHandler.REQUEST_PACKET;
+ }
+
+ @Override
+ public CustomNpcsPermissions.Permission getPermission() {
+ return CustomNpcsPermissions.GLOBAL_ABILITY;
+ }
+
+ @SideOnly(Side.CLIENT)
+ @Override
+ public void sendData(ByteBuf out) throws IOException {
+ ByteBufUtils.writeNBT(out, chainNBT);
+ }
+
+ @Override
+ public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
+ if (!(player instanceof EntityPlayerMP))
+ return;
+
+ if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player))
+ return;
+
+ NBTTagCompound compound = ByteBufUtils.readNBT(in);
+ if (compound == null) return;
+ ChainedAbility chain = new ChainedAbility();
+ chain.readNBT(compound);
+
+ if (chain.getName() != null && !chain.getName().isEmpty()) {
+ AbilityController.Instance.saveChainedAbility(chain);
+ NoppesUtilServer.sendChainedAbilitiesData((EntityPlayerMP) player);
+ GuiDataPacket.sendGuiData((EntityPlayerMP) player, chain.writeNBT());
+ }
+ }
+}
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java
index a4757a20c..b4588935b 100644
--- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java
@@ -3,8 +3,8 @@ 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.AbilityController;
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;
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java
index 487d5f346..ae65c374f 100644
--- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java
@@ -3,7 +3,7 @@ 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.AbilityController;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java
index f42ae5ada..f360b0eff 100644
--- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java
@@ -3,8 +3,8 @@ 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.AbilityController;
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;
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java
index e214689d8..d25935892 100644
--- a/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java
@@ -15,7 +15,6 @@ import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.controllers.AnimationController;
-import noppes.npcs.controllers.data.Animation;
import noppes.npcs.controllers.data.BuiltInAnimation;
import java.io.IOException;
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneTagListPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneTagListPacket.java
index 926c5519c..158896eaf 100644
--- a/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneTagListPacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/request/clone/CloneTagListPacket.java
@@ -10,10 +10,10 @@ 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 kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.controllers.ServerTagMapController;
import noppes.npcs.controllers.data.TagMap;
diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java
index d025fcb26..8930fabc3 100644
--- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java
+++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java
@@ -1,7 +1,5 @@
package kamkeel.npcs.network.packets.request.script;
-import cpw.mods.fml.relauncher.Side;
-import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java
index 3c9b10f1d..6a1a17c76 100644
--- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java
+++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java
@@ -411,6 +411,7 @@ public class AnchorPointHelper {
/**
* 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) {
@@ -444,19 +445,19 @@ public class AnchorPointHelper {
* Apply FULL_MODEL rotation to a world-space offset from entity position.
* Converts the offset to model space, applies FULL_MODEL rotation and pivots,
* then converts back to world space.
- *
+ *
* The rendering pipeline is: entity_pos * body_yaw * scale(-1,-1,1) * FULL_MODEL * parts
* Combined body_yaw + scale maps model space to world space:
- * worldX = mx*cos(yaw) + mz*sin(yaw)
- * worldY = -my (model Y-down → world Y-up)
- * worldZ = mx*sin(yaw) - mz*cos(yaw)
+ * worldX = mx*cos(yaw) + mz*sin(yaw)
+ * worldY = -my (model Y-down → world Y-up)
+ * worldZ = mx*sin(yaw) - mz*cos(yaw)
* This transform is its own inverse for X,Z.
- *
+ *
* FULL_MODEL rotation order matches renderer: vertex sees Z, then Y, then X.
*/
private static Vec3 applyFullModelToWorldOffset(FramePart fullModel, EntityLivingBase entity,
- double worldDX, double worldDY, double worldDZ,
- float bodyYaw, float scale) {
+ double worldDX, double worldDY, double worldDZ,
+ float bodyYaw, float scale) {
float[] rotations = getRotations(fullModel, entity);
float[] pivots = getPivots(fullModel, entity);
diff --git a/src/main/java/kamkeel/npcs/util/BukkitUtil.java b/src/main/java/kamkeel/npcs/util/BukkitUtil.java
index 29f4a522f..1252fc67c 100644
--- a/src/main/java/kamkeel/npcs/util/BukkitUtil.java
+++ b/src/main/java/kamkeel/npcs/util/BukkitUtil.java
@@ -98,6 +98,7 @@ public class BukkitUtil {
/**
* Checks if a specific Bukkit plugin is enabled.
+ *
* @param pluginName the plugin name to check
* @return true if the plugin is enabled
*/
@@ -115,6 +116,7 @@ public class BukkitUtil {
/**
* Gets a Bukkit OfflinePlayer by name.
+ *
* @param playerName the player name
* @return the OfflinePlayer object, or null if not available
*/
@@ -131,6 +133,7 @@ public class BukkitUtil {
/**
* Gets a Bukkit Player by name (online players only).
+ *
* @param playerName the player name
* @return the Player object, or null if not online or not available
*/
@@ -147,6 +150,7 @@ public class BukkitUtil {
/**
* Gets a service provider from Bukkit's ServicesManager.
+ *
* @param serviceClass the service class to get
* @return the service provider, or null if not available
*/
diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java
index bd2ef983c..5c28d5e1d 100644
--- a/src/main/java/kamkeel/npcs/util/Register.java
+++ b/src/main/java/kamkeel/npcs/util/Register.java
@@ -1,13 +1,19 @@
package kamkeel.npcs.util;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import noppes.npcs.LogWriter;
import noppes.npcs.controllers.AnimationController;
import noppes.npcs.controllers.data.Animation;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -68,8 +74,7 @@ public class Register {
}
public AbilityVariant registerVariant(String typeId, String variantName, String group, Consumer configurator) {
- registerVariant(typeId, variantName, () -> new AbilityVariant(variantName, group, configurator));
- return new AbilityVariant(variantName, group, configurator);
+ return registerVariant(typeId, variantName, () -> new AbilityVariant(variantName, group, configurator));
}
public AbilityVariant registerVariant(String typeId, String factoryName, Supplier factory) {
diff --git a/src/main/java/kamkeel/npcs/util/VaultUtil.java b/src/main/java/kamkeel/npcs/util/VaultUtil.java
index c25ea3f3a..47e294e57 100644
--- a/src/main/java/kamkeel/npcs/util/VaultUtil.java
+++ b/src/main/java/kamkeel/npcs/util/VaultUtil.java
@@ -178,6 +178,7 @@ public class VaultUtil {
/**
* Checks if a player has an account in the economy system.
+ *
* @param player the player to check
* @return true if the player has an account
*/
@@ -187,6 +188,7 @@ public class VaultUtil {
/**
* Checks if a player has an account in the economy system.
+ *
* @param playerName the player name to check
* @return true if the player has an account
*/
@@ -207,6 +209,7 @@ public class VaultUtil {
/**
* Gets the balance of a player.
+ *
* @param player the player
* @return the player's balance, or 0 if not available
*/
@@ -216,6 +219,7 @@ public class VaultUtil {
/**
* Gets the balance of a player by name.
+ *
* @param playerName the player name
* @return the player's balance, or 0 if not available
*/
@@ -236,6 +240,7 @@ public class VaultUtil {
/**
* Checks if a player has at least the specified amount.
+ *
* @param player the player
* @param amount the amount to check
* @return true if the player has at least the specified amount
@@ -246,8 +251,9 @@ public class VaultUtil {
/**
* Checks if a player has at least the specified amount.
+ *
* @param playerName the player name
- * @param amount the amount to check
+ * @param amount the amount to check
* @return true if the player has at least the specified amount
*/
public static boolean has(String playerName, double amount) {
@@ -267,6 +273,7 @@ public class VaultUtil {
/**
* Withdraws money from a player's account.
+ *
* @param player the player
* @param amount the amount to withdraw
* @return true if the transaction was successful
@@ -277,8 +284,9 @@ public class VaultUtil {
/**
* Withdraws money from a player's account.
+ *
* @param playerName the player name
- * @param amount the amount to withdraw
+ * @param amount the amount to withdraw
* @return true if the transaction was successful
*/
public static boolean withdrawMoney(String playerName, double amount) {
@@ -301,6 +309,7 @@ public class VaultUtil {
/**
* Adds money to a player's account (deposit).
+ *
* @param player the player
* @param amount the amount to add
* @return true if the transaction was successful
@@ -311,8 +320,9 @@ public class VaultUtil {
/**
* Adds money to a player's account (deposit).
+ *
* @param playerName the player name
- * @param amount the amount to add
+ * @param amount the amount to add
* @return true if the transaction was successful
*/
public static boolean addMoney(String playerName, double amount) {
@@ -336,6 +346,7 @@ public class VaultUtil {
/**
* Sets a player's balance to the specified amount.
* This withdraws all money first, then deposits the new amount.
+ *
* @param player the player
* @param amount the new balance
* @return true if the transaction was successful
@@ -347,8 +358,9 @@ public class VaultUtil {
/**
* Sets a player's balance to the specified amount.
* This withdraws all money first, then deposits the new amount.
+ *
* @param playerName the player name
- * @param amount the new balance
+ * @param amount the new balance
* @return true if the transaction was successful
*/
public static boolean setBalance(String playerName, double amount) {
@@ -368,6 +380,7 @@ public class VaultUtil {
/**
* Formats an amount according to the economy plugin's settings.
+ *
* @param amount the amount to format
* @return the formatted string, or the raw number as string if not available
*/
@@ -384,6 +397,7 @@ public class VaultUtil {
/**
* Gets the plural currency name (e.g., "Dollars").
+ *
* @return the plural currency name, or "coins" if not available
*/
public static String getCurrencyNamePlural() {
@@ -399,6 +413,7 @@ public class VaultUtil {
/**
* Gets the singular currency name (e.g., "Dollar").
+ *
* @return the singular currency name, or "coin" if not available
*/
public static String getCurrencyNameSingular() {
@@ -414,6 +429,7 @@ public class VaultUtil {
/**
* Creates an account for the player if one doesn't exist.
+ *
* @param player the player
* @return true if the account was created or already exists
*/
@@ -423,6 +439,7 @@ public class VaultUtil {
/**
* Creates an account for the player if one doesn't exist.
+ *
* @param playerName the player name
* @return true if the account was created or already exists
*/
@@ -459,6 +476,7 @@ public class VaultUtil {
/**
* Gets a list of all bank names.
+ *
* @return list of bank names, or empty list if not available
*/
@SuppressWarnings("unchecked")
@@ -475,6 +493,7 @@ public class VaultUtil {
/**
* Gets the balance of a bank.
+ *
* @param bankName the bank name
* @return the bank balance, or 0 if not available
*/
@@ -492,8 +511,9 @@ public class VaultUtil {
/**
* Checks if a bank has at least the specified amount.
+ *
* @param bankName the bank name
- * @param amount the amount to check
+ * @param amount the amount to check
* @return true if the bank has at least the specified amount
*/
public static boolean bankHas(String bankName, double amount) {
@@ -510,8 +530,9 @@ public class VaultUtil {
/**
* Withdraws money from a bank.
+ *
* @param bankName the bank name
- * @param amount the amount to withdraw
+ * @param amount the amount to withdraw
* @return true if the transaction was successful
*/
public static boolean bankWithdraw(String bankName, double amount) {
@@ -528,8 +549,9 @@ public class VaultUtil {
/**
* Deposits money into a bank.
+ *
* @param bankName the bank name
- * @param amount the amount to deposit
+ * @param amount the amount to deposit
* @return true if the transaction was successful
*/
public static boolean bankDeposit(String bankName, double amount) {
@@ -546,8 +568,9 @@ public class VaultUtil {
/**
* Transfers money from one player to another.
- * @param from the player to take money from
- * @param to the player to give money to
+ *
+ * @param from the player to take money from
+ * @param to the player to give money to
* @param amount the amount to transfer
* @return true if the transfer was successful
*/
@@ -557,9 +580,10 @@ public class VaultUtil {
/**
* Transfers money from one player to another.
+ *
* @param fromPlayerName the player name to take money from
- * @param toPlayerName the player name to give money to
- * @param amount the amount to transfer
+ * @param toPlayerName the player name to give money to
+ * @param amount the amount to transfer
* @return true if the transfer was successful
*/
public static boolean transfer(String fromPlayerName, String toPlayerName, double amount) {
diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java
index 774e9661e..717c6c0c9 100644
--- a/src/main/java/noppes/npcs/AbstractDataAbilities.java
+++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java
@@ -1,15 +1,23 @@
package noppes.npcs;
+import kamkeel.npcs.controllers.AbilityController;
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.ChainedAbility;
+import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry;
+import kamkeel.npcs.controllers.data.ability.ToggleEntry;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.util.DamageSource;
import noppes.npcs.controllers.AnimationController;
import noppes.npcs.controllers.data.Animation;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* Shared base class for NPC and Player ability data management.
@@ -28,73 +36,119 @@ public abstract class AbstractDataAbilities {
// SHARED RUNTIME STATE (not saved to NBT)
// ═══════════════════════════════════════════════════════════════════
- /** Currently executing ability (null if none) */
+ /**
+ * Currently executing ability (null if none)
+ */
protected Ability currentAbility;
- /** World time when cooldown ends */
+ /**
+ * World time when cooldown ends
+ */
protected long cooldownEndTime = 0;
- /** Rotation lock state */
+ /**
+ * Rotation lock state
+ */
protected boolean rotationLocked = false;
protected float lockedYaw = 0;
protected float lockedPitch = 0;
- /** Position lock state */
+ /**
+ * Position lock state
+ */
protected boolean positionLocked = false;
protected double lockedPosX = 0;
protected double lockedPosY = 0;
protected double lockedPosZ = 0;
+ /**
+ * Chained ability execution state
+ */
+ protected ChainedAbility currentChain;
+ protected int chainEntryIndex = -1;
+ protected int chainDelayRemaining = -1;
+
+ /**
+ * Set when interrupt already rolled cooldown (prevents double cooldown on NPC dazed completion).
+ */
+ protected boolean interruptCooldownRolled = false;
+
// ═══════════════════════════════════════════════════════════════════
// ABSTRACT METHODS - Subclasses must implement
// ═══════════════════════════════════════════════════════════════════
- /** Get the entity this ability data belongs to (NPC or Player). */
+ /**
+ * Get the entity this ability data belongs to (NPC or Player).
+ */
protected abstract EntityLivingBase getEntity();
- /** Get the current target for ability execution. */
+ /**
+ * Get the current target for ability execution.
+ */
protected abstract EntityLivingBase getTarget();
- /** Get the current world time. */
+ /**
+ * Get the current world time.
+ */
protected abstract long getWorldTime();
// -- Event Firing --
- /** Fire tick event. Implementation differs between NPC (NpcAPI) and Player (ScriptController). */
+ /**
+ * Fire tick event. Implementation differs between NPC (NpcAPI) and Player (ScriptController).
+ */
protected abstract void fireTickEvent(Ability ability, EntityLivingBase target);
- /** Fire execute event. Returns true if cancelled. */
+ /**
+ * Fire execute event. Returns true if cancelled.
+ */
protected abstract boolean fireExecuteEvent(Ability ability, EntityLivingBase target);
- /** Fire complete event. */
+ /**
+ * Fire complete event.
+ */
protected abstract void fireCompleteEvent(Ability ability, EntityLivingBase target);
- /** Fire interrupt event. */
+ /**
+ * Fire interrupt event.
+ */
protected abstract void fireInterruptEvent(Ability ability, EntityLivingBase target,
DamageSource source, float damage);
// -- Telegraph --
- /** Spawn telegraphs for an ability. */
+ /**
+ * Spawn telegraphs for an ability.
+ */
protected abstract void spawnTelegraph(Ability ability, EntityLivingBase target);
- /** Remove telegraphs for an ability. */
+ /**
+ * Remove telegraphs for an ability.
+ */
protected abstract void removeTelegraph(Ability ability);
// -- Animation/Sound Data Access --
- /** Set animation data on the entity. */
+ /**
+ * Set animation data on the entity.
+ */
protected abstract void setAnimationData(Animation animation);
- /** Clear animation data on the entity. */
+ /**
+ * Clear animation data on the entity.
+ */
protected abstract void clearAnimationData();
- /** Play a sound at the entity's location. */
+ /**
+ * Play a sound at the entity's location.
+ */
protected abstract void playAbilitySound(String sound);
// -- Rotation (subclass handles extra fields and application) --
- /** Capture rotation values for locking. NPC captures 4 fields, Player captures 2. */
+ /**
+ * Capture rotation values for locking. NPC captures 4 fields, Player captures 2.
+ */
protected abstract void captureLockedRotation();
// -- Completion --
@@ -105,20 +159,44 @@ public abstract class AbstractDataAbilities {
*/
protected abstract void rollCooldown(Ability ability);
- /** Additional completion logic (NPC clears lastTarget, Player clears key/target and syncs). */
+ /**
+ * Additional completion logic (NPC clears lastTarget, Player clears key/target and syncs).
+ */
protected abstract void onAbilityComplete();
+ /**
+ * Roll cooldown after a chained ability completes.
+ * NPC: random(min, max) + chain.cooldownTicks. Player: chain.cooldownTicks only.
+ */
+ protected abstract void rollChainCooldown(ChainedAbility chain);
+
// -- Hooks (optional overrides) --
- /** Called before onExecute in ACTIVE phase. NPC uses this for faceTarget/hitScan. */
- protected void onPreExecute(Ability ability, EntityLivingBase target) {}
+ /**
+ * Called before onExecute in ACTIVE phase. NPC uses this for faceTarget/hitScan.
+ */
+ protected void onPreExecute(Ability ability, EntityLivingBase target) {
+ }
- /** Called after the phase switch. NPC uses this for hit scan updates and movement control. */
- protected void onPostPhaseTick(Ability ability, EntityLivingBase target) {}
+ /**
+ * Called after the phase switch. NPC uses this for hit scan updates and movement control.
+ */
+ protected void onPostPhaseTick(Ability ability, EntityLivingBase target) {
+ }
- /** Additional lock releases in BURST_DELAY. NPC releases hitScan. */
- protected void onBurstDelayReleaseLocks() {}
+ /**
+ * Additional lock releases in BURST_DELAY. NPC releases hitScan.
+ */
+ protected void onBurstDelayReleaseLocks() {
+ }
+
+ /**
+ * Hook for retargeting during chain execution when target dies. NPC overrides, Player returns null.
+ */
+ protected EntityLivingBase retargetForChain() {
+ return null;
+ }
// ═══════════════════════════════════════════════════════════════════
// SHARED ANIMATION METHODS
@@ -158,36 +236,287 @@ public abstract class AbstractDataAbilities {
// STATE QUERIES
// ═══════════════════════════════════════════════════════════════════
- /** Check if an ability is currently executing. */
+ /**
+ * Check if an ability is currently executing.
+ */
public boolean isExecutingAbility() {
return currentAbility != null && currentAbility.isExecuting();
}
- /** Get the currently executing ability. */
+ /**
+ * Get the currently executing ability.
+ */
public Ability getCurrentAbility() {
return currentAbility;
}
+ /**
+ * Check if a chained ability is currently executing.
+ */
+ public boolean isExecutingChain() {
+ return currentChain != null;
+ }
+
+ /**
+ * Get the currently executing chained ability.
+ */
+ public ChainedAbility getCurrentChain() {
+ return currentChain;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // TOGGLE STATE (active toggles, independent of currentAbility)
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Currently active toggles. Key = ability key, Value = toggle entry with state and tick counter.
+ */
+ protected Map activeToggles = new LinkedHashMap<>();
+
+ /**
+ * Cycle a toggle ability to its next state.
+ * Off(0) -> State 1 -> State 2 -> ... -> State N -> Off(0)
+ *
+ * @param key The ability key (e.g., "npcdbc:ki_fist")
+ * @return The new state (0 = off, 1+ = active state number)
+ */
+ public int toggleAbility(String key) {
+ ToggleEntry existing = activeToggles.get(key);
+ if (existing != null) {
+ int currentState = existing.getState();
+ int maxStates = existing.getAbility().getToggleStates();
+ if (currentState < maxStates) {
+ return cycleToggleState(key, existing, currentState, currentState + 1);
+ } else {
+ deactivateToggle(key);
+ return 0;
+ }
+ } else {
+ return activateToggle(key, 1) ? 1 : 0;
+ }
+ }
+
+ /**
+ * Get the current toggle state for an ability.
+ * @return 0 if not active, 1+ for active state
+ */
+ public int getToggleState(String key) {
+ ToggleEntry entry = activeToggles.get(key);
+ return entry != null ? entry.getState() : 0;
+ }
+
+ /**
+ * Set a toggle to a specific state. 0 = deactivate, 1+ = specific state.
+ */
+ public void setToggleState(String key, int state) {
+ if (state <= 0) {
+ if (activeToggles.containsKey(key)) {
+ deactivateToggle(key);
+ }
+ return;
+ }
+ ToggleEntry existing = activeToggles.get(key);
+ if (existing != null) {
+ int currentState = existing.getState();
+ if (currentState != state) {
+ cycleToggleState(key, existing, currentState, state);
+ }
+ } else {
+ activateToggle(key, state);
+ }
+ }
+
+ /**
+ * Check if a toggle ability is currently active (any state > 0).
+ */
+ public boolean isAbilityToggled(String key) {
+ return activeToggles.containsKey(key);
+ }
+
+ /**
+ * Get all active toggle keys.
+ */
+ public Set getActiveToggleKeys() {
+ return new LinkedHashSet<>(activeToggles.keySet());
+ }
+
+ private boolean activateToggle(String key, int state) {
+ Ability ability = AbilityController.Instance != null
+ ? AbilityController.Instance.resolveAbility(key) : null;
+ if (ability == null || !ability.isToggleable()) return false;
+ if (state < 1 || state > ability.getToggleStates()) state = 1;
+
+ if (fireToggleEvent(ability, 0, state)) return false;
+
+ ToggleEntry entry = new ToggleEntry(ability, state);
+ activeToggles.put(key, entry);
+ ability.onToggleOn(getEntity());
+ ability.onToggleStateChanged(getEntity(), 0, state);
+ onToggleStateChanged(key, true, state);
+ return true;
+ }
+
+ private int cycleToggleState(String key, ToggleEntry entry, int oldState, int newState) {
+ Ability ability = entry.getAbility();
+ if (newState < 1 || newState > ability.getToggleStates()) return oldState;
+
+ if (fireToggleEvent(ability, oldState, newState)) return oldState;
+
+ entry.setState(newState);
+ ability.onToggleStateChanged(getEntity(), oldState, newState);
+ onToggleStateChanged(key, true, newState);
+ return newState;
+ }
+
+ private void deactivateToggle(String key) {
+ ToggleEntry entry = activeToggles.get(key);
+ if (entry == null) return;
+ int oldState = entry.getState();
+
+ if (fireToggleEvent(entry.getAbility(), oldState, 0)) return;
+
+ activeToggles.remove(key);
+ entry.getAbility().onToggleOff(getEntity());
+ entry.getAbility().onToggleStateChanged(getEntity(), oldState, 0);
+ onToggleStateChanged(key, false, 0);
+ }
+
+ /**
+ * Tick all active toggles. Call from tick() each game tick.
+ * Toggles with hasActiveToggle=true get their onToggleTick() called.
+ * If onToggleTick returns false, the toggle is auto-deactivated.
+ * Every 10 ticks, fires a toggle update event for ALL active toggles.
+ */
+ protected void tickActiveToggles() {
+ if (activeToggles.isEmpty()) return;
+
+ EntityLivingBase entity = getEntity();
+ List toRemove = null;
+
+ for (Map.Entry mapEntry : activeToggles.entrySet()) {
+ ToggleEntry entry = mapEntry.getValue();
+ entry.incrementTick();
+
+ if (entry.getAbility().hasActiveToggle()) {
+ if (!entry.getAbility().onToggleTick(entity, entry.getTickCount(), entry.getState())) {
+ if (toRemove == null) toRemove = new ArrayList<>();
+ toRemove.add(mapEntry.getKey());
+ continue;
+ }
+ }
+
+ if (entry.getTickCount() % 10 == 0) {
+ boolean enabled = fireToggleUpdateEvent(entry.getAbility(), entry.getTickCount(), entry.getState());
+ if (!enabled) {
+ if (toRemove == null) toRemove = new ArrayList<>();
+ toRemove.add(mapEntry.getKey());
+ }
+ }
+ }
+
+ if (toRemove != null) {
+ for (String key : toRemove) {
+ ToggleEntry entry = activeToggles.remove(key);
+ if (entry != null) {
+ int oldState = entry.getState();
+ entry.getAbility().onToggleOff(entity);
+ entry.getAbility().onToggleStateChanged(entity, oldState, 0);
+ onToggleStateChanged(key, false, 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * Hook for subclass to react to toggle state changes (sync packets, script events).
+ */
+ protected void onToggleStateChanged(String key, boolean active, int state) {
+ }
+
+ /**
+ * Fire a toggle event before state change.
+ * @param oldState Previous state (0 = off)
+ * @param newState Target state (0 = off)
+ * @return true if the event was canceled
+ */
+ protected boolean fireToggleEvent(Ability ability, int oldState, int newState) {
+ return false;
+ }
+
+ /**
+ * Fire a toggle update event every 10 ticks.
+ * @return true if the toggle should remain active, false to force-deactivate
+ */
+ protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) {
+ return true;
+ }
+
+ /**
+ * Clear all active toggles (e.g., on death/reset).
+ * Calls onToggleOff and onToggleStateChanged for each active toggle.
+ */
+ protected void clearActiveToggles() {
+ if (activeToggles.isEmpty()) return;
+ EntityLivingBase entity = getEntity();
+ for (Map.Entry mapEntry : activeToggles.entrySet()) {
+ ToggleEntry entry = mapEntry.getValue();
+ entry.getAbility().onToggleOff(entity);
+ entry.getAbility().onToggleStateChanged(entity, entry.getState(), 0);
+ }
+ activeToggles.clear();
+ }
+
+ /**
+ * Add a toggle entry directly without calling onToggleOn.
+ * Used for client-side sync and NBT restoration.
+ * @param key The ability key
+ * @param state The toggle state (0 = remove, 1+ = set at state)
+ */
+ public void setToggleEntryDirect(String key, int state) {
+ if (state > 0) {
+ Ability ability = AbilityController.Instance != null
+ ? AbilityController.Instance.resolveAbility(key) : null;
+ if (ability != null && ability.isToggleable()) {
+ activeToggles.put(key, new ToggleEntry(ability, state));
+ }
+ } else {
+ activeToggles.remove(key);
+ }
+ }
+
// ═══════════════════════════════════════════════════════════════════
// COOLDOWN MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
- /** Check if on cooldown. */
+ /**
+ * Check if on cooldown.
+ */
public boolean isOnCooldown() {
return getWorldTime() < cooldownEndTime;
}
- /** Get remaining cooldown ticks. */
+ /**
+ * Get remaining cooldown ticks.
+ */
public long getRemainingCooldown() {
long remaining = cooldownEndTime - getWorldTime();
return remaining > 0 ? remaining : 0;
}
- /** Reset cooldown (allow immediate ability use). */
+ /**
+ * Reset cooldown (allow immediate ability use).
+ */
public void resetCooldown() {
cooldownEndTime = 0;
}
+ /**
+ * Set the cooldown end time directly.
+ */
+ public void setCooldownEndTime(long endTime) {
+ cooldownEndTime = endTime;
+ }
+
// ═══════════════════════════════════════════════════════════════════
// POSITION LOCKING
// ═══════════════════════════════════════════════════════════════════
@@ -212,8 +541,11 @@ public abstract class AbstractDataAbilities {
onPositionLockChanged(false);
}
- /** Hook for subclass to react to position lock state changes (e.g., NPC sets data watcher flag). */
- protected void onPositionLockChanged(boolean locked) {}
+ /**
+ * Hook for subclass to react to position lock state changes (e.g., NPC sets data watcher flag).
+ */
+ protected void onPositionLockChanged(boolean locked) {
+ }
// ═══════════════════════════════════════════════════════════════════
// ROTATION LOCKING
@@ -227,8 +559,11 @@ public abstract class AbstractDataAbilities {
onRotationLockChanged(false);
}
- /** Hook for subclass to react to rotation lock state changes (e.g., NPC sets flag, clears hitScan). */
- protected void onRotationLockChanged(boolean locked) {}
+ /**
+ * Hook for subclass to react to rotation lock state changes (e.g., NPC sets flag, clears hitScan).
+ */
+ protected void onRotationLockChanged(boolean locked) {
+ }
// ═══════════════════════════════════════════════════════════════════
// CORE TICK - Phase transition logic shared between NPC and Player
@@ -242,6 +577,24 @@ public abstract class AbstractDataAbilities {
* Subclass-specific behavior is injected via abstract methods and hooks.
*/
protected void tickCurrentAbility() {
+ // Handle chain delay between entries
+ if (chainDelayRemaining > 0) {
+ chainDelayRemaining--;
+ if (chainDelayRemaining <= 0) {
+ EntityLivingBase chainTarget = getTarget();
+ // Check if target is dead and retarget
+ if (currentChain != null && chainTarget != null && chainTarget.isDead) {
+ chainTarget = retargetForChain();
+ if (chainTarget == null) {
+ completeChain();
+ return;
+ }
+ }
+ startChainEntry(chainTarget);
+ }
+ return;
+ }
+
EntityLivingBase entity = getEntity();
EntityLivingBase target = getTarget();
AbilityPhase oldPhase = currentAbility.getPhase();
@@ -254,7 +607,7 @@ public abstract class AbstractDataAbilities {
// Fire extender tick hook (e.g., per-tick resource drain)
if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, entity, target,
- currentAbility.getPhase(), currentAbility.getCurrentTick())) {
+ currentAbility.getPhase(), currentAbility.getCurrentTick())) {
interruptCurrentAbility(null, 0);
return;
}
@@ -318,7 +671,10 @@ public abstract class AbstractDataAbilities {
// Fire execute event (cancelable)
if (fireExecuteEvent(currentAbility, target)) {
- return; // Cancelled
+ // Script cancelled — abort cleanly instead of leaving stuck in ACTIVE
+ currentAbility.interrupt();
+ handleAbilityCompletion(target);
+ return;
}
// Call onExecute
@@ -339,8 +695,10 @@ public abstract class AbstractDataAbilities {
return;
}
- // Auto-complete for burst overlap mode
+ // Auto-complete for burst overlap mode only.
+ // Non-overlap waits for all entities to die naturally (via allDead check in onActiveTick).
if (currentAbility.isBurstEnabled()
+ && currentAbility.isBurstOverlap()
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
@@ -381,6 +739,7 @@ public abstract class AbstractDataAbilities {
/**
* Handle ability completion. Called when ability phase becomes IDLE.
+ * If a chain is active, advances to the next entry instead of completing.
*/
protected void handleAbilityCompletion(EntityLivingBase target) {
if (currentAbility == null) return;
@@ -400,10 +759,42 @@ public abstract class AbstractDataAbilities {
// Release locks
releaseRotationControl();
releaseLockedPosition();
-
- // Subclass-specific completion (cooldown, cleanup, sync)
- rollCooldown(currentAbility);
stopAbilityAnimation();
+
+ // Chain mode: advance to next entry instead of completing
+ if (currentChain != null) {
+ // AFTER semantics: delay from the CURRENT (just-completed) entry
+ ChainedAbilityEntry completedEntry = currentChain.getEntries().get(chainEntryIndex);
+ int delay = completedEntry.getDelayTicks();
+
+ chainEntryIndex++;
+ if (chainEntryIndex < currentChain.getEntries().size()) {
+ // Check if target died and retarget
+ if (target != null && target.isDead) {
+ target = retargetForChain();
+ if (target == null) {
+ completeChain();
+ return;
+ }
+ }
+
+ // Enforce minimum 1-tick delay between chain entries
+ chainDelayRemaining = Math.max(1, delay);
+ currentAbility = null;
+ return;
+ }
+ // All entries complete
+ completeChain();
+ return;
+ }
+
+ // Normal (non-chain) completion
+ if (interruptCooldownRolled) {
+ // Cooldown was already rolled during interrupt (e.g., chain interrupted, NPC ticked through DAZED)
+ interruptCooldownRolled = false;
+ } else {
+ rollCooldown(currentAbility);
+ }
onAbilityComplete();
}
@@ -421,7 +812,10 @@ public abstract class AbstractDataAbilities {
// Fire execute event (cancelable)
if (fireExecuteEvent(ability, target)) {
- return; // Cancelled
+ // Script cancelled — abort cleanly instead of leaving stuck in ACTIVE
+ ability.interrupt();
+ handleAbilityCompletion(target);
+ return;
}
// Call onExecute
@@ -465,6 +859,15 @@ public abstract class AbstractDataAbilities {
releaseRotationControl();
releaseLockedPosition();
+ // If chain was active, roll chain cooldown and clear chain state
+ if (currentChain != null) {
+ rollChainCooldown(currentChain);
+ interruptCooldownRolled = true;
+ currentChain = null;
+ chainEntryIndex = -1;
+ chainDelayRemaining = -1;
+ }
+
// Let subclass handle cleanup
onInterruptComplete();
}
@@ -474,5 +877,107 @@ public abstract class AbstractDataAbilities {
* Hook called after interrupt completes. Subclass handles clearing state and syncing.
* NPC: keeps currentAbility (ticks through DAZED). Player: clears immediately.
*/
- protected void onInterruptComplete() {}
+ protected void onInterruptComplete() {
+ }
+
+ // ═══════════════════════════════════════════════════════════════════
+ // CHAINED ABILITY EXECUTION
+ // ═══════════════════════════════════════════════════════════════════
+
+ /**
+ * Start executing a chained ability. Sets up chain state and starts the first entry.
+ *
+ * @param chain The chained ability to execute (should be a deep copy)
+ * @param target The initial target
+ * @return true if the chain was started
+ */
+ protected boolean startChain(ChainedAbility chain, EntityLivingBase target) {
+ if (chain == null || chain.getEntries().isEmpty()) return false;
+
+ currentChain = chain;
+ chainEntryIndex = 0;
+ chainDelayRemaining = -1;
+
+ // AFTER semantics: no delay before first entry (delay applies after each entry completes)
+ return startChainEntry(target);
+ }
+
+ /**
+ * Start the current chain entry's ability.
+ */
+ protected boolean startChainEntry(EntityLivingBase target) {
+ if (currentChain == null || chainEntryIndex < 0 || chainEntryIndex >= currentChain.getEntries().size()) {
+ completeChain();
+ return false;
+ }
+
+ ChainedAbilityEntry entry = currentChain.getEntries().get(chainEntryIndex);
+ Ability ability = entry.resolve();
+ if (ability == null) {
+ // Broken reference - complete chain
+ completeChain();
+ return false;
+ }
+
+ // Validate the resolved ability's UserType matches the chain's allowed context.
+ // The chain's own UserType was already checked at selection time, but individual
+ // abilities may have stricter restrictions (e.g., PLAYER_ONLY in an NPC chain).
+ if (!ability.getAllowedBy().allowsNpc() && getEntity() instanceof noppes.npcs.entity.EntityNPCInterface) {
+ // Skip this entry - ability doesn't allow NPCs
+ completeChain();
+ return false;
+ }
+ if (!ability.getAllowedBy().allowsPlayer() && getEntity() instanceof net.minecraft.entity.player.EntityPlayer) {
+ // Skip this entry - ability doesn't allow players
+ completeChain();
+ return false;
+ }
+
+ // If windUpAll=false, only the first ability windups; subsequent abilities skip windup
+ if (!currentChain.isWindUpAll() && chainEntryIndex > 0) {
+ ability.setWindUpTicks(0);
+ }
+
+ // Start the ability (skip its own conditions - chain conditions were already checked)
+ currentAbility = ability;
+ ability.start(target);
+
+ if (ability.getPhase() == AbilityPhase.ACTIVE) {
+ // Windup was 0 - capture locks for immediate active phase, then execute
+ if (ability.isRotationLockedDuringActive()) {
+ captureLockedRotation();
+ }
+ if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) {
+ captureLockedPosition();
+ }
+ executeImmediate(ability, target);
+ } else {
+ // Normal windup flow
+ if (ability.isRotationLockedDuringWindup()) {
+ captureLockedRotation();
+ }
+ if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) {
+ captureLockedPosition();
+ }
+ spawnTelegraph(ability, target);
+ playAbilitySound(ability.getWindUpSound());
+ playAbilityAnimation(ability.getWindUpAnimation());
+ }
+
+ return true;
+ }
+
+ /**
+ * Complete the chain execution. Rolls chain cooldown and clears all chain state.
+ */
+ protected void completeChain() {
+ if (currentChain != null) {
+ rollChainCooldown(currentChain);
+ }
+ currentChain = null;
+ chainEntryIndex = -1;
+ chainDelayRemaining = -1;
+ currentAbility = null;
+ onAbilityComplete();
+ }
}
diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java
index ee65ad6a5..e059e9104 100644
--- a/src/main/java/noppes/npcs/CommonProxy.java
+++ b/src/main/java/noppes/npcs/CommonProxy.java
@@ -13,13 +13,13 @@ import net.minecraft.world.World;
import net.minecraftforge.common.util.FakePlayer;
import noppes.npcs.api.IWorld;
import noppes.npcs.blocks.tiles.TileNpcContainer;
+import noppes.npcs.config.ConfigScript;
import noppes.npcs.constants.EnumGuiType;
-import noppes.npcs.containers.ContainerAuction;
+import noppes.npcs.containers.ContainerAnvilRepair;
import noppes.npcs.containers.ContainerAuctionBidding;
-import noppes.npcs.containers.ContainerAuctionTrades;
import noppes.npcs.containers.ContainerAuctionListing;
import noppes.npcs.containers.ContainerAuctionSell;
-import noppes.npcs.containers.ContainerAnvilRepair;
+import noppes.npcs.containers.ContainerAuctionTrades;
import noppes.npcs.containers.ContainerCarpentryBench;
import noppes.npcs.containers.ContainerCrate;
import noppes.npcs.containers.ContainerCustomGui;
@@ -43,7 +43,6 @@ import noppes.npcs.containers.ContainerNpcQuestReward;
import noppes.npcs.containers.ContainerNpcQuestTypeItem;
import noppes.npcs.controllers.data.AnimationData;
import noppes.npcs.controllers.data.PlayerData;
-import noppes.npcs.config.ConfigScript;
import noppes.npcs.entity.EntityNPCInterface;
public class CommonProxy implements IGuiHandler {
diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java
index 892c3648f..f7c76f555 100644
--- a/src/main/java/noppes/npcs/CustomNpcs.java
+++ b/src/main/java/noppes/npcs/CustomNpcs.java
@@ -21,13 +21,22 @@ import io.github.somehussar.janinoloader.api.IDynamicCompilerBuilder;
import kamkeel.npcs.addon.AddonManager;
import kamkeel.npcs.command.CommandKamkeel;
import kamkeel.npcs.command.profile.CommandProfile;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.AttributeController;
import kamkeel.npcs.controllers.ProfileController;
import kamkeel.npcs.controllers.SyncController;
import kamkeel.npcs.controllers.TelegraphController;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.controllers.data.profile.CNPCData;
import kamkeel.npcs.developer.Developer;
+import kamkeel.npcs.entity.EntityAbilityBeam;
+import kamkeel.npcs.entity.EntityAbilityDisc;
+import kamkeel.npcs.entity.EntityAbilityLaser;
+import kamkeel.npcs.entity.EntityAbilityOrb;
+import kamkeel.npcs.entity.EntityAbilitySweeper;
+import kamkeel.npcs.entity.EntityAbilityZone;
+import kamkeel.npcs.entity.EntityEnergyDome;
+import kamkeel.npcs.entity.EntityEnergyPanel;
+import kamkeel.npcs.entity.EntityEnergySlicer;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.util.BukkitUtil;
import net.minecraft.block.Block;
@@ -86,15 +95,6 @@ import noppes.npcs.entity.EntityNpcDragon;
import noppes.npcs.entity.EntityNpcPony;
import noppes.npcs.entity.EntityNpcSlime;
import noppes.npcs.entity.EntityProjectile;
-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 kamkeel.npcs.entity.EntityEnergyDome;
-import kamkeel.npcs.entity.EntityEnergyPanel;
-import kamkeel.npcs.entity.EntityEnergySlicer;
import noppes.npcs.entity.old.EntityNPCDwarfFemale;
import noppes.npcs.entity.old.EntityNPCDwarfMale;
import noppes.npcs.entity.old.EntityNPCElfFemale;
@@ -121,7 +121,7 @@ import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
-@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta3")
+@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta4")
public class CustomNpcs {
@SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy")
@@ -421,6 +421,8 @@ public class CustomNpcs {
AuctionController.Instance.save();
}
+ kamkeel.npcs.entity.EntityEnergyProjectile.clearAllProjectiles();
+
if (FMLCommonHandler.instance().getSide().isClient())
clientJaninoCompiler = null;
globalJaninoCompiler = null;
diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java
index cb1e5104d..988a2f073 100644
--- a/src/main/java/noppes/npcs/DataAbilities.java
+++ b/src/main/java/noppes/npcs/DataAbilities.java
@@ -1,9 +1,12 @@
package noppes.npcs;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
+import kamkeel.npcs.controllers.data.ability.AbilityAction;
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
-import kamkeel.npcs.controllers.data.ability.AbilitySlot;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
+import kamkeel.npcs.controllers.data.ability.IAbilityAction;
+import kamkeel.npcs.controllers.data.ability.ToggleEntry;
import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
@@ -11,6 +14,7 @@ import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
+import net.minecraft.nbt.NBTTagString;
import net.minecraft.util.DamageSource;
import noppes.npcs.controllers.data.Animation;
import noppes.npcs.entity.EntityNPCInterface;
@@ -19,6 +23,7 @@ import noppes.npcs.scripted.event.AbilityEvent;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import java.util.Random;
/**
@@ -35,9 +40,9 @@ public class DataAbilities extends AbstractDataAbilities {
// ═══════════════════════════════════════════════════════════════════
/**
- * List of ability slots this NPC can use (inline or reference).
+ * Unified list of action slots (abilities and chained abilities).
*/
- private List abilitySlots = new ArrayList<>();
+ private List actionSlots = new ArrayList<>();
/**
* Whether the ability system is enabled for this NPC
@@ -83,10 +88,14 @@ public class DataAbilities extends AbstractDataAbilities {
private transient boolean hitScanActive = false;
private transient EntityLivingBase hitScanTarget = null;
- /** Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 */
+ /**
+ * Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15
+ */
private static final int ROTATION_CONTROLLED_FLAG = 16;
- /** Bit flag for position lock in data watcher slot 15 */
+ /**
+ * Bit flag for position lock in data watcher slot 15
+ */
private static final int POSITION_LOCKED_FLAG = 32;
// ═══════════════════════════════════════════════════════════════════
@@ -140,12 +149,27 @@ public class DataAbilities extends AbstractDataAbilities {
@Override
protected void fireInterruptEvent(Ability ability, EntityLivingBase target,
- DamageSource source, float damage) {
+ DamageSource source, float damage) {
AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent(
npc.wrappedNPC, ability, target, source, damage);
NpcAPI.EVENT_BUS.post(interruptEvent);
}
+ @Override
+ protected boolean fireToggleEvent(Ability ability, int oldState, int newState) {
+ AbilityEvent.ToggleEvent event = new AbilityEvent.ToggleEvent(
+ npc.wrappedNPC, ability, oldState, newState);
+ return NpcAPI.EVENT_BUS.post(event);
+ }
+
+ @Override
+ protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) {
+ AbilityEvent.ToggleUpdateEvent event = new AbilityEvent.ToggleUpdateEvent(
+ npc.wrappedNPC, ability, tick, state);
+ NpcAPI.EVENT_BUS.post(event);
+ return event.isEnabled();
+ }
+
@Override
protected void spawnTelegraph(Ability ability, EntityLivingBase target) {
List telegraphs = ability.createTelegraphs(npc, target);
@@ -207,6 +231,20 @@ public class DataAbilities extends AbstractDataAbilities {
cooldownEndTime += ability.getCooldownTicks();
}
+ @Override
+ protected void rollChainCooldown(ChainedAbility chain) {
+ int baseCooldown = minCooldown;
+ if (maxCooldown > minCooldown) {
+ baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1);
+ }
+ cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown + chain.getCooldownTicks();
+ }
+
+ @Override
+ protected EntityLivingBase retargetForChain() {
+ return npc.getAttackTarget();
+ }
+
@Override
protected void onAbilityComplete() {
currentAbility = null;
@@ -230,7 +268,7 @@ public class DataAbilities extends AbstractDataAbilities {
// Update hit scan state - actual facing is deferred to applyRotationControl()
// which runs AFTER super.onLivingUpdate() to override AI look helper
if (ability != null && ability.isExecuting()
- && ability.isHitScanForCurrentPhase() && target != null) {
+ && ability.isHitScanForCurrentPhase() && target != null) {
enableHitScan(target);
} else if (hitScanActive) {
releaseRotationControl();
@@ -288,17 +326,24 @@ public class DataAbilities extends AbstractDataAbilities {
if (rotationLocked || hitScanActive) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
}
+ // Clear chain state
+ currentChain = null;
+ chainEntryIndex = -1;
+ chainDelayRemaining = -1;
return;
}
// Safety: release orphaned locks if no ability is actively executing
- if (currentAbility == null || !currentAbility.isExecuting()) {
+ if ((currentAbility == null || !currentAbility.isExecuting()) && chainDelayRemaining <= 0) {
if (rotationLocked || hitScanActive) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
}
- // Tick current ability if executing (shared logic from base class)
- if (currentAbility != null && currentAbility.isExecuting()) {
+ // Tick active toggles (independent of currentAbility)
+ tickActiveToggles();
+
+ // Tick current ability if executing, or tick chain delay between entries
+ if (chainDelayRemaining > 0 || (currentAbility != null && currentAbility.isExecuting())) {
tickCurrentAbility();
}
}
@@ -329,7 +374,7 @@ public class DataAbilities extends AbstractDataAbilities {
// ═══════════════════════════════════════════════════════════════════
/**
- * Try to select and start an ability for the given target.
+ * Try to select and start an ability or chained ability for the given target.
* Called by CombatHandler when NPC is in combat and ready for an ability.
*
* @param target The combat target
@@ -340,24 +385,61 @@ public class DataAbilities extends AbstractDataAbilities {
return false;
}
- Ability selected = selectAbility(target);
- if (selected == null) {
+ // Build eligible pool from unified action slots
+ List eligible = new ArrayList<>();
+ List weights = new ArrayList<>();
+ int totalWeight = 0;
+
+ for (AbilityAction slot : actionSlots) {
+ IAbilityAction action = slot.getAction();
+ if (action != null && isActionEligible(action, target)) {
+ eligible.add(action);
+ weights.add(action.getWeight());
+ totalWeight += action.getWeight();
+ }
+ }
+
+ if (eligible.isEmpty() || totalWeight <= 0) {
return false;
}
- return startAbility(selected, target);
+ // Weighted random selection
+ int roll = random.nextInt(totalWeight);
+ int cumulative = 0;
+ IAbilityAction selected = null;
+ for (int i = 0; i < eligible.size(); i++) {
+ cumulative += weights.get(i);
+ if (roll < cumulative) {
+ selected = eligible.get(i);
+ break;
+ }
+ }
+ if (selected == null) {
+ selected = eligible.get(eligible.size() - 1);
+ }
+
+ // Dispatch
+ lastTarget = target;
+ if (selected.isChain()) {
+ return startChain((ChainedAbility) selected, target);
+ } else {
+ return startAbility((Ability) selected, target);
+ }
}
/**
* Check if an ability can be selected right now.
*/
public boolean canSelectAbility() {
- if (!enabled || abilitySlots.isEmpty()) {
+ if (!enabled || actionSlots.isEmpty()) {
return false;
}
if (currentAbility != null && currentAbility.isExecuting()) {
return false;
}
+ if (isExecutingChain()) {
+ return false;
+ }
// Check if still on cooldown
if (npc.worldObj.getTotalWorldTime() < cooldownEndTime) {
return false;
@@ -366,65 +448,36 @@ public class DataAbilities extends AbstractDataAbilities {
}
/**
- * Select an ability using weighted random from eligible abilities.
+ * Check if an action (ability or chain) is eligible for use.
*/
- private Ability selectAbility(EntityLivingBase target) {
- List eligible = new ArrayList<>();
- int totalWeight = 0;
-
- for (Ability ability : getAbilities()) {
- if (isAbilityEligible(ability, target)) {
- eligible.add(ability);
- totalWeight += ability.getWeight();
- }
+ private boolean isActionEligible(IAbilityAction action, EntityLivingBase target) {
+ if (!action.getAllowedBy().allowsNpc()) {
+ return false;
}
-
- if (eligible.isEmpty() || totalWeight <= 0) {
- return null;
- }
-
- // Weighted random selection
- int roll = random.nextInt(totalWeight);
- int cumulative = 0;
- for (Ability ability : eligible) {
- cumulative += ability.getWeight();
- if (roll < cumulative) {
- return ability;
- }
- }
-
- return eligible.get(eligible.size() - 1);
- }
-
- /**
- * Check if a specific ability is eligible for use.
- */
- private boolean isAbilityEligible(Ability ability, EntityLivingBase target) {
- // Check UserType allows NPCs
- if (!ability.getAllowedBy().allowsNpc()) {
+ if (!action.isEnabled()) {
return false;
}
- // Check enabled
- if (!ability.isEnabled()) {
+ // Ability-specific: check if already executing
+ if (!action.isChain() && ((Ability) action).isExecuting()) {
return false;
}
- // Check if already executing
- if (ability.isExecuting()) {
+ // Chain-specific: must have entries
+ if (action.isChain() && ((ChainedAbility) action).getEntries().isEmpty()) {
return false;
}
- // Check range
+ // Range check
if (target != null) {
float distance = npc.getDistanceToEntity(target);
- if (distance < ability.getMinRange() || distance > ability.getMaxRange()) {
+ if (distance < action.getMinRange() || distance > action.getMaxRange()) {
return false;
}
}
- // Check conditions
- if (!ability.checkConditions(npc, target)) {
+ // Conditions
+ if (!action.checkConditions(npc, target)) {
return false;
}
@@ -496,7 +549,9 @@ public class DataAbilities extends AbstractDataAbilities {
return false;
}
- currentAbility.onDamageTaken(npc, (EntityLivingBase) source.getEntity(), source, amount);
+ net.minecraft.entity.Entity sourceEntity = source.getEntity();
+ EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null;
+ currentAbility.onDamageTaken(npc, attacker, source, amount);
if (currentAbility.canInterrupt(source)) {
interruptCurrentAbility(source, amount);
@@ -568,6 +623,10 @@ public class DataAbilities extends AbstractDataAbilities {
currentAbility = null;
lastTarget = null;
}
+ // Also clear chain state
+ currentChain = null;
+ chainEntryIndex = -1;
+ chainDelayRemaining = -1;
}
/**
@@ -575,7 +634,7 @@ public class DataAbilities extends AbstractDataAbilities {
* If an ability is currently executing, it will be cancelled.
*
* @param ability The ability to start
- * @param target The target entity (can be null for self-targeted abilities)
+ * @param target The target entity (can be null for self-targeted abilities)
* @return true if the ability was started successfully
*/
public boolean forceStartAbility(Ability ability, EntityLivingBase target) {
@@ -602,7 +661,7 @@ public class DataAbilities extends AbstractDataAbilities {
* Execute an ability on this NPC by key (built-in name or custom UUID).
* The NPC does NOT need to have this ability assigned.
*
- * @param key The ability key (built-in name or custom UUID)
+ * @param key The ability key (built-in name or custom UUID)
* @param target The target entity (can be null for self-targeted abilities)
* @return true if the ability was started successfully
*/
@@ -664,6 +723,7 @@ public class DataAbilities extends AbstractDataAbilities {
*/
public void reset() {
stopCurrentAbility();
+ clearActiveToggles();
// Roll cooldown so NPC doesn't immediately attack after reset
rollCooldownOnReset();
@@ -687,15 +747,22 @@ public class DataAbilities extends AbstractDataAbilities {
}
// ═══════════════════════════════════════════════════════════════════
- // ABILITY LIST MANAGEMENT
+ // ACTION SLOT MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
/**
- * Get resolved abilities from all slots, filtering out broken references.
+ * Get the unified action slot list.
+ */
+ public List getAbilityActions() {
+ return actionSlots;
+ }
+
+ /**
+ * Get resolved abilities from all ability slots, filtering out broken references and chains.
*/
public List getAbilities() {
List resolved = new ArrayList<>();
- for (AbilitySlot slot : abilitySlots) {
+ for (AbilityAction slot : actionSlots) {
Ability a = slot.getAbility();
if (a != null) {
resolved.add(a);
@@ -704,39 +771,62 @@ public class DataAbilities extends AbstractDataAbilities {
return resolved;
}
- /** Get the raw slot list (for GUI access). */
- public List getAbilitySlots() {
- return abilitySlots;
- }
-
- /** Add an inline ability. */
+ /**
+ * Add an inline ability.
+ */
public void addAbility(Ability ability) {
- abilitySlots.add(AbilitySlot.inline(ability));
+ actionSlots.add(AbilityAction.inline(ability));
}
- /** Add a reference ability by key (built-in name or custom ability name). */
+ /**
+ * Add a reference ability by key (built-in name or custom ability name).
+ */
public void addAbilityReference(String key) {
- abilitySlots.add(AbilitySlot.reference(key));
+ actionSlots.add(AbilityAction.abilityReference(key));
}
- public void removeAbility(int index) {
- if (index >= 0 && index < abilitySlots.size()) {
- abilitySlots.remove(index);
+ /**
+ * Add a chained ability reference by name.
+ */
+ public void addChainReference(String name) {
+ if (name != null && !name.isEmpty()) {
+ for (AbilityAction slot : actionSlots) {
+ if (slot.isChainReference() && name.equals(slot.getReferenceId())) {
+ return; // Prevent duplicates
+ }
+ }
+ actionSlots.add(AbilityAction.chainReference(name));
}
}
+ /**
+ * Remove an action slot by index.
+ */
+ public void removeAction(int index) {
+ if (index >= 0 && index < actionSlots.size()) {
+ actionSlots.remove(index);
+ }
+ }
+
+ public void removeAbility(int index) {
+ removeAction(index);
+ }
+
public void removeAbility(String id) {
- abilitySlots.removeIf(slot -> {
- if (slot.isReference()) {
+ actionSlots.removeIf(slot -> {
+ if (slot.isAbilityReference()) {
return slot.getReferenceId().equals(id);
}
+ if (slot.isChainReference()) {
+ return false; // Don't remove chain references via removeAbility
+ }
Ability a = slot.getAbility();
return a != null && a.getId().equals(id);
});
}
public Ability getAbility(String id) {
- for (AbilitySlot slot : abilitySlots) {
+ for (AbilityAction slot : actionSlots) {
Ability a = slot.getAbility();
if (a != null && a.getId().equals(id)) {
return a;
@@ -745,24 +835,28 @@ public class DataAbilities extends AbstractDataAbilities {
return null;
}
- /** Check if a slot at a given index is a reference. */
+ /**
+ * Check if a slot at a given index is a reference.
+ */
public boolean isSlotReference(int index) {
- if (index < 0 || index >= abilitySlots.size()) return false;
- return abilitySlots.get(index).isReference();
+ if (index < 0 || index >= actionSlots.size()) return false;
+ return actionSlots.get(index).isReference();
}
- /** Convert a reference slot to inline. Returns false if resolution fails. */
+ /**
+ * Convert a reference slot to inline. Returns false if resolution fails or is a chain.
+ */
public boolean convertToInline(int index) {
- if (index < 0 || index >= abilitySlots.size()) return false;
- return abilitySlots.get(index).convertToInline();
+ if (index < 0 || index >= actionSlots.size()) return false;
+ return actionSlots.get(index).convertToInline();
}
public void clearAbilities() {
- abilitySlots.clear();
+ actionSlots.clear();
}
public boolean isEmpty() {
- return abilitySlots.isEmpty();
+ return actionSlots.isEmpty();
}
// ═══════════════════════════════════════════════════════════════════
@@ -875,7 +969,7 @@ public class DataAbilities extends AbstractDataAbilities {
/**
* 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.
*/
@@ -977,11 +1071,21 @@ public class DataAbilities extends AbstractDataAbilities {
compound.setInteger("AbilityMinCooldown", minCooldown);
compound.setInteger("AbilityMaxCooldown", maxCooldown);
- NBTTagList abilityList = new NBTTagList();
- for (AbilitySlot slot : abilitySlots) {
- abilityList.appendTag(slot.writeNBT());
+ NBTTagList actionList = new NBTTagList();
+ for (AbilityAction slot : actionSlots) {
+ actionList.appendTag(slot.writeNBT());
}
- compound.setTag("Abilities", abilityList);
+ compound.setTag("AbilityActions", actionList);
+
+ // Active toggles (compound format with state)
+ NBTTagList toggleList = new NBTTagList();
+ for (Map.Entry entry : activeToggles.entrySet()) {
+ NBTTagCompound toggleNbt = new NBTTagCompound();
+ toggleNbt.setString("Key", entry.getKey());
+ toggleNbt.setInteger("State", entry.getValue().getState());
+ toggleList.appendTag(toggleNbt);
+ }
+ compound.setTag("ActiveToggles", toggleList);
return compound;
}
@@ -991,13 +1095,48 @@ public class DataAbilities extends AbstractDataAbilities {
minCooldown = compound.getInteger("AbilityMinCooldown");
maxCooldown = compound.getInteger("AbilityMaxCooldown");
- abilitySlots.clear();
- NBTTagList abilityList = compound.getTagList("Abilities", 10);
- for (int i = 0; i < abilityList.tagCount(); i++) {
- NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i);
- AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT);
- if (slot != null) {
- abilitySlots.add(slot);
+ actionSlots.clear();
+
+ if (compound.hasKey("AbilityActions")) {
+ // New unified format
+ NBTTagList actionList = compound.getTagList("AbilityActions", 10);
+ for (int i = 0; i < actionList.tagCount(); i++) {
+ AbilityAction slot = AbilityAction.fromNBT(actionList.getCompoundTagAt(i));
+ if (slot != null) {
+ actionSlots.add(slot);
+ }
+ }
+ } else {
+ // Legacy migration: read old separate lists
+ if (compound.hasKey("Abilities")) {
+ NBTTagList abilityList = compound.getTagList("Abilities", 10);
+ for (int i = 0; i < abilityList.tagCount(); i++) {
+ AbilityAction slot = AbilityAction.fromNBT(abilityList.getCompoundTagAt(i));
+ if (slot != null) {
+ actionSlots.add(slot);
+ }
+ }
+ }
+ if (compound.hasKey("ChainedAbilities")) {
+ NBTTagList chainList = compound.getTagList("ChainedAbilities", 8);
+ for (int i = 0; i < chainList.tagCount(); i++) {
+ String ref = chainList.getStringTagAt(i);
+ if (ref != null && !ref.isEmpty()) {
+ actionSlots.add(AbilityAction.chainReference(ref));
+ }
+ }
+ }
+ }
+
+ // Active toggles - restore state directly (no onToggleOn callback during load)
+ activeToggles.clear();
+ if (compound.hasKey("ActiveToggles")) {
+ NBTTagList toggleNbt = compound.getTagList("ActiveToggles", 10); // 10 = TAG_COMPOUND
+ for (int i = 0; i < toggleNbt.tagCount(); i++) {
+ NBTTagCompound entry = toggleNbt.getCompoundTagAt(i);
+ String key = entry.getString("Key");
+ int state = entry.hasKey("State") ? entry.getInteger("State") : 1;
+ setToggleEntryDirect(key, state);
}
}
}
diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java
index 2fefb23ac..76029adab 100644
--- a/src/main/java/noppes/npcs/EventHooks.java
+++ b/src/main/java/noppes/npcs/EventHooks.java
@@ -20,9 +20,9 @@ import net.minecraftforge.event.world.WorldEvent;
import noppes.npcs.api.IBlock;
import noppes.npcs.api.IPos;
import noppes.npcs.api.IWorld;
-import noppes.npcs.api.entity.IEnergyProjectile;
import noppes.npcs.api.entity.IAnimatable;
import noppes.npcs.api.entity.ICustomNpc;
+import noppes.npcs.api.entity.IEnergyProjectile;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.entity.IProjectile;
@@ -64,6 +64,7 @@ import noppes.npcs.scripted.event.NpcEvent;
import noppes.npcs.scripted.event.PartyEvent;
import noppes.npcs.scripted.event.ProjectileEvent;
import noppes.npcs.scripted.event.RecipeScriptEvent;
+import noppes.npcs.scripted.event.player.AuctionEvent;
import noppes.npcs.scripted.event.player.CustomGuiEvent;
import noppes.npcs.scripted.event.player.DialogEvent;
import noppes.npcs.scripted.event.player.FactionEvent;
@@ -560,7 +561,7 @@ public class EventHooks {
// ==================== ENERGY BARRIER EVENTS ====================
private static void dispatchBarrierEvent(net.minecraft.entity.Entity barrierEntity, int ownerEntityId,
- noppes.npcs.scripted.event.EnergyBarrierEvent event, EnumScriptType type) {
+ noppes.npcs.scripted.event.EnergyBarrierEvent event, EnumScriptType type) {
ScriptController.Instance.globalNpcScripts.callScript(type, event);
net.minecraft.entity.Entity owner = barrierEntity.worldObj.getEntityByID(ownerEntityId);
@@ -880,6 +881,16 @@ public class EventHooks {
return NpcAPI.EVENT_BUS.post(event);
}
+ public static boolean onPlayerAbilityToggle(PlayerDataScript handler, PlayerAbilityEvent.ToggleEvent event) {
+ handler.callScript(EnumScriptType.ABILITY_TOGGLE, event);
+ return NpcAPI.EVENT_BUS.post(event);
+ }
+
+ public static void onPlayerAbilityToggleUpdate(PlayerDataScript handler, PlayerAbilityEvent.ToggleUpdateEvent event) {
+ handler.callScript(EnumScriptType.ABILITY_TOGGLE_UPDATE, event);
+ NpcAPI.EVENT_BUS.post(event);
+ }
+
public static void onPlayerChangeDim(PlayerDataScript handler, IPlayer player, int fromDim, int toDim) {
PlayerEvent.ChangedDimension event = new PlayerEvent.ChangedDimension(player, fromDim, toDim);
handler.callScript(EnumScriptType.CHANGED_DIM, event);
@@ -1253,4 +1264,31 @@ public class EventHooks {
}
postAnimationEvent(new AnimationEvent.FrameEvent.Exited(animation, frame));
}
+
+ // ==================== AUCTION EVENTS ====================
+
+ public static boolean onAuctionCreate(PlayerDataScript handler, AuctionEvent.CreateEvent event) {
+ handler.callScript(EnumScriptType.AUCTION_CREATE, event);
+ return NpcAPI.EVENT_BUS.post(event);
+ }
+
+ public static boolean onAuctionBid(PlayerDataScript handler, AuctionEvent.BidEvent event) {
+ handler.callScript(EnumScriptType.AUCTION_BID, event);
+ return NpcAPI.EVENT_BUS.post(event);
+ }
+
+ public static boolean onAuctionBuyout(PlayerDataScript handler, AuctionEvent.BuyoutEvent event) {
+ handler.callScript(EnumScriptType.AUCTION_BUYOUT, event);
+ return NpcAPI.EVENT_BUS.post(event);
+ }
+
+ public static boolean onAuctionCancel(PlayerDataScript handler, AuctionEvent.CancelEvent event) {
+ handler.callScript(EnumScriptType.AUCTION_CANCEL, event);
+ return NpcAPI.EVENT_BUS.post(event);
+ }
+
+ public static boolean onAuctionClaim(PlayerDataScript handler, AuctionEvent.ClaimEvent event) {
+ handler.callScript(EnumScriptType.AUCTION_CLAIM, event);
+ return NpcAPI.EVENT_BUS.post(event);
+ }
}
diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java
index bbb19c97d..521c0cb02 100644
--- a/src/main/java/noppes/npcs/NoppesUtilServer.java
+++ b/src/main/java/noppes/npcs/NoppesUtilServer.java
@@ -1,9 +1,9 @@
package noppes.npcs;
import io.netty.buffer.ByteBuf;
+import kamkeel.npcs.controllers.AbilityController;
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;
import kamkeel.npcs.network.packets.data.PlayerDataInfoPacket;
@@ -807,7 +807,7 @@ public class NoppesUtilServer {
public static void sendCustomAbilitiesData(EntityPlayerMP player) {
HashMap map = new HashMap();
- kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance;
+ AbilityController controller = AbilityController.Instance;
if (controller != null) {
int index = 0;
for (String name : controller.getCustomAbilityNames()) {
@@ -819,6 +819,15 @@ public class NoppesUtilServer {
sendScrollData(player, map, EnumScrollData.CUSTOM_ABILITIES);
}
+ public static void sendChainedAbilitiesData(EntityPlayerMP player) {
+ HashMap map = new HashMap();
+ int index = 0;
+ for (String name : AbilityController.Instance.getChainedAbilityNamesSet()) {
+ map.put(name, index++);
+ }
+ sendScrollData(player, map, EnumScrollData.CHAINED_ABILITIES);
+ }
+
public static DialogOption setNpcDialog(int slot, int dialogId, EntityPlayer player) throws IOException {
return setNpcDialog(slot, dialogId, getEditingNpc(player));
}
diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java
index c0405c193..81ff9ed6b 100644
--- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java
+++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java
@@ -6,10 +6,10 @@ import cpw.mods.fml.common.gameevent.PlayerEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import cpw.mods.fml.relauncher.Side;
import kamkeel.npcs.addon.DBCAddon;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.AttributeController;
import kamkeel.npcs.controllers.SyncController;
import kamkeel.npcs.controllers.data.ability.Ability;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.util.AttributeAttackUtil;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
@@ -332,10 +332,10 @@ public class ScriptPlayerEventHandler {
return;
if (event.player.worldObj instanceof WorldServer && event.player instanceof EntityPlayerMP) {
- // Cancel any executing ability on dimension change
+ // Fully reset ability state on dimension change (no cooldown rollover)
PlayerData playerData = PlayerData.get(event.player);
if (playerData != null) {
- playerData.abilityData.interruptCurrentAbility();
+ playerData.abilityData.resetOnDimensionChange();
}
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(event.player);
diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java
index 2639c6ab5..220a0e6ef 100644
--- a/src/main/java/noppes/npcs/ServerTickHandler.java
+++ b/src/main/java/noppes/npcs/ServerTickHandler.java
@@ -6,9 +6,9 @@ import cpw.mods.fml.common.gameevent.TickEvent;
import cpw.mods.fml.common.gameevent.TickEvent.Phase;
import kamkeel.npcs.controllers.ProfileController;
import kamkeel.npcs.controllers.SyncController;
-import noppes.npcs.controllers.AuctionController;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.world.WorldServer;
+import noppes.npcs.controllers.AuctionController;
import noppes.npcs.controllers.ScriptController;
import noppes.npcs.controllers.data.PlayerData;
import noppes.npcs.controllers.data.action.ActionManager;
diff --git a/src/main/java/noppes/npcs/ai/CombatHandler.java b/src/main/java/noppes/npcs/ai/CombatHandler.java
index ff81e4821..97d9a90eb 100644
--- a/src/main/java/noppes/npcs/ai/CombatHandler.java
+++ b/src/main/java/noppes/npcs/ai/CombatHandler.java
@@ -1,7 +1,6 @@
package noppes.npcs.ai;
import kamkeel.npcs.addon.DBCAddon;
-import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
diff --git a/src/main/java/noppes/npcs/blocks/BlockWaypoint.java b/src/main/java/noppes/npcs/blocks/BlockWaypoint.java
index 7e2dd87d3..e91f43d25 100644
--- a/src/main/java/noppes/npcs/blocks/BlockWaypoint.java
+++ b/src/main/java/noppes/npcs/blocks/BlockWaypoint.java
@@ -12,10 +12,8 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
import noppes.npcs.CustomItems;
-import noppes.npcs.CustomNpcs;
import noppes.npcs.CustomNpcsPermissions;
import noppes.npcs.blocks.tiles.TileWaypoint;
-import noppes.npcs.constants.EnumGuiType;
public class BlockWaypoint extends BlockContainer {
diff --git a/src/main/java/noppes/npcs/client/AuctionClientConfig.java b/src/main/java/noppes/npcs/client/AuctionClientConfig.java
index 7d6854e4c..a94bab504 100644
--- a/src/main/java/noppes/npcs/client/AuctionClientConfig.java
+++ b/src/main/java/noppes/npcs/client/AuctionClientConfig.java
@@ -7,17 +7,17 @@ import net.minecraft.nbt.NBTTagCompound;
/**
* Client-side cache for auction configuration synced from server.
* Prevents client from using local ConfigMarket which may differ from server settings.
- *
+ *
* IMPORTANT: This class is CLIENT-ONLY. Server-side NBT writing is done in
* AuctionConfigSync to avoid @SideOnly crashes on dedicated servers.
*/
@SideOnly(Side.CLIENT)
public class AuctionClientConfig {
// Cached values from server
- private static long listingFee = 0;
+ private static volatile long listingFee = 0;
private static String currencyName = "Coins";
private static int auctionDurationHours = 24;
- private static double minBidIncrement = 0.05;
+ private static volatile double minBidIncrement = 0.05;
private static int maxActiveListings = 8;
private static int claimExpirationDays = 20;
private static boolean auctionEnabled = false;
@@ -38,7 +38,7 @@ public class AuctionClientConfig {
claimExpirationDays = compound.getInteger("ClaimExpirationDays");
if (currencyName.isEmpty()) {
- currencyName = "Gold";
+ currencyName = "Coins";
}
}
@@ -56,11 +56,31 @@ public class AuctionClientConfig {
}
// Getters
- public static boolean isAuctionEnabled() { return auctionEnabled; }
- public static long getListingFee() { return listingFee; }
- public static String getCurrencyName() { return currencyName; }
- public static int getAuctionDurationHours() { return auctionDurationHours; }
- public static double getMinBidIncrement() { return minBidIncrement; }
- public static int getMaxActiveListings() { return maxActiveListings; }
- public static int getClaimExpirationDays() { return claimExpirationDays; }
+ public static boolean isAuctionEnabled() {
+ return auctionEnabled;
+ }
+
+ public static long getListingFee() {
+ return listingFee;
+ }
+
+ public static String getCurrencyName() {
+ return currencyName;
+ }
+
+ public static int getAuctionDurationHours() {
+ return auctionDurationHours;
+ }
+
+ public static double getMinBidIncrement() {
+ return minBidIncrement;
+ }
+
+ public static int getMaxActiveListings() {
+ return maxActiveListings;
+ }
+
+ public static int getClaimExpirationDays() {
+ return claimExpirationDays;
+ }
}
diff --git a/src/main/java/noppes/npcs/client/ClientEventHandler.java b/src/main/java/noppes/npcs/client/ClientEventHandler.java
index 6fc690b5a..a73ccf707 100644
--- a/src/main/java/noppes/npcs/client/ClientEventHandler.java
+++ b/src/main/java/noppes/npcs/client/ClientEventHandler.java
@@ -20,12 +20,12 @@ import net.minecraftforge.client.event.RenderHandEvent;
import net.minecraftforge.client.event.RenderLivingEvent;
import net.minecraftforge.client.event.RenderPlayerEvent;
import net.minecraftforge.event.entity.player.ItemTooltipEvent;
-import noppes.npcs.client.gui.player.AuctionTooltipHandler;
import noppes.npcs.client.gui.customoverlay.OverlayCustom;
import noppes.npcs.client.gui.hud.ClientHudManager;
import noppes.npcs.client.gui.hud.CompassHudComponent;
import noppes.npcs.client.gui.hud.EnumHudComponent;
import noppes.npcs.client.gui.hud.QuestTrackingComponent;
+import noppes.npcs.client.gui.player.AuctionTooltipHandler;
import noppes.npcs.client.renderer.MarkRenderer;
import noppes.npcs.client.renderer.RenderCNPCPlayer;
import noppes.npcs.constants.EnumAnimationPart;
@@ -91,9 +91,9 @@ public class ClientEventHandler {
// Block mouse movement during ability rotation lock.
// Allow clicks (button >= 0) and scroll (dwheel != 0) to pass through.
if (mc.theWorld != null && mc.thePlayer != null
- && ClientAbilityState.shouldLockRotation()
- && (event.dx != 0 || event.dy != 0)
- && event.button == -1 && event.dwheel == 0) {
+ && ClientAbilityState.shouldLockRotation()
+ && (event.dx != 0 || event.dy != 0)
+ && event.button == -1 && event.dwheel == 0) {
event.setCanceled(true);
return;
}
diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java
index 0790e3dd3..55359c70d 100644
--- a/src/main/java/noppes/npcs/client/ClientProxy.java
+++ b/src/main/java/noppes/npcs/client/ClientProxy.java
@@ -6,8 +6,27 @@ import cpw.mods.fml.client.registry.RenderingRegistry;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.ObfuscationReflectionHelper;
import kamkeel.npcs.addon.client.DBCClient;
+import kamkeel.npcs.client.renderer.RenderAbilityBeam;
+import kamkeel.npcs.client.renderer.RenderAbilityDisc;
+import kamkeel.npcs.client.renderer.RenderAbilityLaser;
+import kamkeel.npcs.client.renderer.RenderAbilityOrb;
+import kamkeel.npcs.client.renderer.RenderAbilitySweeper;
+import kamkeel.npcs.client.renderer.RenderAbilityZone;
+import kamkeel.npcs.client.renderer.RenderEnergyDome;
+import kamkeel.npcs.client.renderer.RenderEnergyPanel;
+import kamkeel.npcs.client.renderer.RenderEnergySlicer;
import kamkeel.npcs.client.renderer.TelegraphRenderer;
+import kamkeel.npcs.client.renderer.lightning.LightningHandler;
import kamkeel.npcs.controllers.data.telegraph.TelegraphManager;
+import kamkeel.npcs.entity.EntityAbilityBeam;
+import kamkeel.npcs.entity.EntityAbilityDisc;
+import kamkeel.npcs.entity.EntityAbilityLaser;
+import kamkeel.npcs.entity.EntityAbilityOrb;
+import kamkeel.npcs.entity.EntityAbilitySweeper;
+import kamkeel.npcs.entity.EntityAbilityZone;
+import kamkeel.npcs.entity.EntityEnergyDome;
+import kamkeel.npcs.entity.EntityEnergyPanel;
+import kamkeel.npcs.entity.EntityEnergySlicer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.model.ModelBiped;
@@ -88,12 +107,12 @@ import noppes.npcs.client.gui.global.GuiNPCManageAnimations;
import noppes.npcs.client.gui.global.GuiNPCManageBanks;
import noppes.npcs.client.gui.global.GuiNPCManageDialogs;
import noppes.npcs.client.gui.global.GuiNPCManageEffects;
-import noppes.npcs.client.gui.global.GuiNpcManageAbilities;
import noppes.npcs.client.gui.global.GuiNPCManageFactions;
import noppes.npcs.client.gui.global.GuiNPCManageLinked;
import noppes.npcs.client.gui.global.GuiNPCManageQuest;
import noppes.npcs.client.gui.global.GuiNPCManageTags;
import noppes.npcs.client.gui.global.GuiNPCManageTransporters;
+import noppes.npcs.client.gui.global.GuiNpcManageAbilities;
import noppes.npcs.client.gui.global.GuiNpcManageMagic;
import noppes.npcs.client.gui.global.GuiNpcManageRecipes;
import noppes.npcs.client.gui.global.GuiNpcQuestReward;
@@ -105,15 +124,15 @@ import noppes.npcs.client.gui.mainmenu.GuiNpcAI;
import noppes.npcs.client.gui.mainmenu.GuiNpcAdvanced;
import noppes.npcs.client.gui.mainmenu.GuiNpcDisplay;
import noppes.npcs.client.gui.mainmenu.GuiNpcStats;
+import noppes.npcs.client.gui.player.GuiAuctionBidding;
+import noppes.npcs.client.gui.player.GuiAuctionListing;
+import noppes.npcs.client.gui.player.GuiAuctionSell;
+import noppes.npcs.client.gui.player.GuiAuctionTrades;
import noppes.npcs.client.gui.player.GuiBigSign;
import noppes.npcs.client.gui.player.GuiCrate;
import noppes.npcs.client.gui.player.GuiMailbox;
import noppes.npcs.client.gui.player.GuiMailmanWrite;
import noppes.npcs.client.gui.player.GuiNPCBankChest;
-import noppes.npcs.client.gui.player.GuiAuctionBidding;
-import noppes.npcs.client.gui.player.GuiAuctionTrades;
-import noppes.npcs.client.gui.player.GuiAuctionListing;
-import noppes.npcs.client.gui.player.GuiAuctionSell;
import noppes.npcs.client.gui.player.GuiNPCTrader;
import noppes.npcs.client.gui.player.GuiNpcAnvil;
import noppes.npcs.client.gui.player.GuiNpcCarpentryBench;
@@ -148,25 +167,6 @@ import noppes.npcs.client.renderer.RenderNpcCrystal;
import noppes.npcs.client.renderer.RenderNpcDragon;
import noppes.npcs.client.renderer.RenderNpcSlime;
import noppes.npcs.client.renderer.RenderProjectile;
-import kamkeel.npcs.client.renderer.RenderAbilityOrb;
-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.RenderEnergyDome;
-import kamkeel.npcs.client.renderer.RenderEnergyPanel;
-import kamkeel.npcs.client.renderer.RenderEnergySlicer;
-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 kamkeel.npcs.entity.EntityEnergyDome;
-import kamkeel.npcs.entity.EntityEnergyPanel;
-import kamkeel.npcs.entity.EntityEnergySlicer;
import noppes.npcs.client.renderer.blocks.BlockBannerRenderer;
import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer;
import noppes.npcs.client.renderer.blocks.BlockBeamRenderer;
@@ -202,13 +202,16 @@ import noppes.npcs.client.renderer.items.ItemShortLampRenderer;
import noppes.npcs.client.renderer.items.ItemTallLampRenderer;
import noppes.npcs.client.renderer.items.ItemToolRenderer;
import noppes.npcs.client.renderer.items.ScriptedBlockItemRenderer;
-import noppes.npcs.client.ScriptClientConfig;
import noppes.npcs.config.ConfigClient;
import noppes.npcs.config.ConfigItem;
import noppes.npcs.config.ConfigMain;
import noppes.npcs.config.StringCache;
import noppes.npcs.constants.EnumGuiType;
import noppes.npcs.containers.ContainerAnvilRepair;
+import noppes.npcs.containers.ContainerAuctionBidding;
+import noppes.npcs.containers.ContainerAuctionListing;
+import noppes.npcs.containers.ContainerAuctionSell;
+import noppes.npcs.containers.ContainerAuctionTrades;
import noppes.npcs.containers.ContainerCarpentryBench;
import noppes.npcs.containers.ContainerCrate;
import noppes.npcs.containers.ContainerCustomGui;
@@ -221,11 +224,6 @@ import noppes.npcs.containers.ContainerNPCFollower;
import noppes.npcs.containers.ContainerNPCFollowerHire;
import noppes.npcs.containers.ContainerNPCFollowerSetup;
import noppes.npcs.containers.ContainerNPCInv;
-import noppes.npcs.containers.ContainerAuction;
-import noppes.npcs.containers.ContainerAuctionBidding;
-import noppes.npcs.containers.ContainerAuctionTrades;
-import noppes.npcs.containers.ContainerAuctionListing;
-import noppes.npcs.containers.ContainerAuctionSell;
import noppes.npcs.containers.ContainerNPCTrader;
import noppes.npcs.containers.ContainerNPCTraderSetup;
import noppes.npcs.containers.ContainerNpcItemGiver;
@@ -527,9 +525,7 @@ public class ClientProxy extends CommonProxy {
abilityNpc.width = 0.6f;
}
return new GuiNpcManageAbilities(abilityNpc, npc != null);
- }
-
- else if (gui == EnumGuiType.MainMenuGlobal)
+ } else if (gui == EnumGuiType.MainMenuGlobal)
return new GuiNPCGlobalMainMenu(npc);
else if (gui == EnumGuiType.MainMenuAI)
diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java
index d993762ee..5a59461b7 100644
--- a/src/main/java/noppes/npcs/client/ClientTickHandler.java
+++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java
@@ -6,7 +6,7 @@ import cpw.mods.fml.common.gameevent.InputEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import cpw.mods.fml.common.gameevent.TickEvent.Phase;
import kamkeel.npcs.client.renderer.lightning.LightningBolt;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.telegraph.TelegraphManager;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.player.CheckPlayerValue;
@@ -136,8 +136,8 @@ public class ClientTickHandler {
// Prevent jumping when locked (allow gravity only if not flying)
if ((ClientAbilityState.movementLocked || ClientAbilityState.positionLocked)
- && !ClientAbilityState.hasAbilityMovement
- && !AbilityController.Instance.isPlayerFlying(mc.thePlayer)) {
+ && !ClientAbilityState.hasAbilityMovement
+ && !AbilityController.Instance.isPlayerFlying(mc.thePlayer)) {
mc.thePlayer.motionY = Math.min(mc.thePlayer.motionY, 0);
}
}
@@ -226,7 +226,7 @@ public class ClientTickHandler {
// Block movement keys during ability lock (only when no GUI is open and world exists).
// Non-movement keys (ESC, chat, inventory, etc.) always pass through.
if (mc.theWorld != null && mc.currentScreen == null && keyDown
- && ClientAbilityState.shouldSuppressMovementInput() && isMovementKey(key)) {
+ && ClientAbilityState.shouldSuppressMovementInput() && isMovementKey(key)) {
// Unpress the keybind so vanilla doesn't process it
KeyBinding.setKeyBindState(key, false);
return;
diff --git a/src/main/java/noppes/npcs/client/ScriptClientConfig.java b/src/main/java/noppes/npcs/client/ScriptClientConfig.java
index 863f991ba..2af091723 100644
--- a/src/main/java/noppes/npcs/client/ScriptClientConfig.java
+++ b/src/main/java/noppes/npcs/client/ScriptClientConfig.java
@@ -7,7 +7,7 @@ import net.minecraft.nbt.NBTTagCompound;
/**
* Client-side cache for script configuration synced from server.
* Prevents client from using local ConfigScript which may differ from server settings.
- *
+ *
* IMPORTANT: This class is CLIENT-ONLY. Server-side NBT writing is done in
* ScriptConfigSync to avoid @SideOnly crashes on dedicated servers.
*/
@@ -46,16 +46,44 @@ public class ScriptClientConfig {
}
// Setters for individual packet updates
- public static void setScriptingEnabled(boolean value) { scriptingEnabled = value; }
- public static void setRunLoadedScriptsFirst(boolean value) { runLoadedScriptsFirst = value; }
- public static void setGlobalPlayerScripts(boolean value) { globalPlayerScripts = value; }
- public static void setGlobalForgeScripts(boolean value) { globalForgeScripts = value; }
- public static void setGlobalNPCScripts(boolean value) { globalNPCScripts = value; }
+ public static void setScriptingEnabled(boolean value) {
+ scriptingEnabled = value;
+ }
+
+ public static void setRunLoadedScriptsFirst(boolean value) {
+ runLoadedScriptsFirst = value;
+ }
+
+ public static void setGlobalPlayerScripts(boolean value) {
+ globalPlayerScripts = value;
+ }
+
+ public static void setGlobalForgeScripts(boolean value) {
+ globalForgeScripts = value;
+ }
+
+ public static void setGlobalNPCScripts(boolean value) {
+ globalNPCScripts = value;
+ }
// Getters
- public static boolean isScriptingEnabled() { return scriptingEnabled; }
- public static boolean isRunLoadedScriptsFirst() { return runLoadedScriptsFirst; }
- public static boolean isGlobalPlayerScripts() { return globalPlayerScripts; }
- public static boolean isGlobalForgeScripts() { return globalForgeScripts; }
- public static boolean isGlobalNPCScripts() { return globalNPCScripts; }
+ public static boolean isScriptingEnabled() {
+ return scriptingEnabled;
+ }
+
+ public static boolean isRunLoadedScriptsFirst() {
+ return runLoadedScriptsFirst;
+ }
+
+ public static boolean isGlobalPlayerScripts() {
+ return globalPlayerScripts;
+ }
+
+ public static boolean isGlobalForgeScripts() {
+ return globalForgeScripts;
+ }
+
+ public static boolean isGlobalNPCScripts() {
+ return globalNPCScripts;
+ }
}
diff --git a/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java b/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java
index 80f6f687b..eb04c2159 100644
--- a/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java
+++ b/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java
@@ -16,9 +16,9 @@ public class ZoneParticleFX extends EntityFX {
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) {
+ 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;
diff --git a/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerFullscreen.java b/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerFullscreen.java
index 4919ccbae..80e0a971f 100644
--- a/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerFullscreen.java
+++ b/src/main/java/noppes/npcs/client/gui/GuiNpcMobSpawnerFullscreen.java
@@ -74,10 +74,10 @@ public class GuiNpcMobSpawnerFullscreen extends GuiDirectory implements IGuiData
this.posZ = posZ;
// Configure GuiDirectory layout percentages
- leftPanelPercent = 0.15f;
+ leftPanelPercent = 0.15f;
rightPanelPercent = 0.08f;
- minLeftPanelW = 120;
- minRightPanelW = 62;
+ minLeftPanelW = 120;
+ minRightPanelW = 62;
PacketClient.sendClient(new CloneAllTagsPacket());
if (GuiNpcMobSpawner.showingClones == 2) {
diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java
index 86f174bc4..17996759e 100644
--- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java
+++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java
@@ -1,9 +1,6 @@
package noppes.npcs.client.gui;
-import kamkeel.npcs.network.PacketClient;
-import kamkeel.npcs.network.packets.request.dialog.DialogSavePacket;
import net.minecraft.client.gui.GuiButton;
-import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.NoppesUtil;
import noppes.npcs.client.gui.util.GuiCustomScroll;
import noppes.npcs.client.gui.util.GuiMenuTopButton;
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java
index 2e89d3d49..0a83895ed 100644
--- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java
+++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java
@@ -1,9 +1,11 @@
package noppes.npcs.client.gui.advanced;
+import kamkeel.npcs.controllers.AbilityController;
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.AbilityAction;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
+import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket;
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket;
@@ -21,6 +23,7 @@ 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.IAbilityConfigCallback;
+import noppes.npcs.client.gui.util.IChainedAbilityConfigCallback;
import noppes.npcs.client.gui.util.ICustomScrollListener;
import noppes.npcs.client.gui.util.IGuiData;
import noppes.npcs.client.gui.util.IScrollData;
@@ -30,41 +33,90 @@ import noppes.npcs.client.gui.util.SubGuiInterface;
import noppes.npcs.constants.EnumScrollData;
import noppes.npcs.entity.EntityNPCInterface;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.Vector;
/**
* GUI for managing NPC abilities.
* Shows list of assigned abilities with add/remove/edit controls.
*/
-public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, ICustomScrollListener, IGuiData, ITextfieldListener, ISubGuiListener, IAbilityConfigCallback {
+public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, ICustomScrollListener, IGuiData, ITextfieldListener, ISubGuiListener, IAbilityConfigCallback, IChainedAbilityConfigCallback {
+ // ── Button IDs ────────────────────────────────────────────────────────────
+ private static final int BTN_SCROLL_TYPE = 50;
+ private static final int BTN_ADD_ABILITY = 70;
+ private static final int BTN_REMOVE = 71;
+ private static final int BTN_EDIT = 72;
+ private static final int BTN_MOVE_UP = 73;
+ private static final int BTN_MOVE_DOWN = 74;
+ private static final int BTN_LOAD = 75;
+ private static final int BTN_SAVE_PRESET = 76;
+ private static final int BTN_TOGGLE_SLOT = 77;
+ private static final int BTN_ADD_CHAIN = 78;
+ private static final int BTN_ENABLED = 100;
+
+ // ── Scroll IDs ────────────────────────────────────────────────────────────
+ private static final int SCROLL_TYPES = 0;
+ private static final int SCROLL_NPC = 1;
+
+ // ── TextField / Label IDs ─────────────────────────────────────────────────
+ private static final int TF_MIN_COOLDOWN = 101;
+ private static final int TF_MAX_COOLDOWN = 102;
+ private static final int TF_SEARCH = 4;
+ private static final int LBL_TYPES = 1;
+ private static final int LBL_NPC = 2;
+ private static final int LBL_MIN_COOLDOWN = 101;
+ private static final int LBL_MAX_COOLDOWN = 102;
+
+ // ── Scroll lists ──────────────────────────────────────────────────────────
private GuiCustomScroll availableTypesScroll;
private GuiCustomScroll npcAbilitiesScroll;
- // All available ability types (typeId -> index)
+ // ── Available type data ───────────────────────────────────────────────────
private final HashMap allAbilityTypes = new HashMap<>();
private final HashMap filteredAbilityTypes = new HashMap<>();
-
- // Display name to typeId mapping for the available types scroll
private final HashMap displayNameToTypeId = new HashMap<>();
- // NPC's current ability slots
- private final List npcSlots = new ArrayList<>();
+ // ── NPC slot data ─────────────────────────────────────────────────────────
+ private final List npcSlots = new ArrayList<>();
+ private final List rowMapping = new ArrayList<>(); // [slotIdx, entryIdx], entryIdx=-1 = header/standalone
- // Settings
+ // ── NPC settings ──────────────────────────────────────────────────────────
private boolean abilitiesEnabled = false;
private int minCooldown = 20;
private int maxCooldown = 60;
+ // ── Selection state ───────────────────────────────────────────────────────
private String search = "";
private int selectedAbilityIndex = -1;
+ private int selectedSlotIndex = -1;
+ private int selectedEntryIndex = -1;
- // Existing preset names for duplicate checking
+ // ── Pending: chain editing ────────────────────────────────────────────────
+ private ChainedAbility pendingChain = null;
+ private int pendingChainSlotIdx = -1;
+
+ // ── Pending: inline chain-entry editing ──────────────────────────────────
+ private boolean editingChainEntry = false;
+ private int editChainSlotIdx = -1;
+ private int editChainEntryIdx = -1;
+ private boolean editingChainEntryParent = false;
+
+ // ── Pending: save-as-preset ───────────────────────────────────────────────
+ private int pendingSaveSlotIdx = -1;
+ private int pendingSaveEntryIdx = -1; // -1 for standalone ability
private final Set existingPresetNames = new HashSet<>();
- // Pending variant selection
+ // ── Pending: variant selection ────────────────────────────────────────────
private String pendingTypeId = null;
+ // ── Static view state (persists across GUI opens) ─────────────────────────
public static int modIndex = 0;
public static ScrollType scrollType = ScrollType.CNPC;
@@ -83,7 +135,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
public String toString() {
switch (this) {
- case CNPC: return "CustomNPCs";
+ case CNPC:
+ return "CustomNPCs";
case MODDED:
if (Register.isEmpty("ability"))
return "modded";
@@ -91,8 +144,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
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();
+ case ALL:
+ return "filter.all";
+ default:
+ return name();
}
}
}
@@ -103,17 +158,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
int y = guiTop + 10;
- addButton(new GuiNpcButton(50, guiLeft + 5, y, 140, 20, scrollType.toString()));
+ addButton(new GuiNpcButton(BTN_SCROLL_TYPE, guiLeft + 5, y, 140, 20, scrollType.toString()));
// Cooldown range: Min Cooldown and Max Cooldown (spaced apart more)
- addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 210, y + 5));
- GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 260, y, 40, 20, "" + minCooldown);
+ addLabel(new GuiNpcLabel(LBL_MIN_COOLDOWN, "ability.minCooldown", guiLeft + 210, y + 5));
+ GuiNpcTextField minField = new GuiNpcTextField(TF_MIN_COOLDOWN, this, fontRendererObj, guiLeft + 260, y, 40, 20, "" + minCooldown);
minField.setIntegersOnly();
minField.setMinMaxDefault(0, 10000, 20);
addTextField(minField);
- addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 320, y + 5));
- GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 370, y, 40, 20, "" + maxCooldown);
+ addLabel(new GuiNpcLabel(LBL_MAX_COOLDOWN, "ability.maxCooldown", guiLeft + 320, y + 5));
+ GuiNpcTextField maxField = new GuiNpcTextField(TF_MAX_COOLDOWN, this, fontRendererObj, guiLeft + 370, y, 40, 20, "" + maxCooldown);
maxField.setIntegersOnly();
maxField.setMinMaxDefault(0, 10000, 60);
addTextField(maxField);
@@ -121,13 +176,13 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
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;
+ addButton(new GuiNpcButton(BTN_ENABLED, guiLeft + 334, y + 145, 76, 20, new String[]{"gui.disabled", "gui.enabled"}, abilitiesEnabled ? 1 : 0));
+ getButton(BTN_ENABLED).packedFGColour = abilitiesEnabled ? 0x00FF00 : 0xFF0000;
// Left scroll: available ability types
- addLabel(new GuiNpcLabel(1, "ability.availableTypes", guiLeft + 5, y));
+ addLabel(new GuiNpcLabel(LBL_TYPES, "ability.availableTypes", guiLeft + 5, y));
if (availableTypesScroll == null) {
- availableTypesScroll = new GuiCustomScroll(this, 0);
+ availableTypesScroll = new GuiCustomScroll(this, SCROLL_TYPES);
availableTypesScroll.setSize(140, 130);
}
availableTypesScroll.guiLeft = guiLeft + 5;
@@ -136,12 +191,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
addScroll(availableTypesScroll);
// Search bar for types
- addTextField(new GuiNpcTextField(4, this, fontRendererObj, guiLeft + 5, y + 145, 140, 18, search));
+ addTextField(new GuiNpcTextField(TF_SEARCH, this, fontRendererObj, guiLeft + 5, y + 145, 140, 18, search));
// Right scroll: NPC's abilities (widened to use available space)
- addLabel(new GuiNpcLabel(2, "ability.npcAbilities", guiLeft + 210, y));
+ addLabel(new GuiNpcLabel(LBL_NPC, "ability.npcAbilities", guiLeft + 210, y));
if (npcAbilitiesScroll == null) {
- npcAbilitiesScroll = new GuiCustomScroll(this, 1);
+ npcAbilitiesScroll = new GuiCustomScroll(this, SCROLL_NPC);
npcAbilitiesScroll.setSize(200, 130);
} else {
npcAbilitiesScroll.setSize(200, 130);
@@ -153,41 +208,75 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
// Center buttons: Add/Remove
int centerX = guiLeft + 158;
- 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);
+ addButton(new GuiNpcButton(BTN_ADD_ABILITY, centerX, y + 15, 40, 20, ">>>")); // Add
+ addButton(new GuiNpcButton(BTN_REMOVE, centerX, y + 37, 40, 20, "<<<")); // Remove
+ getButton(BTN_REMOVE).setEnabled(selectedAbilityIndex >= 0);
// Load button (under the add/remove arrows)
- addButton(new GuiNpcButton(75, centerX, y + 60, 40, 20, "gui.load"));
+ addButton(new GuiNpcButton(BTN_LOAD, centerX, y + 60, 40, 20, "gui.load"));
- // On/Off toggle for selected ability (under Load button)
- if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
- Ability selectedAbility = npcSlots.get(selectedAbilityIndex).getAbility();
- GuiNpcButton toggleBtn = new GuiNpcButton(77, centerX, y + 112, 40, 20,
- new String[]{"gui.off", "gui.on"}, selectedAbility.isEnabled() ? 1 : 0);
+ // On/Off toggle for selected slot (under Load button) — chains and abilities
+ if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() && selectedEntryIndex == -1) {
+ AbilityAction selectedSlot = npcSlots.get(selectedSlotIndex);
+ boolean isEnabled = false;
+ if (selectedSlot.isChain()) {
+ ChainedAbility chain = selectedSlot.isInlineChain() ? selectedSlot.getInlineChain() : selectedSlot.getChainedAbility();
+ isEnabled = chain != null && chain.isEnabled();
+ } else {
+ Ability ability = selectedSlot.getAbility();
+ isEnabled = ability != null && ability.isEnabled();
+ }
+ GuiNpcButton toggleBtn = new GuiNpcButton(BTN_TOGGLE_SLOT, centerX, y + 112, 40, 20,
+ new String[]{"gui.off", "gui.on"}, isEnabled ? 1 : 0);
addButton(toggleBtn);
}
- // Check if selected ability is built-in (non-editable)
+ // Check if selected slot is editable
boolean selectedIsBuiltIn = false;
- if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
- Ability sel = npcSlots.get(selectedAbilityIndex).getAbility();
- if (sel != null && sel.isBuiltIn()) selectedIsBuiltIn = true;
+ boolean selectedIsChainEntry = selectedEntryIndex >= 0;
+ boolean selectedIsRefChainEntry = false;
+ if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) {
+ AbilityAction sel = npcSlots.get(selectedSlotIndex);
+ if (!selectedIsChainEntry && !sel.isChain()) {
+ Ability a = sel.getAbility();
+ if (a != null && a.isBuiltIn()) selectedIsBuiltIn = true;
+ }
+ // Sub-entries of reference chains are not directly editable from the NPC list
+ if (selectedIsChainEntry && sel.isChainReference()) {
+ selectedIsRefChainEntry = 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 && !selectedIsBuiltIn);
+ addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 210, y + 145, 55, 20, "gui.edit"));
+ getButton(BTN_EDIT).setEnabled(selectedSlotIndex >= 0 && !selectedIsBuiltIn && !selectedIsRefChainEntry);
// Up/Down carrot buttons
- addButton(new GuiNpcButton(73, guiLeft + 270, y + 145, 20, 20, "<"));
- addButton(new GuiNpcButton(74, guiLeft + 292, y + 145, 20, 20, ">"));
- getButton(73).setEnabled(selectedAbilityIndex > 0);
- getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1);
+ addButton(new GuiNpcButton(BTN_MOVE_UP, guiLeft + 270, y + 145, 20, 20, "<"));
+ addButton(new GuiNpcButton(BTN_MOVE_DOWN, guiLeft + 292, y + 145, 20, 20, ">"));
+ getButton(BTN_MOVE_UP).setEnabled(selectedSlotIndex > 0 && selectedEntryIndex == -1);
+ getButton(BTN_MOVE_DOWN).setEnabled(selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() - 1 && selectedEntryIndex == -1);
- // Save button (to the right of carrots, only enabled when ability is selected)
- addButton(new GuiNpcButton(76, centerX, y + 82, 40, 20, "gui.save"));
- getButton(76).setEnabled(selectedAbilityIndex >= 0 && !selectedIsBuiltIn);
+ // Save button — enabled for inline abilities (standalone or chain entry), disabled for references/chains
+ addButton(new GuiNpcButton(BTN_SAVE_PRESET, centerX, y + 82, 40, 20, "gui.save"));
+ boolean canSave = false;
+ if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() && !selectedIsBuiltIn) {
+ AbilityAction saveSlot = npcSlots.get(selectedSlotIndex);
+ if (selectedEntryIndex >= 0) {
+ // Chain entry: only inline entries can be saved
+ ChainedAbility saveChain = saveSlot.isInlineChain() ? saveSlot.getInlineChain() : null;
+ if (saveChain != null && selectedEntryIndex < saveChain.getEntries().size()) {
+ canSave = saveChain.getEntries().get(selectedEntryIndex).isInline();
+ }
+ } else if (!saveSlot.isChain() && !saveSlot.isReference()) {
+ // Standalone inline ability
+ canSave = true;
+ }
+ }
+ getButton(BTN_SAVE_PRESET).setEnabled(canSave);
+
+ // Add Chain button
+ addButton(new GuiNpcButton(BTN_ADD_CHAIN, centerX, y + 135, 40, 20, "ability.addChain"));
}
@Override
@@ -195,7 +284,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
int id = guibutton.id;
// Enabled toggle
- if (id == 100) {
+ if (id == BTN_ENABLED) {
abilitiesEnabled = ((GuiNpcButton) guibutton).getValue() == 1;
initGui();
save();
@@ -203,7 +292,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
}
// Add ability
- if (id == 70) {
+ if (id == BTN_ADD_ABILITY) {
if (availableTypesScroll.hasSelected()) {
String displayName = availableTypesScroll.getSelected();
String typeId = displayNameToTypeId.get(displayName);
@@ -220,7 +309,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
variants.get(0).apply(newAbility);
}
newAbility.setId(UUID.randomUUID().toString());
- npcSlots.add(AbilitySlot.inline(newAbility));
+ npcSlots.add(AbilityAction.inline(newAbility));
selectedAbilityIndex = npcSlots.size() - 1;
updateNpcAbilitiesList();
selectAbilityByIndex(selectedAbilityIndex);
@@ -232,11 +321,23 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
return;
}
- // Remove ability
- if (id == 71) {
- if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
- npcSlots.remove(selectedAbilityIndex);
+ // Remove slot or chain entry
+ if (id == BTN_REMOVE) {
+ if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) {
+ if (selectedEntryIndex >= 0) {
+ // Remove entry from chain — not the whole slot
+ AbilityAction slot = npcSlots.get(selectedSlotIndex);
+ ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : null;
+ if (chain != null && selectedEntryIndex < chain.getEntries().size()) {
+ chain.removeEntry(selectedEntryIndex);
+ }
+ } else {
+ npcSlots.remove(selectedSlotIndex);
+ }
+ selectedSlotIndex = -1;
+ selectedEntryIndex = -1;
selectedAbilityIndex = -1;
+ if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll();
updateNpcAbilitiesList();
initGui();
save();
@@ -244,12 +345,46 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
return;
}
- // Edit ability
- if (id == 72) {
- if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
- AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
- if (slot.isReference()) {
- // Reference slot - ask clone-and-modify vs modify-parent
+ // Edit ability or chain
+ if (id == BTN_EDIT) {
+ if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) {
+ AbilityAction slot = npcSlots.get(selectedSlotIndex);
+
+ if (slot.isChain() && selectedEntryIndex == -1) {
+ // Chain header selected
+ if (slot.isChainReference()) {
+ // Reference chain — offer Clone & Modify / Edit Parent
+ setSubGui(new SubGuiAbilityEditMode());
+ } else {
+ // Inline chain — open chain config directly
+ ChainedAbility chain = slot.getInlineChain();
+ if (chain != null) {
+ pendingChain = chain;
+ pendingChainSlotIdx = selectedSlotIndex;
+ setSubGui(new SubGuiChainedAbilityConfig(chain, this, true, npcSlots));
+ }
+ }
+ } else if (slot.isChain() && selectedEntryIndex >= 0) {
+ // Chain entry selected
+ ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : slot.getChainedAbility();
+ if (chain != null && selectedEntryIndex < chain.getEntries().size()) {
+ ChainedAbilityEntry entry = chain.getEntries().get(selectedEntryIndex);
+ if (entry.isInline() && slot.isInlineChain()) {
+ // Inline entry in inline chain — edit directly
+ Ability a = entry.getInlineAbility();
+ if (a != null) {
+ editingChainEntry = true;
+ editChainSlotIdx = selectedSlotIndex;
+ editChainEntryIdx = selectedEntryIndex;
+ a.setNpcInlineEdit(true);
+ setSubGui(a.createConfigGui(this));
+ }
+ } else {
+ // Reference entry or entry in reference chain — Clone & Modify / Edit Parent
+ setSubGui(new SubGuiAbilityEditMode());
+ }
+ }
+ } else if (slot.isReference()) {
setSubGui(new SubGuiAbilityEditMode());
} else {
Ability ability = slot.getAbility();
@@ -263,14 +398,14 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
}
// Move up (carrot left = earlier in list)
- if (id == 73) {
- if (selectedAbilityIndex > 0) {
- AbilitySlot temp = npcSlots.get(selectedAbilityIndex);
- npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex - 1));
- npcSlots.set(selectedAbilityIndex - 1, temp);
- selectedAbilityIndex--;
+ if (id == BTN_MOVE_UP) {
+ if (selectedSlotIndex > 0 && selectedEntryIndex == -1) {
+ AbilityAction temp = npcSlots.get(selectedSlotIndex);
+ npcSlots.set(selectedSlotIndex, npcSlots.get(selectedSlotIndex - 1));
+ npcSlots.set(selectedSlotIndex - 1, temp);
+ selectedSlotIndex--;
+ selectedAbilityIndex = selectedSlotIndex;
updateNpcAbilitiesList();
- selectAbilityByIndex(selectedAbilityIndex);
initGui();
save();
}
@@ -278,30 +413,53 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
}
// Move down (carrot right = later in list)
- if (id == 74) {
- if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1) {
- AbilitySlot temp = npcSlots.get(selectedAbilityIndex);
- npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex + 1));
- npcSlots.set(selectedAbilityIndex + 1, temp);
- selectedAbilityIndex++;
+ if (id == BTN_MOVE_DOWN) {
+ if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() - 1 && selectedEntryIndex == -1) {
+ AbilityAction temp = npcSlots.get(selectedSlotIndex);
+ npcSlots.set(selectedSlotIndex, npcSlots.get(selectedSlotIndex + 1));
+ npcSlots.set(selectedSlotIndex + 1, temp);
+ selectedSlotIndex++;
+ selectedAbilityIndex = selectedSlotIndex;
updateNpcAbilitiesList();
- selectAbilityByIndex(selectedAbilityIndex);
initGui();
save();
}
return;
}
- // Load ability (opens load dialog to add ability directly to NPC)
- if (id == 75) {
- setSubGui(new SubGuiAbilityLoad(this));
+ // Load ability or chain (opens type choice first)
+ if (id == BTN_LOAD) {
+ setSubGui(new SubGuiLoadTypeChoice());
return;
}
- // Save selected ability as custom preset
- if (id == 76) {
- if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
- Ability abilityToSave = npcSlots.get(selectedAbilityIndex).getAbility();
+ // Save selected ability as custom preset (converts inline → reference)
+ if (id == BTN_SAVE_PRESET) {
+ Ability abilityToSave = null;
+ pendingSaveSlotIdx = -1;
+ pendingSaveEntryIdx = -1;
+
+ if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) {
+ AbilityAction saveSlot = npcSlots.get(selectedSlotIndex);
+
+ if (selectedEntryIndex >= 0) {
+ // Saving a chain entry's inline ability
+ ChainedAbility chain = saveSlot.isInlineChain() ? saveSlot.getInlineChain() : null;
+ if (chain != null && selectedEntryIndex < chain.getEntries().size()) {
+ ChainedAbilityEntry entry = chain.getEntries().get(selectedEntryIndex);
+ if (entry.isInline()) {
+ abilityToSave = entry.getInlineAbility();
+ pendingSaveSlotIdx = selectedSlotIndex;
+ pendingSaveEntryIdx = selectedEntryIndex;
+ }
+ }
+ } else if (!saveSlot.isChain() && !saveSlot.isReference()) {
+ // Saving a standalone inline ability
+ abilityToSave = saveSlot.getAbility();
+ pendingSaveSlotIdx = selectedSlotIndex;
+ pendingSaveEntryIdx = -1;
+ }
+
if (abilityToSave != null) {
if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) {
abilityToSave.setNpcInlineEdit(true);
@@ -314,24 +472,31 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
return;
}
- // Toggle selected ability on/off
- if (id == 77) {
- if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
- AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
- Ability ability = slot.getAbility();
- if (ability != null) {
- boolean enabled = ((GuiNpcButton) guibutton).getValue() == 1;
- slot.setEnabled(enabled);
- updateNpcAbilitiesList();
- selectAbilityByIndex(selectedAbilityIndex);
- initGui();
- save();
- }
+ // Toggle selected slot on/off
+ if (id == BTN_TOGGLE_SLOT) {
+ if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) {
+ AbilityAction slot = npcSlots.get(selectedSlotIndex);
+ boolean enabled = ((GuiNpcButton) guibutton).getValue() == 1;
+ slot.setEnabled(enabled);
+ updateNpcAbilitiesList();
+ initGui();
+ save();
}
return;
}
- if (id == 50) {
+ // Add Chain button
+ if (id == BTN_ADD_CHAIN) {
+ ChainedAbility newChain = new ChainedAbility();
+ newChain.setId(UUID.randomUUID().toString());
+ newChain.setName("NewChain");
+ pendingChain = newChain;
+ pendingChainSlotIdx = -1;
+ setSubGui(new SubGuiChainedAbilityConfig(newChain, this, true, npcSlots));
+ return;
+ }
+
+ if (id == BTN_SCROLL_TYPE) {
if (scrollType != ScrollType.MODDED) {
ScrollType[] values = ScrollType.values();
ScrollType next = values[(scrollType.ordinal() + 1) % values.length];
@@ -397,7 +562,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
// Only process NPC ability data, not saved ability preset data
// SubGuiAbilityLoad also implements IGuiData and may receive responses
// that get forwarded here - ignore those by checking for expected keys
- if (!compound.hasKey("Abilities")) {
+ if (!compound.hasKey("AbilityActions") && !compound.hasKey("Abilities")) {
return;
}
@@ -411,10 +576,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
}
npcSlots.clear();
- NBTTagList abilityList = compound.getTagList("Abilities", 10);
- for (int i = 0; i < abilityList.tagCount(); i++) {
- NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i);
- AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT);
+ // Support both new unified format and legacy format
+ String tagName = compound.hasKey("AbilityActions") ? "AbilityActions" : "Abilities";
+ NBTTagList actionList = compound.getTagList(tagName, 10);
+ for (int i = 0; i < actionList.tagCount(); i++) {
+ NBTTagCompound slotNBT = actionList.getCompoundTagAt(i);
+ AbilityAction slot = AbilityAction.fromNBT(slotNBT);
if (slot != null) {
npcSlots.add(slot);
}
@@ -434,10 +601,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
public void keyTyped(char c, int i) {
super.keyTyped(c, i);
- if (getTextField(4) != null && getTextField(4).isFocused()) {
- if (search.equals(getTextField(4).getText()))
+ if (getTextField(TF_SEARCH) != null && getTextField(TF_SEARCH).isFocused()) {
+ if (search.equals(getTextField(TF_SEARCH).getText()))
return;
- search = getTextField(4).getText().toLowerCase();
+ search = getTextField(TF_SEARCH).getText().toLowerCase();
availableTypesScroll.setList(getFilteredTypeList());
availableTypesScroll.resetScroll();
}
@@ -488,31 +655,34 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
private void updateNpcAbilitiesList() {
List list = new ArrayList<>();
+ rowMapping.clear();
+
for (int i = 0; i < npcSlots.size(); i++) {
- list.add(getAbilityListEntry(i));
- }
+ AbilityAction slot = npcSlots.get(i);
- // Sort: enabled first, then numerically by slot index
- Collections.sort(list, new Comparator() {
- @Override
- public int compare(String a, String b) {
- boolean aEnabled = a.startsWith("\u00A7a");
- boolean bEnabled = b.startsWith("\u00A7a");
- if (aEnabled != bEnabled) return aEnabled ? -1 : 1;
- return extractIndex(a) - extractIndex(b);
- }
+ if (slot.isChain()) {
+ // Chain header row
+ list.add(getChainHeaderEntry(i, slot));
+ rowMapping.add(new int[]{i, -1});
- private int extractIndex(String entry) {
- String stripped = entry.replaceAll("\u00A7.", "").trim();
- int dotIndex = stripped.indexOf(".");
- if (dotIndex > 0) {
- try {
- return Integer.parseInt(stripped.substring(0, dotIndex).replaceAll("[^0-9]", ""));
- } catch (NumberFormatException e) { }
+ // Chain entry rows
+ ChainedAbility chain = slot.getChainedAbility();
+ if (chain == null && slot.isInlineChain()) {
+ chain = slot.getInlineChain();
}
- return Integer.MAX_VALUE;
+ if (chain != null) {
+ for (int e = 0; e < chain.getEntries().size(); e++) {
+ ChainedAbilityEntry entry = chain.getEntries().get(e);
+ list.add(getChainEntryDisplay(entry, e));
+ rowMapping.add(new int[]{i, e});
+ }
+ }
+ } else {
+ // Regular ability row
+ list.add(getAbilityListEntry(i));
+ rowMapping.add(new int[]{i, -1});
}
- });
+ }
if (npcAbilitiesScroll != null) {
npcAbilitiesScroll.setUnsortedList(list);
@@ -526,11 +696,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
* Broken: "[●] 1. > [Missing: id...]" in red
*/
private String getAbilityListEntry(int index) {
- AbilitySlot slot = npcSlots.get(index);
+ AbilityAction slot = npcSlots.get(index);
Ability ability = slot.getAbility();
if (ability == null) {
- // Broken reference
String refId = slot.getReferenceId();
String shortId = refId != null && refId.length() > 8 ? refId.substring(0, 8) + "..." : refId;
return "\u00A7c\u25CF\u00A7r " + (index + 1) + ". \u00A7c> [Missing: " + shortId + "]\u00A7r";
@@ -556,6 +725,33 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
return colorPrefix + (index + 1) + ". " + nameDisplay;
}
+ private String getChainHeaderEntry(int index, AbilityAction slot) {
+ ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : slot.getChainedAbility();
+ String chainName = chain != null ? chain.getDisplayName() : "???";
+ boolean enabled = chain != null && chain.isEnabled();
+ String colorPrefix = enabled ? "\u00A7a\u25CF\u00A7r " : "\u00A7c\u25CF\u00A7r ";
+ if (slot.isChainReference()) {
+ return colorPrefix + (index + 1) + ". \u00A7e> [Chain] " + chainName + "\u00A7r";
+ }
+ return colorPrefix + (index + 1) + ". \u00A7d[Chain] \u00A7r" + chainName;
+ }
+
+ private String getChainEntryDisplay(ChainedAbilityEntry entry, int entryIndex) {
+ if (entry.isInline()) {
+ Ability a = entry.getInlineAbility();
+ String name = a != null ? a.getDisplayName() : "???";
+ return " \u00A78--- \u00A7r" + name;
+ } else {
+ String name = entry.getAbilityReference();
+ if (name == null || name.isEmpty()) name = "???";
+ Ability resolved = entry.resolve();
+ if (resolved != null) {
+ name = resolved.getDisplayName();
+ }
+ return " \u00A78--- \u00A7e> " + name + "\u00A7r";
+ }
+ }
+
private void selectAbilityByIndex(int index) {
if (index >= 0 && index < npcSlots.size()) {
String entry = getAbilityListEntry(index);
@@ -579,11 +775,11 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
compound.setInteger("AbilityMinCooldown", minCooldown);
compound.setInteger("AbilityMaxCooldown", maxCooldown);
- NBTTagList abilityList = new NBTTagList();
- for (AbilitySlot slot : npcSlots) {
- abilityList.appendTag(slot.writeNBT());
+ NBTTagList actionList = new NBTTagList();
+ for (AbilityAction slot : npcSlots) {
+ actionList.appendTag(slot.writeNBT());
}
- compound.setTag("Abilities", abilityList);
+ compound.setTag("AbilityActions", actionList);
PacketClient.sendClient(new AbilitiesNpcSavePacket(compound));
}
@@ -595,8 +791,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
public void loadAbility(Ability loadedAbility) {
if (loadedAbility != null) {
loadedAbility.setId(UUID.randomUUID().toString());
- npcSlots.add(AbilitySlot.inline(loadedAbility));
+ npcSlots.add(AbilityAction.inline(loadedAbility));
selectedAbilityIndex = npcSlots.size() - 1;
+ if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll();
updateNpcAbilitiesList();
selectAbilityByIndex(selectedAbilityIndex);
initGui();
@@ -610,8 +807,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
*/
public void loadAbilityReference(String referenceId) {
if (referenceId != null && !referenceId.isEmpty()) {
- npcSlots.add(AbilitySlot.reference(referenceId));
+ npcSlots.add(AbilityAction.abilityReference(referenceId));
selectedAbilityIndex = npcSlots.size() - 1;
+ if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll();
updateNpcAbilitiesList();
selectAbilityByIndex(selectedAbilityIndex);
initGui();
@@ -629,21 +827,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) {
if (guiCustomScroll == npcAbilitiesScroll) {
- String selected = npcAbilitiesScroll.getSelected();
- if (selected != null) {
- // Parse index from "[●] 1. Name" format - strip color codes first
- String stripped = selected.replaceAll("\u00A7.", "").trim();
- // Now format is "● 1. Name" - find the number after the circle
- int dotIndex = stripped.indexOf(".");
- if (dotIndex > 0) {
- try {
- // Extract just the number portion (skip "● " prefix)
- String numPart = stripped.substring(0, dotIndex).replaceAll("[^0-9]", "");
- selectedAbilityIndex = Integer.parseInt(numPart) - 1;
- } catch (NumberFormatException e) {
- selectedAbilityIndex = -1;
- }
- }
+ int scrollIndex = npcAbilitiesScroll.selected;
+ if (scrollIndex >= 0 && scrollIndex < rowMapping.size()) {
+ int[] mapping = rowMapping.get(scrollIndex);
+ selectedSlotIndex = mapping[0];
+ selectedEntryIndex = mapping[1];
+ selectedAbilityIndex = selectedSlotIndex;
+ } else {
+ selectedSlotIndex = -1;
+ selectedEntryIndex = -1;
+ selectedAbilityIndex = -1;
}
initGui();
}
@@ -651,24 +844,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
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 {
- if (ability != null) {
- ability.setNpcInlineEdit(true);
- setSubGui(ability.createConfigGui(this));
- }
+ if (scroll == npcAbilitiesScroll && selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) {
+ GuiNpcButton editBtn = getButton(BTN_EDIT);
+ if (editBtn != null && editBtn.enabled) {
+ buttonEvent(new GuiNpcButton(BTN_EDIT, 0, 0, 0, 0, ""));
}
}
}
@Override
public void unFocused(GuiNpcTextField textField) {
- if (textField.id == 101) {
+ if (textField.id == TF_MIN_COOLDOWN) {
minCooldown = textField.getInteger();
// Ensure min <= max
if (minCooldown > maxCooldown) {
@@ -676,7 +862,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
initGui();
}
save();
- } else if (textField.id == 102) {
+ } else if (textField.id == TF_MAX_COOLDOWN) {
maxCooldown = textField.getInteger();
// Ensure min <= max
if (maxCooldown < minCooldown) {
@@ -690,69 +876,283 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
public void subGuiClosed(SubGuiInterface subgui) {
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;
+ handleVariantSelectClosed((SubGuiAbilityVariantSelect) subgui);
+ initGui();
+ return;
+ }
+ if (subgui instanceof SubGuiChainedAbilityConfig) {
+ handleChainConfigClosed();
+ initGui();
+ return;
+ }
+ if (subgui instanceof SubGuiAbilityEditMode) {
+ handleEditModeClosed((SubGuiAbilityEditMode) subgui);
+ return;
+ }
+ if (subgui instanceof SubGuiAbilitySaveConfirm) {
+ handleSaveConfirmClosed((SubGuiAbilitySaveConfirm) subgui);
+ initGui();
+ return;
+ }
+ if (subgui instanceof SubGuiLoadTypeChoice) {
+ handleLoadTypeChoiceClosed((SubGuiLoadTypeChoice) subgui);
+ return;
+ }
+ if (subgui instanceof SubGuiChainSelect) {
+ handleChainSelectClosed((SubGuiChainSelect) subgui);
+ return;
+ }
+ if (subgui instanceof SubGuiAbilityConfig) {
+ handleAbilityConfigClosed();
+ return;
+ }
+ }
+
+ private void handleVariantSelectClosed(SubGuiAbilityVariantSelect gui) {
+ int idx = gui.getSelectedIndex();
+ if (idx >= 0 && pendingTypeId != null) {
+ Ability newAbility = AbilityController.Instance.create(pendingTypeId);
+ if (newAbility != null) {
+ gui.getVariants().get(idx).apply(newAbility);
+ newAbility.setId(UUID.randomUUID().toString());
+ npcSlots.add(AbilityAction.inline(newAbility));
+ selectedAbilityIndex = npcSlots.size() - 1;
+ selectedSlotIndex = selectedAbilityIndex;
+ selectedEntryIndex = -1;
+ updateNpcAbilitiesList();
+ save();
+ }
+ }
+ pendingTypeId = null;
+ }
+
+ private void handleChainConfigClosed() {
+ if (pendingChain != null) {
+ if (pendingChainSlotIdx < 0) {
+ // New chain — add to NPC slots
+ npcSlots.add(AbilityAction.inlineChain(pendingChain));
+ selectedSlotIndex = npcSlots.size() - 1;
+ selectedEntryIndex = -1;
+ selectedAbilityIndex = selectedSlotIndex;
+ if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll();
+ }
+ // Existing slot (pendingChainSlotIdx >= 0): already updated in-place, nothing to add
+ pendingChain = null;
+ pendingChainSlotIdx = -1;
+ }
+ updateNpcAbilitiesList();
+ save();
+ }
+
+ private void handleEditModeClosed(SubGuiAbilityEditMode gui) {
+ int mode = gui.getResult();
+ if (mode < 0) return; // cancelled
+
+ if (selectedSlotIndex < 0 || selectedSlotIndex >= npcSlots.size()) return;
+ AbilityAction slot = npcSlots.get(selectedSlotIndex);
+
+ if (slot.isChain() && selectedEntryIndex == -1) {
+ // Chain header (must be CHAIN_REFERENCE)
+ if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) {
+ if (slot.convertToInline()) {
+ ChainedAbility chain = slot.getInlineChain();
+ if (chain != null) {
+ pendingChain = chain;
+ pendingChainSlotIdx = selectedSlotIndex;
+ setSubGui(new SubGuiChainedAbilityConfig(chain, this, true, npcSlots));
+ }
updateNpcAbilitiesList();
- selectAbilityByIndex(selectedAbilityIndex);
save();
}
+ } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) {
+ ChainedAbility chain = slot.getChainedAbility();
+ if (chain != null) {
+ pendingChain = chain;
+ pendingChainSlotIdx = selectedSlotIndex;
+ setSubGui(new SubGuiChainedAbilityConfig(chain, this, true, npcSlots));
+ }
}
- pendingTypeId = null;
- initGui();
- } else if (subgui instanceof SubGuiAbilityEditMode) {
- int mode = ((SubGuiAbilityEditMode) subgui).getResult();
- if (mode < 0) return; // cancelled
-
- if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
- AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
- if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) {
- // Convert reference to inline, then edit
- if (slot.convertToInline()) {
- Ability ability = slot.getAbility();
- if (ability != null) {
- ability.setId(UUID.randomUUID().toString());
- ability.setNpcInlineEdit(true);
- setSubGui(ability.createConfigGui(this));
+ } else if (slot.isChain() && selectedEntryIndex >= 0) {
+ // Chain entry reference
+ if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) {
+ if (slot.isChainReference()) {
+ slot.convertToInline();
+ }
+ ChainedAbility chain = slot.getInlineChain();
+ if (chain != null && selectedEntryIndex < chain.getEntries().size()) {
+ ChainedAbilityEntry entry = chain.getEntries().get(selectedEntryIndex);
+ if (entry.convertToInline()) {
+ Ability a = entry.getInlineAbility();
+ if (a != null) {
+ editingChainEntry = true;
+ editChainSlotIdx = selectedSlotIndex;
+ editChainEntryIdx = selectedEntryIndex;
+ a.setNpcInlineEdit(true);
+ setSubGui(a.createConfigGui(this));
}
- updateNpcAbilitiesList();
- save();
}
- } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) {
- // Edit the resolved ability directly (changes propagate to controller)
- Ability ability = slot.getAbility();
- if (ability != null) {
- setSubGui(ability.createConfigGui(this));
+ }
+ updateNpcAbilitiesList();
+ save();
+ } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) {
+ ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : slot.getChainedAbility();
+ if (chain != null && selectedEntryIndex < chain.getEntries().size()) {
+ ChainedAbilityEntry entry = chain.getEntries().get(selectedEntryIndex);
+ Ability resolved = entry.resolve();
+ if (resolved != null) {
+ editingChainEntryParent = true;
+ setSubGui(resolved.createConfigGui(this));
}
}
}
- } else if (subgui instanceof SubGuiAbilityConfig) {
+ } else if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) {
+ // Standalone ability reference → convert to inline
+ if (slot.convertToInline()) {
+ Ability ability = slot.getAbility();
+ if (ability != null) {
+ ability.setId(UUID.randomUUID().toString());
+ ability.setNpcInlineEdit(true);
+ setSubGui(ability.createConfigGui(this));
+ }
+ updateNpcAbilitiesList();
+ save();
+ }
+ } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) {
+ Ability ability = slot.getAbility();
+ if (ability != null) {
+ setSubGui(ability.createConfigGui(this));
+ }
+ }
+ }
+
+ private void handleSaveConfirmClosed(SubGuiAbilitySaveConfirm gui) {
+ if (gui.wasSaved() && pendingSaveSlotIdx >= 0 && pendingSaveSlotIdx < npcSlots.size()) {
+ if (pendingSaveEntryIdx >= 0) {
+ // Chain entry: convert inline entry to reference
+ AbilityAction slot = npcSlots.get(pendingSaveSlotIdx);
+ ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : null;
+ if (chain != null && pendingSaveEntryIdx < chain.getEntries().size()) {
+ ChainedAbilityEntry entry = chain.getEntries().get(pendingSaveEntryIdx);
+ Ability a = entry.getInlineAbility();
+ if (a != null && a.getName() != null && !a.getName().isEmpty()) {
+ chain.getEntries().set(pendingSaveEntryIdx,
+ ChainedAbilityEntry.reference(a.getName(), entry.getDelayTicks()));
+ }
+ }
+ } else {
+ // Standalone: convert inline slot to reference
+ AbilityAction slot = npcSlots.get(pendingSaveSlotIdx);
+ Ability a = slot.getAbility();
+ if (a != null && a.getName() != null && !a.getName().isEmpty()) {
+ npcSlots.set(pendingSaveSlotIdx, AbilityAction.abilityReference(a.getName()));
+ }
+ }
+ if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll();
updateNpcAbilitiesList();
save();
}
+ pendingSaveSlotIdx = -1;
+ pendingSaveEntryIdx = -1;
+ }
+
+ private void handleLoadTypeChoiceClosed(SubGuiLoadTypeChoice gui) {
+ int result = gui.getResult();
+ if (result == SubGuiLoadTypeChoice.RESULT_ABILITY) {
+ setSubGui(new SubGuiAbilityLoad(this));
+ } else if (result == SubGuiLoadTypeChoice.RESULT_CHAIN) {
+ setSubGui(new SubGuiChainSelect());
+ }
+ }
+
+ private void handleChainSelectClosed(SubGuiChainSelect gui) {
+ String chainName = gui.getSelectedName();
+ if (chainName != null) {
+ loadChainReference(chainName);
+ }
+ }
+
+ private void handleAbilityConfigClosed() {
+ editingChainEntry = false;
+ editChainSlotIdx = -1;
+ editChainEntryIdx = -1;
+ editingChainEntryParent = false;
+ updateNpcAbilitiesList();
+ save();
}
/**
* Called from SubGuiAbilityConfig when ability is saved.
*/
public void onAbilitySaved(Ability ability) {
+ // Check if we were editing a global parent ability via "Modify Parent" on a chain entry
+ if (editingChainEntryParent) {
+ PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
+ editingChainEntryParent = false;
+ return;
+ }
+
+ // Check if we were editing an ability inside a chain entry
+ if (editingChainEntry && editChainSlotIdx >= 0 && editChainSlotIdx < npcSlots.size()) {
+ AbilityAction slot = npcSlots.get(editChainSlotIdx);
+ ChainedAbility chain = slot.isInlineChain() ? slot.getInlineChain() : null;
+ if (chain != null && editChainEntryIdx >= 0 && editChainEntryIdx < chain.getEntries().size()) {
+ // Update the inline ability on the entry — it's already the same object reference,
+ // but refresh the list and save to persist
+ }
+ editingChainEntry = false;
+ editChainSlotIdx = -1;
+ editChainEntryIdx = -1;
+ updateNpcAbilitiesList();
+ save();
+ return;
+ }
+
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
- AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
+ AbilityAction slot = npcSlots.get(selectedAbilityIndex);
if (slot.isReference()) {
- // For reference slots, save back to controller
PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
} else {
- // For inline slots, replace with the updated ability
- npcSlots.set(selectedAbilityIndex, AbilitySlot.inline(ability));
+ npcSlots.set(selectedAbilityIndex, AbilityAction.inline(ability));
}
updateNpcAbilitiesList();
save();
}
}
+
+ @Override
+ public void onChainedAbilitySaved(ChainedAbility chain) {
+ // Handled in subGuiClosed — pendingChain tracks the chain being edited
+ }
+
+ /**
+ * Called from SubGuiAbilityLoad when a chain reference is loaded.
+ */
+ public void loadChainReference(String referenceId) {
+ if (referenceId != null && !referenceId.isEmpty()) {
+ npcSlots.add(AbilityAction.chainReference(referenceId));
+ selectedSlotIndex = npcSlots.size() - 1;
+ selectedEntryIndex = -1;
+ selectedAbilityIndex = selectedSlotIndex;
+ if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll();
+ updateNpcAbilitiesList();
+ initGui();
+ save();
+ }
+ }
+
+ /**
+ * Called from SubGuiAbilityLoad to load a chain as inline.
+ */
+ public void loadChainInline(ChainedAbility chain) {
+ if (chain != null) {
+ npcSlots.add(AbilityAction.inlineChain(chain));
+ selectedSlotIndex = npcSlots.size() - 1;
+ selectedEntryIndex = -1;
+ selectedAbilityIndex = selectedSlotIndex;
+ if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll();
+ updateNpcAbilitiesList();
+ initGui();
+ save();
+ }
+ }
}
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java
index bcfce7687..f662da835 100644
--- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java
@@ -3,10 +3,10 @@ package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.Condition;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldBuilder;
-import noppes.npcs.client.gui.builder.FieldDef;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
+import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.client.gui.util.GuiMenuTopButton;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
@@ -75,11 +75,11 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
for (FieldDef def : fieldDefs) {
String tab = def.getTab();
if (tab != null
- && !TAB_NAME_GENERAL.equals(tab)
- && !TAB_NAME_TYPE.equals(tab)
- && !TAB_NAME_TARGET.equals(tab)
- && !TAB_NAME_EFFECTS.equals(tab)
- && !customTabNames.contains(tab)) {
+ && !TAB_NAME_GENERAL.equals(tab)
+ && !TAB_NAME_TYPE.equals(tab)
+ && !TAB_NAME_TARGET.equals(tab)
+ && !TAB_NAME_EFFECTS.equals(tab)
+ && !customTabNames.contains(tab)) {
customTabNames.add(tab);
}
}
@@ -92,6 +92,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
@Override
public void initGui() {
+ // Commit any focused text field before rebuilding
+ GuiNpcTextField.unfocus();
+
// Save scroll position before super clears scroll windows
GuiScrollWindow oldSw = getScrollableGui(0);
if (oldSw != null && activeTab < tabScrollY.length) {
@@ -187,11 +190,21 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
}
} else {
switch (tabIndex) {
- case TAB_GENERAL: tabName = TAB_NAME_GENERAL; break;
- case TAB_TYPE: tabName = TAB_NAME_TYPE; break;
- case TAB_TARGET: tabName = TAB_NAME_TARGET; break;
- case TAB_EFFECTS: tabName = TAB_NAME_EFFECTS; break;
- default: tabName = TAB_NAME_TYPE; break;
+ case TAB_GENERAL:
+ tabName = TAB_NAME_GENERAL;
+ break;
+ case TAB_TYPE:
+ tabName = TAB_NAME_TYPE;
+ break;
+ case TAB_TARGET:
+ tabName = TAB_NAME_TARGET;
+ break;
+ case TAB_EFFECTS:
+ tabName = TAB_NAME_EFFECTS;
+ break;
+ default:
+ tabName = TAB_NAME_TYPE;
+ break;
}
}
@@ -264,17 +277,36 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
int id = guibutton.id;
// Tab switching — fixed tabs
- if (id == 90) { activeTab = TAB_GENERAL; initGui(); return; }
- if (id == 91) { activeTab = TAB_TYPE; initGui(); return; }
- if (id == 92) { activeTab = TAB_TARGET; initGui(); return; }
- if (id == 93) { activeTab = TAB_EFFECTS; initGui(); return; }
+ if (id == 90) {
+ activeTab = TAB_GENERAL;
+ initGui();
+ return;
+ }
+ if (id == 91) {
+ activeTab = TAB_TYPE;
+ initGui();
+ return;
+ }
+ if (id == 92) {
+ activeTab = TAB_TARGET;
+ initGui();
+ return;
+ }
+ if (id == 93) {
+ activeTab = TAB_EFFECTS;
+ initGui();
+ return;
+ }
// Custom tabs (94+)
if (id >= 94 && id < 94 + customTabNames.size()) {
activeTab = 4 + (id - 94);
initGui();
return;
}
- if (id == -1000) { close(); return; }
+ if (id == -1000) {
+ close();
+ return;
+ }
// Condition buttons (50-80)
if (handleConditionButton(id)) return;
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java
index 4d21f12ca..9e3856a14 100644
--- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java
@@ -3,18 +3,15 @@ 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;
/**
* Dialog asking the user whether to clone-and-modify (convert to inline) or modify the parent ability.
*/
-public class SubGuiAbilityEditMode extends SubGuiInterface {
+public class SubGuiAbilityEditMode extends SubGuiSimpleChoice {
public static final int MODE_CLONE_MODIFY = 0;
public static final int MODE_MODIFY_PARENT = 1;
- private int result = -1;
-
public SubGuiAbilityEditMode() {
setBackground("menubg.png");
xSize = 220;
@@ -39,18 +36,13 @@ public class SubGuiAbilityEditMode extends SubGuiInterface {
@Override
public void buttonEvent(GuiButton guibutton) {
if (guibutton.id == 0) {
- result = MODE_CLONE_MODIFY;
+ setResult(MODE_CLONE_MODIFY);
close();
} else if (guibutton.id == 1) {
- result = MODE_MODIFY_PARENT;
+ setResult(MODE_MODIFY_PARENT);
close();
} else if (guibutton.id == 2) {
- result = -1;
close();
}
}
-
- public int getResult() {
- return result;
- }
}
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java
index 905faa1f4..dda5ee8e6 100644
--- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java
@@ -1,7 +1,7 @@
package noppes.npcs.client.gui.advanced;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket;
import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket;
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java
index 28a7bd0bb..5995c0e9e 100644
--- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java
@@ -3,18 +3,15 @@ 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;
/**
* Dialog asking the user whether to Clone (inline copy) or Reference the loaded ability.
*/
-public class SubGuiAbilityLoadMode extends SubGuiInterface {
+public class SubGuiAbilityLoadMode extends SubGuiSimpleChoice {
public static final int MODE_CLONE = 0;
public static final int MODE_REFERENCE = 1;
- private int result = -1;
-
public SubGuiAbilityLoadMode() {
setBackground("menubg.png");
xSize = 200;
@@ -39,18 +36,13 @@ public class SubGuiAbilityLoadMode extends SubGuiInterface {
@Override
public void buttonEvent(GuiButton guibutton) {
if (guibutton.id == 0) {
- result = MODE_CLONE;
+ setResult(MODE_CLONE);
close();
} else if (guibutton.id == 1) {
- result = MODE_REFERENCE;
+ setResult(MODE_REFERENCE);
close();
} else if (guibutton.id == 2) {
- result = -1;
close();
}
}
-
- public int getResult() {
- return result;
- }
}
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java
index b8a4e0e66..57a363e1f 100644
--- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java
@@ -21,6 +21,7 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGui
private final Ability ability;
private final IAbilityConfigCallback callback;
private final Set existingNames;
+ private boolean saved = false;
public SubGuiAbilitySaveConfirm(Ability ability) {
this(ability, null, null);
@@ -68,6 +69,7 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGui
}
private void doSave() {
+ saved = true;
PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
if (callback != null) {
callback.onAbilitySaved(ability);
@@ -75,6 +77,10 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGui
close();
}
+ public boolean wasSaved() {
+ return saved;
+ }
+
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiDuplicateNameConfirm) {
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java
new file mode 100644
index 000000000..a1d235d51
--- /dev/null
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySelect.java
@@ -0,0 +1,162 @@
+package noppes.npcs.client.gui.advanced;
+
+import kamkeel.npcs.network.PacketClient;
+import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket;
+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.GuiNpcTextField;
+import noppes.npcs.client.gui.util.ICustomScrollListener;
+import noppes.npcs.client.gui.util.IScrollData;
+import noppes.npcs.client.gui.util.SubGuiInterface;
+import noppes.npcs.constants.EnumScrollData;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Vector;
+
+/**
+ * Simple ability name picker. Shows all abilities (custom + built-in) in a
+ * single scroll list with search. Returns the selected ability name.
+ */
+public class SubGuiAbilitySelect extends SubGuiInterface implements ICustomScrollListener, IScrollData {
+
+ private GuiCustomScroll scroll;
+ private HashMap customData = new HashMap<>();
+ private HashMap builtInData = new HashMap<>();
+ private String selectedName = null;
+ private String search = "";
+
+ public SubGuiAbilitySelect() {
+ setBackground("menubg.png");
+ xSize = 220;
+ ySize = 216;
+
+ PacketClient.sendClient(new CustomAbilitiesGetPacket());
+ }
+
+ @Override
+ public void initGui() {
+ super.initGui();
+
+ int y = guiTop + 5;
+
+ addLabel(new GuiNpcLabel(0, "ability.select", guiLeft + 10, y));
+ y += 14;
+
+ if (scroll == null) {
+ scroll = new GuiCustomScroll(this, 0);
+ scroll.setSize(200, 140);
+ }
+ scroll.guiLeft = guiLeft + 10;
+ scroll.guiTop = y;
+ scroll.setList(getFilteredList());
+ if (selectedName != null) {
+ scroll.setSelected(selectedName);
+ }
+ addScroll(scroll);
+
+ y += 143;
+
+ addTextField(new GuiNpcTextField(10, this, fontRendererObj, guiLeft + 10, y, 200, 20, search));
+
+ y += 24;
+
+ addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.select"));
+ addButton(new GuiNpcButton(2, guiLeft + 115, y, 95, 20, "gui.cancel"));
+
+ getButton(0).setEnabled(selectedName != null);
+ }
+
+ private List getFilteredList() {
+ HashMap merged = new HashMap<>();
+ merged.putAll(customData);
+ merged.putAll(builtInData);
+
+ if (search.isEmpty()) {
+ return new ArrayList<>(merged.keySet());
+ }
+ List list = new ArrayList<>();
+ for (String name : merged.keySet()) {
+ if (name.toLowerCase().contains(search.toLowerCase())) {
+ list.add(name);
+ }
+ }
+ return list;
+ }
+
+ @Override
+ public void buttonEvent(GuiButton guibutton) {
+ int id = guibutton.id;
+
+ if (id == 0 && selectedName != null) {
+ close();
+ } else if (id == 2) {
+ selectedName = null;
+ close();
+ }
+ }
+
+ @Override
+ public void keyTyped(char c, int i) {
+ super.keyTyped(c, i);
+ if (getTextField(10) != null && getTextField(10).isFocused()) {
+ String newSearch = getTextField(10).getText();
+ if (!search.equals(newSearch)) {
+ search = newSearch;
+ scroll.resetScroll();
+ scroll.setList(getFilteredList());
+ }
+ }
+ }
+
+ @Override
+ public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) {
+ if (scroll.id == 0) {
+ selectedName = scroll.getSelected();
+ initGui();
+ }
+ }
+
+ @Override
+ public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
+ if (scroll.id == 0 && selection != null && !selection.isEmpty()) {
+ selectedName = selection;
+ close();
+ }
+ }
+
+ @Override
+ public void setData(Vector list, HashMap data, EnumScrollData type) {
+ if (type == EnumScrollData.CUSTOM_ABILITIES) {
+ customData = data;
+ } else if (type == EnumScrollData.BUILTIN_ABILITIES) {
+ builtInData = data;
+ }
+ if (scroll != null) {
+ scroll.setList(getFilteredList());
+ if (selectedName != null) {
+ scroll.setSelected(selectedName);
+ }
+ }
+ initGui();
+ }
+
+ @Override
+ public void setSelected(String selected) {
+ this.selectedName = selected;
+ if (scroll != null) {
+ scroll.setSelected(selected);
+ }
+ }
+
+ /**
+ * Get the selected ability name after the dialog closes.
+ * Returns null if cancelled.
+ */
+ public String getSelectedName() {
+ return selectedName;
+ }
+}
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java
index 5d810862f..99d1fff54 100644
--- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java
@@ -1,6 +1,6 @@
package noppes.npcs.client.gui.advanced;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
+import kamkeel.npcs.controllers.AbilityController;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.resources.I18n;
import noppes.npcs.client.gui.util.GuiCustomScroll;
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainSelect.java
new file mode 100644
index 000000000..f2d858551
--- /dev/null
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainSelect.java
@@ -0,0 +1,92 @@
+package noppes.npcs.client.gui.advanced;
+
+import kamkeel.npcs.controllers.AbilityController;
+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;
+import java.util.Set;
+
+/**
+ * Selector for chained ability presets. Lists chain names from synced data.
+ * Returns the selected chain name via {@link #getSelectedName()}.
+ */
+public class SubGuiChainSelect extends SubGuiInterface implements ICustomScrollListener {
+
+ private GuiCustomScroll scroll;
+ private String selectedName = null;
+
+ public SubGuiChainSelect() {
+ setBackground("menubg.png");
+ xSize = 220;
+ ySize = 200;
+ }
+
+ @Override
+ public void initGui() {
+ super.initGui();
+
+ int y = guiTop + 5;
+ addLabel(new GuiNpcLabel(0, "ability.loadChain", guiLeft + 10, y));
+ y += 14;
+
+ List names = getChainNames();
+
+ if (scroll == null) {
+ scroll = new GuiCustomScroll(this, 0);
+ scroll.setSize(200, 130);
+ }
+ scroll.guiLeft = guiLeft + 10;
+ scroll.guiTop = y;
+ scroll.setList(names);
+ if (selectedName != null) {
+ scroll.setSelected(selectedName);
+ }
+ addScroll(scroll);
+
+ y += 133;
+ addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.select"));
+ addButton(new GuiNpcButton(1, guiLeft + 115, y, 95, 20, "gui.cancel"));
+ getButton(0).setEnabled(selectedName != null);
+ }
+
+ private List getChainNames() {
+ Set names = AbilityController.Instance.getChainedAbilityNamesSet();
+ return new ArrayList<>(names);
+ }
+
+ @Override
+ public void buttonEvent(GuiButton guibutton) {
+ if (guibutton.id == 0 && selectedName != null) {
+ close();
+ } else if (guibutton.id == 1) {
+ selectedName = null;
+ close();
+ }
+ }
+
+ @Override
+ public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) {
+ if (guiCustomScroll.id == 0) {
+ selectedName = guiCustomScroll.getSelected();
+ initGui();
+ }
+ }
+
+ @Override
+ public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) {
+ if (guiCustomScroll.id == 0 && selection != null && !selection.isEmpty()) {
+ selectedName = selection;
+ close();
+ }
+ }
+
+ public String getSelectedName() {
+ return selectedName;
+ }
+}
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java
new file mode 100644
index 000000000..ba3bfb955
--- /dev/null
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java
@@ -0,0 +1,851 @@
+package noppes.npcs.client.gui.advanced;
+
+import kamkeel.npcs.controllers.AbilityController;
+import kamkeel.npcs.controllers.data.ability.Ability;
+import kamkeel.npcs.controllers.data.ability.AbilityAction;
+import kamkeel.npcs.controllers.data.ability.AbilityVariant;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
+import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry;
+import kamkeel.npcs.controllers.data.ability.Condition;
+import kamkeel.npcs.controllers.data.ability.IChainedAbilityFieldProvider;
+import kamkeel.npcs.controllers.data.ability.UserType;
+import kamkeel.npcs.network.PacketClient;
+import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.StatCollector;
+import noppes.npcs.client.gui.builder.FieldDef;
+import noppes.npcs.client.gui.builder.GuiFieldBuilder;
+import noppes.npcs.client.gui.util.GuiMenuTopButton;
+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.GuiScrollWindow;
+import noppes.npcs.client.gui.util.IAbilityConfigCallback;
+import noppes.npcs.client.gui.util.IChainedAbilityConfigCallback;
+import noppes.npcs.client.gui.util.ISubGuiListener;
+import noppes.npcs.client.gui.util.ITextfieldListener;
+import noppes.npcs.client.gui.util.SubGuiInterface;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * SubGui for editing a {@link ChainedAbility}.
+ * Core tabs: General, Entries, Conditions.
+ * Additional tabs are injected dynamically by {@link IChainedAbilityFieldProvider}s.
+ */
+public class SubGuiChainedAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener, IAbilityConfigCallback {
+
+ // ── Declarative field ID ranges ──────────────────────────────────────────
+ private static final int DECLARATIVE_ID_START = 1000;
+ private static final int CLEAR_ID_START = 2000;
+ private static final int LABEL_ID_START = 3000;
+
+ // ── Tab constants (core tabs) ────────────────────────────────────────────
+ private static final int TAB_GENERAL = 0;
+ private static final int TAB_ENTRIES = 1;
+ private static final int TAB_TARGET = 2;
+ private static final int CORE_TAB_COUNT = 3;
+
+ // ── Tab button IDs ────────────────────────────────────────────────────────
+ private static final int BTN_TAB_GENERAL = 90;
+ private static final int BTN_TAB_ENTRIES = 91;
+ private static final int BTN_TAB_TARGET = 92;
+ private static final int BTN_TAB_EXTRA_BASE = 93;
+ private static final int BTN_CLOSE = -1000;
+
+ // ── Entries tab: per-entry ID encoding ─────────────────────────────────────
+ // Actual ID = ENTRY_BASE + i * ENTRY_STRIDE + offset
+ // offset 0 = name/select button, 1 = delay text field, 2 = up, 3 = down, 4 = delete
+ private static final int ENTRY_BASE = 100;
+ private static final int ENTRY_END = 200;
+ private static final int ENTRY_STRIDE = 10;
+ private static final int BTN_ADD_ENTRY = 200;
+
+ // ── Conditions tab: per-condition ID encoding ─────────────────────────────
+ // Actual ID = COND_BASE + i * COND_STRIDE + offset
+ // offset 0 = name/click, 1 = edit, 2 = delete
+ private static final int COND_BASE = 50;
+ private static final int COND_END = 80;
+ private static final int COND_STRIDE = 10;
+ private static final int BTN_ADD_COND = 80;
+
+ // ── Layout constants ──────────────────────────────────────────────────────
+ private static final int L_LABEL_X = 5;
+ private static final int ROW_H = 24;
+ private static final int MAX_ENTRIES = 10;
+
+ private final ChainedAbility chain;
+ private final IChainedAbilityConfigCallback callback;
+ private final boolean npcContext;
+ private final boolean readOnlyEntries;
+ private final List npcSlots;
+ private int activeTab = TAB_GENERAL;
+
+ private List fieldDefs;
+ private List entries;
+ private List conditions;
+
+ // Dynamic tabs injected by field providers (e.g., "Icon")
+ private List extraTabs = new ArrayList<>();
+
+ private GuiFieldBuilder builder;
+ private float[] tabScrollY;
+
+ // Entry editing
+ private int editingEntryIndex = -1;
+ private String pendingTypeId;
+ private boolean editingParentAbility = false;
+
+ // Condition editing
+ private int editingConditionIndex = -1;
+
+ // Save only on explicit X button, not ESC
+ private boolean saveOnClose = false;
+
+ // Track consumed NPC slot indices for deferred removal on save
+ private final List consumedSlotIndices = new ArrayList<>();
+
+ public SubGuiChainedAbilityConfig(ChainedAbility chain, IChainedAbilityConfigCallback callback) {
+ this(chain, callback, false, null, false);
+ }
+
+ public SubGuiChainedAbilityConfig(ChainedAbility chain, IChainedAbilityConfigCallback callback,
+ boolean npcContext, List npcSlots) {
+ this(chain, callback, npcContext, npcSlots, false);
+ }
+
+ public SubGuiChainedAbilityConfig(ChainedAbility chain, IChainedAbilityConfigCallback callback,
+ boolean npcContext, List npcSlots, boolean readOnlyEntries) {
+ this.chain = chain;
+ this.callback = callback;
+ this.npcContext = npcContext;
+ this.readOnlyEntries = readOnlyEntries;
+ this.npcSlots = npcSlots;
+ this.entries = new ArrayList<>(chain.getEntries());
+ this.conditions = new ArrayList<>(chain.getConditions());
+
+ buildFieldDefs();
+
+ setBackground("menubg.png", 217);
+ xSize = 356;
+ ySize = 200;
+ }
+
+ private void buildFieldDefs() {
+ fieldDefs = new ArrayList<>();
+
+ fieldDefs.add(FieldDef.stringField("gui.name", chain::getName, chain::setName).tab("General"));
+ fieldDefs.add(FieldDef.stringField("gui.displayName", chain::getRawDisplayName, chain::setDisplayName).tab("General"));
+ // Computed "Valid For" label (derived from child abilities)
+ fieldDefs.add(FieldDef.labelField("ability.validFor", () -> {
+ UserType ut = computeAllowedBy();
+ return StatCollector.translateToLocal("ability.validFor") + ": \u00A7e" + StatCollector.translateToLocal("ability.userType." + ut.name());
+ }).tab("General"));
+ fieldDefs.add(FieldDef.boolField("gui.enabled", chain::isEnabled, chain::setEnabled).tab("General"));
+ fieldDefs.add(FieldDef.intField("ability.weight", chain::getWeight, chain::setWeight).range(1, 100).tab("General"));
+ fieldDefs.add(FieldDef.boolField("ability.windUpAll", chain::isWindUpAll, chain::setWindUpAll).tab("General"));
+ fieldDefs.add(FieldDef.intField("ability.cooldown", chain::getCooldownTicks, chain::setCooldownTicks).range(0, 12000).tab("General"));
+
+ // ── Target tab ───────────────────────────────────────────────
+ fieldDefs.add(FieldDef.row(
+ FieldDef.floatField("ability.minRange", chain::getMinRange, chain::setMinRange).range(0, 64),
+ FieldDef.floatField("ability.maxRange", chain::getMaxRange, chain::setMaxRange).range(0, 64)
+ ).tab("Target"));
+
+ // External field providers (e.g., DBC Addon injecting an "Icon" tab)
+ if (AbilityController.Instance != null) {
+ for (IChainedAbilityFieldProvider provider : AbilityController.Instance.getChainedFieldProviders()) {
+ provider.addFieldDefinitions(chain, fieldDefs);
+ }
+ }
+
+ // Collect extra tab names from provider-injected fields
+ Set tabNames = new LinkedHashSet<>();
+ for (FieldDef def : fieldDefs) {
+ String tab = def.getTab();
+ if (tab != null && !tab.isEmpty() && !"General".equals(tab) && !"Target".equals(tab)) {
+ tabNames.add(tab);
+ }
+ }
+ extraTabs = new ArrayList<>(tabNames);
+
+ // Size scroll array for all tabs
+ tabScrollY = new float[CORE_TAB_COUNT + extraTabs.size()];
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // COMPUTED USERTYPE
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ /**
+ * Compute the allowed UserType from the current (possibly modified) entries list.
+ * Mirrors ChainedAbility.getAllowedBy() but uses the local entries.
+ */
+ private UserType computeAllowedBy() {
+ if (entries.isEmpty()) return UserType.BOTH;
+ boolean allAllowPlayer = true;
+ boolean allAllowNpc = true;
+ for (ChainedAbilityEntry entry : entries) {
+ Ability a = entry.resolve();
+ if (a == null) continue;
+ UserType ut = a.getAllowedBy();
+ if (!ut.allowsPlayer()) allAllowPlayer = false;
+ if (!ut.allowsNpc()) allAllowNpc = false;
+ }
+ if (allAllowPlayer && allAllowNpc) return UserType.BOTH;
+ if (allAllowPlayer) return UserType.PLAYER_ONLY;
+ if (allAllowNpc) return UserType.NPC_ONLY;
+ return UserType.NPC_ONLY;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // INIT GUI
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ @Override
+ public void initGui() {
+ // Commit any focused text field before rebuilding
+ GuiNpcTextField.unfocus();
+
+ // Save scroll position
+ GuiScrollWindow oldSw = getScrollableGui(0);
+ if (oldSw != null && activeTab < tabScrollY.length) {
+ tabScrollY[activeTab] = oldSw.nextScrollY;
+ }
+
+ super.initGui();
+
+ // Tab buttons — core tabs
+ GuiMenuTopButton generalTab = new GuiMenuTopButton(BTN_TAB_GENERAL, guiLeft + 4, guiTop - 17, "menu.general");
+ generalTab.active = (activeTab == TAB_GENERAL);
+ addTopButton(generalTab);
+
+ GuiMenuTopButton entriesTab = new GuiMenuTopButton(BTN_TAB_ENTRIES, generalTab, "ability.entries");
+ entriesTab.active = (activeTab == TAB_ENTRIES);
+ addTopButton(entriesTab);
+
+ GuiMenuTopButton targetTab = new GuiMenuTopButton(BTN_TAB_TARGET, entriesTab, "script.target");
+ targetTab.active = (activeTab == TAB_TARGET);
+ addTopButton(targetTab);
+
+ // Dynamic extra tabs from field providers
+ GuiMenuTopButton prevTab = targetTab;
+ for (int i = 0; i < extraTabs.size(); i++) {
+ int tabIndex = CORE_TAB_COUNT + i;
+ GuiMenuTopButton extraTab = new GuiMenuTopButton(BTN_TAB_EXTRA_BASE + i, prevTab, extraTabs.get(i));
+ extraTab.active = (activeTab == tabIndex);
+ addTopButton(extraTab);
+ prevTab = extraTab;
+ }
+
+ GuiMenuTopButton closeBtn = new GuiMenuTopButton(BTN_CLOSE, guiLeft + xSize - 22, guiTop - 17, "X");
+ addTopButton(closeBtn);
+
+ // Scroll window dimensions
+ int swX = guiLeft + 4;
+ int swY = guiTop + 5;
+ int swW = xSize - 8;
+ int swH = ySize - 10;
+
+ if (activeTab == TAB_GENERAL) {
+ buildFieldDefTab("General", swX, swY, swW, swH);
+ } else if (activeTab == TAB_ENTRIES) {
+ buildEntriesTab(swX, swY, swW, swH);
+ } else if (activeTab == TAB_TARGET) {
+ buildTargetTab(swX, swY, swW, swH);
+ } else if (activeTab >= CORE_TAB_COUNT) {
+ int extraIndex = activeTab - CORE_TAB_COUNT;
+ if (extraIndex < extraTabs.size()) {
+ buildFieldDefTab(extraTabs.get(extraIndex), swX, swY, swW, swH);
+ }
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // FIELDDEF-BASED TAB (General + dynamic provider tabs)
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ private void buildFieldDefTab(String tabName, int swX, int swY, int swW, int swH) {
+ List tabFields = new ArrayList<>();
+ for (FieldDef def : fieldDefs) {
+ if (tabName.equals(def.getTab()) && def.isVisible()) {
+ tabFields.add(def);
+ }
+ }
+
+ builder = new GuiFieldBuilder(this, fontRendererObj);
+ builder.startIds(DECLARATIVE_ID_START, CLEAR_ID_START, LABEL_ID_START);
+ builder.startY(5);
+
+ GuiScrollWindow sw = builder.buildScrollWindow(tabFields, swX, swY, swW, swH);
+ restoreScroll(sw);
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // ENTRIES TAB
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ private void buildEntriesTab(int swX, int swY, int swW, int swH) {
+ GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0);
+ sw.backgroundColor = 0x88000000;
+ addScrollableGui(0, sw);
+
+ int y = 5;
+ int labelCounter = 0;
+
+ for (int i = 0; i < entries.size() && i < MAX_ENTRIES; i++) {
+ ChainedAbilityEntry entry = entries.get(i);
+
+ // Index label
+ sw.addLabel(new GuiNpcLabel(labelCounter++, (i + 1) + ".", L_LABEL_X, y + 5, 0xFFFFFF));
+
+ // Ability reference/inline button
+ String btnLabel;
+ if (entry.isInline()) {
+ Ability a = entry.getInlineAbility();
+ btnLabel = a != null ? a.getDisplayName() : "\u00A7cEmpty";
+ } else {
+ String refName = entry.getAbilityReference();
+ if (refName == null || refName.isEmpty()) {
+ btnLabel = StatCollector.translateToLocal("gui.select") + "...";
+ } else {
+ Ability resolved = entry.resolve();
+ btnLabel = "\u00A7e> " + (resolved != null ? resolved.getDisplayName() : refName);
+ }
+ }
+ GuiNpcButton entryBtn = new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE, L_LABEL_X + 15, y, 140, 20, btnLabel);
+ if (readOnlyEntries) {
+ entryBtn.setEnabled(false);
+ }
+ sw.addButton(entryBtn);
+
+ if (!readOnlyEntries) {
+ // Delay label + field
+ sw.addLabel(new GuiNpcLabel(labelCounter++, "ability.delay", L_LABEL_X + 160, y + 5, 0xAAAAAA));
+ GuiNpcTextField delayField = new GuiNpcTextField(ENTRY_BASE + i * ENTRY_STRIDE + 1, this, fontRendererObj,
+ L_LABEL_X + 195, y, 40, 20, String.valueOf(entry.getDelayTicks()));
+ delayField.setIntegersOnly();
+ delayField.setMinMaxDefault(0, 6000, 0);
+ sw.addTextField(delayField);
+
+ // Up button
+ if (i > 0) {
+ sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 2, L_LABEL_X + 240, y, 20, 20, "\u2191"));
+ }
+
+ // Down button
+ if (i < entries.size() - 1) {
+ sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 3, L_LABEL_X + 263, y, 20, 20, "\u2193"));
+ }
+
+ // Delete button
+ sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 4, L_LABEL_X + 288, y, 20, 20, "X"));
+ } else {
+ // Read-only: show delay as label only
+ int delay = entry.getDelayTicks();
+ if (delay > 0) {
+ sw.addLabel(new GuiNpcLabel(labelCounter++, delay + "t", L_LABEL_X + 165, y + 5, 0x888888));
+ }
+ }
+
+ y += ROW_H;
+ }
+
+ // Add entry button (hidden in read-only mode)
+ if (!readOnlyEntries && entries.size() < MAX_ENTRIES) {
+ sw.addButton(new GuiNpcButton(BTN_ADD_ENTRY, L_LABEL_X, y, 80, 20, "gui.add"));
+ y += ROW_H;
+ }
+
+ sw.maxScrollY = Math.max(y - swH, 0);
+ restoreScroll(sw);
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // TARGET TAB (FieldDef fields + conditions list)
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ private void buildTargetTab(int swX, int swY, int swW, int swH) {
+ // Build FieldDef fields (ranges, Valid For) first
+ List tabFields = new ArrayList<>();
+ for (FieldDef def : fieldDefs) {
+ if ("Target".equals(def.getTab()) && def.isVisible()) {
+ tabFields.add(def);
+ }
+ }
+
+ builder = new GuiFieldBuilder(this, fontRendererObj);
+ builder.startIds(DECLARATIVE_ID_START, CLEAR_ID_START, LABEL_ID_START);
+ builder.startY(5);
+
+ GuiScrollWindow sw = builder.buildScrollWindow(tabFields, swX, swY, swW, swH);
+
+ // Append conditions list below the FieldDef fields
+ int y = builder.getLastBuildY();
+ int condY = renderConditions(sw, y, builder.getNextLabelId());
+ sw.maxScrollY = Math.max(condY - swH, 0);
+
+ restoreScroll(sw);
+ }
+
+ private int renderConditions(GuiScrollWindow sw, int y, int labelCounter) {
+ y += 3;
+ sw.addLabel(new GuiNpcLabel(labelCounter, "ability.conditions", L_LABEL_X, y + 2, 0xFFFF55));
+ y += 15;
+
+ for (int i = 0; i < conditions.size() && i < 3; i++) {
+ Condition cond = conditions.get(i);
+ String condName = getConditionDisplayName(cond);
+ sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE, L_LABEL_X, y, 140, 20, condName));
+ sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE + 1, L_LABEL_X + 145, y, 40, 20, "gui.edit"));
+ sw.addButton(new GuiNpcButton(COND_BASE + i * COND_STRIDE + 2, L_LABEL_X + 190, y, 20, 20, "X"));
+ y += 22;
+ }
+
+ if (conditions.size() < 3) {
+ sw.addButton(new GuiNpcButton(BTN_ADD_COND, L_LABEL_X, y, 50, 20, "gui.add"));
+ y += ROW_H;
+ }
+
+ return y;
+ }
+
+ private String getConditionDisplayName(Condition cond) {
+ if (cond == null) return "None";
+ String typeId = cond.getTypeId();
+ switch (typeId) {
+ case "hp_above":
+ return StatCollector.translateToLocal("condition.hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%";
+ case "hp_below":
+ return StatCollector.translateToLocal("condition.hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%";
+ case "target_hp_above":
+ return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%";
+ case "target_hp_below":
+ return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%";
+ case "hit_count":
+ Condition.ConditionHitCount hc = (Condition.ConditionHitCount) cond;
+ return StatCollector.translateToLocal("condition.hit_count") + ": " + hc.getRequiredHits() + "/" + hc.getWithinTicks() + "t";
+ default:
+ return typeId;
+ }
+ }
+
+ private float getConditionThreshold(Condition cond) {
+ NBTTagCompound nbt = cond.writeNBT();
+ return nbt.hasKey("threshold") ? nbt.getFloat("threshold") : 0.5f;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // SCROLL RESTORE
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ private void restoreScroll(GuiScrollWindow sw) {
+ if (activeTab < tabScrollY.length) {
+ float restored = Math.min(tabScrollY[activeTab], sw.maxScrollY);
+ sw.nextScrollY = restored;
+ sw.scrollY = restored;
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // BUTTON EVENTS
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ @Override
+ public void buttonEvent(GuiButton guibutton) {
+ int id = guibutton.id;
+
+ // Tab switching — core tabs
+ if (id == BTN_TAB_GENERAL) {
+ activeTab = TAB_GENERAL;
+ initGui();
+ return;
+ }
+ if (id == BTN_TAB_ENTRIES) {
+ activeTab = TAB_ENTRIES;
+ initGui();
+ return;
+ }
+ if (id == BTN_TAB_TARGET) {
+ activeTab = TAB_TARGET;
+ initGui();
+ return;
+ }
+ if (id == BTN_CLOSE) {
+ saveOnClose = true;
+ close();
+ return;
+ }
+
+ // Tab switching — extra tabs
+ if (id >= BTN_TAB_EXTRA_BASE && id < BTN_TAB_EXTRA_BASE + extraTabs.size()) {
+ activeTab = CORE_TAB_COUNT + (id - BTN_TAB_EXTRA_BASE);
+ initGui();
+ return;
+ }
+
+ // Entries tab
+ if (activeTab == TAB_ENTRIES) {
+ if (handleEntryButton(id)) return;
+ }
+
+ // Target tab (includes conditions)
+ if (activeTab == TAB_TARGET) {
+ if (handleConditionButton(id)) return;
+ }
+
+ // FieldDef-based tabs (General + extra tabs) — declarative field handling
+ if (isFieldDefTab() && builder != null) {
+ if (builder.handleButtonEvent(id, guibutton)) {
+ if (!hasSubGui()) {
+ initGui();
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Whether the current active tab uses the declarative FieldDef system.
+ */
+ private boolean isFieldDefTab() {
+ return activeTab == TAB_GENERAL || activeTab == TAB_TARGET || activeTab >= CORE_TAB_COUNT;
+ }
+
+ private boolean handleEntryButton(int id) {
+ // Add entry
+ if (id == BTN_ADD_ENTRY) {
+ if (entries.size() < MAX_ENTRIES) {
+ // Both contexts use entry source selector; hide NPC option in global context
+ setSubGui(new SubGuiChainedEntrySource(npcContext));
+ }
+ return true;
+ }
+
+ // Per-entry buttons (ENTRY_BASE to ENTRY_END)
+ if (id >= ENTRY_BASE && id < ENTRY_END) {
+ int entryIndex = (id - ENTRY_BASE) / ENTRY_STRIDE;
+ int action = (id - ENTRY_BASE) % ENTRY_STRIDE;
+
+ if (entryIndex < 0 || entryIndex >= entries.size()) return false;
+
+ switch (action) {
+ case 0: // Entry name button — reference: clone/modify dialog; inline: edit config
+ ChainedAbilityEntry clickedEntry = entries.get(entryIndex);
+ if (clickedEntry.isReference()) {
+ editingEntryIndex = entryIndex;
+ setSubGui(new SubGuiAbilityEditMode());
+ } else if (clickedEntry.isInline() && clickedEntry.getInlineAbility() != null) {
+ editingEntryIndex = entryIndex;
+ setSubGui(new SubGuiAbilityConfig(clickedEntry.getInlineAbility(), this));
+ }
+ return true;
+ case 2: // Up
+ if (entryIndex > 0) {
+ ChainedAbilityEntry entry = entries.remove(entryIndex);
+ entries.add(entryIndex - 1, entry);
+ initGui();
+ }
+ return true;
+ case 3: // Down
+ if (entryIndex < entries.size() - 1) {
+ ChainedAbilityEntry entry = entries.remove(entryIndex);
+ entries.add(entryIndex + 1, entry);
+ initGui();
+ }
+ return true;
+ case 4: // Delete
+ entries.remove(entryIndex);
+ initGui();
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean handleConditionButton(int id) {
+ if (id >= COND_BASE && id < COND_END) {
+ int condIndex = (id - COND_BASE) / COND_STRIDE;
+ int action = (id - COND_BASE) % COND_STRIDE;
+ if (action == 0 || action == 1) {
+ if (condIndex < conditions.size()) {
+ editingConditionIndex = condIndex;
+ setSubGui(new SubGuiConditionEdit(conditions.get(condIndex)));
+ }
+ } else if (action == 2) {
+ if (condIndex < conditions.size()) {
+ conditions.remove(condIndex);
+ initGui();
+ }
+ }
+ return true;
+ }
+ if (id == BTN_ADD_COND) {
+ if (conditions.size() < 3) {
+ editingConditionIndex = conditions.size();
+ setSubGui(new SubGuiConditionEdit(null));
+ }
+ return true;
+ }
+ return false;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // TEXT FIELD EVENTS
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ @Override
+ public void unFocused(GuiNpcTextField textField) {
+ int id = textField.id;
+
+ // FieldDef-based tabs — declarative fields
+ if (isFieldDefTab() && builder != null && id >= DECLARATIVE_ID_START) {
+ if (builder.handleTextFieldEvent(id, textField)) {
+ initGui();
+ }
+ return;
+ }
+
+ // Entries tab - delay fields (ENTRY_BASE + i * ENTRY_STRIDE + 1)
+ if (activeTab == TAB_ENTRIES && id >= ENTRY_BASE + 1 && id < ENTRY_END) {
+ int entryIndex = (id - ENTRY_BASE - 1) / ENTRY_STRIDE;
+ if (entryIndex >= 0 && entryIndex < entries.size()) {
+ entries.get(entryIndex).setDelayTicks(textField.getInteger());
+ }
+ }
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // SUB GUI CLOSED
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ @Override
+ public void subGuiClosed(SubGuiInterface subgui) {
+ // FieldDef-based tabs — declarative sub-gui handling
+ if (isFieldDefTab() && builder != null) {
+ if (builder.handleSubGuiClosed(subgui)) {
+ initGui();
+ return;
+ }
+ }
+
+ if (subgui instanceof SubGuiAbilitySelect) {
+ handleAbilitySelectClosed((SubGuiAbilitySelect) subgui);
+ initGui();
+ return;
+ }
+ if (subgui instanceof SubGuiChainedEntrySource) {
+ handleEntrySourceClosed((SubGuiChainedEntrySource) subgui);
+ return;
+ }
+ if (subgui instanceof SubGuiNpcSlotPicker) {
+ handleSlotPickerClosed((SubGuiNpcSlotPicker) subgui);
+ initGui();
+ return;
+ }
+ if (subgui instanceof SubGuiAbilityVariantSelect) {
+ handleVariantSelectClosed((SubGuiAbilityVariantSelect) subgui);
+ if (!hasSubGui()) initGui();
+ return;
+ }
+ if (subgui instanceof SubGuiAbilityTypeSelect) {
+ handleTypeSelectClosed((SubGuiAbilityTypeSelect) subgui);
+ if (!hasSubGui()) initGui();
+ return;
+ }
+ if (subgui instanceof SubGuiAbilityEditMode) {
+ handleEditModeClosed((SubGuiAbilityEditMode) subgui);
+ if (!hasSubGui()) initGui();
+ return;
+ }
+ if (subgui instanceof SubGuiAbilityConfig) {
+ editingEntryIndex = -1;
+ editingParentAbility = false;
+ initGui();
+ return;
+ }
+ if (subgui instanceof SubGuiConditionEdit) {
+ handleConditionEditClosed((SubGuiConditionEdit) subgui);
+ initGui();
+ return;
+ }
+ }
+
+ private void handleAbilitySelectClosed(SubGuiAbilitySelect gui) {
+ String selectedName = gui.getSelectedName();
+ if (selectedName != null) {
+ if (editingEntryIndex >= 0 && editingEntryIndex < entries.size()) {
+ int delay = entries.get(editingEntryIndex).getDelayTicks();
+ entries.set(editingEntryIndex, ChainedAbilityEntry.reference(selectedName, delay));
+ } else {
+ entries.add(ChainedAbilityEntry.reference(selectedName, 0));
+ }
+ }
+ editingEntryIndex = -1;
+ }
+
+ private void handleEntrySourceClosed(SubGuiChainedEntrySource gui) {
+ int source = gui.getResult();
+ if (source == SubGuiChainedEntrySource.SOURCE_NPC_SLOTS) {
+ setSubGui(new SubGuiNpcSlotPicker(npcSlots));
+ } else if (source == SubGuiChainedEntrySource.SOURCE_LOAD_PRESET) {
+ setSubGui(new SubGuiAbilitySelect());
+ } else if (source == SubGuiChainedEntrySource.SOURCE_CREATE_NEW) {
+ setSubGui(new SubGuiAbilityTypeSelect());
+ }
+ // Don't initGui — we're opening another SubGui
+ }
+
+ private void handleSlotPickerClosed(SubGuiNpcSlotPicker gui) {
+ int slotIndex = gui.getSelectedIndex();
+ if (slotIndex >= 0 && npcSlots != null && slotIndex < npcSlots.size()) {
+ AbilityAction consumed = npcSlots.get(slotIndex);
+ Ability ability = consumed.getAbility();
+ if (ability != null) {
+ if (consumed.getSlotType() == AbilityAction.SlotType.INLINE_ABILITY) {
+ entries.add(ChainedAbilityEntry.inline(ability, 0));
+ } else {
+ entries.add(ChainedAbilityEntry.reference(consumed.getReferenceId(), 0));
+ }
+ consumedSlotIndices.add(slotIndex);
+ }
+ }
+ }
+
+ private void handleTypeSelectClosed(SubGuiAbilityTypeSelect gui) {
+ String typeId = gui.getSelectedTypeId();
+ if (typeId != null) {
+ List 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(java.util.UUID.randomUUID().toString());
+ entries.add(ChainedAbilityEntry.inline(newAbility, 0));
+ editingEntryIndex = entries.size() - 1;
+ setSubGui(new SubGuiAbilityConfig(newAbility, this));
+ }
+ }
+ }
+
+ private void handleVariantSelectClosed(SubGuiAbilityVariantSelect gui) {
+ int idx = gui.getSelectedIndex();
+ if (idx >= 0 && pendingTypeId != null) {
+ Ability newAbility = AbilityController.Instance.create(pendingTypeId);
+ if (newAbility != null) {
+ gui.getVariants().get(idx).apply(newAbility);
+ newAbility.setId(java.util.UUID.randomUUID().toString());
+ entries.add(ChainedAbilityEntry.inline(newAbility, 0));
+ editingEntryIndex = entries.size() - 1;
+ pendingTypeId = null;
+ setSubGui(new SubGuiAbilityConfig(newAbility, this));
+ return;
+ }
+ }
+ pendingTypeId = null;
+ }
+
+ private void handleEditModeClosed(SubGuiAbilityEditMode gui) {
+ int mode = gui.getResult();
+ if (mode < 0 || editingEntryIndex < 0 || editingEntryIndex >= entries.size()) {
+ editingEntryIndex = -1;
+ return;
+ }
+
+ ChainedAbilityEntry entry = entries.get(editingEntryIndex);
+
+ if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) {
+ if (entry.convertToInline()) {
+ Ability a = entry.getInlineAbility();
+ if (a != null) {
+ a.setId(java.util.UUID.randomUUID().toString());
+ setSubGui(new SubGuiAbilityConfig(a, this));
+ return;
+ }
+ }
+ editingEntryIndex = -1;
+ } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) {
+ Ability resolved = entry.resolve();
+ if (resolved != null) {
+ editingParentAbility = true;
+ setSubGui(resolved.createConfigGui(this));
+ return;
+ }
+ editingEntryIndex = -1;
+ }
+ }
+
+ private void handleConditionEditClosed(SubGuiConditionEdit gui) {
+ Condition result = gui.getResult();
+ if (result != null && editingConditionIndex >= 0) {
+ if (editingConditionIndex < conditions.size()) {
+ conditions.set(editingConditionIndex, result);
+ } else {
+ conditions.add(result);
+ }
+ }
+ editingConditionIndex = -1;
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // CLOSE / APPLY
+ // ═══════════════════════════════════════════════════════════════════════════
+
+ @Override
+ public void onAbilitySaved(Ability ability) {
+ if (editingParentAbility) {
+ // Persist the global parent ability to the server
+ PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
+ editingParentAbility = false;
+ return;
+ }
+ // Inline ability mutated in-place; entry already holds the reference
+ }
+
+ @Override
+ public void close() {
+ if (saveOnClose) {
+ GuiNpcTextField.unfocus();
+ applyToChain();
+ callback.onChainedAbilitySaved(chain);
+ }
+ super.close();
+ }
+
+
+ private void applyToChain() {
+ // Entries
+ chain.getEntries().clear();
+ for (ChainedAbilityEntry entry : entries) {
+ chain.addEntry(entry);
+ }
+
+ // Conditions
+ chain.getConditions().clear();
+ for (Condition c : conditions) {
+ chain.getConditions().add(c);
+ }
+
+ // Remove consumed NPC slots (descending order to preserve indices)
+ if (npcSlots != null && !consumedSlotIndices.isEmpty()) {
+ Collections.sort(consumedSlotIndices, Collections.reverseOrder());
+ for (int idx : consumedSlotIndices) {
+ if (idx < npcSlots.size()) {
+ npcSlots.remove(idx);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedEntrySource.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedEntrySource.java
new file mode 100644
index 000000000..2c544a937
--- /dev/null
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedEntrySource.java
@@ -0,0 +1,58 @@
+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;
+
+/**
+ * Choice dialog for adding an entry to a chain.
+ * Options: From NPC Slots (optional), Load Preset, Create New.
+ */
+public class SubGuiChainedEntrySource extends SubGuiSimpleChoice {
+
+ public static final int SOURCE_NONE = -1;
+ public static final int SOURCE_NPC_SLOTS = 0;
+ public static final int SOURCE_LOAD_PRESET = 1;
+ public static final int SOURCE_CREATE_NEW = 2;
+
+ private final boolean showNpcOption;
+
+ public SubGuiChainedEntrySource() {
+ this(true);
+ }
+
+ public SubGuiChainedEntrySource(boolean showNpcOption) {
+ this.showNpcOption = showNpcOption;
+ setBackground("menubg.png");
+ xSize = 200;
+ ySize = showNpcOption ? 110 : 88;
+ }
+
+ @Override
+ public void initGui() {
+ super.initGui();
+
+ int y = guiTop + 10;
+ addLabel(new GuiNpcLabel(0, "ability.entrySource", guiLeft + 10, y));
+
+ y += 20;
+ if (showNpcOption) {
+ addButton(new GuiNpcButton(0, guiLeft + 10, y, 180, 20, "ability.fromNpc"));
+ y += 22;
+ }
+
+ addButton(new GuiNpcButton(1, guiLeft + 10, y, 180, 20, "ability.byReference"));
+
+ y += 22;
+ addButton(new GuiNpcButton(2, guiLeft + 10, y, 180, 20, "ability.createNew"));
+ }
+
+ @Override
+ public void buttonEvent(GuiButton guibutton) {
+ int id = guibutton.id;
+ if (id >= 0 && id <= 2) {
+ setResult(id);
+ close();
+ }
+ }
+}
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiLoadTypeChoice.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiLoadTypeChoice.java
new file mode 100644
index 000000000..cb3fd4cb3
--- /dev/null
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiLoadTypeChoice.java
@@ -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;
+
+/**
+ * Choice dialog for the Load button: Load Ability or Load Chain.
+ */
+public class SubGuiLoadTypeChoice extends SubGuiSimpleChoice {
+
+ public static final int RESULT_NONE = -1;
+ public static final int RESULT_ABILITY = 0;
+ public static final int RESULT_CHAIN = 1;
+
+ public SubGuiLoadTypeChoice() {
+ setBackground("menubg.png");
+ xSize = 200;
+ ySize = 80;
+ }
+
+ @Override
+ public void initGui() {
+ super.initGui();
+
+ int y = guiTop + 10;
+ addLabel(new GuiNpcLabel(0, "ability.loadType", guiLeft + 10, y));
+
+ y += 20;
+ addButton(new GuiNpcButton(0, guiLeft + 10, y, 85, 20, "ability.loadAbility"));
+ addButton(new GuiNpcButton(1, guiLeft + 105, y, 85, 20, "ability.loadChain"));
+
+ y += 24;
+ addButton(new GuiNpcButton(2, guiLeft + 55, y, 90, 20, "gui.cancel"));
+ }
+
+ @Override
+ public void buttonEvent(GuiButton guibutton) {
+ int id = guibutton.id;
+ if (id == 0 || id == 1) {
+ setResult(id);
+ close();
+ } else if (id == 2) {
+ close();
+ }
+ }
+}
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiNpcSlotPicker.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiNpcSlotPicker.java
new file mode 100644
index 000000000..90086857f
--- /dev/null
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiNpcSlotPicker.java
@@ -0,0 +1,113 @@
+package noppes.npcs.client.gui.advanced;
+
+import kamkeel.npcs.controllers.data.ability.Ability;
+import kamkeel.npcs.controllers.data.ability.AbilityAction;
+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.ICustomScrollListener;
+import noppes.npcs.client.gui.util.SubGuiInterface;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Picker for selecting an NPC ability slot to consume into a chain entry.
+ * Shows standalone (non-chain) ability slots from the NPC's current list.
+ */
+public class SubGuiNpcSlotPicker extends SubGuiInterface implements ICustomScrollListener {
+
+ private final List npcSlots;
+ private final List validIndices = new ArrayList<>();
+ private GuiCustomScroll scroll;
+ private int selectedIndex = -1;
+
+ public SubGuiNpcSlotPicker(List npcSlots) {
+ this.npcSlots = npcSlots;
+ setBackground("menubg.png");
+ xSize = 220;
+ ySize = 200;
+ }
+
+ @Override
+ public void initGui() {
+ super.initGui();
+
+ int y = guiTop + 5;
+ addLabel(new GuiNpcLabel(0, "ability.selectSlot", guiLeft + 10, y));
+ y += 14;
+
+ List displayList = new ArrayList<>();
+ validIndices.clear();
+
+ for (int i = 0; i < npcSlots.size(); i++) {
+ AbilityAction slot = npcSlots.get(i);
+ // Only show standalone abilities (not chains)
+ if (slot.isChain()) continue;
+
+ Ability ability = slot.getAbility();
+ if (ability == null) continue;
+
+ String typeName = I18n.format(ability.getTypeId());
+ String customName = ability.getName();
+ String display;
+ if (customName != null && !customName.isEmpty() && !customName.equals(typeName)) {
+ display = (i + 1) + ". " + customName + " (" + typeName + ")";
+ } else {
+ display = (i + 1) + ". " + typeName;
+ }
+
+ if (slot.isReference()) {
+ display = "\u00A7e" + display + " (Ref)";
+ }
+
+ displayList.add(display);
+ validIndices.add(i);
+ }
+
+ if (scroll == null) {
+ scroll = new GuiCustomScroll(this, 0);
+ scroll.setSize(200, 130);
+ }
+ scroll.guiLeft = guiLeft + 10;
+ scroll.guiTop = y;
+ scroll.setList(displayList);
+ addScroll(scroll);
+
+ y += 133;
+ addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.select"));
+ addButton(new GuiNpcButton(1, guiLeft + 115, y, 95, 20, "gui.cancel"));
+ getButton(0).setEnabled(selectedIndex >= 0);
+ }
+
+ @Override
+ public void buttonEvent(GuiButton guibutton) {
+ if (guibutton.id == 0 && selectedIndex >= 0) {
+ close();
+ } else if (guibutton.id == 1) {
+ selectedIndex = -1;
+ close();
+ }
+ }
+
+ @Override
+ public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) {
+ if (guiCustomScroll.id == 0 && guiCustomScroll.selected >= 0 && guiCustomScroll.selected < validIndices.size()) {
+ selectedIndex = validIndices.get(guiCustomScroll.selected);
+ initGui();
+ }
+ }
+
+ @Override
+ public void customScrollDoubleClicked(String selection, GuiCustomScroll guiCustomScroll) {
+ if (guiCustomScroll.id == 0 && selectedIndex >= 0) {
+ close();
+ }
+ }
+
+ public int getSelectedIndex() {
+ return selectedIndex;
+ }
+}
diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiSimpleChoice.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiSimpleChoice.java
new file mode 100644
index 000000000..1fb3e8fe8
--- /dev/null
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiSimpleChoice.java
@@ -0,0 +1,23 @@
+package noppes.npcs.client.gui.advanced;
+
+import noppes.npcs.client.gui.util.SubGuiInterface;
+
+/**
+ * Base class for simple choice dialogs that present a few buttons and return an int result.
+ * Subclasses define their own layout in {@code initGui()} and call {@code setResult()} from {@code buttonEvent()}.
+ * The parent reads the result via {@code getResult()} in {@code subGuiClosed()}.
+ *
+ * Result is -1 by default (cancelled / no selection).
+ */
+public abstract class SubGuiSimpleChoice extends SubGuiInterface {
+
+ private int result = -1;
+
+ protected final void setResult(int result) {
+ this.result = result;
+ }
+
+ public int getResult() {
+ return result;
+ }
+}
diff --git a/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java b/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java
index aec4b766a..66e433f45 100644
--- a/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java
+++ b/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java
@@ -2,12 +2,12 @@ package noppes.npcs.client.gui.builder;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
-import noppes.npcs.controllers.AnimationController;
-import noppes.npcs.controllers.data.Animation;
import noppes.npcs.client.gui.SubGuiColorSelector;
import noppes.npcs.client.gui.select.GuiAnimationSelection;
import noppes.npcs.client.gui.select.GuiSoundSelection;
import noppes.npcs.client.gui.util.SubGuiInterface;
+import noppes.npcs.controllers.AnimationController;
+import noppes.npcs.controllers.data.Animation;
import java.util.List;
import java.util.function.BooleanSupplier;
@@ -175,8 +175,8 @@ public class FieldDef {
}
public static FieldDef animSubGui(String label,
- Supplier idGetter, Consumer idSetter,
- Supplier nameGetter, Consumer nameSetter) {
+ Supplier idGetter, Consumer idSetter,
+ Supplier nameGetter, Consumer nameSetter) {
return subGuiField(label,
() -> new GuiAnimationSelection(idGetter.get(), nameGetter.get()),
gui -> {
@@ -202,7 +202,10 @@ public class FieldDef {
}
return "gui.none";
})
- .clearable(() -> { idSetter.accept(-1); nameSetter.accept(""); });
+ .clearable(() -> {
+ idSetter.accept(-1);
+ nameSetter.accept("");
+ });
}
// ═══════════════════════════════════════════════════════════════════
@@ -264,21 +267,65 @@ public class FieldDef {
// GETTERS
// ═══════════════════════════════════════════════════════════════════
- public String getLabel() { return label; }
- public FieldType getType() { return type; }
- public String getTab() { return tab; }
- public boolean isVisible() { return visibleWhen.getAsBoolean(); }
- public boolean isEnabled() { return enabledWhen.getAsBoolean(); }
- public String getHoverText() { return hoverText; }
- public float getMin() { return min; }
- public float getMax() { return max; }
- public boolean hasRange() { return min != Float.NEGATIVE_INFINITY || max != Float.POSITIVE_INFINITY; }
- public Class extends Enum>> getEnumClass() { return enumClass; }
- public String[] getStringEnumValues() { return stringEnumValues; }
- public Supplier getSubGuiFactory() { return subGuiFactory; }
- public Consumer getSubGuiResultHandler() { return subGuiResultHandler; }
- public boolean hasClearAction() { return clearAction != null; }
- public Runnable getClearAction() { return clearAction; }
+ public String getLabel() {
+ return label;
+ }
+
+ public FieldType getType() {
+ return type;
+ }
+
+ public String getTab() {
+ return tab;
+ }
+
+ public boolean isVisible() {
+ return visibleWhen.getAsBoolean();
+ }
+
+ public boolean isEnabled() {
+ return enabledWhen.getAsBoolean();
+ }
+
+ public String getHoverText() {
+ return hoverText;
+ }
+
+ public float getMin() {
+ return min;
+ }
+
+ public float getMax() {
+ return max;
+ }
+
+ public boolean hasRange() {
+ return min != Float.NEGATIVE_INFINITY || max != Float.POSITIVE_INFINITY;
+ }
+
+ public Class extends Enum>> getEnumClass() {
+ return enumClass;
+ }
+
+ public String[] getStringEnumValues() {
+ return stringEnumValues;
+ }
+
+ public Supplier getSubGuiFactory() {
+ return subGuiFactory;
+ }
+
+ public Consumer getSubGuiResultHandler() {
+ return subGuiResultHandler;
+ }
+
+ public boolean hasClearAction() {
+ return clearAction != null;
+ }
+
+ public Runnable getClearAction() {
+ return clearAction;
+ }
public Object getValue() {
return getter != null ? getter.get() : null;
@@ -305,8 +352,13 @@ public class FieldDef {
return buttonTextColorSupplier != null ? buttonTextColorSupplier.get() : null;
}
- public FieldDef getLeftChild() { return leftChild; }
- public FieldDef getRightChild() { return rightChild; }
+ public FieldDef getLeftChild() {
+ return leftChild;
+ }
+
+ public FieldDef getRightChild() {
+ return rightChild;
+ }
// ═══════════════════════════════════════════════════════════════════
// LIST MANIPULATION (for mod injection)
@@ -356,7 +408,7 @@ public class FieldDef {
*
* @param fields The field list to modify
* @param targetLabel The label of the field to modify
- * @param condition The condition to replace
+ * @param condition The condition to replace
* @return true if the target was found and the condition was modified
*/
public static boolean modifyVisibility(List fields, String targetLabel, BooleanSupplier condition) {
diff --git a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java
index 1ef3d52e5..bb069b8f5 100644
--- a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java
+++ b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java
@@ -69,13 +69,32 @@ public class GuiFieldBuilder {
// CONFIG CHAINING
// ═══════════════════════════════════════════════════════════════════
- public GuiFieldBuilder scrollWindowId(int id) { this.scrollWindowId = id; return this; }
- public GuiFieldBuilder contentRight(int v) { this.contentRight = v; return this; }
- public GuiFieldBuilder startIds(int widget, int clear, int label) {
- this.widgetId = widget; this.clearId = clear; this.labelId = label; return this;
+ public GuiFieldBuilder scrollWindowId(int id) {
+ this.scrollWindowId = id;
+ return this;
+ }
+
+ public GuiFieldBuilder contentRight(int v) {
+ this.contentRight = v;
+ return this;
+ }
+
+ public GuiFieldBuilder startIds(int widget, int clear, int label) {
+ this.widgetId = widget;
+ this.clearId = clear;
+ this.labelId = label;
+ return this;
+ }
+
+ public GuiFieldBuilder startY(int y) {
+ this.startY = y;
+ return this;
+ }
+
+ public GuiFieldBuilder rowHeight(int h) {
+ this.rowHeight = h;
+ return this;
}
- public GuiFieldBuilder startY(int y) { this.startY = y; return this; }
- public GuiFieldBuilder rowHeight(int h) { this.rowHeight = h; return this; }
// ═══════════════════════════════════════════════════════════════════
// BUILD
@@ -98,6 +117,7 @@ public class GuiFieldBuilder {
/**
* Processes all fields and adds widgets to the scroll window.
+ *
* @return the final Y position after all fields
*/
protected int build(List fields) {
@@ -173,8 +193,14 @@ public class GuiFieldBuilder {
}
private int lastHandledIndex = -1;
- protected void setLastHandledIndex(int index) { this.lastHandledIndex = index; }
- protected int getLastHandledIndex() { return lastHandledIndex; }
+
+ protected void setLastHandledIndex(int index) {
+ this.lastHandledIndex = index;
+ }
+
+ protected int getLastHandledIndex() {
+ return lastHandledIndex;
+ }
// ═══════════════════════════════════════════════════════════════════
// FIELD RENDERING
@@ -268,7 +294,10 @@ public class GuiFieldBuilder {
String curVal = def.getStringEnumValue();
int selected = 0;
for (int i = 0; i < values.length; i++) {
- if (values[i].equals(curVal)) { selected = i; break; }
+ if (values[i].equals(curVal)) {
+ selected = i;
+ break;
+ }
}
GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, fieldW, 20, values, selected);
if (!def.isEnabled()) btn.setEnabled(false);
@@ -321,14 +350,21 @@ public class GuiFieldBuilder {
protected int getFullFieldWidth(FieldDef def) {
switch (def.getType()) {
case FLOAT:
- case INT: return 60;
- case STRING: return 200;
- case BOOLEAN: return 50;
- case LABEL: return 60;
+ case INT:
+ return 60;
+ case STRING:
+ return 200;
+ case BOOLEAN:
+ return 50;
+ case LABEL:
+ return 60;
case ENUM:
- case STRING_ENUM: return 120;
- case SUB_GUI: return def.hasClearAction() ? 175 : 200;
- default: return 80;
+ case STRING_ENUM:
+ return 120;
+ case SUB_GUI:
+ return def.hasClearAction() ? 175 : 200;
+ default:
+ return 80;
}
}
@@ -410,7 +446,8 @@ public class GuiFieldBuilder {
def.setValue(newVal);
return true;
}
- } catch (NumberFormatException ignored) {}
+ } catch (NumberFormatException ignored) {
+ }
return false;
case INT:
try {
@@ -421,7 +458,8 @@ public class GuiFieldBuilder {
def.setValue(newVal);
return true;
}
- } catch (NumberFormatException ignored) {}
+ } catch (NumberFormatException ignored) {
+ }
return false;
case STRING:
String newVal = field.getText();
@@ -453,10 +491,27 @@ public class GuiFieldBuilder {
// GETTERS
// ═══════════════════════════════════════════════════════════════════
- public Map getButtonFieldMap() { return buttonFieldMap; }
- public Map getTextFieldMap() { return textFieldMap; }
- public Map getClearFieldMap() { return clearFieldMap; }
- public GuiScrollWindow getScrollWindow() { return sw; }
- public int getLastBuildY() { return lastBuildY; }
- public int getNextLabelId() { return labelId; }
+ public Map getButtonFieldMap() {
+ return buttonFieldMap;
+ }
+
+ public Map getTextFieldMap() {
+ return textFieldMap;
+ }
+
+ public Map getClearFieldMap() {
+ return clearFieldMap;
+ }
+
+ public GuiScrollWindow getScrollWindow() {
+ return sw;
+ }
+
+ public int getLastBuildY() {
+ return lastBuildY;
+ }
+
+ public int getNextLabelId() {
+ return labelId;
+ }
}
diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java
index d016e7c77..63e78dd11 100644
--- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java
+++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java
@@ -1,11 +1,15 @@
package noppes.npcs.client.gui.global;
-import kamkeel.npcs.client.renderer.TelegraphRenderer;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
-import kamkeel.npcs.controllers.data.ability.AbilityController;
-import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
+import kamkeel.npcs.controllers.data.ability.AbilityVariant;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket;
+import kamkeel.npcs.network.packets.request.ability.ChainedAbilitiesGetPacket;
+import kamkeel.npcs.network.packets.request.ability.ChainedAbilityGetPacket;
+import kamkeel.npcs.network.packets.request.ability.ChainedAbilityRemovePacket;
+import kamkeel.npcs.network.packets.request.ability.ChainedAbilitySavePacket;
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,10 +20,9 @@ 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.SubGuiChainedAbilityConfig;
import noppes.npcs.client.gui.advanced.SubGuiDuplicateNameConfirm;
import noppes.npcs.client.gui.util.AbilityPreviewExecutor;
import noppes.npcs.client.gui.util.GuiAbilityInterface;
@@ -29,6 +32,7 @@ import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
import noppes.npcs.client.gui.util.GuiTexturedButton;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
+import noppes.npcs.client.gui.util.IChainedAbilityConfigCallback;
import noppes.npcs.client.gui.util.ICustomScrollListener;
import noppes.npcs.client.gui.util.IGuiData;
import noppes.npcs.client.gui.util.IScrollData;
@@ -46,7 +50,7 @@ import java.util.Vector;
/**
* Global GUI for managing saved ability presets with 3D preview.
- *
+ *
* Features:
* - Left side: 3D preview of NPC with ability effects
* - Right side: Ability list with search
@@ -54,8 +58,29 @@ import java.util.Vector;
* - Toggle between Custom and Built-in ability views
*/
public class GuiNpcManageAbilities extends GuiAbilityInterface
- implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback,
- ITextfieldListener, GuiYesNoCallback, IScrollData, IGuiData {
+ implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback,
+ IChainedAbilityConfigCallback, ITextfieldListener, GuiYesNoCallback,
+ IScrollData, IGuiData {
+
+ // ── Button IDs ────────────────────────────────────────────────────────────
+ private static final int BTN_REMOVE = 1;
+ private static final int BTN_ADD = 2;
+ private static final int BTN_TOGGLE_VIEW = 10;
+ private static final int BTN_EDIT = 100;
+ private static final int BTN_PREVIEW_PLAY = 91;
+ private static final int BTN_PREVIEW_PAUSE = 92;
+ private static final int BTN_PREVIEW_STOP = 93;
+
+ // ── TextField / Label IDs ─────────────────────────────────────────────────
+ private static final int TF_SEARCH = 55;
+ private static final int LBL_PREVIEW_STATUS = 90;
+
+ // ── Scroll IDs ────────────────────────────────────────────────────────────
+ private static final int SCROLL_MAIN = 0;
+
+ // ── Confirm dialog IDs ────────────────────────────────────────────────────
+ private static final int CONFIRM_REMOVE_ABILITY = 1;
+ private static final int CONFIRM_REMOVE_CHAIN = 2;
// ==================== DATA ====================
private GuiCustomScroll scroll;
@@ -65,10 +90,18 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
private String search = "";
private Ability selectedAbility = null;
- // ==================== BUILT-IN TOGGLE ====================
+ // ==================== VIEW MODE ====================
+ // 0 = Custom, 1 = Built-in, 2 = Chained
+ private int viewMode = 0;
private boolean showingBuiltIn = false;
+ private boolean showingChained = false;
private boolean currentIsBuiltIn = false;
+ // ==================== CHAINED DATA ====================
+ private HashMap chainedData = new HashMap<>();
+ private ChainedAbility selectedChain = null;
+ private ChainedAbility pendingSaveChain = null;
+
// ==================== PENDING SAVE ====================
private Ability pendingSaveAbility = null;
private boolean pendingNewCreation = false;
@@ -103,45 +136,51 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
public void initGui() {
super.initGui();
- // 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));
+ // Toggle button — cycles: Custom → Built-in → Chained
+ String toggleLabel = showingChained ? "gui.chained" : (showingBuiltIn ? "gui.builtin" : "gui.custom");
+ addButton(new GuiNpcButton(BTN_TOGGLE_VIEW, guiLeft + 368, guiTop + 52, 45, 20, toggleLabel));
- if (!showingBuiltIn) {
- // Add button — only for custom view
- addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 8, 45, 20, "gui.add"));
-
- // 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() && customData.containsKey(selected));
+ if (showingChained) {
+ // Chained view buttons
+ addButton(new GuiNpcButton(BTN_ADD, guiLeft + 368, guiTop + 8, 45, 20, "gui.add"));
+ addButton(new GuiNpcButton(BTN_REMOVE, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove"));
+ getButton(BTN_REMOVE).setEnabled(selected != null && !selected.isEmpty() && chainedData.containsKey(selected));
+ addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 368, guiTop + 74, 45, 20, "gui.edit"));
+ getButton(BTN_EDIT).setEnabled(selected != null && !selected.isEmpty() && selectedChain != null);
+ } else if (!showingBuiltIn) {
+ // Custom view buttons
+ addButton(new GuiNpcButton(BTN_ADD, guiLeft + 368, guiTop + 8, 45, 20, "gui.add"));
+ addButton(new GuiNpcButton(BTN_REMOVE, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove"));
+ getButton(BTN_REMOVE).setEnabled(selected != null && !selected.isEmpty() && customData.containsKey(selected));
+ if (!currentIsBuiltIn) {
+ addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 368, guiTop + 74, 45, 20, "gui.edit"));
+ getButton(BTN_EDIT).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null);
+ }
}
- // 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)
+ // Scroll list (right side)
if (scroll == null) {
- scroll = new GuiCustomScroll(this, 0);
+ scroll = new GuiCustomScroll(this, SCROLL_MAIN);
scroll.setSize(143, 185);
}
scroll.guiLeft = guiLeft + 220;
scroll.guiTop = guiTop + 4;
addScroll(scroll);
- // Update scroll list
scroll.setList(getSearchList());
if (selected != null) {
scroll.setSelected(selected);
}
// Search bar
- addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search));
+ addTextField(new GuiNpcTextField(TF_SEARCH, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search));
- // Show ability info only if one is selected
- if (selectedAbility == null || selected == null) {
+ // Check if we have something to preview
+ if (showingChained) {
+ if (selectedChain == null || selected == null || selectedChain.getEntries().isEmpty()) {
+ return;
+ }
+ } else if (selectedAbility == null || selected == null) {
return;
}
@@ -154,22 +193,20 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
boolean isActive = previewExecutor.isActive();
if (!isPlaying || isPaused) {
- // Show Play button
String statusKey = isPaused ? "animation.paused" : "animation.stopped";
- addLabel(new GuiNpcLabel(90, statusKey, guiLeft + playButtonOffsetX, guiTop + 198));
- addButton(new GuiTexturedButton(91, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 11, 20, animTexture, 18, 71));
+ addLabel(new GuiNpcLabel(LBL_PREVIEW_STATUS, statusKey, guiLeft + playButtonOffsetX, guiTop + 198));
+ addButton(new GuiTexturedButton(BTN_PREVIEW_PLAY, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 11, 20, animTexture, 18, 71));
} else {
- // Show Pause button
- addLabel(new GuiNpcLabel(90, "animation.playing", guiLeft + playButtonOffsetX, guiTop + 198));
- addButton(new GuiTexturedButton(92, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 14, 20, animTexture, 0, 71));
+ addLabel(new GuiNpcLabel(LBL_PREVIEW_STATUS, "animation.playing", guiLeft + playButtonOffsetX, guiTop + 198));
+ addButton(new GuiTexturedButton(BTN_PREVIEW_PAUSE, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 14, 20, animTexture, 0, 71));
}
if (isActive) {
- // Show Stop button
- addButton(new GuiTexturedButton(93, "", guiLeft + playButtonOffsetX + 90, guiTop + 192, 14, 20, animTexture, 33, 71));
+ addButton(new GuiTexturedButton(BTN_PREVIEW_STOP, "", guiLeft + playButtonOffsetX + 90, guiTop + 192, 14, 20, animTexture, 33, 71));
}
}
private HashMap getCurrentData() {
+ if (showingChained) return chainedData;
return showingBuiltIn ? builtInData : customData;
}
@@ -240,59 +277,94 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
int id = guibutton.id;
- // Toggle between custom and built-in views
- if (id == 10) {
- showingBuiltIn = !showingBuiltIn;
+ // Toggle: Custom → Built-in → Chained → Custom
+ if (id == BTN_TOGGLE_VIEW) {
+ if (!showingBuiltIn && !showingChained) {
+ // Custom → Built-in
+ showingBuiltIn = true;
+ showingChained = false;
+ } else if (showingBuiltIn) {
+ // Built-in → Chained
+ showingBuiltIn = false;
+ showingChained = true;
+ } else {
+ // Chained → Custom
+ showingChained = false;
+ showingBuiltIn = false;
+ }
selected = null;
selectedAbility = null;
+ selectedChain = null;
currentIsBuiltIn = false;
search = "";
previewExecutor.stop();
if (scroll != null) {
scroll.clear();
}
- PacketClient.sendClient(new CustomAbilitiesGetPacket());
+ if (showingChained) {
+ PacketClient.sendClient(new ChainedAbilitiesGetPacket());
+ } else {
+ PacketClient.sendClient(new CustomAbilitiesGetPacket());
+ }
initGui();
return;
}
- if (id == 2 && !showingBuiltIn) {
+ // Preview controls (shared across all views)
+ if (id == BTN_PREVIEW_PLAY) {
+ if (previewExecutor.isPaused()) {
+ previewExecutor.play();
+ } else if (showingChained && selectedChain != null) {
+ previewExecutor.startChainPreview(selectedChain, npc);
+ } else if (selectedAbility != null) {
+ previewExecutor.startPreview(selectedAbility, npc);
+ }
+ initGui();
+ return;
+ } else if (id == BTN_PREVIEW_PAUSE) {
+ previewExecutor.pause();
+ initGui();
+ return;
+ } else if (id == BTN_PREVIEW_STOP) {
+ previewExecutor.stop();
+ initGui();
+ return;
+ }
+
+ if (showingChained) {
+ if (id == BTN_ADD) {
+ // New chained ability
+ ChainedAbility newChain = new ChainedAbility("New Chain");
+ setSubGui(new SubGuiChainedAbilityConfig(newChain, this));
+ } else if (id == BTN_REMOVE && selected != null) {
+ GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), CONFIRM_REMOVE_CHAIN);
+ displayGuiScreen(guiyesno);
+ } else if (id == BTN_EDIT && selectedChain != null) {
+ previewExecutor.stop();
+ setSubGui(new SubGuiChainedAbilityConfig(selectedChain, this));
+ }
+ return;
+ }
+
+ if (id == BTN_ADD && !showingBuiltIn) {
// Add — open type selection
setSubGui(new SubGuiAbilityTypeSelect());
- } else if (id == 1 && !showingBuiltIn && selected != null) {
- GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1);
+ } else if (id == BTN_REMOVE && !showingBuiltIn && selected != null) {
+ GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), CONFIRM_REMOVE_ABILITY);
displayGuiScreen(guiyesno);
- } else if (id == 100 && !showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) {
+ } else if (id == BTN_EDIT && !showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) {
// Edit
previewExecutor.stop();
setSubGui(selectedAbility.createConfigGui(this));
- } else if (id == 91) {
- // Play button
- if (selectedAbility != null) {
- if (previewExecutor.isPaused()) {
- previewExecutor.play();
- } else {
- previewExecutor.startPreview(selectedAbility, npc);
- }
- initGui();
- }
- } else if (id == 92) {
- // Pause button
- previewExecutor.pause();
- initGui();
- } else if (id == 93) {
- // Stop button
- previewExecutor.stop();
- initGui();
}
}
@Override
public void keyTyped(char c, int i) {
super.keyTyped(c, i);
- if (getTextField(55) != null && getTextField(55).isFocused()) {
- if (!search.equals(getTextField(55).getText())) {
- search = getTextField(55).getText();
+ if (getTextField(TF_SEARCH) != null && getTextField(TF_SEARCH).isFocused()) {
+ if (!search.equals(getTextField(TF_SEARCH).getText())) {
+ search = getTextField(TF_SEARCH).getText();
scroll.resetScroll();
scroll.setList(getSearchList());
}
@@ -301,13 +373,15 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
@Override
public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) {
- if (scroll.id == 0) {
+ if (scroll.id == SCROLL_MAIN) {
String newSelection = scroll.getSelected();
if (newSelection != null && !newSelection.equals(selected)) {
previewExecutor.stop();
selected = newSelection;
- if (showingBuiltIn) {
+ if (showingChained) {
+ PacketClient.sendClient(new ChainedAbilityGetPacket(selected));
+ } else if (showingBuiltIn) {
currentIsBuiltIn = true;
PacketClient.sendClient(new BuiltInAbilityGetPacket(selected));
} else {
@@ -320,9 +394,10 @@ 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 — only for custom, non-built-in abilities
- if (!showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) {
+ if (scroll.id == SCROLL_MAIN && selection != null && !selection.isEmpty()) {
+ if (showingChained && selectedChain != null) {
+ setSubGui(new SubGuiChainedAbilityConfig(selectedChain, this));
+ } else if (!showingBuiltIn && !showingChained && !currentIsBuiltIn && selectedAbility != null) {
previewExecutor.stop();
setSubGui(selectedAbility.createConfigGui(this));
}
@@ -334,7 +409,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
if (type == EnumScrollData.CUSTOM_ABILITIES) {
String prevSelected = scroll != null ? scroll.getSelected() : null;
this.customData = data;
- if (!showingBuiltIn && scroll != null) {
+ if (!showingBuiltIn && !showingChained && scroll != null) {
scroll.setList(getSearchList());
if (prevSelected != null && customData.containsKey(prevSelected)) {
scroll.setSelected(prevSelected);
@@ -350,11 +425,33 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
}
}
initGui();
+ } else if (type == EnumScrollData.CHAINED_ABILITIES) {
+ String prevSelected = scroll != null ? scroll.getSelected() : null;
+ this.chainedData = data;
+ if (showingChained && scroll != null) {
+ scroll.setList(getSearchList());
+ if (prevSelected != null && chainedData.containsKey(prevSelected)) {
+ scroll.setSelected(prevSelected);
+ }
+ }
+ initGui();
}
}
@Override
public void setGuiData(NBTTagCompound compound) {
+ // Detect chained ability response (has "Entries" tag)
+ if (showingChained && compound.hasKey("Entries")) {
+ selectedChain = new ChainedAbility();
+ selectedChain.readNBT(compound);
+ selected = selectedChain.getName();
+ if (scroll != null) {
+ scroll.setSelected(selected);
+ }
+ initGui();
+ return;
+ }
+
// Check if this is a built-in ability response
currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn");
@@ -380,90 +477,131 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
@Override
public void subGuiClosed(SubGuiInterface subgui) {
+ if (subgui instanceof SubGuiChainedAbilityConfig) {
+ handleChainConfigClosed();
+ initGui();
+ return;
+ }
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);
- if (hasDuplicateName(newAbility)) {
- pendingSaveAbility = newAbility;
- pendingNewCreation = true;
- pendingTypeId = null;
- setSubGui(new SubGuiDuplicateNameConfirm());
- return;
- }
- pendingTypeId = null;
- openConfig(newAbility);
- return;
- }
- }
- pendingTypeId = null;
+ if (handleVariantSelectClosed((SubGuiAbilityVariantSelect) subgui)) return;
} else if (subgui instanceof SubGuiAbilityTypeSelect) {
- String typeId = ((SubGuiAbilityTypeSelect) subgui).getSelectedTypeId();
- if (typeId != null) {
- java.util.List 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);
- }
- if (hasDuplicateName(newAbility)) {
- pendingSaveAbility = newAbility;
- pendingNewCreation = true;
- setSubGui(new SubGuiDuplicateNameConfirm());
- return;
- }
- openConfig(newAbility);
- return;
- }
- }
+ if (handleTypeSelectClosed((SubGuiAbilityTypeSelect) subgui)) 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;
- }
- }
+ if (handleDuplicateNameClosed((SubGuiDuplicateNameConfirm) subgui)) 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;
+ if (handlePendingSave()) return;
}
initGui();
}
+ private void handleChainConfigClosed() {
+ if (pendingSaveChain != null) {
+ PacketClient.sendClient(new ChainedAbilitySavePacket(pendingSaveChain.writeNBT()));
+ PacketClient.sendClient(new ChainedAbilitiesGetPacket());
+ selected = pendingSaveChain.getName();
+ pendingSaveChain = null;
+ }
+ }
+
+ /**
+ * @return true if a SubGui was opened (caller should return early, skipping initGui)
+ */
+ private boolean handleVariantSelectClosed(SubGuiAbilityVariantSelect gui) {
+ int idx = gui.getSelectedIndex();
+ if (idx >= 0 && pendingTypeId != null) {
+ Ability newAbility = AbilityController.Instance.create(pendingTypeId);
+ if (newAbility != null) {
+ gui.getVariants().get(idx).apply(newAbility);
+ if (hasDuplicateName(newAbility)) {
+ pendingSaveAbility = newAbility;
+ pendingNewCreation = true;
+ pendingTypeId = null;
+ setSubGui(new SubGuiDuplicateNameConfirm());
+ return true;
+ }
+ pendingTypeId = null;
+ openConfig(newAbility);
+ return true;
+ }
+ }
+ pendingTypeId = null;
+ return false;
+ }
+
+ /**
+ * @return true if a SubGui was opened (caller should return early, skipping initGui)
+ */
+ private boolean handleTypeSelectClosed(SubGuiAbilityTypeSelect gui) {
+ String typeId = gui.getSelectedTypeId();
+ if (typeId != null) {
+ java.util.List variants = AbilityController.Instance.getVariantsForType(typeId);
+ if (variants.size() > 1) {
+ pendingTypeId = typeId;
+ setSubGui(new SubGuiAbilityVariantSelect(variants));
+ return true;
+ }
+ Ability newAbility = AbilityController.Instance.create(typeId);
+ if (newAbility != null) {
+ if (variants.size() == 1) {
+ variants.get(0).apply(newAbility);
+ }
+ if (hasDuplicateName(newAbility)) {
+ pendingSaveAbility = newAbility;
+ pendingNewCreation = true;
+ setSubGui(new SubGuiDuplicateNameConfirm());
+ return true;
+ }
+ openConfig(newAbility);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return true if a SubGui was opened (caller should return early, skipping initGui)
+ */
+ private boolean handleDuplicateNameClosed(SubGuiDuplicateNameConfirm gui) {
+ if (gui.isConfirmed() && pendingSaveAbility != null) {
+ if (pendingNewCreation) {
+ openConfig(pendingSaveAbility);
+ pendingSaveAbility = null;
+ pendingNewCreation = false;
+ return true;
+ } else {
+ PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
+ PacketClient.sendClient(new CustomAbilitiesGetPacket());
+ pendingSaveAbility = null;
+ }
+ } else if (pendingSaveAbility != null) {
+ if (pendingNewCreation) {
+ pendingSaveAbility = null;
+ pendingNewCreation = false;
+ } else {
+ Ability ability = pendingSaveAbility;
+ pendingSaveAbility = null;
+ setSubGui(ability.createConfigGui(this));
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return true if a SubGui was opened (caller should return early, skipping initGui)
+ */
+ private boolean handlePendingSave() {
+ if (hasDuplicateName(pendingSaveAbility)) {
+ pendingNewCreation = false;
+ setSubGui(new SubGuiDuplicateNameConfirm());
+ return true;
+ }
+ PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
+ PacketClient.sendClient(new CustomAbilitiesGetPacket());
+ pendingSaveAbility = null;
+ return false;
+ }
+
private void openConfig(Ability ability) {
selectedAbility = ability;
selected = ability.getName();
@@ -474,9 +612,12 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
private boolean hasDuplicateName(Ability ability) {
String name = ability.getName();
if (name == null || name.isEmpty()) return false;
- String oldName = ability.getId();
- // A duplicate exists if the name is already in use and it's not the same ability being edited
- return customData.containsKey(name) && !name.equals(oldName);
+ if (!customData.containsKey(name)) return false;
+ // Name exists — check if it belongs to a different ability (by UUID)
+ String uuid = ability.getId();
+ if (uuid == null || uuid.isEmpty()) return true; // New ability with conflicting name
+ Ability existing = AbilityController.Instance.getCustomAbilityByName(name);
+ return existing == null || !uuid.equals(existing.getId());
}
@Override
@@ -484,12 +625,18 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
NoppesUtil.openGUI(player, this);
if (!result) return;
- if (id == 1 && selected != null) {
+ if (id == CONFIRM_REMOVE_ABILITY && selected != null) {
PacketClient.sendClient(new CustomAbilityRemovePacket(selected));
scroll.clear();
selected = null;
selectedAbility = null;
initGui();
+ } else if (id == CONFIRM_REMOVE_CHAIN && selected != null) {
+ PacketClient.sendClient(new ChainedAbilityRemovePacket(selected));
+ scroll.clear();
+ selected = null;
+ selectedChain = null;
+ initGui();
}
}
@@ -499,6 +646,11 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
pendingSaveAbility = ability;
}
+ @Override
+ public void onChainedAbilitySaved(ChainedAbility chain) {
+ pendingSaveChain = chain;
+ }
+
@Override
public void unFocused(GuiNpcTextField textfield) {
// Search field handling is done in keyTyped
diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java
index 11ffa198d..f07cb5ac6 100644
--- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java
+++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java
@@ -18,13 +18,14 @@ import noppes.npcs.containers.ContainerAuctionTrades;
import noppes.npcs.containers.SlotAuctionDisplay;
import noppes.npcs.controllers.data.AuctionClaim;
import noppes.npcs.controllers.data.AuctionListing;
+import noppes.npcs.util.AuctionFormatUtil;
import java.util.List;
/**
* Handles adding auction information to item tooltips when viewing auction GUIs.
* Called from ItemTooltipEvent in ClientEventHandler.
- *
+ *
* Uses slot-based lookup (slot index) instead of ItemStack comparison to correctly
* handle duplicate items with different auction data (e.g., two porkchops at different prices).
*/
@@ -88,25 +89,25 @@ public class AuctionTooltipHandler {
// Current bid or starting price
if (listing.hasBids()) {
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid")
- .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid)));
+ .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.currentBid)));
tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount));
} else {
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.startingPrice")
- .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.startingPrice)));
+ .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.startingPrice)));
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids"));
}
// Buyout price
if (listing.hasBuyout()) {
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.buyout")
- .replace("%s", EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice)));
+ .replace("%s", EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrencyWithName(listing.buyoutPrice)));
}
// Time remaining
long timeRemaining = listing.getTimeRemaining();
String timeText = StatCollector.translateToLocal("auction.timeLeft")
- .replace("%s", formatTimeRemaining(timeRemaining));
- tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
+ .replace("%s", AuctionFormatUtil.formatTimeRemaining(timeRemaining));
+ tooltip.add((AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
}
// ========== Trades Page ==========
@@ -144,7 +145,7 @@ public class AuctionTooltipHandler {
if (listing.hasBids()) {
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid")
- .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid)));
+ .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.currentBid)));
tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount));
} else {
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids"));
@@ -152,8 +153,8 @@ public class AuctionTooltipHandler {
long timeRemaining = listing.getTimeRemaining();
String timeText = StatCollector.translateToLocal("auction.timeLeft")
- .replace("%s", formatTimeRemaining(timeRemaining));
- tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
+ .replace("%s", AuctionFormatUtil.formatTimeRemaining(timeRemaining));
+ tooltip.add((AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
tooltip.add("");
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickToCancel"));
@@ -164,13 +165,13 @@ public class AuctionTooltipHandler {
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.bidding"));
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid")
- .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid)));
+ .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.currentBid)));
tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount));
long timeRemaining = listing.getTimeRemaining();
String timeText = StatCollector.translateToLocal("auction.timeLeft")
- .replace("%s", formatTimeRemaining(timeRemaining));
- tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
+ .replace("%s", AuctionFormatUtil.formatTimeRemaining(timeRemaining));
+ tooltip.add((AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
tooltip.add("");
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.clickToRebid"));
@@ -210,7 +211,7 @@ public class AuctionTooltipHandler {
// Amount for currency claims
if (claim.type.isCurrency()) {
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount")
- .replace("%s", EnumChatFormatting.GOLD + formatCurrency(claim.currency)));
+ .replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(claim.currency)));
}
// Expiration
@@ -254,48 +255,4 @@ public class AuctionTooltipHandler {
}
}
- // ========== Formatting Utilities ==========
-
- private static String formatCurrency(long amount) {
- if (amount < 1000) {
- return amount + " " + AuctionClientConfig.getCurrencyName();
- }
- StringBuilder sb = new StringBuilder();
- String str = "" + amount;
- int count = 0;
- for (int i = str.length() - 1; i >= 0; i--) {
- if (count > 0 && count % 3 == 0) {
- sb.insert(0, ',');
- }
- sb.insert(0, str.charAt(i));
- count++;
- }
- return sb.toString() + " " + AuctionClientConfig.getCurrencyName();
- }
-
- private static String formatTimeRemaining(long milliseconds) {
- if (milliseconds <= 0) {
- return StatCollector.translateToLocal("auction.ended");
- }
-
- long seconds = (milliseconds / 1000) % 60;
- long minutes = (milliseconds / 60000) % 60;
- long hours = (milliseconds / 3600000) % 24;
- long days = milliseconds / 86400000;
-
- StringBuilder sb = new StringBuilder();
- if (days > 0) {
- sb.append(days).append("d ");
- }
- if (hours > 0 || days > 0) {
- sb.append(hours).append("h ");
- }
- if (minutes > 0 || hours > 0 || days > 0) {
- sb.append(minutes).append("m");
- } else {
- sb.append(seconds).append("s");
- }
-
- return sb.toString();
- }
}
diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java
index afe47fb44..61634bb0a 100644
--- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java
+++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java
@@ -15,6 +15,7 @@ import noppes.npcs.client.gui.util.SubGuiInterface;
import noppes.npcs.containers.ContainerAuctionBidding;
import noppes.npcs.controllers.data.AuctionListing;
import noppes.npcs.entity.EntityNPCInterface;
+import noppes.npcs.util.AuctionFormatUtil;
import org.lwjgl.opengl.GL11;
import java.util.ArrayList;
@@ -36,7 +37,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis
private int btnBidY = 110;
private int btnBuyoutX = 82;
private int btnBuyoutY = 110;
- private int infoX = 110;
+ private int infoX = 105;
private int infoY = 50;
// Button IDs
@@ -81,7 +82,9 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis
return -1;
}
- /** Update button visibility based on listing state */
+ /**
+ * Update button visibility based on listing state
+ */
private void updateButtonState() {
if (listing == null) {
if (btnBid != null) btnBid.setVisible(false);
@@ -184,9 +187,11 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis
drawAuctionInfo();
}
- /** Draw auction listing information on the right side */
+ /**
+ * Draw auction listing information on the right side
+ */
private void drawAuctionInfo() {
- int x = guiLeft + 105;
+ int x = guiLeft + infoX;
int y = guiTop + infoY;
int xRight = guiLeft + 215;
@@ -206,7 +211,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis
String bidLabel = StatCollector.translateToLocal("auction.info.currentBid") + ":";
fontRendererObj.drawString(EnumChatFormatting.YELLOW + bidLabel, x, y, 0xFFFFFF);
- label = formatCurrency(listing.currentBid);
+ label = AuctionFormatUtil.formatCurrency(listing.currentBid);
length = fontRendererObj.getStringWidth(label);
fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF);
y += lineHeight + 4;
@@ -223,9 +228,9 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis
String startLabel = StatCollector.translateToLocal("auction.info.startingPrice");
fontRendererObj.drawString(EnumChatFormatting.YELLOW + startLabel, x, y, 0xFFFFFF);
- label = formatCurrency(listing.startingPrice);
+ label = AuctionFormatUtil.formatCurrency(listing.startingPrice);
length = fontRendererObj.getStringWidth(label);
- fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF);
+ fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF);
y += lineHeight + 4;
}
@@ -234,9 +239,9 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis
String buyoutLabel = StatCollector.translateToLocal("auction.info.buyoutPrice");
fontRendererObj.drawString(EnumChatFormatting.GREEN + buyoutLabel, x, y, 0xFFFFFF);
- label = formatCurrency(listing.buyoutPrice);
+ label = AuctionFormatUtil.formatCurrency(listing.buyoutPrice);
length = fontRendererObj.getStringWidth(label);
- fontRendererObj.drawString(EnumChatFormatting.GREEN + label, xRight - length, y, 0xFFFFFF);
+ fontRendererObj.drawString(EnumChatFormatting.GREEN + label, xRight - length, y, 0xFFFFFF);
y += lineHeight + 4;
}
@@ -244,9 +249,9 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis
String timeLabel = StatCollector.translateToLocal("auction.info.timeLeft");
fontRendererObj.drawString(EnumChatFormatting.GRAY + timeLabel, x, y, 0xFFFFFF);
long timeRemaining = listing.getTimeRemaining();
- EnumChatFormatting timeColor = timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE;
+ EnumChatFormatting timeColor = AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE;
- label = timeColor + formatTimeRemaining(timeRemaining);
+ label = timeColor + AuctionFormatUtil.formatTimeRemaining(timeRemaining);
length = fontRendererObj.getStringWidth(label);
fontRendererObj.drawString(label, xRight - length, y, 0xFFFFFF);
y += lineHeight + 4;
@@ -278,14 +283,14 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis
if (listing != null) {
long minBid = listing.getMinimumBid(AuctionClientConfig.getMinBidIncrement());
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.bid.minimum")
- .replace("%s", EnumChatFormatting.WHITE + formatCurrency(minBid) + " " + AuctionClientConfig.getCurrencyName()));
+ .replace("%s", EnumChatFormatting.WHITE + AuctionFormatUtil.formatCurrency(minBid) + " " + AuctionClientConfig.getCurrencyName()));
}
} else if (btnBuyout != null && btnBuyout.visible && btnBuyout.isHovered()) {
tooltip = new ArrayList<>();
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.bid.buyout"));
if (listing != null && listing.hasBuyout()) {
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.info.buyoutPrice") + ":");
- tooltip.add(EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice) + " " + AuctionClientConfig.getCurrencyName());
+ tooltip.add(EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrency(listing.buyoutPrice) + " " + AuctionClientConfig.getCurrencyName());
}
}
@@ -305,5 +310,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis
// Block all slot interactions - view only
}
- public AuctionListing getListing() { return listing; }
+ public AuctionListing getListing() {
+ return listing;
+ }
}
diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java
index c34223550..048260d4c 100644
--- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java
+++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java
@@ -34,6 +34,9 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface {
public static final int PAGE_SELL = 1;
public static final int PAGE_CLAIMS = 2;
+ // Overlay color
+ protected static final int OVERLAY_DARK = 0xC0000000;
+
// Navigation button IDs
protected static final int BTN_NAV_LISTINGS = 100;
protected static final int BTN_NAV_SELL = 101;
@@ -66,7 +69,9 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface {
initNavigationButtons();
}
- /** Initialize navigation buttons */
+ /**
+ * Initialize navigation buttons
+ */
protected void initNavigationButtons() {
int page = getCurrentPage();
@@ -86,7 +91,9 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface {
addButton(btnClaims);
}
- /** Returns current page constant - implement in subclass */
+ /**
+ * Returns current page constant - implement in subclass
+ */
protected abstract int getCurrentPage();
@Override
@@ -133,7 +140,9 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface {
}
}
- /** Draw navigation button tooltips */
+ /**
+ * Draw navigation button tooltips
+ */
protected void drawNavButtonTooltips(int mouseX, int mouseY) {
List tooltip = null;
if (btnListings != null && btnListings.isHovered()) tooltip = btnListings.getTooltipLines();
@@ -151,14 +160,18 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface {
// ========== Slot Drawing Utilities ==========
- /** Draw auction slot background (18x18) */
+ /**
+ * Draw auction slot background (18x18)
+ */
protected void drawAuctionSlot(int x, int y) {
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
mc.renderEngine.bindTexture(AUCTION_SLOT);
Gui.func_146110_a(x, y, 0, 0, 18, 18, 18, 18);
}
- /** Draw colored overlay on slot (ARGB format) */
+ /**
+ * Draw colored overlay on slot (ARGB format)
+ */
protected void drawColoredOverlay(int x, int y, int color) {
GL11.glDisable(GL11.GL_LIGHTING);
GL11.glDisable(GL11.GL_DEPTH_TEST);
@@ -170,12 +183,16 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface {
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
}
- /** Draw dark overlay for unavailable slots */
+ /**
+ * Draw dark overlay for unavailable slots
+ */
protected void drawDarkenedOverlay(int x, int y) {
- drawColoredOverlay(x, y, 0xC0000000);
+ drawColoredOverlay(x, y, OVERLAY_DARK);
}
- /** Draw icon texture on slot (16x16) */
+ /**
+ * Draw icon texture on slot (16x16)
+ */
protected void drawIconOverlay(int x, int y, ResourceLocation icon) {
GL11.glPushMatrix();
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
@@ -189,42 +206,11 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface {
GL11.glPopMatrix();
}
- /** Subclasses implement this to draw specific content */
+ /**
+ * Subclasses implement this to draw specific content
+ */
protected abstract void drawAuctionContent(float partialTicks, int mouseX, int mouseY);
- // ========== Formatting Utilities ==========
-
- /** Format currency with commas (e.g., 1000 -> "1,000") */
- protected String formatCurrency(long amount) {
- if (amount < 1000) return "" + amount;
- StringBuilder sb = new StringBuilder();
- String str = "" + amount;
- int count = 0;
- for (int i = str.length() - 1; i >= 0; i--) {
- if (count > 0 && count % 3 == 0) sb.insert(0, ',');
- sb.insert(0, str.charAt(i));
- count++;
- }
- return sb.toString();
- }
-
- /** Format time remaining (e.g., "2d 5h 30m") */
- protected String formatTimeRemaining(long ms) {
- if (ms <= 0) return "Ended";
-
- long seconds = (ms / 1000) % 60;
- long minutes = (ms / 60000) % 60;
- long hours = (ms / 3600000) % 24;
- long days = ms / 86400000;
-
- StringBuilder sb = new StringBuilder();
- if (days > 0) sb.append(days).append("d ");
- if (hours > 0 || days > 0) sb.append(hours).append("h ");
- if (minutes > 0 || hours > 0 || days > 0) sb.append(minutes).append("m");
- else sb.append(seconds).append("s");
- return sb.toString();
- }
-
@Override
public void save() {
// Override in subclasses if needed
diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java
index 381747c6a..85312f5ad 100644
--- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java
+++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java
@@ -58,8 +58,6 @@ public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiLis
private AuctionFilter filter;
private int currentPage = 0;
private int totalPages = 1;
- private int totalListings = 0;
- private boolean dataLoaded = false;
// Buttons defined on this page
private GuiAuctionNavButton btnFilter;
@@ -100,12 +98,16 @@ public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiLis
return PAGE_LISTINGS;
}
- /** Request listings from server */
+ /**
+ * Request listings from server
+ */
private void requestListings() {
AuctionActionPacket.requestListings(filter, currentPage);
}
- /** Update pagination button visibility based on current page state */
+ /**
+ * Update pagination button visibility based on current page state
+ */
private void updatePaginationVisibility() {
if (btnPageUp != null) {
btnPageUp.setVisible(currentPage > 0);
@@ -195,7 +197,8 @@ public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiLis
}
@Override
- public void subGuiClosed(SubGuiInterface subgui) {}
+ public void subGuiClosed(SubGuiInterface subgui) {
+ }
// ========== Data Handling ==========
@@ -204,7 +207,6 @@ public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiLis
if (compound.hasKey("ListingsData")) {
currentPage = compound.getInteger("Page");
totalPages = compound.getInteger("TotalPages");
- totalListings = compound.getInteger("TotalListings");
// Load listings
listingContainer.displayInventory.clear();
@@ -213,7 +215,7 @@ public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiLis
AuctionListing listing = AuctionListing.fromNBT(list.getCompoundTagAt(i));
listingContainer.displayInventory.setListing(i, listing);
}
- dataLoaded = true;
+ updatePaginationVisibility();
} else if (compound.hasKey("ListingsRefresh")) {
requestListings();
}
@@ -297,6 +299,11 @@ public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiLis
// Block other slot interactions
}
- public AuctionFilter getFilter() { return filter; }
- public ContainerAuctionListing getListingContainer() { return listingContainer; }
+ public AuctionFilter getFilter() {
+ return filter;
+ }
+
+ public ContainerAuctionListing getListingContainer() {
+ return listingContainer;
+ }
}
diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java
index a8784b3b0..af87ab39b 100644
--- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java
+++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java
@@ -7,15 +7,17 @@ import net.minecraft.item.ItemStack;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.StatCollector;
+import noppes.npcs.client.AuctionClientConfig;
import noppes.npcs.client.NoppesUtil;
import noppes.npcs.client.gui.util.GuiAuctionNavButton;
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.ITextfieldListener;
-import noppes.npcs.client.AuctionClientConfig;
import noppes.npcs.containers.ContainerAuctionSell;
import noppes.npcs.entity.EntityNPCInterface;
+import noppes.npcs.util.AuctionFormatUtil;
+import org.lwjgl.opengl.GL11;
import java.util.ArrayList;
import java.util.List;
@@ -103,7 +105,9 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis
return PAGE_SELL;
}
- /** Update visibility of buyout price field based on toggle state */
+ /**
+ * Update visibility of buyout price field based on toggle state
+ */
private void updateBuyoutVisibility() {
if (lblBuyout != null) {
lblBuyout.enabled = allowBuyout;
@@ -117,7 +121,9 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis
}
}
- /** Update the buyout toggle button tooltip */
+ /**
+ * Update the buyout toggle button tooltip
+ */
private void updateBuyoutTooltip() {
if (btnAllowBuyout == null) return;
@@ -239,7 +245,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis
// Draw listing fee info (uses cached config from server)
String feeText = StatCollector.translateToLocal("auction.sell.fee")
- .replace("%s", formatCurrency(AuctionClientConfig.getListingFee()) + " " + AuctionClientConfig.getCurrencyName());
+ .replace("%s", AuctionFormatUtil.formatCurrency(AuctionClientConfig.getListingFee()) + " " + AuctionClientConfig.getCurrencyName());
fontRendererObj.drawString(EnumChatFormatting.GRAY + feeText, guiLeft + contentX, guiTop + contentY + 77, 0xFFFFFF);
// Draw error message if any
@@ -250,7 +256,9 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis
drawSellSlotIndicator(slotX, slotY);
}
- /** Draw indicator showing staged vs available items */
+ /**
+ * Draw indicator showing staged vs available items
+ */
private void drawSellSlotIndicator(int slotX, int slotY) {
ItemStack sellItem = sellContainer.getItemToSell();
if (sellItem != null) {
@@ -308,7 +316,9 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis
drawSellPageTooltips(mouseX, mouseY);
}
- /** Draw tooltips for sell page buttons */
+ /**
+ * Draw tooltips for sell page buttons
+ */
private void drawSellPageTooltips(int mouseX, int mouseY) {
List tooltip = null;
@@ -326,7 +336,11 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis
}
if (tooltip != null && !tooltip.isEmpty()) {
+ GL11.glPushMatrix();
+ GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj);
+ GL11.glPopAttrib();
+ GL11.glPopMatrix();
}
}
}
diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java
index f52d5fa63..45a527dca 100644
--- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java
+++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java
@@ -50,7 +50,7 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData {
private int pendingSlot = -1;
private PendingOp pendingOp = PendingOp.NONE;
- private enum PendingOp { NONE, CANCEL, CLAIM }
+ private enum PendingOp {NONE, CANCEL, CLAIM}
public GuiAuctionTrades(EntityNPCInterface npc, ContainerAuctionTrades container) {
super(npc, container);
@@ -117,7 +117,9 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData {
}
}
- /** Handle click on pending operation slot */
+ /**
+ * Handle click on pending operation slot
+ */
private void handlePendingClick(int mouseButton) {
if (pendingOp == PendingOp.CANCEL && mouseButton == 1) {
// Right-click confirms cancel
@@ -159,14 +161,18 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData {
}
}
- /** Set pending operation and hide slot item */
+ /**
+ * Set pending operation and hide slot item
+ */
private void setPending(int slot, PendingOp op) {
pendingSlot = slot;
pendingOp = op;
tradesContainer.setHiddenSlot(slot);
}
- /** Clear pending operation and restore slot */
+ /**
+ * Clear pending operation and restore slot
+ */
private void clearPending() {
if (pendingSlot >= 0) {
tradesContainer.clearHiddenSlot();
@@ -175,13 +181,17 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData {
pendingOp = PendingOp.NONE;
}
- /** Play confirmation sound */
+ /**
+ * Play confirmation sound
+ */
private void playConfirmSound() {
mc.getSoundHandler().playSound(PositionedSoundRecord.func_147674_a(
new ResourceLocation("random.orb"), 1.0F));
}
- /** Get slot index at mouse position */
+ /**
+ * Get slot index at mouse position
+ */
private int getSlotAt(int mouseX, int mouseY) {
int relX = mouseX - guiLeft - GRID_X;
int relY = mouseY - guiTop - GRID_Y;
@@ -239,7 +249,9 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData {
}
}
- /** Get tint color for slot based on contents */
+ /**
+ * Get tint color for slot based on contents
+ */
private int getSlotTint(int slot) {
AuctionListing listing = tradesContainer.getListingAt(slot);
AuctionClaim claim = tradesContainer.getClaimAt(slot);
@@ -281,7 +293,9 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData {
}
}
- /** Draw tooltip for a trade slot */
+ /**
+ * Draw tooltip for a trade slot
+ */
private void drawTradeTooltip(int slot, int x, int y) {
List tooltip = new ArrayList<>();
diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java
index a1793c545..8d7ef3ed4 100644
--- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java
+++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java
@@ -10,6 +10,7 @@ import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
import noppes.npcs.client.gui.util.ITextfieldListener;
import noppes.npcs.client.gui.util.SubGuiInterface;
+import noppes.npcs.util.AuctionFormatUtil;
/**
* Sub GUI for placing a bid on an auction.
@@ -49,13 +50,13 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe
// Minimum bid info
String minText = StatCollector.translateToLocal("auction.bid.minimum")
- .replace("%s", EnumChatFormatting.DARK_RED + formatCurrency(minimumBid) + " " + currencyName);
+ .replace("%s", EnumChatFormatting.DARK_RED + AuctionFormatUtil.formatCurrency(minimumBid) + " " + currencyName);
addLabel(new GuiNpcLabel(1, minText, guiLeft + 15, y));
y += 14;
// Your balance
String balanceText = StatCollector.translateToLocal("auction.bid.yourBalance")
- .replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName);
+ .replace("%s", EnumChatFormatting.DARK_GREEN + AuctionFormatUtil.formatCurrency(playerBalance) + " " + currencyName);
addLabel(new GuiNpcLabel(2, balanceText, guiLeft + 15, y));
y += 18;
@@ -77,7 +78,7 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe
int totalBtnWidth = btnWidth * 2 + btnSpacing;
int btnX = centerX - totalBtnWidth / 2;
- addButton(new GuiNpcButton(btnBidId, btnX, y, btnWidth, 20, "auction.bid.placeBid"));
+ addButton(new GuiNpcButton(btnBidId, btnX, y, btnWidth, 20, "auction.bid.place"));
addButton(new GuiNpcButton(btnCancelId, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel"));
}
@@ -117,7 +118,7 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe
// Validate
if (bidAmount < minimumBid) {
errorMessage = StatCollector.translateToLocal("auction.error.bidTooLow")
- .replace("%s", formatCurrency(minimumBid));
+ .replace("%s", AuctionFormatUtil.formatCurrency(minimumBid));
updateErrorLabel();
return;
}
@@ -144,17 +145,4 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe
public boolean wasSuccessful() {
return successful;
}
-
- private String formatCurrency(long amount) {
- if (amount < 1000) return "" + amount;
- StringBuilder sb = new StringBuilder();
- String str = "" + amount;
- int count = 0;
- for (int i = str.length() - 1; i >= 0; i--) {
- if (count > 0 && count % 3 == 0) sb.insert(0, ',');
- sb.insert(0, str.charAt(i));
- count++;
- }
- return sb.toString();
- }
}
diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java
index b63471047..4884c7ae4 100644
--- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java
+++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java
@@ -8,6 +8,7 @@ import noppes.npcs.client.AuctionClientConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.SubGuiInterface;
+import noppes.npcs.util.AuctionFormatUtil;
/**
* Sub GUI for confirming a buyout purchase.
@@ -54,14 +55,14 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface {
y += 16;
// Buyout price
- String priceText = EnumChatFormatting.DARK_RED + formatCurrency(buyoutPrice) + " " + currencyName;
+ String priceText = EnumChatFormatting.DARK_RED + AuctionFormatUtil.formatCurrency(buyoutPrice) + " " + currencyName;
int priceWidth = fontRendererObj.getStringWidth(priceText);
addLabel(new GuiNpcLabel(2, priceText, centerX - priceWidth / 2, y));
y += 20;
// Your balance
String balanceLabel = StatCollector.translateToLocal("auction.bid.yourBalance");
- String balanceText = balanceLabel.replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName);
+ String balanceText = balanceLabel.replace("%s", EnumChatFormatting.DARK_GREEN + AuctionFormatUtil.formatCurrency(playerBalance) + " " + currencyName);
addLabel(new GuiNpcLabel(3, balanceText, guiLeft + 20, y));
y += 14;
@@ -105,17 +106,4 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface {
public boolean wasSuccessful() {
return successful;
}
-
- private String formatCurrency(long amount) {
- if (amount < 1000) return "" + amount;
- StringBuilder sb = new StringBuilder();
- String str = "" + amount;
- int count = 0;
- for (int i = str.length() - 1; i >= 0; i--) {
- if (count > 0 && count % 3 == 0) sb.insert(0, ',');
- sb.insert(0, str.charAt(i));
- count++;
- }
- return sb.toString();
- }
}
diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java
index 7161722d5..e6f2ce61c 100644
--- a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java
+++ b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java
@@ -5,7 +5,6 @@ import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket;
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.GuiNpcTextField;
import noppes.npcs.client.gui.util.ICustomScrollListener;
import noppes.npcs.client.gui.util.IScrollData;
diff --git a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java
index 6ed26666f..bde8ae194 100644
--- a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java
+++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java
@@ -2,8 +2,11 @@ package noppes.npcs.client.gui.util;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
+import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
+import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry;
import kamkeel.npcs.controllers.data.ability.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import net.minecraft.entity.Entity;
@@ -19,10 +22,10 @@ import java.util.List;
/**
* Client-side executor for previewing abilities in the GUI.
- *
+ *
* Uses unified execution: calls the REAL ability methods (onWindUpTick, onExecute, onActiveTick)
* with previewMode=true. Abilities skip damage/effects/sounds when isPreview() returns true.
- *
+ *
* Lifecycle:
* 1. WINDUP phase - real onWindUpTick() called, telegraph shown, entities may spawn
* 2. ACTIVE phase - real onExecute() then onActiveTick() called, physics simulated
@@ -36,8 +39,11 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
private Ability previewAbility;
private EntityNPCInterface previewNpc;
- /** Executor's own phase tracking (separate from ability.getPhase()) */
- private enum ExecutorPhase { IDLE, WINDUP, ACTIVE, EXTENDED }
+ /**
+ * Executor's own phase tracking (separate from ability.getPhase())
+ */
+ private enum ExecutorPhase {IDLE, WINDUP, ACTIVE, EXTENDED, CHAIN_DELAY}
+
private ExecutorPhase executorPhase = ExecutorPhase.IDLE;
private int currentTick = 0;
@@ -59,6 +65,14 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
private TelegraphInstance previewTelegraph;
private List previewEntities = new ArrayList<>();
+ // ==================== CHAIN PREVIEW ====================
+ private ChainedAbility previewChain;
+ private List chainAbilities;
+ private List chainDelays;
+ private int chainIndex = -1;
+ private int chainDelayRemaining = 0;
+ private boolean chainWindUpAll = true;
+
// ==================== CALLBACKS ====================
private GuiAbilityInterface parentGui;
@@ -70,28 +84,21 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
}
/**
- * Start previewing an ability using unified execution.
+ * Start previewing a single ability using unified execution.
* Sets previewMode=true on the ability, then calls its real methods.
*/
public void startPreview(Ability ability, EntityNPCInterface npc) {
stop();
- this.previewAbility = ability;
this.previewNpc = npc;
- this.currentTick = 0;
this.playing = true;
this.paused = false;
- 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);
@@ -100,10 +107,86 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
parentGui.startTrackingMovement();
}
+ beginAbilityPreview(ability);
+ }
+
+ /**
+ * Start previewing a chained ability — plays all entries in sequence with delays.
+ * Each entry is resolved and deep-copied to avoid modifying originals.
+ */
+ public void startChainPreview(ChainedAbility chain, EntityNPCInterface npc) {
+ stop();
+
+ this.previewNpc = npc;
+ this.playing = true;
+ this.paused = false;
+ this.previewChain = chain;
+ this.chainWindUpAll = chain.isWindUpAll();
+
+ // Save NPC start position for restoring later
+ this.npcStartX = npc.posX;
+ this.npcStartY = npc.posY;
+ this.npcStartZ = npc.posZ;
+
+ // Resolve all entries to deep-copied abilities
+ chainAbilities = new ArrayList<>();
+ chainDelays = new ArrayList<>();
+ for (ChainedAbilityEntry entry : chain.getEntries()) {
+ Ability resolved = entry.resolve();
+ if (resolved != null && AbilityController.Instance != null) {
+ Ability copy = AbilityController.Instance.fromNBT(resolved.writeNBT());
+ if (copy != null) {
+ chainAbilities.add(copy);
+ chainDelays.add(entry.getDelayTicks());
+ }
+ }
+ }
+
+ if (chainAbilities.isEmpty()) {
+ playing = false;
+ return;
+ }
+
+ // Create invisible fake target NPC for targeting
+ createFakeTarget(npc);
+
+ // Start tracking NPC movement
+ if (parentGui != null) {
+ parentGui.startTrackingMovement();
+ }
+
+ chainIndex = 0;
+ beginAbilityPreview(chainAbilities.get(0));
+ }
+
+ /**
+ * Begin previewing a single ability (shared by single and chain preview).
+ * Cleans up the previous ability if any (for chain advancement).
+ */
+ private void beginAbilityPreview(Ability ability) {
+ // Clean up previous ability if any (for chain advancement)
+ if (previewAbility != null) {
+ previewAbility.cleanup();
+ previewAbility.setPreviewMode(false);
+ previewAbility.setPreviewEntityHandler(null);
+ previewAbility.reset();
+ }
+
+ this.previewAbility = ability;
+ this.currentTick = 0;
+ this.maxPreviewDuration = ability.getMaxPreviewDuration();
+
+ // Enable preview mode on the ability
+ ability.setPreviewMode(true);
+ ability.setPreviewEntityHandler(this);
+
// Use the ability's real start() method with the fake target
ability.start(fakeTarget);
- if (ability.getWindUpTicks() <= 0) {
+ // Skip windup for non-first chain entries when windUpAll is false
+ boolean skipWindup = previewChain != null && !chainWindUpAll && chainIndex > 0;
+
+ if (ability.getWindUpTicks() <= 0 || skipWindup) {
// No windup — go straight to active
this.executorPhase = ExecutorPhase.ACTIVE;
transitionToActive();
@@ -112,12 +195,12 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
this.executorPhase = ExecutorPhase.WINDUP;
// Create telegraph for preview
- previewTelegraph = ability.createTelegraph(npc, fakeTarget);
+ previewTelegraph = ability.createTelegraph(previewNpc, fakeTarget);
// Start windup animation
Animation windUpAnim = ability.getWindUpAnimation();
if (windUpAnim != null) {
- AnimationData data = npc.display.animationData;
+ AnimationData data = previewNpc.display.animationData;
data.setEnabled(true);
data.setAnimation(windUpAnim);
data.animation.paused = false;
@@ -213,6 +296,13 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
executorPhase = ExecutorPhase.IDLE;
currentTick = 0;
+ // Clear chain state
+ previewChain = null;
+ chainAbilities = null;
+ chainDelays = null;
+ chainIndex = -1;
+ chainDelayRemaining = 0;
+
previewTelegraph = null;
if (parentGui != null) {
@@ -283,6 +373,9 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
case EXTENDED:
tickExtended();
break;
+ case CHAIN_DELAY:
+ tickChainDelay();
+ break;
default:
break;
}
@@ -306,13 +399,21 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
// Check if the ability signaled completion (phase went to IDLE)
if (previewAbility.getPhase() == AbilityPhase.IDLE) {
- transitionToExtended();
+ if (hasNextChainEntry()) {
+ transitionToChainDelay();
+ } else {
+ transitionToExtended();
+ }
return;
}
// Safety timeout
if (currentTick >= maxPreviewDuration) {
- transitionToExtended();
+ if (hasNextChainEntry()) {
+ transitionToChainDelay();
+ } else {
+ transitionToExtended();
+ }
}
}
@@ -323,6 +424,44 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
}
}
+ // ==================== CHAIN ADVANCEMENT ====================
+
+ private boolean hasNextChainEntry() {
+ return previewChain != null && chainAbilities != null && chainIndex < chainAbilities.size() - 1;
+ }
+
+ private void transitionToChainDelay() {
+ executorPhase = ExecutorPhase.CHAIN_DELAY;
+ currentTick = 0;
+
+ // Use the NEXT entry's delay
+ int nextIndex = chainIndex + 1;
+ chainDelayRemaining = (nextIndex < chainDelays.size()) ? chainDelays.get(nextIndex) : 0;
+
+ // Clear current animation
+ if (previewNpc != null) {
+ AnimationData data = previewNpc.display.animationData;
+ data.setAnimation(new Animation());
+ }
+ }
+
+ private void tickChainDelay() {
+ if (chainDelayRemaining > 0) {
+ chainDelayRemaining--;
+ return;
+ }
+ advanceChainEntry();
+ }
+
+ private void advanceChainEntry() {
+ chainIndex++;
+ if (chainIndex >= chainAbilities.size()) {
+ transitionToExtended();
+ return;
+ }
+ beginAbilityPreview(chainAbilities.get(chainIndex));
+ }
+
private void transitionToActive() {
executorPhase = ExecutorPhase.ACTIVE;
currentTick = 0;
@@ -424,33 +563,73 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
// ==================== GETTERS ====================
- public boolean isPlaying() { return playing; }
- public boolean isPaused() { return paused; }
- public boolean isActive() { return playing || paused; }
+ public boolean isPlaying() {
+ return playing;
+ }
+
+ public boolean isPaused() {
+ return paused;
+ }
+
+ public boolean isActive() {
+ return playing || paused;
+ }
+
+ public boolean isChainPreview() {
+ return previewChain != null;
+ }
+
public AbilityPhase getPhase() {
switch (executorPhase) {
- case WINDUP: return AbilityPhase.WINDUP;
- case ACTIVE: return AbilityPhase.ACTIVE;
- default: return AbilityPhase.IDLE;
+ 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 getPreviewEntities() { return previewEntities; }
- public Ability getPreviewAbility() { return previewAbility; }
- public EntityNPCInterface getPreviewNpc() { return previewNpc; }
+
+ public int getCurrentTick() {
+ return currentTick;
+ }
+
+ public TelegraphInstance getTelegraph() {
+ return previewTelegraph;
+ }
+
+ public List getPreviewEntities() {
+ return previewEntities;
+ }
+
+ public Ability getPreviewAbility() {
+ return previewAbility;
+ }
+
+ public EntityNPCInterface getPreviewNpc() {
+ return previewNpc;
+ }
public String getStatusString() {
if (!playing && !paused) {
return "Stopped";
- } else if (paused) {
- return "Paused";
+ }
+
+ String chainPrefix = "";
+ if (previewChain != null && chainAbilities != null) {
+ chainPrefix = "Chain " + (chainIndex + 1) + "/" + chainAbilities.size() + " - ";
+ }
+
+ if (paused) {
+ return chainPrefix + "Paused";
} else if (executorPhase == ExecutorPhase.WINDUP) {
- return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0);
+ return chainPrefix + "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0);
} else if (executorPhase == ExecutorPhase.ACTIVE) {
- return "Active: " + currentTick + "/" + maxPreviewDuration;
+ return chainPrefix + "Active: " + currentTick + "/" + maxPreviewDuration;
} else if (executorPhase == ExecutorPhase.EXTENDED) {
- return "Extended: " + currentTick + "/" + EXTENDED_DURATION;
+ return chainPrefix + "Extended: " + currentTick + "/" + EXTENDED_DURATION;
+ } else if (executorPhase == ExecutorPhase.CHAIN_DELAY) {
+ return chainPrefix + "Delay: " + chainDelayRemaining;
}
return "";
}
diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java
index 3d0573bf2..4bf2ffd58 100644
--- a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java
+++ b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java
@@ -21,7 +21,7 @@ import java.util.List;
/**
* Base GUI class for ability preview with fixed camera.
- *
+ *
* Key concepts:
* - NPC positioned on left side of preview area
* - NPC faces a fixed direction (NPC_FACING_YAW)
@@ -33,15 +33,23 @@ import java.util.List;
public class GuiAbilityInterface extends GuiNPCInterface2 {
// ==================== FIXED CAMERA SETTINGS ====================
- /** Fixed camera zoom level (higher = more zoomed in) */
+ /**
+ * Fixed camera zoom level (higher = more zoomed in)
+ */
protected static float CAMERA_ZOOM = 45.0f;
- /** NPC facing direction - affects anchor points and visual facing */
+ /**
+ * NPC facing direction - affects anchor points and visual facing
+ */
protected static float NPC_FACING_YAW = 160.0f;
- /** Camera pitch (slight downward angle) */
+ /**
+ * Camera pitch (slight downward angle)
+ */
protected static float CAMERA_PITCH = 10.0f;
// ==================== VIEWING ROTATION ====================
- /** Viewing direction rotation (rotates entire 3D space) */
+ /**
+ * Viewing direction rotation (rotates entire 3D space)
+ */
protected static float viewRotation = 0.0f;
private GuiNpcButton btnLeft, btnRight;
@@ -49,21 +57,33 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
// ==================== NPC STATE ====================
public EntityNPCInterface npc;
- /** NPC starting position (saved when preview starts, used to track movement) */
+ /**
+ * NPC starting position (saved when preview starts, used to track movement)
+ */
protected double npcStartX, npcStartY, npcStartZ;
- /** Whether we're tracking NPC movement */
+ /**
+ * Whether we're tracking NPC movement
+ */
protected boolean trackingNpcMovement = false;
- /** Entities to render in preview space */
+ /**
+ * Entities to render in preview space
+ */
protected List previewEntities = new ArrayList<>();
- /** Current telegraph to render in preview */
+ /**
+ * Current telegraph to render in preview
+ */
protected TelegraphInstance previewTelegraph;
// ==================== LAYOUT ====================
- /** Preview area bounds */
+ /**
+ * Preview area bounds
+ */
protected int previewX, previewY, previewWidth, previewHeight;
- /** NPC render position on screen */
+ /**
+ * NPC render position on screen
+ */
protected int npcScreenX, npcScreenY;
public int xOffset = 0;
@@ -74,7 +94,7 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
}
/**
- * @param npc The NPC for preview rendering
+ * @param npc The NPC for preview rendering
* @param hasMenuNpc If false, the top menu tabs (Display, Stats, etc.) are hidden.
* Use false when opened from the global menu without a real NPC.
*/
@@ -99,8 +119,8 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
previewHeight = 180;
// NPC position: left side of preview, slightly below center
- npcScreenX = previewX + (int)(previewWidth * 0.33f) + xOffset;
- npcScreenY = previewY + (int)(previewHeight * 0.85f) + yOffset;
+ npcScreenX = previewX + (int) (previewWidth * 0.33f) + xOffset;
+ npcScreenY = previewY + (int) (previewHeight * 0.85f) + yOffset;
// Viewing rotation buttons (below preview area)
int btnY = guiTop + 192;
@@ -351,7 +371,7 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
super.drawBackground();
drawGradientRect(previewX, previewY, previewX + previewWidth, previewY + previewHeight,
- 0xc0101010, 0xd0101010);
+ 0xc0101010, 0xd0101010);
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
}
@@ -364,8 +384,9 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
@Override
public void keyTyped(char par1, int par2) {
+ boolean hadSubGui = hasSubGui();
super.keyTyped(par1, par2);
- if (par2 == 1) {
+ if (par2 == 1 && !hadSubGui) {
close();
}
}
diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java
index 665dd2259..a71c9762f 100644
--- a/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java
+++ b/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java
@@ -64,14 +64,14 @@ public class GuiAuctionNavButton extends GuiNpcButton {
/**
* Create a fully customizable button.
*
- * @param id Button ID
- * @param x X position
- * @param y Y position
- * @param buttonSize Size of the button background texture (e.g., 18 for 18x18, 10 for 10x10)
- * @param iconSize Size of the icon texture (e.g., 16 for 16x16, 8 for 8x8)
- * @param tooltipKey Lang key for tooltip
- * @param iconTexture Icon texture to draw inside the button
- * @param textureNormal Background texture for normal state
+ * @param id Button ID
+ * @param x X position
+ * @param y Y position
+ * @param buttonSize Size of the button background texture (e.g., 18 for 18x18, 10 for 10x10)
+ * @param iconSize Size of the icon texture (e.g., 16 for 16x16, 8 for 8x8)
+ * @param tooltipKey Lang key for tooltip
+ * @param iconTexture Icon texture to draw inside the button
+ * @param textureNormal Background texture for normal state
* @param texturePressed Background texture for pressed/hover state
*/
public GuiAuctionNavButton(int id, int x, int y, int buttonSize, int iconSize, String tooltipKey,
diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiDirectory.java b/src/main/java/noppes/npcs/client/gui/util/GuiDirectory.java
index 750c60668..957237610 100644
--- a/src/main/java/noppes/npcs/client/gui/util/GuiDirectory.java
+++ b/src/main/java/noppes/npcs/client/gui/util/GuiDirectory.java
@@ -4,29 +4,29 @@ package noppes.npcs.client.gui.util;
* Abstract base class for fullscreen directory-browsing GUIs.
* Provides a configurable three-panel layout (left nav, center content, right actions)
* with percentage-based sizing, panel backgrounds/borders, and an optional resizable divider.
- *
+ *
* Subclasses override abstract hooks to populate each panel region.
* Designed for reuse across Cloner, Quest Manager, Dialog Manager, etc.
*/
public abstract class GuiDirectory extends GuiNPCInterface implements ICustomScrollListener, ISubGuiListener {
// ===== LAYOUT CONFIG (subclasses may override in constructor) =====
- protected float leftPanelPercent = 0.20f;
+ protected float leftPanelPercent = 0.20f;
protected float rightPanelPercent = 0.10f;
- protected int minLeftPanelW = 80;
+ protected int minLeftPanelW = 80;
protected int minRightPanelW = 40;
- protected int minCenterW = 100;
+ protected int minCenterW = 100;
// ===== LAYOUT CONSTANTS =====
- protected int pad = 10;
+ protected int pad = 10;
protected int topBarH = 24;
- protected int gap = 4;
- protected int btnH = 20;
+ protected int gap = 4;
+ protected int btnH = 20;
// ===== PANEL COLORS =====
- protected int panelBg = 0xC0101010;
+ protected int panelBg = 0xC0101010;
protected int panelBorder = 0xFF333333;
- protected int topBarBg = 0xC0181818;
+ protected int topBarBg = 0xC0181818;
// ===== COMPUTED LAYOUT (populated by computeLayout) =====
protected int originX, originY, usableW, usableH;
@@ -35,10 +35,10 @@ public abstract class GuiDirectory extends GuiNPCInterface implements ICustomScr
protected int rightX;
// ===== OPTIONAL RESIZABLE DIVIDER (between left and center) =====
- protected boolean enableDivider = false;
- protected int dividerWidth = 5;
- protected int dividerLineHeight = 20;
- protected int minDividerPanelW = 50;
+ protected boolean enableDivider = false;
+ protected int dividerWidth = 5;
+ protected int dividerLineHeight = 20;
+ protected int minDividerPanelW = 50;
private int dividerOffset;
private boolean isDragging = false;
private int dragStartX;
@@ -57,8 +57,8 @@ public abstract class GuiDirectory extends GuiNPCInterface implements ICustomScr
usableW = width - 2 * pad;
usableH = height - 2 * pad;
- leftPanelW = Math.max(minLeftPanelW, (int)(usableW * leftPanelPercent));
- rightPanelW = Math.max(minRightPanelW, (int)(usableW * rightPanelPercent));
+ leftPanelW = Math.max(minLeftPanelW, (int) (usableW * leftPanelPercent));
+ rightPanelW = Math.max(minRightPanelW, (int) (usableW * rightPanelPercent));
if (rightPanelPercent <= 0) {
rightPanelW = 0;
@@ -73,7 +73,7 @@ public abstract class GuiDirectory extends GuiNPCInterface implements ICustomScr
contentX = originX + leftPanelW + gap;
contentY = originY + topBarH + gap;
contentH = usableH - topBarH - gap;
- rightX = contentX + contentW + gap;
+ rightX = contentX + contentW + gap;
if (enableDivider && dividerOffset == 0) {
dividerOffset = leftPanelW;
@@ -128,7 +128,9 @@ public abstract class GuiDirectory extends GuiNPCInterface implements ICustomScr
drawRect(divX + 1, handleTop, divX + dividerWidth - 1, handleTop + dividerLineHeight, 0xFF707070);
}
- /** Override for extra drawing after super.drawScreen (e.g. status text) */
+ /**
+ * Override for extra drawing after super.drawScreen (e.g. status text)
+ */
protected void drawOverlay(int mouseX, int mouseY, float partialTicks) {
}
@@ -177,7 +179,9 @@ public abstract class GuiDirectory extends GuiNPCInterface implements ICustomScr
super.mouseMovedOrUp(mouseX, mouseY, state);
}
- /** Called when divider is dragged. Override to update scroll sizes. */
+ /**
+ * Called when divider is dragged. Override to update scroll sizes.
+ */
protected void onDividerMoved(int newOffset) {
}
@@ -192,15 +196,23 @@ public abstract class GuiDirectory extends GuiNPCInterface implements ICustomScr
// ===== ABSTRACT HOOKS (subclass must implement) =====
- /** Populate top bar buttons. topBtnY is the y-coordinate for the button row. */
+ /**
+ * Populate top bar buttons. topBtnY is the y-coordinate for the button row.
+ */
protected abstract void initTopBar(int topBtnY);
- /** Populate the left navigation panel (scrolls, search fields, buttons). */
+ /**
+ * Populate the left navigation panel (scrolls, search fields, buttons).
+ */
protected abstract void initLeftPanel();
- /** Populate the center content panel (main scroll list). */
+ /**
+ * Populate the center content panel (main scroll list).
+ */
protected abstract void initCenterPanel();
- /** Populate the right action panel. startY is the first button y-coordinate. */
+ /**
+ * Populate the right action panel. startY is the first button y-coordinate.
+ */
protected abstract void initRightPanel(int startY);
}
diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java b/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java
index 5ca2edfb0..37ce2b789 100644
--- a/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java
+++ b/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java
@@ -34,7 +34,7 @@ public class GuiModelInterface2 extends GuiNPCInterface2 {
}
/**
- * @param npc The NPC for preview rendering
+ * @param npc The NPC for preview rendering
* @param hasMenuNpc If false, the top menu tabs (Display, Stats, etc.) are hidden.
* Use false when opened from the global menu without a real NPC.
*/
@@ -151,8 +151,9 @@ public class GuiModelInterface2 extends GuiNPCInterface2 {
@Override
public void keyTyped(char par1, int par2) {
+ boolean hadSubGui = hasSubGui();
super.keyTyped(par1, par2);
- if (par2 == 1) {
+ if (par2 == 1 && !hadSubGui) {
close();
}
}
diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcLabel.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcLabel.java
index 60b4afdb6..4a1ac4b24 100644
--- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcLabel.java
+++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcLabel.java
@@ -60,12 +60,13 @@ public class GuiNpcLabel extends Gui {
/**
* Draws hover tooltip if the mouse is over this label.
- * @param localMouseX Mouse X in scroll-local coordinates (for hit testing)
- * @param localMouseY Mouse Y in scroll-local coordinates (for hit testing)
+ *
+ * @param localMouseX Mouse X in scroll-local coordinates (for hit testing)
+ * @param localMouseY Mouse Y in scroll-local coordinates (for hit testing)
* @param screenMouseX Mouse X in screen coordinates (for tooltip positioning)
* @param screenMouseY Mouse Y in screen coordinates (for tooltip positioning)
- * @param hasSubGui Whether a sub-gui is open
- * @param fr FontRenderer for measuring label width
+ * @param hasSubGui Whether a sub-gui is open
+ * @param fr FontRenderer for measuring label width
*/
public void drawHover(int localMouseX, int localMouseY, int screenMouseX, int screenMouseY, boolean hasSubGui, FontRenderer fr) {
if (hasSubGui || hoverableText.isEmpty()) return;
diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java
index a30b53fa8..e6e02d10e 100644
--- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java
+++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java
@@ -1,17 +1,17 @@
package noppes.npcs.client.gui.util;
+import kamkeel.npcs.util.TextSplitter;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.GuiTextField;
-import net.minecraft.util.StatCollector;
import net.minecraft.client.gui.ScaledResolution;
+import net.minecraft.client.renderer.RenderHelper;
+import net.minecraft.util.StatCollector;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
-import net.minecraft.client.renderer.RenderHelper;
-import kamkeel.npcs.util.TextSplitter;
+
import java.util.List;
-import java.util.Iterator;
public class GuiNpcTextField extends GuiTextField {
public boolean enabled = true;
@@ -224,7 +224,7 @@ public class GuiNpcTextField extends GuiTextField {
public boolean isMouseOver(int mouseX, int mouseY) {
return mouseX >= xPosition && mouseX < xPosition + width &&
- mouseY >= yPosition && mouseY < yPosition + height;
+ mouseY >= yPosition && mouseY < yPosition + height;
}
public void drawHover(int mouseX, int mouseY, boolean hasSubGui) {
@@ -334,14 +334,14 @@ public class GuiNpcTextField extends GuiTextField {
}
protected void drawGradientRect(int left, int top, int right, int bottom, int startColor, int endColor, int zLevel) {
- float f = (float)(startColor >> 24 & 255) / 255.0F;
- float f1 = (float)(startColor >> 16 & 255) / 255.0F;
- float f2 = (float)(startColor >> 8 & 255) / 255.0F;
- float f3 = (float)(startColor & 255) / 255.0F;
- float f4 = (float)(endColor >> 24 & 255) / 255.0F;
- float f5 = (float)(endColor >> 16 & 255) / 255.0F;
- float f6 = (float)(endColor >> 8 & 255) / 255.0F;
- float f7 = (float)(endColor & 255) / 255.0F;
+ float f = (float) (startColor >> 24 & 255) / 255.0F;
+ float f1 = (float) (startColor >> 16 & 255) / 255.0F;
+ float f2 = (float) (startColor >> 8 & 255) / 255.0F;
+ float f3 = (float) (startColor & 255) / 255.0F;
+ float f4 = (float) (endColor >> 24 & 255) / 255.0F;
+ float f5 = (float) (endColor >> 16 & 255) / 255.0F;
+ float f6 = (float) (endColor >> 8 & 255) / 255.0F;
+ float f7 = (float) (endColor & 255) / 255.0F;
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glEnable(GL11.GL_BLEND);
GL11.glDisable(GL11.GL_ALPHA_TEST);
@@ -350,11 +350,11 @@ public class GuiNpcTextField extends GuiTextField {
net.minecraft.client.renderer.Tessellator tessellator = net.minecraft.client.renderer.Tessellator.instance;
tessellator.startDrawingQuads();
tessellator.setColorRGBA_F(f1, f2, f3, f);
- tessellator.addVertex((double)right, (double)top, (double)zLevel);
- tessellator.addVertex((double)left, (double)top, (double)zLevel);
+ tessellator.addVertex((double) right, (double) top, (double) zLevel);
+ tessellator.addVertex((double) left, (double) top, (double) zLevel);
tessellator.setColorRGBA_F(f5, f6, f7, f4);
- tessellator.addVertex((double)left, (double)bottom, (double)zLevel);
- tessellator.addVertex((double)right, (double)bottom, (double)zLevel);
+ tessellator.addVertex((double) left, (double) bottom, (double) zLevel);
+ tessellator.addVertex((double) right, (double) bottom, (double) zLevel);
tessellator.draw();
GL11.glShadeModel(GL11.GL_FLAT);
GL11.glDisable(GL11.GL_BLEND);
diff --git a/src/main/java/noppes/npcs/client/gui/util/IChainedAbilityConfigCallback.java b/src/main/java/noppes/npcs/client/gui/util/IChainedAbilityConfigCallback.java
new file mode 100644
index 000000000..f714ea2c7
--- /dev/null
+++ b/src/main/java/noppes/npcs/client/gui/util/IChainedAbilityConfigCallback.java
@@ -0,0 +1,10 @@
+package noppes.npcs.client.gui.util;
+
+import kamkeel.npcs.controllers.data.ability.ChainedAbility;
+
+/**
+ * Callback for when a chained ability is saved from the config SubGui.
+ */
+public interface IChainedAbilityConfigCallback {
+ void onChainedAbilitySaved(ChainedAbility chain);
+}
diff --git a/src/main/java/noppes/npcs/client/gui/util/IClonerGui.java b/src/main/java/noppes/npcs/client/gui/util/IClonerGui.java
index db4989b56..60346560a 100644
--- a/src/main/java/noppes/npcs/client/gui/util/IClonerGui.java
+++ b/src/main/java/noppes/npcs/client/gui/util/IClonerGui.java
@@ -8,6 +8,8 @@ import java.util.UUID;
public interface IClonerGui {
int getShowingClones();
+
HashMap getTags();
+
TagMap getTagMap();
}
diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java
index e14861033..976452d08 100644
--- a/src/main/java/noppes/npcs/config/ConfigMarket.java
+++ b/src/main/java/noppes/npcs/config/ConfigMarket.java
@@ -79,11 +79,12 @@ public class ConfigMarket {
CurrencyName = config.get(CURRENCY, "Currency Name", "Coins",
"Display name for the currency (e.g. 'Coins', 'Gold', 'Credits')").getString();
- StartingBalance = config.get(CURRENCY, "Starting Balance", 0,
- "Starting currency balance for new players").getInt(0);
+ StartingBalance = parseLongConfig(config.get(CURRENCY, "Starting Balance", "0",
+ "Starting currency balance for new players").getString(), 0);
- MaxBalance = config.get(CURRENCY, "Max Balance", Integer.MAX_VALUE,
- "Maximum currency balance a player can have").getInt(Integer.MAX_VALUE);
+ MaxBalance = parseLongConfig(config.get(CURRENCY, "Max Balance", String.valueOf(Long.MAX_VALUE),
+ "Maximum currency balance a player can have").getString(), Long.MAX_VALUE);
+ if (MaxBalance < 0) MaxBalance = Long.MAX_VALUE;
// =========================================
// Auction Settings
@@ -95,16 +96,19 @@ public class ConfigMarket {
AuctionDurationHours = config.get(AUCTION, "Auction Duration Hours", 24,
"Default duration for auctions in hours").getInt(24);
+ if (AuctionDurationHours < 1) AuctionDurationHours = 1;
- ListingFee = config.get(AUCTION, "Listing Fee", 10,
- "Flat fee charged when creating a listing").getInt(10);
+ ListingFee = parseLongConfig(config.get(AUCTION, "Listing Fee", "10",
+ "Flat fee charged when creating a listing").getString(), 10);
+ if (ListingFee < 0) ListingFee = 0;
- MinimumListingPrice = config.get(AUCTION, "Minimum Listing Price", 1,
- "Minimum starting price for auction listings").getInt(1);
+ MinimumListingPrice = parseLongConfig(config.get(AUCTION, "Minimum Listing Price", "1",
+ "Minimum starting price for auction listings").getString(), 1);
if (MinimumListingPrice < 1) MinimumListingPrice = 1;
SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05,
"Percentage of sale price taken as tax (0.05 = 5%). Tax is deleted as a currency sink.").getDouble(0.05);
+ SalesTaxPercent = Math.max(0.0, Math.min(1.0, SalesTaxPercent));
int maxTrades = config.get(AUCTION, "Default Max Trades", 8,
"Default maximum number of trade slots per player (listings + bids + claims). Min: 1, Max: 45. Players can have more via customnpcs.auction.trades.X permissions.").getInt(8);
@@ -112,26 +116,30 @@ public class ConfigMarket {
SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 2,
"When a bid is placed with less than this many minutes remaining, the auction is extended to this duration").getInt(2);
+ if (SnipeProtectionMinutes < 0) SnipeProtectionMinutes = 0;
ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 20,
"Number of days before unclaimed items/currency are deleted").getInt(20);
+ if (ClaimExpirationDays < 1) ClaimExpirationDays = 1;
MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05,
"Minimum bid increment as a percentage of current bid (0.05 = 5%)").getDouble(0.05);
+ MinBidIncrementPercent = Math.max(0.0, Math.min(1.0, MinBidIncrementPercent));
CancellationPenaltyPercent = config.get(AUCTION, "Cancellation Penalty Percent", 0.10,
"Percentage of current bid taken as penalty when seller cancels an auction with bids (0.10 = 10%)").getDouble(0.10);
+ CancellationPenaltyPercent = Math.max(0.0, Math.min(1.0, CancellationPenaltyPercent));
// =========================================
// Auction Blacklist Settings
// =========================================
config.setCategoryComment(AUCTION_BLACKLIST,
"Item Blacklist settings for the Auction House.\n" +
- "Prevents specific items, mods, or items with certain NBT tags from being listed.\n\n" +
- "ITEM FORMAT: Use 'modid:itemname' format (e.g., 'minecraft:bedrock', 'customnpcs:npcWand')\n" +
- "WILDCARDS: Use * for wildcards (e.g., 'customnpcs:npc*' blocks all items starting with 'npc')\n" +
- "MOD FORMAT: Use just the mod ID (e.g., 'projecte' blocks all items from that mod)\n" +
- "NBT FORMAT: Use the NBT tag key name (e.g., 'AdminOnly' blocks items with that tag)");
+ "Prevents specific items, mods, or items with certain NBT tags from being listed.\n\n" +
+ "ITEM FORMAT: Use 'modid:itemname' format (e.g., 'minecraft:bedrock', 'customnpcs:npcWand')\n" +
+ "WILDCARDS: Use * for wildcards (e.g., 'customnpcs:npc*' blocks all items starting with 'npc')\n" +
+ "MOD FORMAT: Use just the mod ID (e.g., 'projecte' blocks all items from that mod)\n" +
+ "NBT FORMAT: Use the NBT tag key name (e.g., 'AdminOnly' blocks items with that tag)");
BlacklistEnabled = config.get(AUCTION_BLACKLIST, "Enable Blacklist", true,
"Enable item blacklist checking when creating listings").getBoolean(true);
@@ -149,17 +157,17 @@ public class ConfigMarket {
"customnpcs:npcSoulstoneFilled"
},
"Items that cannot be listed on the Auction House.\n" +
- "Format: modid:itemname (supports * wildcards)\n" +
- "Example: 'minecraft:diamond_sword' or 'customnpcs:npc*'").getStringList();
+ "Format: modid:itemname (supports * wildcards)\n" +
+ "Example: 'minecraft:diamond_sword' or 'customnpcs:npc*'").getStringList();
BlacklistedMods = config.get(AUCTION_BLACKLIST, "Blacklisted Mods", new String[]{},
"All items from these mods are blocked from the Auction House.\n" +
- "Format: modid (e.g., 'projecte', 'thaumcraft')").getStringList();
+ "Format: modid (e.g., 'projecte', 'thaumcraft')").getStringList();
BlacklistedNBTTags = config.get(AUCTION_BLACKLIST, "Blacklisted NBT Tags", new String[]{},
"Items containing any of these NBT tag keys are blocked.\n" +
- "Checks the root level of the item's NBT compound.\n" +
- "Example: 'AdminOnly', 'CreativeMode'").getStringList();
+ "Checks the root level of the item's NBT compound.\n" +
+ "Example: 'AdminOnly', 'CreativeMode'").getStringList();
// =========================================
// Auction Logging Settings
@@ -237,4 +245,13 @@ public class ConfigMarket {
}
}
}
+
+ private static long parseLongConfig(String value, long defaultValue) {
+ if (value == null || value.trim().isEmpty()) return defaultValue;
+ try {
+ return Long.parseLong(value.trim());
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
}
diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java b/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java
new file mode 100644
index 000000000..6c6fdce44
--- /dev/null
+++ b/src/main/java/noppes/npcs/constants/EnumAuctionLogAction.java
@@ -0,0 +1,44 @@
+package noppes.npcs.constants;
+
+import noppes.npcs.config.ConfigMarket;
+
+/**
+ * Enum for auction log action types.
+ * Used by AuctionController.logAuction() for type-safe logging.
+ */
+public enum EnumAuctionLogAction {
+ CREATED,
+ BID,
+ BUYOUT,
+ SOLD,
+ EXPIRED,
+ CANCELLED,
+ CLAIMED,
+ CLAIM_EXPIRED;
+
+ /**
+ * Check if this action should be logged based on config settings
+ */
+ public boolean shouldLog() {
+ if (!ConfigMarket.AuctionLoggingEnabled) return false;
+ switch (this) {
+ case CREATED:
+ return ConfigMarket.LogAuctionCreated;
+ case BID:
+ return ConfigMarket.LogAuctionBid;
+ case BUYOUT:
+ return ConfigMarket.LogAuctionBuyout;
+ case SOLD:
+ return ConfigMarket.LogAuctionSold;
+ case EXPIRED:
+ return ConfigMarket.LogAuctionExpired;
+ case CANCELLED:
+ return ConfigMarket.LogAuctionCancelled;
+ case CLAIMED:
+ case CLAIM_EXPIRED:
+ return ConfigMarket.LogAuctionClaimed;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionPage.java b/src/main/java/noppes/npcs/constants/EnumAuctionPage.java
new file mode 100644
index 000000000..7f98cb2f0
--- /dev/null
+++ b/src/main/java/noppes/npcs/constants/EnumAuctionPage.java
@@ -0,0 +1,28 @@
+package noppes.npcs.constants;
+
+/**
+ * Enum for auction GUI page navigation.
+ * Maps page indices to GUI types for packet-based navigation.
+ */
+public enum EnumAuctionPage {
+ LISTINGS(EnumGuiType.PlayerAuction),
+ SELL(EnumGuiType.PlayerAuctionSell),
+ TRADES(EnumGuiType.PlayerAuctionTrades);
+
+ private final EnumGuiType guiType;
+
+ EnumAuctionPage(EnumGuiType guiType) {
+ this.guiType = guiType;
+ }
+
+ public EnumGuiType getGuiType() {
+ return guiType;
+ }
+
+ public static EnumAuctionPage fromOrdinal(int ordinal) {
+ if (ordinal >= 0 && ordinal < values().length) {
+ return values()[ordinal];
+ }
+ return LISTINGS;
+ }
+}
diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java
index 9e2588103..5b5a318b3 100644
--- a/src/main/java/noppes/npcs/constants/EnumScriptType.java
+++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java
@@ -126,6 +126,8 @@ public enum EnumScriptType {
ABILITY_COMPLETE("abilityComplete"),
ABILITY_HIT("abilityHit"),
ABILITY_TICK("abilityTick"),
+ ABILITY_TOGGLE("abilityToggle"),
+ ABILITY_TOGGLE_UPDATE("abilityToggleUpdate"),
ENERGY_PROJECTILE_FIRED("energyProjectileFired"),
ENERGY_PROJECTILE_TICK("energyProjectileTick"),
@@ -137,7 +139,14 @@ public enum EnumScriptType {
ENERGY_BARRIER_SPAWNED("energyBarrierSpawned"),
ENERGY_BARRIER_TICK("energyBarrierTick"),
ENERGY_BARRIER_HIT("energyBarrierHit"),
- ENERGY_BARRIER_DESTROYED("energyBarrierDestroyed");
+ ENERGY_BARRIER_DESTROYED("energyBarrierDestroyed"),
+
+ // Auction Events
+ AUCTION_CREATE("auctionCreate"),
+ AUCTION_BID("auctionBid"),
+ AUCTION_BUYOUT("auctionBuyout"),
+ AUCTION_CANCEL("auctionCancel"),
+ AUCTION_CLAIM("auctionClaim");
public String function;
diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java
index cbec6ad34..3f680c188 100644
--- a/src/main/java/noppes/npcs/constants/EnumScrollData.java
+++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java
@@ -10,5 +10,6 @@ public enum EnumScrollData {
CUSTOM_ABILITIES,
BUILTIN_ABILITIES,
ANIMATIONS,
- BUILTIN_ANIMATIONS
+ BUILTIN_ANIMATIONS,
+ CHAINED_ABILITIES
}
diff --git a/src/main/java/noppes/npcs/constants/EnumTradeSlotType.java b/src/main/java/noppes/npcs/constants/EnumTradeSlotType.java
new file mode 100644
index 000000000..16850a79e
--- /dev/null
+++ b/src/main/java/noppes/npcs/constants/EnumTradeSlotType.java
@@ -0,0 +1,35 @@
+package noppes.npcs.constants;
+
+/**
+ * Enum for trade slot types in the player's auction trades view.
+ * Used by ContainerAuctionTrades to identify what each slot contains.
+ */
+public enum EnumTradeSlotType {
+ EMPTY,
+ SELLING,
+ BIDDING,
+ CLAIM;
+
+ public boolean isSelling() {
+ return this == SELLING;
+ }
+
+ public boolean isBidding() {
+ return this == BIDDING;
+ }
+
+ public boolean isClaim() {
+ return this == CLAIM;
+ }
+
+ public boolean isEmpty() {
+ return this == EMPTY;
+ }
+
+ public static EnumTradeSlotType fromOrdinal(int ordinal) {
+ if (ordinal >= 0 && ordinal < values().length) {
+ return values()[ordinal];
+ }
+ return EMPTY;
+ }
+}
diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java
index c5861927a..9b06001ff 100644
--- a/src/main/java/noppes/npcs/constants/ScriptContext.java
+++ b/src/main/java/noppes/npcs/constants/ScriptContext.java
@@ -2,9 +2,11 @@ package noppes.npcs.constants;
import noppes.npcs.api.event.IAbilityEvent;
import noppes.npcs.api.event.IAnimationEvent;
+import noppes.npcs.api.event.IAuctionEvent;
import noppes.npcs.api.event.IBlockEvent;
import noppes.npcs.api.event.ICustomGuiEvent;
import noppes.npcs.api.event.IDialogEvent;
+import noppes.npcs.api.event.IEnergyBarrierEvent;
import noppes.npcs.api.event.IEnergyProjectileEvent;
import noppes.npcs.api.event.IFactionEvent;
import noppes.npcs.api.event.IForgeEvent;
@@ -57,6 +59,7 @@ public class ScriptContext {
INpcEvent.class,
IProjectileEvent.class,
IEnergyProjectileEvent.class,
+ IEnergyBarrierEvent.class,
IAbilityEvent.class,
IAnimationEvent.class
);
@@ -70,7 +73,9 @@ public class ScriptContext {
IFactionEvent.class,
ICustomGuiEvent.class,
IEnergyProjectileEvent.class,
- IPlayerAbilityEvent.class
+ IEnergyBarrierEvent.class,
+ IPlayerAbilityEvent.class,
+ IAuctionEvent.class
);
public static final ScriptContext BLOCK = register("BLOCK", "block",
diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java
index 2ccaa2e52..0741fdd9a 100644
--- a/src/main/java/noppes/npcs/containers/ContainerAuction.java
+++ b/src/main/java/noppes/npcs/containers/ContainerAuction.java
@@ -71,6 +71,7 @@ public class ContainerAuction extends ContainerNpcInterface {
/**
* Get the ItemStack in a player inventory slot.
+ *
* @param containerSlotIndex The container slot index (0-35 for player inv)
*/
public ItemStack getPlayerInventoryStack(int containerSlotIndex) {
diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java
index 2b40000ac..2b6f9a67f 100644
--- a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java
+++ b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java
@@ -14,9 +14,9 @@ import noppes.npcs.entity.EntityNPCInterface;
*/
public class ContainerAuctionBidding extends ContainerAuction {
// Display slot layout
- private int itemSlotX = 71;
- private int itemSlotY = 76;
- private int itemSlotIndex = PLAYER_INV_SLOT_COUNT;
+ private static final int ITEM_SLOT_X = 71;
+ private static final int ITEM_SLOT_Y = 76;
+ private static final int ITEM_SLOT_INDEX = PLAYER_INV_SLOT_COUNT;
private final IInventory displayInventory;
private AuctionListing listing;
@@ -26,11 +26,16 @@ public class ContainerAuctionBidding extends ContainerAuction {
displayInventory = new InventoryBasic("Display", false, 1);
// Display slot (read-only)
- addSlotToContainer(new Slot(displayInventory, 0, itemSlotX, itemSlotY) {
+ addSlotToContainer(new Slot(displayInventory, 0, ITEM_SLOT_X, ITEM_SLOT_Y) {
@Override
- public boolean isItemValid(ItemStack stack) { return false; }
+ public boolean isItemValid(ItemStack stack) {
+ return false;
+ }
+
@Override
- public boolean canTakeStack(EntityPlayer player) { return false; }
+ public boolean canTakeStack(EntityPlayer player) {
+ return false;
+ }
});
}
@@ -58,6 +63,6 @@ public class ContainerAuctionBidding extends ContainerAuction {
* Check if this is the display slot.
*/
public boolean isDisplaySlot(int slotIndex) {
- return slotIndex == itemSlotIndex;
+ return slotIndex == ITEM_SLOT_INDEX;
}
}
diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java
index 14b381f40..f703a206d 100644
--- a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java
+++ b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java
@@ -13,10 +13,10 @@ import noppes.npcs.entity.EntityNPCInterface;
* Items are cloned from inventory, not moved. Validation happens on submit.
*/
public class ContainerAuctionSell extends ContainerAuction {
- private int sellSlotX = 59;
- private int sellSlotY = 101;
- private int sellSlotIndex = PLAYER_INV_SLOT_COUNT;
- private int maxStack = 64;
+ private static final int SELL_SLOT_X = 59;
+ private static final int SELL_SLOT_Y = 101;
+ private static final int SELL_SLOT_INDEX = PLAYER_INV_SLOT_COUNT;
+ private static final int MAX_STACK = 64;
private final IInventory sellInventory;
@@ -25,11 +25,16 @@ public class ContainerAuctionSell extends ContainerAuction {
sellInventory = new InventoryBasic("Sell", false, 1);
// Sell slot blocks direct interaction
- addSlotToContainer(new Slot(sellInventory, 0, sellSlotX, sellSlotY) {
+ addSlotToContainer(new Slot(sellInventory, 0, SELL_SLOT_X, SELL_SLOT_Y) {
@Override
- public boolean isItemValid(ItemStack stack) { return false; }
+ public boolean isItemValid(ItemStack stack) {
+ return false;
+ }
+
@Override
- public boolean canTakeStack(EntityPlayer player) { return false; }
+ public boolean canTakeStack(EntityPlayer player) {
+ return false;
+ }
});
}
@@ -44,8 +49,9 @@ public class ContainerAuctionSell extends ContainerAuction {
/**
* Add items from inventory to sell slot.
* Not limited by inventory - just stages items for sale. Validation on submit.
+ *
* @param sourceSlot Inventory slot index
- * @param fullStack True=full stack, False=1 item
+ * @param fullStack True=full stack, False=1 item
*/
public void addToSellSlot(int sourceSlot, boolean fullStack) {
if (!isPlayerInventorySlot(sourceSlot)) return;
@@ -59,11 +65,11 @@ public class ContainerAuctionSell extends ContainerAuction {
if (sellStack == null) {
// Empty slot - create new stack
ItemStack newStack = source.copy();
- newStack.stackSize = Math.min(toAdd, maxStack);
+ newStack.stackSize = Math.min(toAdd, MAX_STACK);
sellInventory.setInventorySlotContents(0, newStack);
} else if (itemsMatch(sellStack, source)) {
// Same item - add to existing (capped at 64)
- int space = maxStack - sellStack.stackSize;
+ int space = MAX_STACK - sellStack.stackSize;
int add = Math.min(toAdd, space);
if (add > 0) {
sellStack.stackSize += add;
@@ -71,13 +77,14 @@ public class ContainerAuctionSell extends ContainerAuction {
} else {
// Different item - replace
ItemStack newStack = source.copy();
- newStack.stackSize = Math.min(toAdd, maxStack);
+ newStack.stackSize = Math.min(toAdd, MAX_STACK);
sellInventory.setInventorySlotContents(0, newStack);
}
}
/**
* Remove items from sell slot.
+ *
* @param removeAll True=clear slot, False=remove 1 item
*/
public void removeFromSellSlot(boolean removeAll) {
@@ -91,12 +98,16 @@ public class ContainerAuctionSell extends ContainerAuction {
}
}
- /** Check if sell slot is this container slot index */
+ /**
+ * Check if sell slot is this container slot index
+ */
public boolean isSellSlot(int slotIndex) {
- return slotIndex == sellSlotIndex;
+ return slotIndex == SELL_SLOT_INDEX;
}
- /** Check if two stacks match using NoppesUtilPlayer.compareItems (same item, damage, and NBT) */
+ /**
+ * Check if two stacks match using NoppesUtilPlayer.compareItems (same item, damage, and NBT)
+ */
private boolean itemsMatch(ItemStack a, ItemStack b) {
return NoppesUtilPlayer.compareItems(a, b, false, false);
}
diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java
index eab491c70..187d50d13 100644
--- a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java
+++ b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java
@@ -5,12 +5,14 @@ import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import noppes.npcs.constants.EnumClaimType;
+import noppes.npcs.constants.EnumTradeSlotType;
import noppes.npcs.controllers.AuctionController;
import noppes.npcs.controllers.data.AuctionClaim;
import noppes.npcs.controllers.data.AuctionListing;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
@@ -34,7 +36,7 @@ public class ContainerAuctionTrades extends ContainerAuction {
private int maxTradeSlots = 8; // Player's max trade slots (permission-aware, synced from server)
// Slot type tracking for proper accessor methods
- private int[] slotTypes = new int[SLOT_COUNT]; // 0=empty, 1=selling, 2=bidding, 3=claim
+ private EnumTradeSlotType[] slotTypes = new EnumTradeSlotType[SLOT_COUNT];
private int[] slotDataIndex = new int[SLOT_COUNT]; // Index into respective list
public ContainerAuctionTrades(EntityNPCInterface npc, EntityPlayer player) {
@@ -53,7 +55,9 @@ public class ContainerAuctionTrades extends ContainerAuction {
refreshData();
}
- /** Refresh data from controller (server-side only) */
+ /**
+ * Refresh data from controller (server-side only)
+ */
public void refreshData() {
// Clear existing data
clearData();
@@ -68,7 +72,9 @@ public class ContainerAuctionTrades extends ContainerAuction {
populateDisplay(newListings, newBids, newClaims);
}
- /** Set data from NBT received from server (client-side) */
+ /**
+ * Set data from NBT received from server (client-side)
+ */
public void setTradesData(NBTTagCompound compound) {
// Clear existing data
clearData();
@@ -109,7 +115,9 @@ public class ContainerAuctionTrades extends ContainerAuction {
populateDisplay(newListings, newBids, newClaims);
}
- /** Clear all data and reset display */
+ /**
+ * Clear all data and reset display
+ */
private void clearData() {
activeListings.clear();
activeBids.clear();
@@ -117,13 +125,15 @@ public class ContainerAuctionTrades extends ContainerAuction {
displayInventory.clear();
// Reset slot tracking
+ Arrays.fill(slotTypes, EnumTradeSlotType.EMPTY);
for (int i = 0; i < SLOT_COUNT; i++) {
- slotTypes[i] = 0;
slotDataIndex[i] = -1;
}
}
- /** Populate display from provided data lists */
+ /**
+ * Populate display from provided data lists
+ */
private void populateDisplay(List newListings, List newBids, List allClaims) {
activeListings = new ArrayList<>(newListings);
activeBids = new ArrayList<>(newBids);
@@ -156,7 +166,7 @@ public class ContainerAuctionTrades extends ContainerAuction {
if (slot >= SLOT_COUNT) break;
claims.add(claim);
displayInventory.setClaimItem(slot, claim);
- slotTypes[slot] = 3; // claim
+ slotTypes[slot] = EnumTradeSlotType.CLAIM;
slotDataIndex[slot] = claims.size() - 1;
slot++;
}
@@ -166,7 +176,7 @@ public class ContainerAuctionTrades extends ContainerAuction {
if (slot >= SLOT_COUNT) break;
claims.add(claim);
displayInventory.setClaimItem(slot, claim);
- slotTypes[slot] = 3; // claim
+ slotTypes[slot] = EnumTradeSlotType.CLAIM;
slotDataIndex[slot] = claims.size() - 1;
slot++;
}
@@ -176,7 +186,7 @@ public class ContainerAuctionTrades extends ContainerAuction {
if (slot >= SLOT_COUNT) break;
claims.add(claim);
displayInventory.setClaimItem(slot, claim);
- slotTypes[slot] = 3; // claim
+ slotTypes[slot] = EnumTradeSlotType.CLAIM;
slotDataIndex[slot] = claims.size() - 1;
slot++;
}
@@ -186,7 +196,7 @@ public class ContainerAuctionTrades extends ContainerAuction {
for (AuctionListing listing : activeListings) {
if (slot >= SLOT_COUNT) break;
if (listing.item != null) displayInventory.setInventorySlotContents(slot, listing.item.copy());
- slotTypes[slot] = 1; // selling
+ slotTypes[slot] = EnumTradeSlotType.SELLING;
slotDataIndex[slot] = listingIdx;
slot++;
listingIdx++;
@@ -197,7 +207,7 @@ public class ContainerAuctionTrades extends ContainerAuction {
for (AuctionListing bid : activeBids) {
if (slot >= SLOT_COUNT) break;
if (bid.item != null) displayInventory.setInventorySlotContents(slot, bid.item.copy());
- slotTypes[slot] = 2; // bidding
+ slotTypes[slot] = EnumTradeSlotType.BIDDING;
slotDataIndex[slot] = bidIdx;
slot++;
bidIdx++;
@@ -208,7 +218,7 @@ public class ContainerAuctionTrades extends ContainerAuction {
if (slot >= SLOT_COUNT) break;
claims.add(claim);
displayInventory.setClaimItem(slot, claim);
- slotTypes[slot] = 3; // claim
+ slotTypes[slot] = EnumTradeSlotType.CLAIM;
slotDataIndex[slot] = claims.size() - 1;
slot++;
}
@@ -216,25 +226,29 @@ public class ContainerAuctionTrades extends ContainerAuction {
// ========== Slot Data Accessors ==========
- /** Get listing at slot (or null if claim/empty) */
+ /**
+ * Get listing at slot (or null if claim/empty)
+ */
public AuctionListing getListingAt(int slot) {
if (slot < 0 || slot >= SLOT_COUNT) return null;
- int type = slotTypes[slot];
+ EnumTradeSlotType type = slotTypes[slot];
int idx = slotDataIndex[slot];
if (idx < 0) return null;
- if (type == 1 && idx < activeListings.size()) {
+ if (type == EnumTradeSlotType.SELLING && idx < activeListings.size()) {
return activeListings.get(idx);
- } else if (type == 2 && idx < activeBids.size()) {
+ } else if (type == EnumTradeSlotType.BIDDING && idx < activeBids.size()) {
return activeBids.get(idx);
}
return null;
}
- /** Get claim at slot (or null if listing/empty) */
+ /**
+ * Get claim at slot (or null if listing/empty)
+ */
public AuctionClaim getClaimAt(int slot) {
if (slot < 0 || slot >= SLOT_COUNT) return null;
- if (slotTypes[slot] == 3) {
+ if (slotTypes[slot] == EnumTradeSlotType.CLAIM) {
int idx = slotDataIndex[slot];
if (idx >= 0 && idx < claims.size()) {
return claims.get(idx);
@@ -243,17 +257,23 @@ public class ContainerAuctionTrades extends ContainerAuction {
return null;
}
- /** Check if slot is player's active listing */
+ /**
+ * Check if slot is player's active listing
+ */
public boolean isSellingAt(int slot) {
- return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 1;
+ return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == EnumTradeSlotType.SELLING;
}
- /** Check if slot is player's active bid */
+ /**
+ * Check if slot is player's active bid
+ */
public boolean isBiddingAt(int slot) {
- return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 2;
+ return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == EnumTradeSlotType.BIDDING;
}
- /** Find listing matching item (searches both selling and bidding) */
+ /**
+ * Find listing matching item (searches both selling and bidding)
+ */
public AuctionListing getListingForItem(ItemStack stack) {
if (stack == null) return null;
for (AuctionListing listing : activeListings) {
@@ -265,7 +285,9 @@ public class ContainerAuctionTrades extends ContainerAuction {
return null;
}
- /** Find listing in selling list */
+ /**
+ * Find listing in selling list
+ */
public AuctionListing getSellingListingForItem(ItemStack stack) {
if (stack == null) return null;
for (AuctionListing listing : activeListings) {
@@ -274,7 +296,9 @@ public class ContainerAuctionTrades extends ContainerAuction {
return null;
}
- /** Find listing in bidding list */
+ /**
+ * Find listing in bidding list
+ */
public AuctionListing getBiddingListingForItem(ItemStack stack) {
if (stack == null) return null;
for (AuctionListing bid : activeBids) {
@@ -283,7 +307,9 @@ public class ContainerAuctionTrades extends ContainerAuction {
return null;
}
- /** Find claim matching item */
+ /**
+ * Find claim matching item
+ */
public AuctionClaim getClaimForItem(ItemStack stack) {
if (stack == null) return null;
for (AuctionClaim claim : claims) {
@@ -294,14 +320,27 @@ public class ContainerAuctionTrades extends ContainerAuction {
// ========== List Getters ==========
- public List getActiveListings() { return activeListings; }
- public List getActiveBids() { return activeBids; }
- public List getClaims() { return claims; }
- public int getTotalTradeCount() { return activeListings.size() + activeBids.size() + claims.size(); }
+ public List getActiveListings() {
+ return activeListings;
+ }
+
+ public List getActiveBids() {
+ return activeBids;
+ }
+
+ public List getClaims() {
+ return claims;
+ }
+
+ public int getTotalTradeCount() {
+ return activeListings.size() + activeBids.size() + claims.size();
+ }
// ========== Slot Hiding (for pending operations) ==========
- /** Hide slot for pending operation display */
+ /**
+ * Hide slot for pending operation display
+ */
public void setHiddenSlot(int slot) {
if (hiddenSlot >= 0 && hiddenSlot < SLOT_COUNT) {
restoreSlotItem(hiddenSlot);
@@ -312,7 +351,9 @@ public class ContainerAuctionTrades extends ContainerAuction {
}
}
- /** Restore hidden slot */
+ /**
+ * Restore hidden slot
+ */
public void clearHiddenSlot() {
if (hiddenSlot >= 0 && hiddenSlot < SLOT_COUNT) {
restoreSlotItem(hiddenSlot);
@@ -320,34 +361,42 @@ public class ContainerAuctionTrades extends ContainerAuction {
hiddenSlot = -1;
}
- /** Restore item at slot from data using slot tracking */
+ /**
+ * Restore item at slot from data using slot tracking
+ */
private void restoreSlotItem(int slot) {
if (slot < 0 || slot >= SLOT_COUNT) return;
- int type = slotTypes[slot];
+ EnumTradeSlotType type = slotTypes[slot];
int idx = slotDataIndex[slot];
if (idx < 0) return;
- if (type == 1 && idx < activeListings.size()) {
+ if (type == EnumTradeSlotType.SELLING && idx < activeListings.size()) {
// Selling
AuctionListing listing = activeListings.get(idx);
if (listing != null && listing.item != null) {
displayInventory.setInventorySlotContents(slot, listing.item.copy());
}
- } else if (type == 2 && idx < activeBids.size()) {
+ } else if (type == EnumTradeSlotType.BIDDING && idx < activeBids.size()) {
// Bidding
AuctionListing bid = activeBids.get(idx);
if (bid != null && bid.item != null) {
displayInventory.setInventorySlotContents(slot, bid.item.copy());
}
- } else if (type == 3 && idx < claims.size()) {
+ } else if (type == EnumTradeSlotType.CLAIM && idx < claims.size()) {
// Claim
displayInventory.setClaimItem(slot, claims.get(idx));
}
}
- public int getHiddenSlot() { return hiddenSlot; }
+ public int getHiddenSlot() {
+ return hiddenSlot;
+ }
- /** Get player's max trade slots (synced from server based on permissions) */
- public int getMaxTradeSlots() { return maxTradeSlots; }
+ /**
+ * Get player's max trade slots (synced from server based on permissions)
+ */
+ public int getMaxTradeSlots() {
+ return maxTradeSlots;
+ }
}
diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java
index e7c29f324..bfbf978d2 100644
--- a/src/main/java/noppes/npcs/controllers/AnimationController.java
+++ b/src/main/java/noppes/npcs/controllers/AnimationController.java
@@ -22,14 +22,17 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
-import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
@@ -364,6 +367,7 @@ public class AnimationController implements IAnimationHandler {
/**
* Get a built-in animation by name.
+ *
* @param name The animation name (case-insensitive)
* @return The built-in animation, or null if not found
*/
@@ -395,6 +399,7 @@ public class AnimationController implements IAnimationHandler {
/**
* Check if an animation name is a built-in animation.
+ *
* @param name The animation name to check
* @return true if this is a built-in animation
*/
diff --git a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java
index 9b8c1f995..dae214f5c 100644
--- a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java
+++ b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java
@@ -7,7 +7,7 @@ import noppes.npcs.config.ConfigMarket;
/**
* Server-side utility for syncing auction configuration to clients.
* Handles Vault integration for currency name resolution.
- *
+ *
* This class is SERVER-SIDE ONLY. Client reads via AuctionClientConfig.
*/
public class AuctionConfigSync {
@@ -15,7 +15,7 @@ public class AuctionConfigSync {
/**
* Write auction config to NBT for sending to client.
* Called on server side when building login packet.
- *
+ *
* Currency name is resolved from Vault if enabled and available,
* otherwise falls back to ConfigMarket.CurrencyName.
*/
@@ -40,7 +40,7 @@ public class AuctionConfigSync {
public static String getEffectiveCurrencyName() {
if (ConfigMarket.UseVault && VaultUtil.isEnabled()) {
String vaultName = VaultUtil.getCurrencyNamePlural();
- if (vaultName != null && !vaultName.isEmpty() && !vaultName.equals("coins")) {
+ if (vaultName != null && !vaultName.isEmpty()) {
return vaultName;
}
}
diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java
index e6ca0c6cf..7ea79baeb 100644
--- a/src/main/java/noppes/npcs/controllers/AuctionController.java
+++ b/src/main/java/noppes/npcs/controllers/AuctionController.java
@@ -6,17 +6,18 @@ import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompressedStreamTools;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
-import net.minecraft.server.MinecraftServer;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.EnumChatFormatting;
import noppes.npcs.CustomNpcs;
import noppes.npcs.CustomNpcsPermissions;
+import noppes.npcs.EventHooks;
import noppes.npcs.LogWriter;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.handler.IAuctionHandler;
import noppes.npcs.api.handler.data.IAuctionListing;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.config.ConfigMarket;
+import noppes.npcs.constants.EnumAuctionLogAction;
import noppes.npcs.constants.EnumAuctionSort;
import noppes.npcs.constants.EnumAuctionStatus;
import noppes.npcs.constants.EnumClaimType;
@@ -26,24 +27,32 @@ import noppes.npcs.controllers.data.AuctionClaim;
import noppes.npcs.controllers.data.AuctionFilter;
import noppes.npcs.controllers.data.AuctionListing;
import noppes.npcs.controllers.data.PlayerData;
+import noppes.npcs.controllers.data.PlayerDataScript;
import noppes.npcs.controllers.data.PlayerTradeData;
+import noppes.npcs.scripted.NpcAPI;
+import noppes.npcs.scripted.event.player.AuctionEvent;
import noppes.npcs.util.CustomNPCsThreader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Main controller for the Auction system.
- *
+ *
* Thread Safety:
* - Uses ConcurrentHashMap for all data stores - no locking required
* - Claims are stored in PlayerData, not here - reduces contention
* - Saves are performed asynchronously on the CNPC+ thread
- *
+ *
* Performance:
* - Auction processing runs every 30 seconds (600 ticks)
* - Save interval is every 30 seconds (600 ticks)
@@ -60,29 +69,41 @@ public class AuctionController implements IAuctionHandler {
// Player Indices for O(1) Lookups
// =========================================
- /** Listings by seller UUID - for quick "my listings" lookups */
+ /**
+ * Listings by seller UUID - for quick "my listings" lookups
+ */
private final Map> playerListingIds = new ConcurrentHashMap<>();
- /** Listings where player is current high bidder */
+ /**
+ * Listings where player is current high bidder
+ */
private final Map> playerBidIds = new ConcurrentHashMap<>();
- /** Cached max trade slots per player - computed on login, cleared on logout */
+ /**
+ * Cached max trade slots per player - computed on login, cleared on logout
+ */
private final Map playerMaxTradesCache = new ConcurrentHashMap<>();
// =========================================
// Threading & State
// =========================================
- /** Flag indicating data needs to be saved */
+ /**
+ * Flag indicating data needs to be saved
+ */
private final AtomicBoolean dirty = new AtomicBoolean(false);
- /** Flag to prevent concurrent saves */
+ /**
+ * Flag to prevent concurrent saves
+ */
private final AtomicBoolean saving = new AtomicBoolean(false);
private String filePath = "";
private int tickCounter = 0;
- /** Process auctions and save every 30 seconds (600 ticks) */
+ /**
+ * Process auctions and save every 30 seconds (600 ticks)
+ */
private static final int TICK_INTERVAL = 600;
public AuctionController() {
@@ -138,52 +159,56 @@ public class AuctionController implements IAuctionHandler {
}
private void endAuction(AuctionListing listing) {
- listing.status = EnumAuctionStatus.ENDED;
+ synchronized (listing) {
+ if (listing.status != EnumAuctionStatus.ACTIVE) return;
- // Remove from seller's active listings
- removeFromPlayerListings(listing.sellerUUID, listing.id);
+ listing.status = EnumAuctionStatus.ENDED;
- // Remove from bidder's active bids if any
- if (listing.highBidderUUID != null) {
- removeFromPlayerBids(listing.highBidderUUID, listing.id);
- }
+ // Remove from seller's active listings
+ removeFromPlayerListings(listing.sellerUUID, listing.id);
- // Get item display name safely
- String itemDisplayName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item";
+ // Remove from bidder's active bids if any
+ if (listing.highBidderUUID != null) {
+ removeFromPlayerBids(listing.highBidderUUID, listing.id);
+ }
- if (listing.hasBids()) {
- // Auction sold - create claims
- AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(
- listing.highBidderUUID, listing.highBidderName, listing.id, listing.item);
- addClaimToPlayer(listing.highBidderUUID, itemClaim);
+ // Get item display name safely
+ String itemDisplayName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item";
- long saleAmount = listing.currentBid;
- long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent);
- long sellerReceives = saleAmount - tax;
+ if (listing.hasBids()) {
+ // Auction sold - create claims
+ AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(
+ listing.highBidderUUID, listing.highBidderName, listing.id, listing.item);
+ addClaimToPlayer(listing.highBidderUUID, itemClaim);
- AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim(
- listing.sellerUUID, listing.sellerName, listing.id, sellerReceives,
- itemDisplayName, listing.highBidderName);
- addClaimToPlayer(listing.sellerUUID, currencyClaim);
+ long saleAmount = listing.currentBid;
+ long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent);
+ long sellerReceives = saleAmount - tax;
- // Notifications
- sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id,
- "You won the auction for " + itemDisplayName + "!");
- sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
- "Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
+ AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim(
+ listing.sellerUUID, listing.sellerName, listing.id, sellerReceives,
+ itemDisplayName, listing.highBidderName);
+ addClaimToPlayer(listing.sellerUUID, currencyClaim);
- logAuction("SOLD", listing.sellerName, itemDisplayName, saleAmount,
- "Winner: " + listing.highBidderName + ", Tax: " + tax);
- } else {
- // No bids - return item to seller
- AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim(
- listing.sellerUUID, listing.sellerName, listing.id, listing.item);
- addClaimToPlayer(listing.sellerUUID, returnClaim);
+ // Notifications
+ sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id,
+ "You won the auction for " + itemDisplayName + "!");
+ sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
+ "Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
- sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id,
- "Your auction for " + itemDisplayName + " expired with no bids.");
+ logAuction(EnumAuctionLogAction.SOLD, listing.sellerName, itemDisplayName, saleAmount,
+ "Winner: " + listing.highBidderName + ", Tax: " + tax);
+ } else {
+ // No bids - return item to seller
+ AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim(
+ listing.sellerUUID, listing.sellerName, listing.id, listing.item);
+ addClaimToPlayer(listing.sellerUUID, returnClaim);
- logAuction("EXPIRED", listing.sellerName, itemDisplayName, listing.startingPrice, "No bids");
+ sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id,
+ "Your auction for " + itemDisplayName + " expired with no bids.");
+
+ logAuction(EnumAuctionLogAction.EXPIRED, listing.sellerName, itemDisplayName, listing.startingPrice, "No bids");
+ }
}
}
@@ -280,6 +305,15 @@ public class AuctionController implements IAuctionHandler {
return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ").";
}
+ // Fire auction create event (cancelable)
+ PlayerDataScript createHandler = ScriptController.Instance.getPlayerScripts(player);
+ IPlayer iCreator = (IPlayer) NpcAPI.Instance().getIEntity(player);
+ AuctionEvent.CreateEvent createEvent = new AuctionEvent.CreateEvent(
+ iCreator, NpcAPI.Instance().getIItemStack(item), startingPrice, buyoutPrice);
+ if (EventHooks.onAuctionCreate(createHandler, createEvent)) {
+ return "Auction creation was cancelled.";
+ }
+
// Deduct fee
if (!currency.withdraw(fee)) {
return "Failed to deduct listing fee.";
@@ -295,7 +329,7 @@ public class AuctionController implements IAuctionHandler {
markDirty();
- logAuction("CREATED", playerName, item.getDisplayName(), startingPrice,
+ logAuction(EnumAuctionLogAction.CREATED, playerName, item.getDisplayName(), startingPrice,
"Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee);
return null; // Success
@@ -307,55 +341,74 @@ public class AuctionController implements IAuctionHandler {
return "Listing not found.";
}
- if (!listing.status.canCancel()) {
- return "This auction cannot be cancelled.";
+ synchronized (listing) {
+ if (!listing.status.canCancel()) {
+ return "This auction cannot be cancelled.";
+ }
+
+ UUID playerUUID = player.getUniqueID();
+ if (!isAdmin && !listing.isSeller(playerUUID)) {
+ return "You can only cancel your own listings.";
+ }
+
+ // Fire auction cancel event (cancelable)
+ PlayerDataScript cancelHandler = ScriptController.Instance.getPlayerScripts(player);
+ IPlayer iCanceller = (IPlayer) NpcAPI.Instance().getIEntity(player);
+ AuctionEvent.CancelEvent cancelEvent = new AuctionEvent.CancelEvent(iCanceller, listing, isAdmin);
+ if (EventHooks.onAuctionCancel(cancelHandler, cancelEvent)) {
+ return "Auction cancellation was prevented.";
+ }
+
+ // Handle penalty if there are bids
+ if (listing.hasBids()) {
+ long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent);
+
+ // Refund bidder (no item - can't rebid on cancelled auction)
+ AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
+ listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid,
+ listing.item.getDisplayName(), listing.sellerName, null);
+ addClaimToPlayer(listing.highBidderUUID, refundClaim);
+
+ // Remove from bidder's active bids
+ removeFromPlayerBids(listing.highBidderUUID, listing.id);
+
+ sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id,
+ "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded.");
+
+ // Return item to seller
+ AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim(
+ listing.sellerUUID, listing.sellerName, listing.id, listing.item);
+ addClaimToPlayer(listing.sellerUUID, itemClaim);
+
+ // Deduct cancellation penalty from seller
+ if (penalty > 0) {
+ PlayerData sellerData = PlayerDataController.Instance.getData(listing.sellerUUID);
+ if (sellerData != null) {
+ sellerData.tradeData.withdraw(penalty);
+ sellerData.save();
+ }
+ }
+
+ logAuction(EnumAuctionLogAction.CANCELLED, listing.sellerName, listing.item.getDisplayName(), listing.currentBid,
+ "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName +
+ (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : ""));
+ } else {
+ // No bids - just return item
+ AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim(
+ listing.sellerUUID, listing.sellerName, listing.id, listing.item);
+ addClaimToPlayer(listing.sellerUUID, itemClaim);
+
+ logAuction(EnumAuctionLogAction.CANCELLED, listing.sellerName, listing.item.getDisplayName(), listing.startingPrice,
+ "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : ""));
+ }
+
+ // Remove from seller's active listings
+ removeFromPlayerListings(listing.sellerUUID, listing.id);
+
+ listing.status = EnumAuctionStatus.CANCELLED;
+ markDirty();
}
- UUID playerUUID = player.getUniqueID();
- if (!isAdmin && !listing.isSeller(playerUUID)) {
- return "You can only cancel your own listings.";
- }
-
- // Handle penalty if there are bids
- if (listing.hasBids()) {
- long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent);
-
- // Refund bidder (no item - can't rebid on cancelled auction)
- AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
- listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid,
- listing.item.getDisplayName(), listing.sellerName, null);
- addClaimToPlayer(listing.highBidderUUID, refundClaim);
-
- // Remove from bidder's active bids
- removeFromPlayerBids(listing.highBidderUUID, listing.id);
-
- sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id,
- "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded.");
-
- // Return item to seller
- AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim(
- listing.sellerUUID, listing.sellerName, listing.id, listing.item);
- addClaimToPlayer(listing.sellerUUID, itemClaim);
-
- logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.currentBid,
- "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName +
- (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : ""));
- } else {
- // No bids - just return item
- AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim(
- listing.sellerUUID, listing.sellerName, listing.id, listing.item);
- addClaimToPlayer(listing.sellerUUID, itemClaim);
-
- logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice,
- "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : ""));
- }
-
- // Remove from seller's active listings
- removeFromPlayerListings(listing.sellerUUID, listing.id);
-
- listing.status = EnumAuctionStatus.CANCELLED;
- markDirty();
-
return null; // Success
}
@@ -376,75 +429,85 @@ public class AuctionController implements IAuctionHandler {
return "Listing not found.";
}
- // Check status and expiry
- if (!listing.status.canBid()) {
- return "This auction has ended.";
+ synchronized (listing) {
+ // Check status and expiry
+ if (!listing.status.canBid()) {
+ return "This auction has ended.";
+ }
+
+ if (listing.isExpired()) {
+ return "This auction has ended.";
+ }
+
+ // CRITICAL: Prevent bidding on own items
+ if (listing.isSeller(playerUUID)) {
+ return "You cannot bid on your own auction.";
+ }
+
+ // Prevent rebidding if already highest bidder
+ if (listing.isHighBidder(playerUUID)) {
+ return "You are already the highest bidder. Use buyout if you want to purchase immediately.";
+ }
+
+ // Check minimum bid
+ long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent);
+ if (bidAmount < minBid) {
+ return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + ".";
+ }
+
+ // Check currency
+ PlayerData playerData = PlayerData.get(player);
+ if (playerData == null) {
+ return "Could not access player data.";
+ }
+
+ PlayerTradeData currency = playerData.tradeData;
+
+ // Store previous bidder info
+ UUID previousBidder = listing.highBidderUUID;
+ long previousBid = listing.currentBid;
+
+ // Full bid amount charged since we don't allow rebidding
+ long amountToCharge = bidAmount;
+
+ if (!currency.canAfford(amountToCharge)) {
+ return "You cannot afford this bid.";
+ }
+
+ // Fire auction bid event (cancelable)
+ PlayerDataScript bidHandler = ScriptController.Instance.getPlayerScripts(player);
+ IPlayer iBidder = (IPlayer) NpcAPI.Instance().getIEntity(player);
+ AuctionEvent.BidEvent bidEvent = new AuctionEvent.BidEvent(iBidder, listing, bidAmount);
+ if (EventHooks.onAuctionBid(bidHandler, bidEvent)) {
+ return "Bid was cancelled.";
+ }
+
+ // Deduct bid from new bidder FIRST
+ if (!currency.withdraw(amountToCharge)) {
+ return "Failed to deduct bid amount.";
+ }
+
+ // Refund previous bidder (include item for rebid option)
+ if (listing.hasBids() && previousBidder != null) {
+ AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
+ previousBidder, listing.highBidderName, listing.id, previousBid,
+ listing.item.getDisplayName(), playerName, listing.item);
+ addClaimToPlayer(previousBidder, refundClaim);
+
+ // Remove from previous bidder's active bids
+ removeFromPlayerBids(previousBidder, listing.id);
+
+ sendNotificationToPlayer(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id,
+ "You were outbid on " + listing.item.getDisplayName() + "!");
+ }
+
+ // Update listing
+ listing.highBidderUUID = playerUUID;
+ listing.highBidderName = playerName;
+ listing.currentBid = bidAmount;
+ listing.bidCount++;
}
- if (listing.isExpired()) {
- return "This auction has ended.";
- }
-
- // CRITICAL: Prevent bidding on own items
- if (listing.isSeller(playerUUID)) {
- return "You cannot bid on your own auction.";
- }
-
- // Prevent rebidding if already highest bidder
- if (listing.isHighBidder(playerUUID)) {
- return "You are already the highest bidder. Use buyout if you want to purchase immediately.";
- }
-
- // Check minimum bid
- long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent);
- if (bidAmount < minBid) {
- return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + ".";
- }
-
- // Check currency
- PlayerData playerData = PlayerData.get(player);
- if (playerData == null) {
- return "Could not access player data.";
- }
-
- PlayerTradeData currency = playerData.tradeData;
-
- // Store previous bidder info
- UUID previousBidder = listing.highBidderUUID;
- long previousBid = listing.currentBid;
-
- // Full bid amount charged since we don't allow rebidding
- long amountToCharge = bidAmount;
-
- if (!currency.canAfford(amountToCharge)) {
- return "You cannot afford this bid.";
- }
-
- // Deduct bid from new bidder FIRST
- if (!currency.withdraw(amountToCharge)) {
- return "Failed to deduct bid amount.";
- }
-
- // Refund previous bidder (include item for rebid option)
- if (listing.hasBids() && previousBidder != null) {
- AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
- previousBidder, listing.highBidderName, listing.id, previousBid,
- listing.item.getDisplayName(), playerName, listing.item);
- addClaimToPlayer(previousBidder, refundClaim);
-
- // Remove from previous bidder's active bids
- removeFromPlayerBids(previousBidder, listing.id);
-
- sendNotificationToPlayer(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id,
- "You were outbid on " + listing.item.getDisplayName() + "!");
- }
-
- // Update listing
- listing.highBidderUUID = playerUUID;
- listing.highBidderName = playerName;
- listing.currentBid = bidAmount;
- listing.bidCount++;
-
// Add to new bidder's active bids
addToPlayerBids(playerUUID, listing.id);
@@ -453,7 +516,7 @@ public class AuctionController implements IAuctionHandler {
markDirty();
- logAuction("BID", playerName, listing.item.getDisplayName(), bidAmount,
+ logAuction(EnumAuctionLogAction.BID, playerName, listing.item.getDisplayName(), bidAmount,
"Bids: " + listing.bidCount + ", Seller: " + listing.sellerName);
return null; // Success
@@ -472,97 +535,107 @@ public class AuctionController implements IAuctionHandler {
return "Listing not found.";
}
- // Check status and expiry
- if (!listing.status.canBid()) {
- return "This auction has ended.";
- }
-
- if (listing.isExpired()) {
- return "This auction has ended.";
- }
-
- if (!listing.hasBuyout()) {
- return "This auction does not have a buyout price.";
- }
-
- // CRITICAL: Prevent buying own items
- if (listing.isSeller(playerUUID)) {
- return "You cannot buy your own auction.";
- }
-
- // Check currency
- PlayerData playerData = PlayerData.get(player);
- if (playerData == null) {
- return "Could not access player data.";
- }
-
- PlayerTradeData currency = playerData.tradeData;
-
- // If buyer is already the high bidder, only charge the difference
- boolean isCurrentBidder = listing.isHighBidder(playerUUID);
- long amountToCharge = isCurrentBidder
- ? (listing.buyoutPrice - listing.currentBid)
- : listing.buyoutPrice;
-
- if (!currency.canAfford(amountToCharge)) {
- if (isCurrentBidder) {
- return "You cannot afford the remaining " + amountToCharge + " " + ConfigMarket.CurrencyName + " for buyout.";
+ synchronized (listing) {
+ // Check status and expiry
+ if (!listing.status.canBid()) {
+ return "This auction has ended.";
}
- return "You cannot afford the buyout price.";
+
+ if (listing.isExpired()) {
+ return "This auction has ended.";
+ }
+
+ if (!listing.hasBuyout()) {
+ return "This auction does not have a buyout price.";
+ }
+
+ // CRITICAL: Prevent buying own items
+ if (listing.isSeller(playerUUID)) {
+ return "You cannot buy your own auction.";
+ }
+
+ // Check currency
+ PlayerData playerData = PlayerData.get(player);
+ if (playerData == null) {
+ return "Could not access player data.";
+ }
+
+ PlayerTradeData currency = playerData.tradeData;
+
+ // If buyer is already the high bidder, only charge the difference
+ boolean isCurrentBidder = listing.isHighBidder(playerUUID);
+ long amountToCharge = isCurrentBidder
+ ? (listing.buyoutPrice - listing.currentBid)
+ : listing.buyoutPrice;
+
+ if (!currency.canAfford(amountToCharge)) {
+ if (isCurrentBidder) {
+ return "You cannot afford the remaining " + amountToCharge + " " + ConfigMarket.CurrencyName + " for buyout.";
+ }
+ return "You cannot afford the buyout price.";
+ }
+
+ // Fire auction buyout event (cancelable)
+ PlayerDataScript buyoutHandler = ScriptController.Instance.getPlayerScripts(player);
+ IPlayer iBuyer = (IPlayer) NpcAPI.Instance().getIEntity(player);
+ AuctionEvent.BuyoutEvent buyoutEvent = new AuctionEvent.BuyoutEvent(iBuyer, listing);
+ if (EventHooks.onAuctionBuyout(buyoutHandler, buyoutEvent)) {
+ return "Buyout was cancelled.";
+ }
+
+ // Deduct buyout amount
+ if (!currency.withdraw(amountToCharge)) {
+ return "Failed to deduct buyout amount.";
+ }
+
+ // Refund previous bidder if any AND they're not the buyer (no item - auction ended via buyout)
+ if (listing.hasBids() && listing.highBidderUUID != null && !isCurrentBidder) {
+ AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
+ listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid,
+ listing.item.getDisplayName(), playerName, null);
+ addClaimToPlayer(listing.highBidderUUID, refundClaim);
+
+ // Remove from previous bidder's active bids
+ removeFromPlayerBids(listing.highBidderUUID, listing.id);
+
+ sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id,
+ "The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded.");
+ } else if (isCurrentBidder) {
+ // Current bidder is buying out - just remove from their active bids
+ removeFromPlayerBids(playerUUID, listing.id);
+ }
+
+ // Create claims
+ AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item);
+ addClaimToPlayer(playerUUID, itemClaim);
+
+ long saleAmount = listing.buyoutPrice;
+ long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent);
+ long sellerReceives = saleAmount - tax;
+
+ AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim(
+ listing.sellerUUID, listing.sellerName, listing.id, sellerReceives,
+ listing.item.getDisplayName(), playerName);
+ addClaimToPlayer(listing.sellerUUID, currencyClaim);
+
+ // Notifications
+ sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
+ "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
+
+ // Remove from seller's active listings
+ removeFromPlayerListings(listing.sellerUUID, listing.id);
+
+ listing.status = EnumAuctionStatus.ENDED;
+ listing.highBidderUUID = playerUUID;
+ listing.highBidderName = playerName;
+ listing.currentBid = listing.buyoutPrice;
+
+ markDirty();
+
+ logAuction(EnumAuctionLogAction.BUYOUT, playerName, listing.item.getDisplayName(), listing.buyoutPrice,
+ "Seller: " + listing.sellerName + ", Tax: " + tax);
}
- // Deduct buyout amount
- if (!currency.withdraw(amountToCharge)) {
- return "Failed to deduct buyout amount.";
- }
-
- // Refund previous bidder if any AND they're not the buyer (no item - auction ended via buyout)
- if (listing.hasBids() && listing.highBidderUUID != null && !isCurrentBidder) {
- AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
- listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid,
- listing.item.getDisplayName(), playerName, null);
- addClaimToPlayer(listing.highBidderUUID, refundClaim);
-
- // Remove from previous bidder's active bids
- removeFromPlayerBids(listing.highBidderUUID, listing.id);
-
- sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id,
- "The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded.");
- } else if (isCurrentBidder) {
- // Current bidder is buying out - just remove from their active bids
- removeFromPlayerBids(playerUUID, listing.id);
- }
-
- // Create claims
- AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item);
- addClaimToPlayer(playerUUID, itemClaim);
-
- long saleAmount = listing.buyoutPrice;
- long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent);
- long sellerReceives = saleAmount - tax;
-
- AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim(
- listing.sellerUUID, listing.sellerName, listing.id, sellerReceives,
- listing.item.getDisplayName(), playerName);
- addClaimToPlayer(listing.sellerUUID, currencyClaim);
-
- // Notifications
- sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
- "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
-
- // Remove from seller's active listings
- removeFromPlayerListings(listing.sellerUUID, listing.id);
-
- listing.status = EnumAuctionStatus.ENDED;
- listing.highBidderUUID = playerUUID;
- listing.highBidderName = playerName;
- listing.currentBid = listing.buyoutPrice;
-
- markDirty();
-
- logAuction("BUYOUT", playerName, listing.item.getDisplayName(), listing.buyoutPrice,
- "Seller: " + listing.sellerName + ", Tax: " + tax);
-
return null; // Success
}
@@ -615,6 +688,14 @@ public class AuctionController implements IAuctionHandler {
return "Item data is missing.";
}
+ // Fire auction claim event (cancelable)
+ PlayerDataScript itemClaimHandler = ScriptController.Instance.getPlayerScripts(player);
+ IPlayer iItemClaimer = (IPlayer) NpcAPI.Instance().getIEntity(player);
+ AuctionEvent.ClaimEvent itemClaimEvent = new AuctionEvent.ClaimEvent(iItemClaimer, claim);
+ if (EventHooks.onAuctionClaim(itemClaimHandler, itemClaimEvent)) {
+ return "Claim was prevented.";
+ }
+
// Check inventory space
if (!player.inventory.addItemStackToInventory(claim.item.copy())) {
return "Your inventory is full.";
@@ -628,7 +709,7 @@ public class AuctionController implements IAuctionHandler {
// Cleanup listing if no more claims
cleanupListingIfComplete(claim.listingId);
- logAuction("CLAIMED", player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed");
+ logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed");
return null; // Success
}
@@ -652,6 +733,14 @@ public class AuctionController implements IAuctionHandler {
return "This is not a currency claim.";
}
+ // Fire auction claim event (cancelable)
+ PlayerDataScript currencyClaimHandler = ScriptController.Instance.getPlayerScripts(player);
+ IPlayer iCurrencyClaimer = (IPlayer) NpcAPI.Instance().getIEntity(player);
+ AuctionEvent.ClaimEvent currencyClaimEvent = new AuctionEvent.ClaimEvent(iCurrencyClaimer, claim);
+ if (EventHooks.onAuctionClaim(currencyClaimHandler, currencyClaimEvent)) {
+ return "Claim was prevented.";
+ }
+
if (!playerData.tradeData.deposit(claim.currency)) {
return "Failed to deposit currency.";
}
@@ -664,7 +753,7 @@ public class AuctionController implements IAuctionHandler {
// Cleanup listing if no more claims
cleanupListingIfComplete(claim.listingId);
- logAuction("CLAIMED", player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency,
+ logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency,
claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed");
return null; // Success
@@ -993,7 +1082,7 @@ public class AuctionController implements IAuctionHandler {
if (expired > 0) {
playerData.updateClient = true;
playerData.save();
- logAuction("CLAIM_EXPIRED", player.getCommandSenderName(), "Multiple", expired,
+ logAuction(EnumAuctionLogAction.CLAIM_EXPIRED, player.getCommandSenderName(), "Multiple", expired,
expired + " claims expired on login");
}
}
@@ -1041,52 +1130,28 @@ public class AuctionController implements IAuctionHandler {
// Logging
// =========================================
- private void logAuction(String action, String playerName, String itemName, long amount, String details) {
- if (!ConfigMarket.AuctionLoggingEnabled) return;
+ private void logAuction(EnumAuctionLogAction action, String playerName, String itemName, long amount, String details) {
+ if (!action.shouldLog()) return;
- boolean shouldLog = false;
- switch (action) {
- case "CREATED":
- shouldLog = ConfigMarket.LogAuctionCreated;
- break;
- case "BID":
- shouldLog = ConfigMarket.LogAuctionBid;
- break;
- case "BUYOUT":
- shouldLog = ConfigMarket.LogAuctionBuyout;
- break;
- case "SOLD":
- shouldLog = ConfigMarket.LogAuctionSold;
- break;
- case "EXPIRED":
- shouldLog = ConfigMarket.LogAuctionExpired;
- break;
- case "CANCELLED":
- shouldLog = ConfigMarket.LogAuctionCancelled;
- break;
- case "CLAIMED":
- case "CLAIM_EXPIRED":
- shouldLog = ConfigMarket.LogAuctionClaimed;
- break;
- }
-
- if (shouldLog) {
- String logMessage = String.format("[AUCTION:%s] Player: %s, Item: %s, Amount: %d, Details: %s",
- action, playerName, itemName, amount, details);
- LogWriter.info(logMessage);
- }
+ String logMessage = String.format("[AUCTION:%s] Player: %s, Item: %s, Amount: %d, Details: %s",
+ action.name(), playerName, itemName, amount, details);
+ LogWriter.info(logMessage);
}
// =========================================
// Save / Load - Async on CNPC+ Thread
// =========================================
- /** Mark data as dirty - will be saved on next save cycle */
+ /**
+ * Mark data as dirty - will be saved on next save cycle
+ */
private void markDirty() {
dirty.set(true);
}
- /** Save synchronously - used for server shutdown */
+ /**
+ * Save synchronously - used for server shutdown
+ */
public void save() {
if (!dirty.compareAndSet(true, false)) {
return; // Nothing to save
@@ -1094,7 +1159,9 @@ public class AuctionController implements IAuctionHandler {
saveInternal();
}
- /** Save asynchronously on CNPC+ thread */
+ /**
+ * Save asynchronously on CNPC+ thread
+ */
public void saveAsync() {
if (!dirty.compareAndSet(true, false)) {
return; // Nothing to save
@@ -1122,7 +1189,9 @@ public class AuctionController implements IAuctionHandler {
});
}
- /** Internal synchronous save */
+ /**
+ * Internal synchronous save
+ */
private void saveInternal() {
try {
NBTTagCompound compound = writeToNBT(new NBTTagCompound());
@@ -1141,7 +1210,9 @@ public class AuctionController implements IAuctionHandler {
File fileOld = new File(saveDir, "auction.dat_old");
File fileCurrent = new File(saveDir, "auction.dat");
- CompressedStreamTools.writeCompressed(compound, new FileOutputStream(fileNew));
+ try (FileOutputStream fos = new FileOutputStream(fileNew)) {
+ CompressedStreamTools.writeCompressed(compound, fos);
+ }
// Backup rotation
if (fileOld.exists()) fileOld.delete();
@@ -1189,7 +1260,9 @@ public class AuctionController implements IAuctionHandler {
}
}
- /** Rebuild all indices from loaded data */
+ /**
+ * Rebuild all indices from loaded data
+ */
private void rebuildIndices() {
playerListingIds.clear();
playerBidIds.clear();
@@ -1210,7 +1283,10 @@ public class AuctionController implements IAuctionHandler {
}
private void loadFromFile(File file) throws Exception {
- NBTTagCompound compound = CompressedStreamTools.readCompressed(new FileInputStream(file));
+ NBTTagCompound compound;
+ try (FileInputStream fis = new FileInputStream(file)) {
+ compound = CompressedStreamTools.readCompressed(fis);
+ }
readFromNBT(compound);
}
@@ -1398,15 +1474,20 @@ public class AuctionController implements IAuctionHandler {
}
public void adminCancelListing(String listingId, EntityPlayer admin, String reason) {
+ AuctionListing listing = listings.get(listingId);
+ String itemName = listing != null && listing.item != null ? listing.item.getDisplayName() : "Unknown";
cancelListing(listingId, admin, true);
+ if (reason != null && !reason.isEmpty()) {
+ logAuction(EnumAuctionLogAction.CANCELLED, admin.getCommandSenderName(), itemName, 0, "Admin cancel - Reason: " + reason);
+ }
}
public void clearEndedListings() {
// Remove ended/cancelled listings (claims are in PlayerData now)
listings.entrySet().removeIf(entry ->
entry.getValue().status == EnumAuctionStatus.CLAIMED ||
- entry.getValue().status == EnumAuctionStatus.ENDED ||
- entry.getValue().status == EnumAuctionStatus.CANCELLED);
+ entry.getValue().status == EnumAuctionStatus.ENDED ||
+ entry.getValue().status == EnumAuctionStatus.CANCELLED);
// Rebuild indices
rebuildIndices();
diff --git a/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java b/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java
index 5a9d32ed3..9e322ea10 100644
--- a/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java
+++ b/src/main/java/noppes/npcs/controllers/ScriptConfigSync.java
@@ -5,7 +5,7 @@ import noppes.npcs.config.ConfigScript;
/**
* Server-side utility for syncing script configuration to clients.
- *
+ *
* This class is SERVER-SIDE ONLY. Client reads via ScriptClientConfig.
*/
public class ScriptConfigSync {
diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java
index 88a12e20d..dd8c18550 100644
--- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java
+++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java
@@ -2,11 +2,12 @@ package noppes.npcs.controllers;
import noppes.npcs.api.event.IAbilityEvent;
import noppes.npcs.api.event.IAnimationEvent;
-import noppes.npcs.api.event.IPlayerAbilityEvent;
+import noppes.npcs.api.event.IAuctionEvent;
import noppes.npcs.api.event.IBlockEvent;
import noppes.npcs.api.event.ICustomGuiEvent;
import noppes.npcs.api.event.ICustomNPCsEvent;
import noppes.npcs.api.event.IDialogEvent;
+import noppes.npcs.api.event.IEnergyBarrierEvent;
import noppes.npcs.api.event.IEnergyProjectileEvent;
import noppes.npcs.api.event.IFactionEvent;
import noppes.npcs.api.event.IForgeEvent;
@@ -14,6 +15,7 @@ import noppes.npcs.api.event.IItemEvent;
import noppes.npcs.api.event.ILinkedItemEvent;
import noppes.npcs.api.event.INpcEvent;
import noppes.npcs.api.event.IPartyEvent;
+import noppes.npcs.api.event.IPlayerAbilityEvent;
import noppes.npcs.api.event.IPlayerEvent;
import noppes.npcs.api.event.IProjectileEvent;
import noppes.npcs.api.event.IQuestEvent;
@@ -123,6 +125,12 @@ public class ScriptHookController implements IScriptHookHandler {
hook(NPC, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class);
hook(NPC, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class);
+ // Energy Barrier
+ hook(NPC, ENERGY_BARRIER_SPAWNED, IEnergyBarrierEvent.SpawnedEvent.class);
+ hook(NPC, ENERGY_BARRIER_TICK, IEnergyBarrierEvent.UpdateEvent.class);
+ hook(NPC, ENERGY_BARRIER_HIT, IEnergyBarrierEvent.HitEvent.class);
+ hook(NPC, ENERGY_BARRIER_DESTROYED, IEnergyBarrierEvent.DestroyedEvent.class);
+
// Animation
hook(NPC, ANIMATION_START, IAnimationEvent.Started.class);
hook(NPC, ANIMATION_END, IAnimationEvent.Ended.class);
@@ -230,6 +238,12 @@ public class ScriptHookController implements IScriptHookHandler {
hook(PLAYER, ENERGY_PROJECTILE_BLOCK_IMPACT, IEnergyProjectileEvent.BlockImpactEvent.class);
hook(PLAYER, ENERGY_PROJECTILE_EXPIRED, IEnergyProjectileEvent.ExpiredEvent.class);
+ // Energy Barrier
+ hook(PLAYER, ENERGY_BARRIER_SPAWNED, IEnergyBarrierEvent.SpawnedEvent.class);
+ hook(PLAYER, ENERGY_BARRIER_TICK, IEnergyBarrierEvent.UpdateEvent.class);
+ hook(PLAYER, ENERGY_BARRIER_HIT, IEnergyBarrierEvent.HitEvent.class);
+ hook(PLAYER, ENERGY_BARRIER_DESTROYED, IEnergyBarrierEvent.DestroyedEvent.class);
+
// Ability
hook(PLAYER, ABILITY_START, IPlayerAbilityEvent.StartEvent.class);
hook(PLAYER, ABILITY_EXECUTE, IPlayerAbilityEvent.ExecuteEvent.class);
@@ -238,6 +252,13 @@ public class ScriptHookController implements IScriptHookHandler {
hook(PLAYER, ABILITY_INTERRUPT, IPlayerAbilityEvent.InterruptEvent.class);
hook(PLAYER, ABILITY_COMPLETE, IPlayerAbilityEvent.CompleteEvent.class);
+ // Auction
+ hook(PLAYER, AUCTION_CREATE, IAuctionEvent.CreateEvent.class);
+ hook(PLAYER, AUCTION_BID, IAuctionEvent.BidEvent.class);
+ hook(PLAYER, AUCTION_BUYOUT, IAuctionEvent.BuyoutEvent.class);
+ hook(PLAYER, AUCTION_CANCEL, IAuctionEvent.CancelEvent.class);
+ hook(PLAYER, AUCTION_CLAIM, IAuctionEvent.ClaimEvent.class);
+
// Profile
hook(PLAYER, PROFILE_CHANGE, IPlayerEvent.ProfileEvent.Changed.class);
hook(PLAYER, PROFILE_REMOVE, IPlayerEvent.ProfileEvent.Removed.class);
diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java
index 75a1a714a..1f4d331a4 100644
--- a/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java
+++ b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java
@@ -10,15 +10,16 @@ import noppes.npcs.config.ConfigMarket;
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
public class AuctionBlacklist {
- private static Set blacklistedItems = new HashSet<>();
- private static Set