Merge branch 'dev' into syntax-highlighter-unified

This commit is contained in:
bigguy345 committed 2026-02-18 05:32:25 +02:00
commit 375c41daba
79 files changed
+5137 -1857

No files matched your search

+1 -1
Submodule src/api updated: efa8721d36...22bce4b2ac.
@@ -2,7 +2,6 @@ package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer;
import kamkeel.npcs.entity.EntityAbilityBeam;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.entity.Entity;
@@ -24,7 +23,7 @@ import java.util.List;
* Design inspired by LouisXIV's energy rendering system.
*/
@SideOnly(Side.CLIENT)
public class RenderAbilityBeam extends RenderAbilityProjectile {
public class RenderAbilityBeam extends RenderEnergyAbility {
@Override
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
@@ -117,7 +116,7 @@ public class RenderAbilityBeam extends RenderAbilityProjectile {
// Render lightning during charging if enabled
if (beam.hasLightningEffect()) {
renderAttachedLightning(beam, scale);
renderAttachedLightning(beam, 0.6f * 0.5f, scale);
}
GL11.glPushMatrix();
@@ -545,7 +544,7 @@ public class RenderAbilityBeam extends RenderAbilityProjectile {
// Render lightning BEFORE rotation so it crackles in all directions (only on head)
if (beam.hasLightningEffect()) {
renderAttachedLightning(beam, scale);
renderAttachedLightning(beam, 0.6f * 0.5f, scale);
}
// Rotation
@@ -579,31 +578,4 @@ public class RenderAbilityBeam extends RenderAbilityProjectile {
GL11.glPopMatrix();
}
/**
* Render fading lightning arcs attached to the beam head (in local space).
*/
private void renderAttachedLightning(EntityAbilityBeam beam, float headScale) {
AttachedLightningRenderer.LightningState state = getLightningState(beam);
float density = beam.getLightningDensity();
// Lightning radius extends outward from inner surface (innerScale = 0.6)
float innerRadius = 0.6f * headScale * 0.5f;
float radius = innerRadius + beam.getLightningRadius() * headScale;
int outerColor = beam.getOuterColor();
int innerColor = beam.getInnerColor();
int fadeTime = beam.getLightningFadeTime();
state.update(density, radius, outerColor, innerColor, fadeTime);
state.render();
}
/**
* Get or create the lightning state for an entity.
*/
private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityBeam beam) {
if (beam.lightningState == null) {
beam.lightningState = new AttachedLightningRenderer.LightningState();
}
return (AttachedLightningRenderer.LightningState) beam.lightningState;
}
}
@@ -2,7 +2,6 @@ package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer;
import kamkeel.npcs.entity.EntityAbilityDisc;
import net.minecraft.entity.Entity;
import org.lwjgl.opengl.GL11;
@@ -14,7 +13,7 @@ import org.lwjgl.opengl.GL11;
* Design inspired by LouisXIV's energy rendering system.
*/
@SideOnly(Side.CLIENT)
public class RenderAbilityDisc extends RenderAbilityProjectile {
public class RenderAbilityDisc extends RenderEnergyAbility {
@Override
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
@@ -37,7 +36,7 @@ public class RenderAbilityDisc extends RenderAbilityProjectile {
// Render lightning BEFORE rotation so it crackles in all directions
if (disc.hasLightningEffect()) {
renderAttachedLightning(disc, radius * scale);
renderAttachedLightning(disc, 0.6f * 0.5f, radius * scale);
}
GL11.glPushMatrix();
@@ -76,34 +75,6 @@ public class RenderAbilityDisc extends RenderAbilityProjectile {
restoreRenderState();
}
/**
* Render fading lightning arcs attached to the disc (in local space).
*/
private void renderAttachedLightning(EntityAbilityDisc disc, float discSize) {
AttachedLightningRenderer.LightningState state = getLightningState(disc);
float density = disc.getLightningDensity();
// Lightning radius extends outward from inner surface (innerScale = 0.6)
float innerRadius = 0.6f * discSize * 0.5f;
float radius = innerRadius + disc.getLightningRadius() * discSize;
int outerColor = disc.getOuterColor();
int innerColor = disc.getInnerColor();
int fadeTime = disc.getLightningFadeTime();
state.update(density, radius, outerColor, innerColor, fadeTime);
state.render();
}
/**
* Get or create the lightning state for an entity.
*/
private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityDisc disc) {
if (disc.lightningState == null) {
disc.lightningState = new AttachedLightningRenderer.LightningState();
}
return (AttachedLightningRenderer.LightningState) disc.lightningState;
}
/**
* Render a flat square (thin cube) centered at origin.
* The square lies flat on the XZ plane with thin Y height.
@@ -3,7 +3,6 @@ package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityAbilityLaser;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.entity.Entity;
@@ -15,7 +14,7 @@ import org.lwjgl.opengl.GL11;
* Design inspired by LouisXIV's energy rendering system.
*/
@SideOnly(Side.CLIENT)
public class RenderAbilityLaser extends RenderAbilityProjectile {
public class RenderAbilityLaser extends RenderEnergyAbility {
@Override
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
@@ -147,7 +146,7 @@ public class RenderAbilityLaser extends RenderAbilityProjectile {
// Render lightning during charging if enabled
if (laser.hasLightningEffect()) {
renderAttachedLightning(laser, scale);
renderAttachedLightning(laser, 0.6f * 0.5f, scale);
}
GL11.glPushMatrix();
@@ -170,7 +169,7 @@ public class RenderAbilityLaser extends RenderAbilityProjectile {
// Render inner core
GL11.glScalef(innerScale, innerScale, innerScale);
renderCube(innerColor, 1.0f, 0.5f);
renderCube(innerColor, laser.getInnerAlpha(), 0.5f);
GL11.glPopMatrix();
GL11.glPopMatrix();
@@ -215,31 +214,6 @@ public class RenderAbilityLaser extends RenderAbilityProjectile {
}
private void renderAttachedLightning(EntityAbilityLaser laser, float headScale) {
AttachedLightningRenderer.LightningState state = getLightningState(laser);
float density = laser.getLightningDensity();
// Lightning radius extends outward from inner surface (innerScale = 0.6)
float innerRadius = 0.6f * headScale * 0.5f;
float radius = innerRadius + laser.getLightningRadius() * headScale;
int outerColor = laser.getOuterColor();
int innerColor = laser.getInnerColor();
int fadeTime = laser.getLightningFadeTime();
state.update(density, radius, outerColor, innerColor, fadeTime);
state.render();
}
/**
* Get or create the main lightning state for the laser.
*/
private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityLaser laser) {
if (laser.lightningState == null) {
laser.lightningState = new AttachedLightningRenderer.LightningState();
}
return (AttachedLightningRenderer.LightningState) laser.lightningState;
}
/**
* Get or create the tip lightning state for the laser.
* Stored as a secondary object in lightningState2.
@@ -2,7 +2,6 @@ package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer;
import kamkeel.npcs.entity.EntityAbilityOrb;
import net.minecraft.entity.Entity;
import org.lwjgl.opengl.GL11;
@@ -12,7 +11,7 @@ import org.lwjgl.opengl.GL11;
* Minecraft-style blocky appearance using Tessellator.
*/
@SideOnly(Side.CLIENT)
public class RenderAbilityOrb extends RenderAbilityProjectile {
public class RenderAbilityOrb extends RenderEnergyAbility {
private static final int LIGHTNING_FADE_TICKS = 6;
@@ -35,7 +34,7 @@ public class RenderAbilityOrb extends RenderAbilityProjectile {
// Render lightning BEFORE rotation so it crackles in all directions
if (orb.hasLightningEffect()) {
renderAttachedLightning(orb, scale);
renderAttachedLightning(orb, 0.6f * 0.5f, scale);
}
// Use entity's interpolated rotation for smooth spinning
@@ -63,7 +62,7 @@ public class RenderAbilityOrb extends RenderAbilityProjectile {
// Render inner cube (solid)
GL11.glScalef(innerScale, innerScale, innerScale);
renderCube(orb.getInnerColor(), 1.0f, 0.5f);
renderCube(orb.getInnerColor(), orb.getInnerAlpha(), 0.5f);
GL11.glPopMatrix();
GL11.glPopMatrix();
@@ -71,36 +70,4 @@ public class RenderAbilityOrb extends RenderAbilityProjectile {
restoreRenderState();
}
/**
* Render fading lightning arcs attached to the orb (in local space).
* Arcs persist for a few ticks and fade out, while staying attached to the orb.
*/
private void renderAttachedLightning(EntityAbilityOrb orb, float orbScale) {
// Get or create the lightning state for this entity
AttachedLightningRenderer.LightningState state = getLightningState(orb);
float density = orb.getLightningDensity();
// Lightning radius extends outward from inner surface (innerScale = 0.6)
float innerRadius = 0.6f * orbScale * 0.5f; // Half size of inner cube
float radius = innerRadius + orb.getLightningRadius() * orbScale;
int outerColor = orb.getOuterColor();
int innerColor = orb.getInnerColor();
int fadeTime = orb.getLightningFadeTime();
// Update: age existing arcs, spawn new ones based on density
state.update(density, radius, outerColor, innerColor, fadeTime);
// Render all active arcs in local space (they move with the orb)
state.render();
}
/**
* Get or create the lightning state for an entity.
*/
private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityOrb orb) {
if (orb.lightningState == null) {
orb.lightningState = new AttachedLightningRenderer.LightningState();
}
return (AttachedLightningRenderer.LightningState) orb.lightningState;
}
}
@@ -2,6 +2,8 @@ package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer;
import kamkeel.npcs.entity.EntityEnergyAbility;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.entity.Render;
@@ -17,11 +19,11 @@ import org.lwjgl.opengl.GL12;
* Design inspired by LouisXIV's energy rendering system.
*/
@SideOnly(Side.CLIENT)
public abstract class RenderAbilityProjectile extends Render {
public abstract class RenderEnergyAbility extends Render {
protected static final ResourceLocation WHITE_TEXTURE = new ResourceLocation("customnpcs", "textures/entity/white.png");
public RenderAbilityProjectile() {
public RenderEnergyAbility() {
this.shadowSize = 0.0f;
}
@@ -252,6 +254,40 @@ public abstract class RenderAbilityProjectile extends Render {
tess.draw();
}
// ==================== SHARED LIGHTNING ====================
/**
* Get or create the lightning state for an energy entity.
* Uses the lightningState field on EntityEnergyAbility (stored as Object
* to avoid client class loading on server).
*/
protected AttachedLightningRenderer.LightningState getLightningState(EntityEnergyAbility entity) {
if (entity.lightningState == null) {
entity.lightningState = new AttachedLightningRenderer.LightningState();
}
return (AttachedLightningRenderer.LightningState) entity.lightningState;
}
/**
* Render attached lightning arcs around an 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
*/
protected void renderAttachedLightning(EntityEnergyAbility entity, float innerScale, float baseSize) {
AttachedLightningRenderer.LightningState state = getLightningState(entity);
float density = entity.getLightningDensity();
float innerRadius = innerScale * baseSize;
float radius = innerRadius + entity.getLightningRadius() * baseSize;
int outerColor = entity.getOuterColor();
int innerColor = entity.getInnerColor();
int fadeTime = entity.getLightningFadeTime();
state.update(density, radius, outerColor, innerColor, fadeTime);
state.render();
}
@Override
protected ResourceLocation getEntityTexture(Entity entity) {
return WHITE_TEXTURE;
@@ -0,0 +1,23 @@
package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.entity.EntityEnergyBarrier;
/**
* Intermediate renderer base for energy barrier entities (Dome, Panel).
* Provides shared hit flash alpha computation.
*/
@SideOnly(Side.CLIENT)
public abstract class RenderEnergyBarrier extends RenderEnergyAbility {
/**
* Compute the alpha boost from a hit flash effect.
* Flash value decays from 4 to 0, producing alpha from 0.3 to 0.
*/
protected float computeFlashAlpha(EntityEnergyBarrier barrier) {
byte flash = barrier.getHitFlash();
if (flash <= 0) return 0.0f;
return flash / 4.0f * 0.3f;
}
}
@@ -0,0 +1,112 @@
package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.entity.EntityEnergyDome;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.entity.Entity;
import org.lwjgl.opengl.GL11;
/**
* Renders the Dome entity as a translucent sphere.
* Inner layer solid, outer layer translucent glow.
* Optional skybox texture rendered on the interior surface.
*/
@SideOnly(Side.CLIENT)
public class RenderEnergyDome extends RenderEnergyBarrier {
@Override
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
EntityEnergyDome dome = (EntityEnergyDome) entity;
float radius = dome.getDomeRadius();
float healthPercent = dome.getHealthPercent();
setupRenderState();
GL11.glPushMatrix();
GL11.glTranslated(x, y, z);
// Lightning BEFORE any rotation (uniform spread)
if (dome.hasLightningEffect()) {
renderAttachedLightning(dome, 0.95f, radius);
}
// Hit flash effect
float flashAlpha = computeFlashAlpha(dome);
float innerScale = 0.95f;
// Render outer dome (translucent glow)
if (dome.isOuterColorEnabled()) {
float outerScale = 1.0f + dome.getOuterColorWidth() * 0.1f;
float outerAlpha = dome.getOuterColorAlpha() * healthPercent;
GL11.glDepthMask(false);
renderSphere(dome.getOuterColor(), outerAlpha + flashAlpha, radius * outerScale, 8);
GL11.glDepthMask(true);
}
// TODO: Skybox Feature
// if (dome.isSkyboxEnabled() && !dome.getSkyboxTexture().isEmpty()) {
// renderSkyboxSphere(dome.getSkyboxTexture(), radius, radius * 0.94f, 32, x, y, z);
// }
// Render inner dome
renderSphere(dome.getInnerColor(), dome.getInnerAlpha(), radius * innerScale, 8);
GL11.glPopMatrix();
restoreRenderState();
}
/**
* Render a sphere made of quad panels.
* Uses latitude/longitude segments to form a full sphere.
*/
private void renderSphere(int color, float alpha, float radius, int segments) {
float[] rgb = extractRGB(color);
Tessellator tess = Tessellator.instance;
tess.startDrawingQuads();
tess.setColorRGBA_F(rgb[0], rgb[1], rgb[2], alpha);
for (int lat = 0; lat < segments; lat++) {
float theta1 = (float) (Math.PI * lat / segments - Math.PI * 0.5);
float theta2 = (float) (Math.PI * (lat + 1) / segments - Math.PI * 0.5);
float y1 = (float) Math.sin(theta1) * radius;
float y2 = (float) Math.sin(theta2) * radius;
float r1 = (float) Math.cos(theta1) * radius;
float r2 = (float) Math.cos(theta2) * radius;
for (int lon = 0; lon < segments; lon++) {
float phi1 = (float) (2 * Math.PI * lon / segments);
float phi2 = (float) (2 * Math.PI * (lon + 1) / segments);
float x1 = (float) Math.cos(phi1) * r1;
float z1 = (float) Math.sin(phi1) * r1;
float x2 = (float) Math.cos(phi2) * r1;
float z2 = (float) Math.sin(phi2) * r1;
float x3 = (float) Math.cos(phi2) * r2;
float z3 = (float) Math.sin(phi2) * r2;
float x4 = (float) Math.cos(phi1) * r2;
float z4 = (float) Math.sin(phi1) * r2;
float nx = (x1 + x2 + x3 + x4) * 0.25f;
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; }
tess.setNormal(nx, ny, nz);
tess.addVertex(x1, y1, z1);
tess.addVertex(x2, y1, z2);
tess.addVertex(x3, y2, z3);
tess.addVertex(x4, y2, z4);
}
}
tess.draw();
}
// TODO: Skybox Feature — renderSkyboxSphere method removed
}
@@ -0,0 +1,132 @@
package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.entity.EntityEnergyPanel;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.entity.Entity;
import org.lwjgl.opengl.GL11;
/**
* Renders the Energy Panel entity as a flat translucent rectangle.
* Used by both Energy Wall and Energy Shield abilities.
* Voxel-blocky style with inner/outer layers.
*/
@SideOnly(Side.CLIENT)
public class RenderEnergyPanel extends RenderEnergyBarrier {
@Override
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
EntityEnergyPanel panel = (EntityEnergyPanel) entity;
float width = panel.getPanelData().panelWidth;
float height = panel.getPanelData().panelHeight;
float panelYaw = panel.getPanelYaw();
float healthPercent = panel.getHealthPercent();
setupRenderState();
GL11.glPushMatrix();
GL11.glTranslated(x, y, z);
// Lightning BEFORE rotation for uniform spread (like Orb pattern)
if (panel.hasLightningEffect()) {
renderAttachedLightning(panel, 0.9f, Math.max(width, height) * 0.5f);
}
// Rotate panel to face the correct direction
GL11.glRotatef(-panelYaw, 0.0f, 1.0f, 0.0f);
// Hit flash
float flashAlpha = computeFlashAlpha(panel);
// Subtle pulsing
float pulseTime = entity.ticksExisted + partialTicks;
float pulse = (float) Math.sin(pulseTime * 0.08f) * 0.02f;
float scale = 1.0f + pulse;
GL11.glPushMatrix();
GL11.glScalef(scale, scale, scale);
float innerScale = 0.9f;
float panelThickness = 0.1f;
// Render outer panel (translucent glow)
if (panel.isOuterColorEnabled()) {
float outerScale = 1.0f + panel.getOuterColorWidth() * 0.15f;
float outerAlpha = panel.getOuterColorAlpha() * healthPercent;
GL11.glDepthMask(false);
renderPanel(panel.getOuterColor(), outerAlpha + flashAlpha,
width * 0.5f * outerScale, height * 0.5f * outerScale, panelThickness * 1.5f);
GL11.glDepthMask(true);
}
// Render inner panel (uses configurable innerAlpha for semi-transparency)
renderPanel(panel.getInnerColor(), panel.getInnerAlpha(),
width * 0.5f * innerScale, height * 0.5f * innerScale, panelThickness);
GL11.glPopMatrix();
GL11.glPopMatrix();
restoreRenderState();
}
/**
* Render a flat panel (thin box) centered at origin.
* Panel faces along the Z axis (width on X, height on Y).
* All 6 faces batched into a single draw call.
*/
private void renderPanel(int color, float alpha, float halfWidth, float halfHeight, float halfThickness) {
float[] rgb = extractRGB(color);
float r = rgb[0], g = rgb[1], b = rgb[2];
Tessellator tess = Tessellator.instance;
tess.startDrawingQuads();
tess.setColorRGBA_F(r, g, b, alpha);
// Front face (z+)
tess.setNormal(0, 0, 1);
tess.addVertex(-halfWidth, -halfHeight, halfThickness);
tess.addVertex(halfWidth, -halfHeight, halfThickness);
tess.addVertex(halfWidth, halfHeight, halfThickness);
tess.addVertex(-halfWidth, halfHeight, halfThickness);
// Back face (z-)
tess.setNormal(0, 0, -1);
tess.addVertex(halfWidth, -halfHeight, -halfThickness);
tess.addVertex(-halfWidth, -halfHeight, -halfThickness);
tess.addVertex(-halfWidth, halfHeight, -halfThickness);
tess.addVertex(halfWidth, halfHeight, -halfThickness);
// Top edge (y+)
tess.setNormal(0, 1, 0);
tess.addVertex(-halfWidth, halfHeight, halfThickness);
tess.addVertex(halfWidth, halfHeight, halfThickness);
tess.addVertex(halfWidth, halfHeight, -halfThickness);
tess.addVertex(-halfWidth, halfHeight, -halfThickness);
// Bottom edge (y-)
tess.setNormal(0, -1, 0);
tess.addVertex(-halfWidth, -halfHeight, -halfThickness);
tess.addVertex(halfWidth, -halfHeight, -halfThickness);
tess.addVertex(halfWidth, -halfHeight, halfThickness);
tess.addVertex(-halfWidth, -halfHeight, halfThickness);
// Right edge (x+)
tess.setNormal(1, 0, 0);
tess.addVertex(halfWidth, -halfHeight, halfThickness);
tess.addVertex(halfWidth, -halfHeight, -halfThickness);
tess.addVertex(halfWidth, halfHeight, -halfThickness);
tess.addVertex(halfWidth, halfHeight, halfThickness);
// Left edge (x-)
tess.setNormal(-1, 0, 0);
tess.addVertex(-halfWidth, -halfHeight, -halfThickness);
tess.addVertex(-halfWidth, -halfHeight, halfThickness);
tess.addVertex(-halfWidth, halfHeight, halfThickness);
tess.addVertex(-halfWidth, halfHeight, -halfThickness);
tess.draw();
}
}
@@ -0,0 +1,125 @@
package kamkeel.npcs.client.renderer;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.entity.EntityEnergySlicer;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.entity.Entity;
import org.lwjgl.opengl.GL11;
/**
* Renders the Energy Slicer entity as a thin wide blade.
* Oriented perpendicular to travel direction.
* Inner solid core with outer translucent glow.
*/
@SideOnly(Side.CLIENT)
public class RenderEnergySlicer extends RenderEnergyAbility {
@Override
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
EntityEnergySlicer slicer = (EntityEnergySlicer) entity;
float width = slicer.getSliceWidth();
float thickness = slicer.getSliceThickness();
// During charging, width/thickness grow from 0.01 to target via updateCharging().
// Use current dimensions directly - no separate sizeRatio needed.
if (width <= 0.01f) return;
setupRenderState();
GL11.glPushMatrix();
GL11.glTranslated(x, y, z);
// Lightning BEFORE rotation for uniform spread (like Orb pattern)
if (slicer.hasLightningEffect()) {
renderAttachedLightning(slicer, 0.7f, width * 0.5f);
}
// Orient perpendicular to travel direction
float travelYaw = slicer.getTravelYaw();
float travelPitch = slicer.getTravelPitch();
GL11.glRotatef(travelYaw, 0.0f, 1.0f, 0.0f);
GL11.glRotatef(travelPitch, 1.0f, 0.0f, 0.0f);
float innerScale = 0.7f;
// Render outer blade (translucent glow)
if (slicer.isOuterColorEnabled()) {
float outerScale = 1.0f + slicer.getOuterColorWidth() * 0.2f;
GL11.glDepthMask(false);
renderBlade(slicer.getOuterColor(), slicer.getOuterColorAlpha(),
width * 0.5f * outerScale, thickness * 2.0f, 0.05f);
GL11.glDepthMask(true);
}
// Render inner blade (solid core, alpha 1.0 like Orb pattern)
renderBlade(slicer.getInnerColor(), 1.0f,
width * 0.5f * innerScale, thickness, 0.03f);
GL11.glPopMatrix();
restoreRenderState();
}
/**
* Render a thin wide blade shape.
* The blade extends along X (width), thin on Y (thickness), and very thin on Z (depth).
* All 6 faces batched into a single draw call.
*/
private void renderBlade(int color, float alpha, float halfWidth, float halfHeight, float halfDepth) {
float[] rgb = extractRGB(color);
float r = rgb[0], g = rgb[1], b = rgb[2];
Tessellator tess = Tessellator.instance;
tess.startDrawingQuads();
// Front face (z+)
tess.setColorRGBA_F(r, g, b, alpha);
tess.setNormal(0, 0, 1);
tess.addVertex(-halfWidth, -halfHeight, halfDepth);
tess.addVertex(halfWidth, -halfHeight, halfDepth);
tess.addVertex(halfWidth, halfHeight, halfDepth);
tess.addVertex(-halfWidth, halfHeight, halfDepth);
// Back face (z-)
tess.setNormal(0, 0, -1);
tess.addVertex(halfWidth, -halfHeight, -halfDepth);
tess.addVertex(-halfWidth, -halfHeight, -halfDepth);
tess.addVertex(-halfWidth, halfHeight, -halfDepth);
tess.addVertex(halfWidth, halfHeight, -halfDepth);
// Top edge (y+)
tess.setColorRGBA_F(r, g, b, alpha * 0.8f);
tess.setNormal(0, 1, 0);
tess.addVertex(-halfWidth, halfHeight, halfDepth);
tess.addVertex(halfWidth, halfHeight, halfDepth);
tess.addVertex(halfWidth, halfHeight, -halfDepth);
tess.addVertex(-halfWidth, halfHeight, -halfDepth);
// Bottom edge (y-)
tess.setNormal(0, -1, 0);
tess.addVertex(-halfWidth, -halfHeight, -halfDepth);
tess.addVertex(halfWidth, -halfHeight, -halfDepth);
tess.addVertex(halfWidth, -halfHeight, halfDepth);
tess.addVertex(-halfWidth, -halfHeight, halfDepth);
// Right edge (x+)
tess.setColorRGBA_F(r, g, b, alpha * 0.6f);
tess.setNormal(1, 0, 0);
tess.addVertex(halfWidth, -halfHeight, halfDepth);
tess.addVertex(halfWidth, -halfHeight, -halfDepth);
tess.addVertex(halfWidth, halfHeight, -halfDepth);
tess.addVertex(halfWidth, halfHeight, halfDepth);
// Left edge (x-)
tess.setNormal(-1, 0, 0);
tess.addVertex(-halfWidth, -halfHeight, -halfDepth);
tess.addVertex(-halfWidth, -halfHeight, halfDepth);
tess.addVertex(-halfWidth, halfHeight, halfDepth);
tess.addVertex(-halfWidth, halfHeight, -halfDepth);
tess.draw();
}
}
@@ -33,20 +33,19 @@ public class AbilityCommand extends CommandKamkeelBase {
desc = "List all custom abilities"
)
public void list(ICommandSender sender, String[] args) {
Set<String> uuids = AbilityController.Instance.getCustomAbilityIds();
if (uuids.isEmpty()) {
Set<String> names = AbilityController.Instance.getCustomAbilityNames();
if (names.isEmpty()) {
sendResult(sender, "No custom abilities found.");
return;
}
sendResult(sender, "Custom Abilities (" + uuids.size() + "):");
for (String uuid : uuids) {
Ability ability = AbilityController.Instance.getCustomAbility(uuid);
sendResult(sender, "Custom Abilities (" + names.size() + "):");
for (String name : names) {
Ability ability = AbilityController.Instance.getCustomAbility(name);
if (ability != null) {
String name = ability.getName() != null ? ability.getName() : uuid;
sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ") [" + uuid + "]");
sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ")");
} else {
sendResult(sender, " - \u00A7b" + uuid);
sendResult(sender, " - \u00A7b" + name);
}
}
}
@@ -82,23 +81,23 @@ public class AbilityCommand extends CommandKamkeelBase {
}
@SubCommand(
desc = "Delete a custom ability by UUID",
usage = "<uuid>"
desc = "Delete a custom ability by name",
usage = "<name>"
)
public void delete(ICommandSender sender, String[] args) {
if (args.length < 1) {
sendError(sender, "Usage: /kam ability delete <uuid>");
sendError(sender, "Usage: /kam ability delete <name>");
return;
}
String uuid = args[0];
if (!AbilityController.Instance.hasCustomAbility(uuid)) {
sendError(sender, "No custom ability with UUID: " + uuid);
String name = joinArgs(args, 0);
if (!AbilityController.Instance.hasCustomAbilityName(name)) {
sendError(sender, "No custom ability with name: " + name);
return;
}
AbilityController.Instance.deleteCustomAbility(uuid);
sendResult(sender, "Deleted ability: \u00A7b" + uuid);
AbilityController.Instance.deleteCustomAbility(name);
sendResult(sender, "Deleted ability: \u00A7b" + name);
}
@SubCommand(
@@ -189,7 +188,7 @@ public class AbilityCommand extends CommandKamkeelBase {
return;
}
// Use the ability's canonical ID for storage (registry key for built-in, UUID for custom)
// Use the ability's canonical ID for storage (registry key for built-in, name for custom)
String canonicalKey = ability.getId();
if (canonicalKey == null || canonicalKey.isEmpty()) {
canonicalKey = abilityKey; // Fallback to user-provided key
@@ -81,10 +81,12 @@ public abstract class Ability implements IAbility {
// Animations (global animation IDs, -1 = none)
protected int windUpAnimationId = -1; // Animation to play during wind up (user animations)
protected int activeAnimationId = -1; // Animation to play during active phase (user animations)
protected int dazedAnimationId = -1; // Animation to play during dazed phase (user animations)
// Built-in animation names (empty = none, takes priority over IDs if set)
protected String windUpAnimationName = ""; // Built-in animation name for wind up
protected String activeAnimationName = ""; // Built-in animation name for active phase
protected String dazedAnimationName = "Ability_Generic_Dazed"; // Built-in animation name for dazed phase
// Telegraph configuration
protected boolean showTelegraph = true;
@@ -101,7 +103,7 @@ public abstract class Ability implements IAbility {
protected boolean ignoreCooldown = false;
// Configurable potion effects
protected List<AbilityEffect> effects = new ArrayList<>();
protected List<AbilityPotionEffect> effects = new ArrayList<>();
// Burst
protected boolean burstEnabled = false;
@@ -376,7 +378,7 @@ public abstract class Ability implements IAbility {
if (entity == null || effects.isEmpty()) {
return;
}
for (AbilityEffect effect : effects) {
for (AbilityPotionEffect effect : effects) {
effect.apply(entity);
}
}
@@ -597,6 +599,10 @@ public abstract class Ability implements IAbility {
this::getActiveAnimationId, this::setActiveAnimationId,
this::getActiveAnimationName, this::setActiveAnimationName)
.tab("Effects"));
defs.add(FieldDef.animSubGui("ability.dazedAnimation",
this::getDazedAnimationId, this::setDazedAnimationId,
this::getDazedAnimationName, this::setDazedAnimationName)
.tab("Effects"));
TelegraphType tType = getTelegraphType();
if (tType != null && tType != TelegraphType.NONE) {
@@ -1133,8 +1139,10 @@ public abstract class Ability implements IAbility {
nbt.setString("activeSound", activeSound);
nbt.setInteger("windUpAnimationId", windUpAnimationId);
nbt.setInteger("activeAnimationId", activeAnimationId);
nbt.setInteger("dazedAnimationId", dazedAnimationId);
nbt.setString("windUpAnimationName", windUpAnimationName);
nbt.setString("activeAnimationName", activeAnimationName);
nbt.setString("dazedAnimationName", dazedAnimationName);
nbt.setBoolean("showTelegraph", showTelegraph);
nbt.setString("telegraphType", telegraphType.name());
nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset);
@@ -1156,7 +1164,7 @@ public abstract class Ability implements IAbility {
// Effects
NBTTagList effectList = new NBTTagList();
for (AbilityEffect effect : effects) {
for (AbilityPotionEffect effect : effects) {
effectList.appendTag(effect.writeNBT());
}
nbt.setTag("effects", effectList);
@@ -1192,8 +1200,10 @@ public abstract class Ability implements IAbility {
activeSound = nbt.getString("activeSound");
windUpAnimationId = nbt.getInteger("windUpAnimationId");
activeAnimationId = nbt.getInteger("activeAnimationId");
dazedAnimationId = nbt.getInteger("dazedAnimationId");
windUpAnimationName = nbt.getString("windUpAnimationName");
activeAnimationName = nbt.getString("activeAnimationName");
dazedAnimationName = nbt.getString("dazedAnimationName");
showTelegraph = nbt.getBoolean("showTelegraph");
try {
telegraphType = TelegraphType.valueOf(nbt.getString("telegraphType"));
@@ -1225,7 +1235,7 @@ public abstract class Ability implements IAbility {
if (nbt.hasKey("effects")) {
NBTTagList effectList = nbt.getTagList("effects", 10);
for (int i = 0; i < effectList.tagCount(); i++) {
AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i));
AbilityPotionEffect effect = AbilityPotionEffect.fromNBT(effectList.getCompoundTagAt(i));
if (effect != null && effect.isValid()) {
effects.add(effect);
}
@@ -1614,6 +1624,36 @@ public abstract class Ability implements IAbility {
this.activeAnimationName = activeAnimationName != null ? activeAnimationName : "";
}
public Animation getDazedAnimation() {
if (AnimationController.Instance == null) return null;
// Built-in animation by name takes priority
if (dazedAnimationName != null && !dazedAnimationName.isEmpty()) {
return (Animation) AnimationController.Instance.get(dazedAnimationName, true);
}
// Fall back to user animation by ID
if (dazedAnimationId >= 0) {
return (Animation) AnimationController.Instance.get(dazedAnimationId);
}
return null;
}
public int getDazedAnimationId() {
return dazedAnimationId;
}
public void setDazedAnimationId(int dazedAnimationId) {
this.dazedAnimationId = dazedAnimationId;
}
public String getDazedAnimationName() {
return dazedAnimationName;
}
public void setDazedAnimationName(String dazedAnimationName) {
this.dazedAnimationName = dazedAnimationName != null ? dazedAnimationName : "";
}
public boolean isShowTelegraph() {
return showTelegraph;
}
@@ -1687,15 +1727,15 @@ public abstract class Ability implements IAbility {
this.ignoreCooldown = ignoreCooldown;
}
public List<AbilityEffect> getEffects() {
public List<AbilityPotionEffect> getEffects() {
return effects;
}
public void setEffects(List<AbilityEffect> effects) {
public void setEffects(List<AbilityPotionEffect> effects) {
this.effects = effects != null ? effects : new ArrayList<>();
}
public void addEffect(AbilityEffect effect) {
public void addEffect(AbilityPotionEffect effect) {
if (effect != null && effect.isValid()) {
effects.add(effect);
}
@@ -2,6 +2,15 @@ package kamkeel.npcs.controllers.data.ability;
import kamkeel.npcs.controllers.data.ability.type.*;
import kamkeel.npcs.controllers.SyncController;
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;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityLaserShot;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityOrb;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityShield;
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.nbt.NBTTagCompound;
import noppes.npcs.CustomNpcs;
@@ -15,6 +24,7 @@ import java.io.File;
import java.util.*;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.Pattern;
public class AbilityController implements IAbilityHandler {
@@ -31,6 +41,11 @@ public class AbilityController implements IAbilityHandler {
private final List<IAbilityExtender> extenders = new ArrayList<>();
private final List<Predicate<EntityPlayer>> flightCheckers = new ArrayList<>();
// ── Legacy Migration ─────────────────────────────────────────────────────
private static final Pattern UUID_PATTERN =
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);
// ── Derived State ────────────────────────────────────────────────────────
private final Set<String> builtInTypeIds = new HashSet<>();
@@ -99,6 +114,11 @@ public class AbilityController implements IAbilityHandler {
registerType("cnpc:hazard", AbilityHazard::new);
registerType("cnpc:trap", AbilityTrap::new);
registerType("cnpc:dome", AbilityDome::new);
registerType("cnpc:wall", AbilityWall::new);
registerType("cnpc:shield", AbilityShield::new);
registerType("cnpc:slicer", AbilitySlicer::new);
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -142,6 +162,7 @@ public class AbilityController implements IAbilityHandler {
public void load() {
customAbilities.clear();
int migrated = 0;
File dir = getDir();
File[] files = dir.exists() ? dir.listFiles() : null;
@@ -150,14 +171,39 @@ public class AbilityController implements IAbilityHandler {
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
try {
String filename = file.getName();
String uuid = filename.substring(0, filename.length() - 5);
String key = filename.substring(0, filename.length() - 5);
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
Ability ability = fromNBT(nbt);
if (ability != null) {
ability.setId(uuid);
customAbilities.put(uuid, ability);
if (ability == null) continue;
// Legacy migration: UUID-named files get renamed to name-based
if (UUID_PATTERN.matcher(key).matches()) {
String name = ability.getName();
if (name == null || name.isEmpty()) {
name = key; // Fallback to UUID if no name stored
}
// Ensure unique name
while (hasCustomAbilityName(name) || nameFileExists(dir, name)) {
name = name + "_";
}
ability.setName(name);
// Rename file from UUID.json to name.json
File newFile = new File(dir, name + ".json");
if (file.renameTo(newFile)) {
LogWriter.info("Migrated ability file: " + key + ".json -> " + name + ".json");
} else {
LogWriter.error("Failed to rename ability file: " + key + ".json -> " + name + ".json");
}
key = name;
migrated++;
}
ability.setId(key);
customAbilities.put(key, ability);
} catch (Exception e) {
LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e);
}
@@ -165,21 +211,57 @@ public class AbilityController implements IAbilityHandler {
}
customAbilityRevision++;
if (migrated > 0) {
LogWriter.info("Migrated " + migrated + " UUID-named abilities to name-based files");
}
LogWriter.info("Loaded " + customAbilities.size() + " custom abilities");
}
private boolean nameFileExists(File dir, String name) {
return new File(dir, name + ".json").exists();
}
public boolean saveCustomAbility(Ability ability) {
if (ability == null) return false;
String uuid = ability.getId();
if (uuid == null || uuid.isEmpty()) {
uuid = UUID.randomUUID().toString();
ability.setId(uuid);
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();
if (isNew) {
while (hasCustomAbilityName(name)) {
name = name + "_";
}
ability.setName(name);
}
// 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, uuid + ".json_new");
File fileCurrent = new File(dir, uuid + ".json");
File fileNew = new File(dir, name + ".json_new");
File fileCurrent = new File(dir, name + ".json");
try {
NBTTagCompound nbt = ability.writeNBT();
@@ -193,52 +275,43 @@ public class AbilityController implements IAbilityHandler {
fileNew.delete();
}
customAbilities.put(uuid, ability);
customAbilities.put(name, ability);
customAbilityRevision++;
LogWriter.info("Saved custom ability: " + ability.getName() + " [" + uuid + "]");
LogWriter.info("Saved custom ability: " + name);
SyncController.syncAllCustomAbilities();
return true;
} catch (Exception e) {
LogWriter.error("Error saving custom ability: " + uuid, e);
LogWriter.error("Error saving custom ability: " + name, e);
return false;
}
}
public boolean deleteCustomAbility(String uuid) {
if (uuid == null || uuid.isEmpty()) return false;
public boolean deleteCustomAbility(String name) {
if (name == null || name.isEmpty()) return false;
Ability removed = customAbilities.remove(uuid);
Ability removed = customAbilities.remove(name);
if (removed == null) return false;
File dir = getDir();
File file = new File(dir, uuid + ".json");
File file = new File(dir, name + ".json");
if (file.exists()) {
file.delete();
}
customAbilityRevision++;
LogWriter.info("Deleted custom ability: " + uuid);
LogWriter.info("Deleted custom ability: " + name);
SyncController.syncAllCustomAbilities();
return true;
}
public Ability getCustomAbility(String uuid) {
return customAbilities.get(uuid);
public Ability getCustomAbility(String name) {
return customAbilities.get(name);
}
public boolean hasCustomAbility(String uuid) {
return customAbilities.containsKey(uuid);
}
public Set<String> getCustomAbilityIds() {
public Set<String> getCustomAbilityNames() {
return new LinkedHashSet<>(customAbilities.keySet());
}
public String getCustomAbilityName(String uuid) {
Ability a = customAbilities.get(uuid);
return a != null ? a.getName() : null;
}
public Map<String, Ability> getCustomAbilities() {
return customAbilities;
}
@@ -286,7 +359,7 @@ public class AbilityController implements IAbilityHandler {
}
}
// Custom: exact UUID
// Custom: exact name
Ability custom = customAbilities.get(key);
if (custom != null) return custom.deepCopy();
@@ -308,7 +381,7 @@ public class AbilityController implements IAbilityHandler {
keys.add(id);
}
}
// Use ability names for custom abilities (instead of UUIDs) for readable tab completion
// Custom abilities keyed by name
for (Ability ability : customAbilities.values()) {
String name = ability.getName();
if (name != null && !name.isEmpty()) {
@@ -328,7 +401,7 @@ public class AbilityController implements IAbilityHandler {
}
}
}
// Use ability names for custom abilities (instead of UUIDs) for readable tab completion
// Custom abilities keyed by name
for (Ability ability : customAbilities.values()) {
if (ability.getAllowedBy().allowsPlayer()) {
String name = ability.getName();
@@ -364,7 +437,7 @@ public class AbilityController implements IAbilityHandler {
if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) return true;
}
// Custom: exact UUID
// Custom: exact name
if (customAbilities.containsKey(key)) return true;
// Custom: case-insensitive name
@@ -497,6 +570,33 @@ public class AbilityController implements IAbilityHandler {
return abilityTypes.containsKey(typeId);
}
public boolean isAllowedByPlayer(String typeId) {
if (abilityTypes.containsKey(typeId)) {
Ability temp = abilityTypes.get(typeId).get();
return temp.getAllowedBy() == UserType.PLAYER_ONLY;
}
return false;
}
public boolean isAllowedByNPC(String typeId) {
if (abilityTypes.containsKey(typeId)) {
Ability temp = abilityTypes.get(typeId).get();
return temp.getAllowedBy() == UserType.NPC_ONLY;
}
return false;
}
public boolean isAllowedByBoth(String typeId) {
if (abilityTypes.containsKey(typeId)) {
Ability temp = abilityTypes.get(typeId).get();
return temp.getAllowedBy() == UserType.BOTH;
}
return false;
}
@Override
public String[] getAbilityNameArray() {
return builtAbilities.keySet().toArray(new String[0]);
@@ -508,17 +608,17 @@ public class AbilityController implements IAbilityHandler {
}
@Override
public String[] getCustomAbilityIdArray() {
public String[] getCustomAbilityNameArray() {
return customAbilities.keySet().toArray(new String[0]);
}
@Override
public boolean hasCustomAbilityId(String uuid) {
return customAbilities.containsKey(uuid);
public boolean hasCustomAbilityName(String name) {
return customAbilities.containsKey(name);
}
@Override
public boolean deleteCustomAbilityById(String uuid) {
return deleteCustomAbility(uuid);
public boolean deleteCustomAbilityByName(String name) {
return deleteCustomAbility(name);
}
}
@@ -9,17 +9,17 @@ import noppes.npcs.constants.EnumPotionType;
* Represents a configurable potion effect that can be applied by abilities.
* Uses the unified EnumPotionType enum shared across melee, ranged, and ability systems.
*/
public class AbilityEffect {
public class AbilityPotionEffect {
private EnumPotionType type = EnumPotionType.None;
private int manualPotionId = 0;
private int durationTicks = 60;
private int amplifier = 0; // 0-10
public AbilityEffect() {
public AbilityPotionEffect() {
}
public AbilityEffect(EnumPotionType type, int durationTicks, int amplifier) {
public AbilityPotionEffect(EnumPotionType type, int durationTicks, int amplifier) {
this.type = type;
this.durationTicks = Math.max(1, durationTicks);
this.amplifier = Math.max(0, Math.min(10, amplifier));
@@ -28,8 +28,8 @@ public class AbilityEffect {
/**
* Creates a copy of this effect.
*/
public AbilityEffect copy() {
AbilityEffect copy = new AbilityEffect(type, durationTicks, amplifier);
public AbilityPotionEffect copy() {
AbilityPotionEffect copy = new AbilityPotionEffect(type, durationTicks, amplifier);
copy.manualPotionId = this.manualPotionId;
return copy;
}
@@ -81,10 +81,10 @@ public class AbilityEffect {
}
/**
* Creates an AbilityEffect from NBT.
* Creates an AbilityPotionEffect from NBT.
*/
public static AbilityEffect fromNBT(NBTTagCompound nbt) {
AbilityEffect effect = new AbilityEffect();
public static AbilityPotionEffect fromNBT(NBTTagCompound nbt) {
AbilityPotionEffect effect = new AbilityPotionEffect();
effect.readNBT(nbt);
return effect;
}
@@ -33,7 +33,7 @@ public class AbilitySlot {
}
/**
* Create a slot that references a built-in ability (by name) or custom ability (by UUID).
* Create a slot that references a built-in ability (by name) or custom ability (by name).
*/
public static AbilitySlot reference(String referenceId) {
AbilitySlot slot = new AbilitySlot();
@@ -0,0 +1,159 @@
package kamkeel.npcs.controllers.data.ability.data;
import net.minecraft.nbt.NBTTagCompound;
import java.util.HashMap;
import java.util.Map;
/**
* Groups barrier properties shared by energy barrier abilities (Dome, Wall, Shield).
* Handles health/durability, duration, and per-projectile-type damage multipliers.
*/
public class EnergyBarrierData {
// Knockback target display keys
private static final String[] KNOCKBACK_TARGET_KEYS = {"gui.both", "gui.players", "gui.npcs"};
public float maxHealth = 100.0f;
public boolean useHealth = true;
public int durationTicks = 200;
public boolean useDuration = true;
public float defaultMultiplier = 1.0f;
// Knockback settings
public boolean knockbackEnabled = false;
public float knockbackStrength = 1.0f;
/** 0=Both, 1=Player only, 2=NPC only */
public int knockbackTarget = 0;
// Melee settings
public boolean meleeEnabled = false;
public float meleeDamageMultiplier = 1.0f;
/**
* Per-projectile-type damage multipliers.
* Key is the ability typeId (e.g., "ability.cnpc.orb", "ability.cnpc.beam").
* Value is the multiplier applied to damage dealt to this barrier.
*/
private Map<String, Float> damageMultipliers = new HashMap<>();
public EnergyBarrierData() {}
public EnergyBarrierData(float maxHealth, boolean useHealth, int durationTicks, boolean useDuration) {
this.maxHealth = maxHealth;
this.useHealth = useHealth;
this.durationTicks = durationTicks;
this.useDuration = useDuration;
}
// ==================== MULTIPLIER METHODS ====================
public float getMultiplier(String abilityTypeId) {
Float mult = damageMultipliers.get(abilityTypeId);
return mult != null ? mult : defaultMultiplier;
}
public void setMultiplier(String abilityTypeId, float multiplier) {
damageMultipliers.put(abilityTypeId, multiplier);
}
public void removeMultiplier(String abilityTypeId) {
damageMultipliers.remove(abilityTypeId);
}
public Map<String, Float> getDamageMultipliers() {
return damageMultipliers;
}
public void setDamageMultipliers(Map<String, Float> multipliers) {
this.damageMultipliers = multipliers != null ? multipliers : new HashMap<>();
}
// ==================== GETTERS & SETTERS ====================
public float getMaxHealth() { return maxHealth; }
public void setMaxHealth(float maxHealth) { this.maxHealth = Math.max(0, maxHealth); }
public boolean isUseHealth() { return useHealth; }
public void setUseHealth(boolean useHealth) { this.useHealth = useHealth; }
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 String getKnockbackTargetKey() {
return KNOCKBACK_TARGET_KEYS[Math.max(0, Math.min(knockbackTarget, 2))];
}
public void setKnockbackTargetFromKey(String key) {
for (int i = 0; i < KNOCKBACK_TARGET_KEYS.length; i++) {
if (KNOCKBACK_TARGET_KEYS[i].equals(key)) {
knockbackTarget = i;
return;
}
}
}
// ==================== NBT ====================
public void writeNBT(NBTTagCompound nbt) {
nbt.setFloat("barrierMaxHealth", maxHealth);
nbt.setBoolean("barrierUseHealth", useHealth);
nbt.setInteger("barrierDuration", durationTicks);
nbt.setBoolean("barrierUseDuration", useDuration);
nbt.setFloat("barrierDefaultMult", defaultMultiplier);
nbt.setBoolean("barrierKnockback", knockbackEnabled);
nbt.setFloat("barrierKnockbackStr", knockbackStrength);
nbt.setInteger("barrierKnockbackTarget", knockbackTarget);
nbt.setBoolean("barrierMelee", meleeEnabled);
nbt.setFloat("barrierMeleeMult", meleeDamageMultiplier);
NBTTagCompound multNbt = new NBTTagCompound();
for (Map.Entry<String, Float> entry : damageMultipliers.entrySet()) {
multNbt.setFloat(entry.getKey(), entry.getValue());
}
nbt.setTag("barrierMultipliers", multNbt);
}
public void readNBT(NBTTagCompound nbt) {
maxHealth = nbt.hasKey("barrierMaxHealth") ? nbt.getFloat("barrierMaxHealth") : 100.0f;
useHealth = !nbt.hasKey("barrierUseHealth") || nbt.getBoolean("barrierUseHealth");
durationTicks = nbt.hasKey("barrierDuration") ? nbt.getInteger("barrierDuration") : 200;
useDuration = !nbt.hasKey("barrierUseDuration") || nbt.getBoolean("barrierUseDuration");
defaultMultiplier = nbt.hasKey("barrierDefaultMult") ? nbt.getFloat("barrierDefaultMult") : 1.0f;
knockbackEnabled = nbt.hasKey("barrierKnockback") && nbt.getBoolean("barrierKnockback");
knockbackStrength = nbt.hasKey("barrierKnockbackStr") ? nbt.getFloat("barrierKnockbackStr") : 1.0f;
knockbackTarget = nbt.hasKey("barrierKnockbackTarget") ? nbt.getInteger("barrierKnockbackTarget") : 0;
meleeEnabled = nbt.hasKey("barrierMelee") && nbt.getBoolean("barrierMelee");
meleeDamageMultiplier = nbt.hasKey("barrierMeleeMult") ? nbt.getFloat("barrierMeleeMult") : 1.0f;
damageMultipliers.clear();
if (nbt.hasKey("barrierMultipliers")) {
NBTTagCompound multNbt = nbt.getCompoundTag("barrierMultipliers");
@SuppressWarnings("unchecked")
java.util.Set<String> keys = multNbt.func_150296_c();
for (String key : keys) {
damageMultipliers.put(key, multNbt.getFloat(key));
}
}
}
public EnergyBarrierData copy() {
EnergyBarrierData copy = new EnergyBarrierData(maxHealth, useHealth, durationTicks, useDuration);
copy.defaultMultiplier = defaultMultiplier;
copy.knockbackEnabled = knockbackEnabled;
copy.knockbackStrength = knockbackStrength;
copy.knockbackTarget = knockbackTarget;
copy.meleeEnabled = meleeEnabled;
copy.meleeDamageMultiplier = meleeDamageMultiplier;
copy.damageMultipliers = new HashMap<>(damageMultipliers);
return copy;
}
}
@@ -13,8 +13,13 @@ public class EnergyDisplayData implements IEnergyDisplayData {
public boolean outerColorEnabled = true;
public float outerColorWidth = 0.4f;
public float outerColorAlpha = 0.5f;
public float innerAlpha = 1.0f;
public float rotationSpeed = 4.0f;
// TODO: Skybox Feature
// public boolean skyboxEnabled = false;
// public String skyboxTexture = "";
public EnergyDisplayData() {}
public EnergyDisplayData(int innerColor, int outerColor) {
@@ -32,6 +37,12 @@ public class EnergyDisplayData implements IEnergyDisplayData {
this.rotationSpeed = rotationSpeed;
}
public EnergyDisplayData(int innerColor, int outerColor, boolean outerColorEnabled,
float outerColorWidth, float outerColorAlpha, float innerAlpha, float rotationSpeed) {
this(innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed);
this.innerAlpha = innerAlpha;
}
@Override
public int getInnerColor() {
return innerColor;
@@ -82,6 +93,16 @@ public class EnergyDisplayData implements IEnergyDisplayData {
this.outerColorAlpha = outerColorAlpha;
}
@Override
public float getInnerAlpha() {
return innerAlpha;
}
@Override
public void setInnerAlpha(float innerAlpha) {
this.innerAlpha = innerAlpha;
}
@Override
public float getRotationSpeed() {
return rotationSpeed;
@@ -98,7 +119,11 @@ public class EnergyDisplayData implements IEnergyDisplayData {
nbt.setBoolean("outerColorEnabled", outerColorEnabled);
nbt.setFloat("outerColorWidth", outerColorWidth);
nbt.setFloat("outerColorAlpha", outerColorAlpha);
nbt.setFloat("innerAlpha", innerAlpha);
nbt.setFloat("rotationSpeed", rotationSpeed);
// TODO: Skybox Feature
// nbt.setBoolean("skyboxEnabled", skyboxEnabled);
// nbt.setString("skyboxTexture", skyboxTexture);
}
public void readNBT(NBTTagCompound nbt) {
@@ -107,10 +132,18 @@ public class EnergyDisplayData implements IEnergyDisplayData {
outerColorEnabled = nbt.getBoolean("outerColorEnabled");
outerColorWidth = nbt.getFloat("outerColorWidth");
outerColorAlpha = nbt.getFloat("outerColorAlpha");
innerAlpha = nbt.hasKey("innerAlpha") ? nbt.getFloat("innerAlpha") : 1.0f;
rotationSpeed = nbt.getFloat("rotationSpeed");
// TODO: Skybox Feature
// skyboxEnabled = nbt.hasKey("skyboxEnabled") && nbt.getBoolean("skyboxEnabled");
// skyboxTexture = nbt.hasKey("skyboxTexture") ? nbt.getString("skyboxTexture") : "";
}
public EnergyDisplayData copy() {
return new EnergyDisplayData(innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed);
EnergyDisplayData copy = new EnergyDisplayData(innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, innerAlpha, rotationSpeed);
// TODO: Skybox Feature
// copy.skyboxEnabled = skyboxEnabled;
// copy.skyboxTexture = skyboxTexture;
return copy;
}
}
@@ -0,0 +1,81 @@
package kamkeel.npcs.controllers.data.ability.data;
import net.minecraft.nbt.NBTTagCompound;
/**
* Groups panel-specific properties for energy wall/shield abilities.
* Handles dimensions, positioning, and launch behavior.
*/
public class EnergyPanelData {
public float panelWidth = 3.0f;
public float panelHeight = 3.0f;
public float heightOffset = 0.0f;
// Launch properties (Wall only)
public boolean launching = false;
public float launchSpeed = 0.5f;
public float launchDamage = 8.0f;
public float launchKnockback = 2.0f;
public EnergyPanelData() {}
public EnergyPanelData(float panelWidth, float panelHeight, float heightOffset) {
this.panelWidth = panelWidth;
this.panelHeight = panelHeight;
this.heightOffset = heightOffset;
}
// ==================== GETTERS & SETTERS ====================
public float getPanelWidth() { return panelWidth; }
public void setPanelWidth(float panelWidth) { this.panelWidth = Math.max(0.5f, panelWidth); }
public float getPanelHeight() { return panelHeight; }
public void setPanelHeight(float panelHeight) { this.panelHeight = Math.max(0.5f, panelHeight); }
public float getHeightOffset() { return heightOffset; }
public void setHeightOffset(float heightOffset) { this.heightOffset = heightOffset; }
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 ====================
public void writeNBT(NBTTagCompound nbt) {
nbt.setFloat("panelWidth", panelWidth);
nbt.setFloat("panelHeight", panelHeight);
nbt.setFloat("panelHeightOffset", heightOffset);
nbt.setBoolean("panelLaunching", launching);
nbt.setFloat("panelLaunchSpeed", launchSpeed);
nbt.setFloat("panelLaunchDamage", launchDamage);
nbt.setFloat("panelLaunchKnockback", launchKnockback);
}
public void readNBT(NBTTagCompound nbt) {
panelWidth = nbt.hasKey("panelWidth") ? nbt.getFloat("panelWidth") : 3.0f;
panelHeight = nbt.hasKey("panelHeight") ? nbt.getFloat("panelHeight") : 3.0f;
heightOffset = nbt.hasKey("panelHeightOffset") ? nbt.getFloat("panelHeightOffset") : 0.0f;
launching = nbt.hasKey("panelLaunching") && nbt.getBoolean("panelLaunching");
launchSpeed = nbt.hasKey("panelLaunchSpeed") ? nbt.getFloat("panelLaunchSpeed") : 0.5f;
launchDamage = nbt.hasKey("panelLaunchDamage") ? nbt.getFloat("panelLaunchDamage") : 8.0f;
launchKnockback = nbt.hasKey("panelLaunchKnockback") ? nbt.getFloat("panelLaunchKnockback") : 2.0f;
}
public EnergyPanelData copy() {
EnergyPanelData copy = new EnergyPanelData(panelWidth, panelHeight, heightOffset);
copy.launching = launching;
copy.launchSpeed = launchSpeed;
copy.launchDamage = launchDamage;
copy.launchKnockback = launchKnockback;
return copy;
}
}
@@ -2,7 +2,7 @@ package kamkeel.npcs.controllers.data.ability.gui;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -55,14 +55,14 @@ public class AbilityFieldBuilder extends GuiFieldBuilder {
sw.addLabel(new GuiNpcLabel(labelId++, def.getLabel(), colLLabel, y + 2, 0xFFFF55));
y += 15;
List<AbilityEffect> effects = (List<AbilityEffect>) def.getValue();
List<AbilityPotionEffect> effects = (List<AbilityPotionEffect>) def.getValue();
if (effects == null) effects = new ArrayList<>();
String[] typeNames = EnumPotionType.getLangKeysNoNone();
String[] ampValues = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
for (int e = 0; e < effects.size() && e < 5; e++) {
AbilityEffect effect = effects.get(e);
AbilityPotionEffect effect = effects.get(e);
// Type selector button (index offset by -1 since None is excluded)
int typeIdx = effect.getType().ordinal() - 1;
@@ -144,7 +144,7 @@ public class AbilityFieldBuilder extends GuiFieldBuilder {
FieldDef def = buttonFieldMap.get(buttonId);
if (def == null) def = clearFieldMap.get(buttonId);
if (def != null && def.getType() == FieldType.EFFECTS_LIST) {
List<AbilityEffect> effects = (List<AbilityEffect>) def.getValue();
List<AbilityPotionEffect> effects = (List<AbilityPotionEffect>) def.getValue();
if (effects == null) return false;
int effectIdx = meta[0];
int action = meta[1];
@@ -167,7 +167,7 @@ public class AbilityFieldBuilder extends GuiFieldBuilder {
return true;
case 4: // Add
if (effects.size() < 5) {
effects.add(new AbilityEffect(EnumPotionType.Slowness, 60, 0));
effects.add(new AbilityPotionEffect(EnumPotionType.Slowness, 60, 0));
}
return true;
}
@@ -184,7 +184,7 @@ public class AbilityFieldBuilder extends GuiFieldBuilder {
if (meta != null) {
FieldDef def = textFieldMap.get(textFieldId);
if (def != null && def.getType() == FieldType.EFFECTS_LIST) {
List<AbilityEffect> effects = (List<AbilityEffect>) def.getValue();
List<AbilityPotionEffect> effects = (List<AbilityPotionEffect>) def.getValue();
if (effects != null && meta[0] < effects.size()) {
if (meta[1] == 1) { // duration field
effects.get(meta[0]).setDurationTicks(field.getInteger());
@@ -2,7 +2,7 @@ package kamkeel.npcs.controllers.data.ability.gui;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.client.gui.builder.FieldType;
@@ -18,10 +18,10 @@ import java.util.function.Supplier;
public class AbilityFieldDefs {
@SuppressWarnings("unchecked")
public static FieldDef effectsListField(String label, Supplier<List<AbilityEffect>> getter, Consumer<List<AbilityEffect>> setter) {
public static FieldDef effectsListField(String label, Supplier<List<AbilityPotionEffect>> getter, Consumer<List<AbilityPotionEffect>> setter) {
return FieldDef.custom(label, FieldType.EFFECTS_LIST,
() -> getter.get(),
v -> setter.accept((List<AbilityEffect>) v));
v -> setter.accept((List<AbilityPotionEffect>) v));
}
}
@@ -373,7 +373,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
FieldDef.soundSubGui("ability.counterSound", this::getCounterSound, this::setCounterSound)
.tab("Effects").visibleWhen(this::canCounter));
FieldDef.insertAfter(defs, "ability.activeAnimation",
FieldDef.insertAfter(defs, "ability.dazedAnimation",
FieldDef.animSubGui("ability.counterAnimation",
this::getCounterAnimationId, this::setCounterAnimationId,
this::getCounterAnimationName, this::setCounterAnimationName)
@@ -27,6 +27,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
private float knockback = 2.0f;
private float hitLength = 4.0f; // How far in front of the caster the hit reaches
private float hitWidth = 3.0f; // How wide to each side (total width = hitWidth * 2)
private int hitDelayTicks = 0;
private int activeDisplayTicks = 10; // How long active animation plays before completing
public AbilityHeavyHit() {
@@ -70,10 +71,24 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
if (caster.worldObj.isRemote && !isPreview()) {
signalCompletion();
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (isPreview()) {
if (tick >= (activeDisplayTicks + hitDelayTicks))
signalCompletion();
return;
}
if (!isPreview()) {
if (tick >= (activeDisplayTicks + hitDelayTicks)) {
signalCompletion();
return;
}
if (tick == hitDelayTicks) {
// Calculate forward and right vectors from caster yaw
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double forwardX = -Math.sin(yawRad);
@@ -118,12 +133,6 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (tick >= activeDisplayTicks)
signalCompletion();
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("damage", damage);
@@ -131,6 +140,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
nbt.setFloat("hitLength", hitLength);
nbt.setFloat("hitWidth", hitWidth);
nbt.setInteger("activeDisplayTicks", activeDisplayTicks);
nbt.setInteger("hitDelayTicks", hitDelayTicks);
}
@Override
@@ -139,7 +149,8 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
this.knockback = nbt.getFloat("knockback");
this.hitLength = nbt.getFloat("hitLength");
this.hitWidth = nbt.getFloat("hitWidth");
this.activeDisplayTicks = nbt.hasKey("activeDisplayTicks") ? nbt.getInteger("activeDisplayTicks") : 10;
this.hitDelayTicks = nbt.getInteger("hitDelayTicks");
this.activeDisplayTicks = nbt.getInteger("activeDisplayTicks");
}
// Getters & Setters
@@ -183,6 +194,14 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
this.activeDisplayTicks = activeDisplayTicks;
}
public int getHitDelayTicks() {
return hitDelayTicks;
}
public void setHitDelayTicks(int hitDelayTicks) {
this.hitDelayTicks = hitDelayTicks;
}
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
@@ -196,7 +215,11 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
FieldDef.floatField("ability.hitLength", this::getHitLength, this::setHitLength),
FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth)
),
FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200),
FieldDef.section("ability.section.timing"),
FieldDef.row(
FieldDef.intField("ability.hitDelayTicks", this::getHitDelayTicks, this::setHitDelayTicks).min(0),
FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200)
),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
@@ -1,6 +1,6 @@
package kamkeel.npcs.controllers.data.ability.type;
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.entity.EntityAbilityZone;
@@ -119,7 +119,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
particleMotion = 1; particleDir = "mc:mobSpell";
colorData.innerColor = 0x44DD44; colorData.outerColor = 0x116611;
windUpColor = 0x6044DD44; activeColor = 0xC044FF44;
effects.add(new AbilityEffect(EnumPotionType.Poison, 100, 0));
effects.add(new AbilityPotionEffect(EnumPotionType.Poison, 100, 0));
break;
case "EXPLOSIVE":
groundAlpha = 0.04f;
@@ -127,7 +127,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
particleMotion = 0; particleDir = "mc:smoke";
colorData.innerColor = 0xFF6611; colorData.outerColor = 0xCC2200;
windUpColor = 0x60FF6611; activeColor = 0xC0FF4400;
effects.add(new AbilityEffect(EnumPotionType.Fire, 60, 0));
effects.add(new AbilityPotionEffect(EnumPotionType.Fire, 60, 0));
break;
case "CURSED":
groundAlpha = 0.04f;
@@ -135,7 +135,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
particleMotion = 1; particleDir = "mc:portal";
colorData.innerColor = 0xAA44FF; colorData.outerColor = 0x6622BB;
windUpColor = 0x60AA44FF; activeColor = 0xC0CC66FF;
effects.add(new AbilityEffect(EnumPotionType.Weakness, 100, 0));
effects.add(new AbilityPotionEffect(EnumPotionType.Weakness, 100, 0));
break;
case "SHOCK":
groundAlpha = 0.04f;
@@ -143,7 +143,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
particleMotion = 2; particleDir = "mc:enchantmenttable";
colorData.innerColor = 0x4488FF; colorData.outerColor = 0x2244BB;
windUpColor = 0x604488FF; activeColor = 0xC066AAFF;
effects.add(new AbilityEffect(EnumPotionType.MiningFatigue, 80, 1));
effects.add(new AbilityPotionEffect(EnumPotionType.MiningFatigue, 80, 1));
break;
case "SNARE":
groundAlpha = 0.04f;
@@ -151,7 +151,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
particleMotion = 1; particleDir = "mc:snowshovel";
colorData.innerColor = 0x88CCFF; colorData.outerColor = 0x4488CC;
windUpColor = 0x6088CCFF; activeColor = 0xC0AADDFF;
effects.add(new AbilityEffect(EnumPotionType.Slowness, 100, 1));
effects.add(new AbilityPotionEffect(EnumPotionType.Slowness, 100, 1));
break;
case "HIDDEN":
default:
@@ -1,14 +1,13 @@
package kamkeel.npcs.controllers.data.ability.type;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.UserType;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityAbilityZone.AccentStyle;
import kamkeel.npcs.entity.EntityAbilityZone.ParticleMotion;
@@ -220,7 +219,7 @@ public abstract class AbilityZone extends Ability {
particleMotion = 1; particleGlow = true; particleDir = "mc:mobSpell";
colorData.innerColor = 0x44DD44; colorData.outerColor = 0x116611;
windUpColor = 0x6044DD44; activeColor = 0xC044FF44;
effects.add(new AbilityEffect(EnumPotionType.Poison, 100, 0));
effects.add(new AbilityPotionEffect(EnumPotionType.Poison, 100, 0));
break;
case "INFERNO":
groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3;
@@ -229,7 +228,7 @@ public abstract class AbilityZone extends Ability {
particleMotion = 0; particleGlow = true; particleDir = "mc:flame";
colorData.innerColor = 0xFF6611; colorData.outerColor = 0xCC2200;
windUpColor = 0x60FF6611; activeColor = 0xC0FF4400;
effects.add(new AbilityEffect(EnumPotionType.Fire, 60, 0));
effects.add(new AbilityPotionEffect(EnumPotionType.Fire, 60, 0));
break;
case "ARCANE":
groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1;
@@ -238,7 +237,7 @@ public abstract class AbilityZone extends Ability {
particleMotion = 1; particleGlow = true; particleDir = "mc:portal";
colorData.innerColor = 0xAA44FF; colorData.outerColor = 0x6622BB;
windUpColor = 0x60AA44FF; activeColor = 0xC0CC66FF;
effects.add(new AbilityEffect(EnumPotionType.Weakness, 100, 0));
effects.add(new AbilityPotionEffect(EnumPotionType.Weakness, 100, 0));
break;
case "ELECTRIC":
groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1;
@@ -247,7 +246,7 @@ public abstract class AbilityZone extends Ability {
particleMotion = 2; particleGlow = true; particleDir = "mc:enchantmenttable";
colorData.innerColor = 0x4488FF; colorData.outerColor = 0x2244BB;
windUpColor = 0x604488FF; activeColor = 0xC066AAFF;
effects.add(new AbilityEffect(EnumPotionType.MiningFatigue, 80, 1));
effects.add(new AbilityPotionEffect(EnumPotionType.MiningFatigue, 80, 1));
break;
case "FROST":
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
@@ -256,7 +255,7 @@ public abstract class AbilityZone extends Ability {
particleMotion = 1; particleGlow = true; particleDir = "mc:snowshovel";
colorData.innerColor = 0x88CCFF; colorData.outerColor = 0x4488CC;
windUpColor = 0x6088CCFF; activeColor = 0xC0AADDFF;
effects.add(new AbilityEffect(EnumPotionType.Slowness, 100, 1));
effects.add(new AbilityPotionEffect(EnumPotionType.Slowness, 100, 1));
break;
case "DEFAULT":
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
@@ -1,9 +1,8 @@
package kamkeel.npcs.controllers.data.ability.type;
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.AbilityVariant;
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;
@@ -25,7 +24,7 @@ import java.util.List;
* Energy Beam ability: A homing head with a trailing path.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
*/
public class AbilityBeam extends AbstractEnergyProjectileAbility<EntityAbilityBeam> implements IAbilityEnergyBeam {
public class AbilityBeam extends AbilityEnergyProjectile<EntityAbilityBeam> implements IAbilityEnergyBeam {
private float beamWidth = 0.4f;
private float headSize = 0.6f;
@@ -1,4 +1,4 @@
package kamkeel.npcs.controllers.data.ability.type;
package kamkeel.npcs.controllers.data.ability.type.energy;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@@ -23,7 +23,7 @@ import java.util.List;
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
* Has optional boomerang behavior to return to owner.
*/
public class AbilityDisc extends AbstractEnergyProjectileAbility<EntityAbilityDisc> implements IAbilityDisc {
public class AbilityDisc extends AbilityEnergyProjectile<EntityAbilityDisc> implements IAbilityDisc {
private float discRadius = 1.0f;
private float discThickness = 0.2f;
@@ -0,0 +1,130 @@
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.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.EnergyBarrierData;
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;
import java.util.Arrays;
import java.util.List;
/**
* Energy Dome ability: Spawns a spherical barrier centered on the caster.
* Blocks incoming energy projectiles with configurable damage multipliers.
* Duration and/or HP based.
*/
public class AbilityDome extends AbilityEnergyBarrier {
private float domeRadius = 5.0f;
private boolean followCaster = false;
public AbilityDome() {
super(
new EnergyDisplayData(0x44CCFF, 0x2288FF, true, 0.3f, 0.35f, 0.5f, 0.0f),
new EnergyBarrierData(100.0f, true, 200, true)
);
this.typeId = "ability.cnpc.dome";
this.name = "Dome";
this.targetingMode = TargetingMode.SELF;
this.cooldownTicks = 100;
this.windUpTicks = 30;
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.RING;
this.showTelegraph = true;
this.windUpAnimationName = "";
this.activeAnimationName = "";
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityEnergyBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target) {
EntityEnergyDome dome = new EntityEnergyDome(
caster.worldObj, caster,
caster.posX, caster.posY, caster.posZ,
domeRadius, displayData.copy(), lightningData.copy(), barrierData.copy()
);
dome.setFollowCaster(followCaster);
dome.setSourceAbility(this);
return dome;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.SELF};
}
@Override
public float getTelegraphRadius() {
return domeRadius;
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.small", a -> {
AbilityDome dome = (AbilityDome) a;
a.setName("Small Dome");
dome.setDomeRadius(3.0f);
dome.setBarrierMaxHealth(60.0f);
}),
new AbilityVariant("ability.variant.large", a -> {
AbilityDome dome = (AbilityDome) a;
a.setName("Large Dome");
dome.setDomeRadius(8.0f);
dome.setBarrierMaxHealth(200.0f);
dome.setBarrierDuration(300);
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeBarrierTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("domeRadius", domeRadius);
nbt.setBoolean("followCaster", followCaster);
}
@Override
protected void readBarrierTypeNBT(NBTTagCompound nbt) {
this.domeRadius = nbt.hasKey("domeRadius") ? nbt.getFloat("domeRadius") : 5.0f;
this.followCaster = nbt.hasKey("followCaster") && nbt.getBoolean("followCaster");
}
// ==================== GETTERS & SETTERS ====================
public float getDomeRadius() { return domeRadius; }
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 ====================
@SideOnly(Side.CLIENT)
@Override
protected void addBarrierTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.floatField("ability.domeRadius", this::getDomeRadius, this::setDomeRadius)
.range(1.0f, 30.0f));
defs.add(FieldDef.boolField("ability.followCaster", this::isFollowCaster, this::setFollowCaster)
.hover("ability.hover.followCaster"));
}
}
@@ -0,0 +1,134 @@
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.data.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.List;
/**
* Abstract base for all energy-based abilities (Projectile, Barrier, Sweeper, etc.).
* Provides shared visual data (colors, lightning) and corresponding API/GUI/NBT.
*/
public abstract class AbilityEnergy extends Ability {
protected EnergyDisplayData displayData;
protected EnergyLightningData lightningData;
protected AbilityEnergy(EnergyDisplayData displayData) {
this.displayData = displayData;
this.lightningData = new EnergyLightningData();
}
// ==================== DISPLAY DATA ====================
public int getInnerColor() { return displayData.innerColor; }
public void setInnerColor(int color) { displayData.innerColor = color; }
public float getInnerAlpha() { return displayData.innerAlpha; }
public void setInnerAlpha(float alpha) { displayData.innerAlpha = alpha; }
public int getOuterColor() { return displayData.outerColor; }
public void setOuterColor(int color) { displayData.outerColor = color; }
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 float getLightningDensity() { return lightningData.lightningDensity; }
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 ====================
protected void writeEnergyNBT(NBTTagCompound nbt) {
displayData.writeNBT(nbt);
lightningData.writeNBT(nbt);
}
protected void readEnergyNBT(NBTTagCompound nbt) {
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
}
// ==================== GUI HELPERS ====================
/**
* Add complete visual tab definitions (colors section + effects/lightning section).
* Subclasses can call this directly or use the individual methods for more control.
*/
@SideOnly(Side.CLIENT)
protected void addEnergyVisualDefinitions(List<FieldDef> defs) {
addEnergyColorDefinitions(defs);
addEnergyEffectDefinitions(defs);
}
/**
* Add the colors section to the visual tab.
*/
@SideOnly(Side.CLIENT)
protected void addEnergyColorDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor),
FieldDef.floatField("ability.innerAlpha", this::getInnerAlpha, this::setInnerAlpha).range(0, 1)
).tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
}
/**
* Add the effects section (lightning) to the visual tab.
* Override to insert additional fields before lightning (e.g., rotationSpeed).
*/
@SideOnly(Side.CLIENT)
protected void addEnergyEffectDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
addEnergyLightningDefinitions(defs);
}
/**
* Add just the lightning fields (no section header) to the visual tab.
*/
@SideOnly(Side.CLIENT)
protected void addEnergyLightningDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
}
}
@@ -0,0 +1,215 @@
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.data.EnergyBarrierData;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.List;
/**
* Abstract base for energy barrier abilities (Dome, Wall, Shield).
* Handles shared visual data, barrier configuration, and lifecycle management.
*/
public abstract class AbilityEnergyBarrier extends AbilityEnergy {
protected final EnergyBarrierData barrierData;
// Runtime entity tracking
protected transient EntityEnergyBarrier barrierEntity;
protected AbilityEnergyBarrier(EnergyDisplayData displayData, EnergyBarrierData barrierData) {
super(displayData);
this.barrierData = barrierData;
}
// ==================== ABSTRACT METHODS ====================
/**
* Create and spawn the barrier entity during execution.
*/
protected abstract EntityEnergyBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target);
/**
* Add type-specific GUI field definitions.
*/
@SideOnly(Side.CLIENT)
protected abstract void addBarrierTypeDefinitions(List<FieldDef> defs);
/**
* Write type-specific fields to NBT.
*/
protected abstract void writeBarrierTypeNBT(NBTTagCompound nbt);
/**
* Read type-specific fields from NBT.
*/
protected abstract void readBarrierTypeNBT(NBTTagCompound nbt);
// ==================== LIFECYCLE ====================
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean hasDamage() {
return false; // Barriers are defensive
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (caster.worldObj.isRemote) return;
if (tick == 1) {
barrierEntity = createBarrierEntity(caster, target);
if (barrierEntity != null) {
barrierEntity.setupCharging(getWindUpTicks());
spawnAbilityEntity(barrierEntity);
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
if (barrierEntity != null && !barrierEntity.isDead) {
// Barrier was spawned during windup — transition to active
barrierEntity.finishCharging();
} else {
// No windup or entity died — create fresh
barrierEntity = createBarrierEntity(caster, target);
if (barrierEntity != null) {
spawnAbilityEntity(barrierEntity);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (barrierEntity == null || barrierEntity.isDead) {
signalCompletion();
return;
}
// Duration tracked by entity itself, but check here too as a failsafe
if (barrierData.useDuration && tick >= barrierData.durationTicks) {
cleanup();
signalCompletion();
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
if (barrierEntity != null && !barrierEntity.isDead) {
barrierEntity.setDead();
}
barrierEntity = null;
}
// ==================== NBT ====================
@Override
public final void writeTypeNBT(NBTTagCompound nbt) {
writeBarrierTypeNBT(nbt);
writeEnergyNBT(nbt);
barrierData.writeNBT(nbt);
}
@Override
public final void readTypeNBT(NBTTagCompound nbt) {
readBarrierTypeNBT(nbt);
readEnergyNBT(nbt);
barrierData.readNBT(nbt);
}
// ==================== API GETTERS & SETTERS ====================
// Display and lightning data inherited from AbilityEnergy
// Barrier data
public float getBarrierMaxHealth() { return barrierData.maxHealth; }
public void setBarrierMaxHealth(float maxHealth) { barrierData.setMaxHealth(maxHealth); }
public boolean isUseHealth() { return barrierData.useHealth; }
public void setUseHealth(boolean useHealth) { barrierData.useHealth = useHealth; }
public int getBarrierDuration() { return barrierData.durationTicks; }
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); }
// 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; }
// ==================== GUI ====================
@SideOnly(Side.CLIENT)
@Override
public final void getAbilityDefinitions(List<FieldDef> defs) {
// Type-specific fields first
addBarrierTypeDefinitions(defs);
// Barrier section
defs.add(FieldDef.section("ability.section.barrier"));
defs.add(FieldDef.boolField("ability.useHealth", this::isUseHealth, this::setUseHealth));
defs.add(FieldDef.floatField("ability.maxHealth", this::getBarrierMaxHealth, this::setBarrierMaxHealth)
.visibleWhen(this::isUseHealth));
defs.add(FieldDef.boolField("ability.useDuration", this::isUseDuration, this::setUseDuration));
defs.add(FieldDef.intField("ability.duration", this::getBarrierDuration, this::setBarrierDuration)
.range(1, 6000).visibleWhen(this::isUseDuration));
defs.add(FieldDef.floatField("ability.defaultMultiplier", this::getDefaultMultiplier, this::setDefaultMultiplier));
// Knockback section
defs.add(FieldDef.section("ability.section.knockback"));
defs.add(FieldDef.boolField("ability.knockbackEnabled", this::isKnockbackEnabled, this::setKnockbackEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.knockbackStrength", this::getKnockbackStrength, this::setKnockbackStrength)
.range(0, 10).visibleWhen(this::isKnockbackEnabled),
FieldDef.stringEnumField("ability.knockbackTarget", EnergyBarrierData.getKnockbackTargetKeys(),
this::getKnockbackTargetKey, this::setKnockbackTargetKey)
.visibleWhen(this::isKnockbackEnabled)
));
// Melee section
defs.add(FieldDef.section("ability.section.melee"));
defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled));
defs.add(FieldDef.floatField("ability.meleeDamageMultiplier", this::getMeleeDamageMultiplier, this::setMeleeDamageMultiplier)
.range(0, 10).visibleWhen(this::isMeleeEnabled));
// Visual tab - colors + effects (from AbilityEnergy)
addEnergyVisualDefinitions(defs);
}
}
@@ -1,4 +1,4 @@
package kamkeel.npcs.controllers.data.ability.type;
package kamkeel.npcs.controllers.data.ability.type.energy;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@@ -6,11 +6,10 @@ 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.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityProjectile;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
@@ -30,8 +29,7 @@ import java.util.List;
*
* @param <E> The entity type this ability spawns
*/
public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityProjectile>
extends Ability implements IAbilityEnergyProjectile {
public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile> extends AbilityEnergy implements IAbilityEnergyProjectile {
protected static final int MAX_PROJECTILES = 8;
@@ -39,9 +37,7 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
protected int projectileCount = 1;
protected int fireDelay = 0;
// Shared visual data
protected EnergyDisplayData displayData;
protected EnergyLightningData lightningData;
// Trajectory data
protected EnergyTrajectoryData trajectoryData;
// Shared combat/movement data
@@ -57,17 +53,16 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
// ==================== CONSTRUCTOR ====================
protected AbstractEnergyProjectileAbility(
protected AbilityEnergyProjectile(
EnergyDisplayData displayData,
EnergyCombatData combatData,
EnergyHomingData homingData,
EnergyLifespanData lifespanData
) {
this.displayData = displayData;
super(displayData);
this.combatData = combatData;
this.homingData = homingData;
this.lifespanData = lifespanData;
this.lightningData = new EnergyLightningData();
this.trajectoryData = new EnergyTrajectoryData();
initProjectiles(1);
}
@@ -207,9 +202,13 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
/**
* Fire an entity if it's valid.
* Clears charging state first so the entity can move and check collisions.
*/
protected void fireEntitySafe(E entity, EntityLivingBase target) {
if (entity != null && !entity.isDead) {
if (entity.isCharging()) {
entity.setCharging(false);
}
fireEntity(entity, target);
}
}
@@ -266,6 +265,18 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
cleanup();
}
entities = createAllEntities(caster, target);
// If there's a fire delay, put unfired entities in charging state
// to prevent collision checks before they're actually fired.
// Entity[0] will be fired immediately below, so skip it.
if (fireDelay > 0 && projectileCount > 1) {
for (int i = 1; i < projectileCount; i++) {
if (entities[i] != null) {
entities[i].setCharging(true);
}
}
}
for (E entity : entities) {
spawnAbilityEntity(entity);
}
@@ -304,7 +315,8 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
if (entity == null || entity.isDead) continue;
// Check if entity was removed from world (chunk unload, etc.)
if (tick > 5 && entity.worldObj != null
// 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.setDead();
continue;
@@ -390,8 +402,7 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
nbt.setInteger("fireDelay", fireDelay);
// Shared data classes
displayData.writeNBT(nbt);
lightningData.writeNBT(nbt);
writeEnergyNBT(nbt);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
@@ -415,8 +426,7 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
this.fireDelay = nbt.getInteger("fireDelay");
// Shared data classes
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
readEnergyNBT(nbt);
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
@@ -474,37 +484,7 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float range) { homingData.homingRange = range; }
// Display data - primary colors
@Override public int getInnerColor() { return displayData.innerColor; }
@Override public void setInnerColor(int color) { displayData.innerColor = color; }
@Override public int getOuterColor() { return displayData.outerColor; }
@Override public void setOuterColor(int color) { displayData.outerColor = color; }
@Override public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
@Override public void setOuterColorEnabled(boolean enabled) { displayData.outerColorEnabled = enabled; }
@Override public float getOuterColorWidth() { return displayData.outerColorWidth; }
@Override public void setOuterColorWidth(float width) { displayData.outerColorWidth = width; }
@Override public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
@Override public void setOuterColorAlpha(float alpha) { displayData.outerColorAlpha = alpha; }
public float getRotationSpeed() { return displayData.rotationSpeed; }
public void setRotationSpeed(float speed) { displayData.rotationSpeed = speed; }
// Lightning data
@Override public boolean hasLightningEffect() { return lightningData.lightningEffect; }
@Override public void setLightningEffect(boolean enabled) { lightningData.lightningEffect = enabled; }
@Override public float getLightningDensity() { return lightningData.lightningDensity; }
@Override public void setLightningDensity(float density) { lightningData.lightningDensity = density; }
@Override public float getLightningRadius() { return lightningData.lightningRadius; }
@Override public void setLightningRadius(float radius) { lightningData.lightningRadius = radius; }
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
public void setLightningFadeTime(int fadeTime) { lightningData.lightningFadeTime = fadeTime; }
// Display and lightning data inherited from AbilityEnergy
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
@@ -557,33 +537,14 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
// Type-specific fields first
addTypeDefinitions(defs);
// Shared visual tab - primary colors
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
// Shared visual tab - colors + effects (from AbilityEnergy)
addEnergyColorDefinitions(defs);
// Shared visual tab - effects
// Effects section with projectile-specific rotationSpeed before lightning
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
addEnergyLightningDefinitions(defs);
// Shared visual tab - per-projectile sections
for (int i = 0; i < MAX_PROJECTILES; i++) {
@@ -1,4 +1,4 @@
package kamkeel.npcs.controllers.data.ability.type;
package kamkeel.npcs.controllers.data.ability.type.energy;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@@ -25,7 +25,7 @@ import java.util.List;
* Travels in a straight line from origin, damaging all entities it passes through.
* Single-projectile only (always projectileCount=1).
*/
public class AbilityLaserShot extends AbstractEnergyProjectileAbility<EntityAbilityLaser> implements IAbilityLaserShot {
public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser> implements IAbilityLaserShot {
private float laserWidth = 0.3f;
private float expansionSpeed = 3.0f;
@@ -73,7 +73,7 @@ public class AbilityLaserShot extends AbstractEnergyProjectileAbility<EntityAbil
/**
* LINE telegraph: positioned at caster, aimed toward target.
* Overrides the circle telegraph from AbstractEnergyProjectileAbility.
* Overrides the circle telegraph from AbilityEnergyProjectile.
*/
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
@@ -1,4 +1,4 @@
package kamkeel.npcs.controllers.data.ability.type;
package kamkeel.npcs.controllers.data.ability.type.energy;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@@ -22,7 +22,7 @@ import java.util.List;
* Orb ability: Spawns homing projectile sphere(s) that track target.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
*/
public class AbilityOrb extends AbstractEnergyProjectileAbility<EntityAbilityOrb> implements IAbilityOrb {
public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> implements IAbilityOrb {
private float orbSize = 1.0f;
@@ -0,0 +1,143 @@
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.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.UserType;
import kamkeel.npcs.controllers.data.ability.data.EnergyBarrierData;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
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;
import java.util.Arrays;
import java.util.List;
/**
* Energy Shield ability: Spawns a flat energy panel held in front of the caster.
* The shield follows the caster's position and rotates with their look direction.
* Cannot be placed — it's always held.
*/
public class AbilityShield extends AbilityEnergyBarrier {
private float shieldWidth = 2.5f;
private float shieldHeight = 2.5f;
public AbilityShield() {
super(
new EnergyDisplayData(0xFFDD44, 0xFFAA00, true, 0.3f, 0.4f, 0.5f, 0.0f),
new EnergyBarrierData(60.0f, true, 120, true)
);
this.typeId = "ability.cnpc.shield";
this.name = "Shield";
this.targetingMode = TargetingMode.SELF;
this.cooldownTicks = 60;
this.windUpTicks = 10;
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
this.windUpAnimationName = "";
this.activeAnimationName = "";
this.allowedBy = UserType.BOTH;
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityEnergyBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target) {
EnergyPanelData panelData = new EnergyPanelData(shieldWidth, shieldHeight, 0.0f);
float frontDist = 1.5f;
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double spawnX = caster.posX + (-Math.sin(yawRad) * frontDist);
double spawnY = caster.posY + (caster.height * 0.5f);
double spawnZ = caster.posZ + (Math.cos(yawRad) * frontDist);
EntityEnergyPanel panel = new EntityEnergyPanel(
caster.worldObj, caster,
spawnX, spawnY, spawnZ, caster.rotationYaw,
EntityEnergyPanel.PanelMode.HELD,
displayData.copy(), lightningData.copy(), barrierData.copy(), panelData
);
panel.setSourceAbility(this);
return panel;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.SELF};
}
@Override
public float getTelegraphRadius() {
return 0; // No telegraph for shield
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.small", a -> {
AbilityShield shield = (AbilityShield) a;
a.setName("Small Shield");
shield.setShieldWidth(1.5f);
shield.setShieldHeight(1.5f);
shield.setBarrierMaxHealth(40.0f);
}),
new AbilityVariant("ability.variant.large", a -> {
AbilityShield shield = (AbilityShield) a;
a.setName("Large Shield");
shield.setShieldWidth(4.0f);
shield.setShieldHeight(4.0f);
shield.setBarrierMaxHealth(120.0f);
shield.setBarrierDuration(200);
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeBarrierTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("shieldWidth", shieldWidth);
nbt.setFloat("shieldHeight", shieldHeight);
}
@Override
protected void readBarrierTypeNBT(NBTTagCompound nbt) {
this.shieldWidth = nbt.hasKey("shieldWidth") ? nbt.getFloat("shieldWidth") : 2.5f;
this.shieldHeight = nbt.hasKey("shieldHeight") ? nbt.getFloat("shieldHeight") : 2.5f;
}
// ==================== GETTERS & SETTERS ====================
public float getShieldWidth() { return shieldWidth; }
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 ====================
@SideOnly(Side.CLIENT)
@Override
protected void addBarrierTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.floatField("gui.width", this::getShieldWidth, this::setShieldWidth).range(0.5f, 10.0f),
FieldDef.floatField("gui.height", this::getShieldHeight, this::setShieldHeight).range(0.5f, 10.0f)
));
}
}
@@ -0,0 +1,176 @@
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.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.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityEnergySlicer;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.List;
/**
* Energy Slicer ability: Fires a thin, wide blade projectile in a straight line.
* JJK Cleave/Dismantle inspired. Piercing by default.
* Extends AbilityEnergyProjectile to leverage the existing projectile infrastructure.
*/
public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
private float sliceWidth = 3.0f;
private float sliceThickness = 0.15f;
private boolean piercing = true;
public AbilitySlicer() {
super(
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)
);
this.typeId = "ability.cnpc.slicer";
this.name = "Slicer";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 30.0f;
this.minRange = 3.0f;
this.cooldownTicks = 40;
this.windUpTicks = 15;
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpAnimationName = "";
this.activeAnimationName = "";
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityEnergySlicer createEntity(EntityLivingBase caster, EntityLivingBase target,
Vec3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityEnergySlicer(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
sliceWidth, sliceThickness,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
piercing
);
}
@Override
protected void fireEntity(EntityEnergySlicer slicer, EntityLivingBase target) {
if (isPreview()) {
slicer.startPreviewFiring();
} else {
slicer.startMoving(target);
}
}
@Override
protected void setupEntityCharging(EntityEnergySlicer slicer, ProjectileData projData, int index) {
slicer.setupSlicerCharging(projData.anchor, windUpTicks, sliceWidth, sliceThickness);
}
@Override
protected void setupEntityPreview(EntityEnergySlicer slicer, EntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
slicer.setupPreview(caster, sliceWidth, sliceThickness, resolved, lightningData, projData.anchor, windUpTicks);
}
@Override
protected EntityEnergySlicer[] createEntityArray(int size) {
return new EntityEnergySlicer[size];
}
@Override
protected float getProjectileTelegraphRadius() {
return sliceWidth * 0.75f;
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Slicer");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilitySlicer slicer = (AbilitySlicer) a;
a.setName("Double Slicer");
slicer.setProjectileCount(2);
slicer.setFireDelay(5);
a.setWindUpAnimationName("");
a.setActiveAnimationName("");
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeTypeSpecificNBT(NBTTagCompound nbt) {
nbt.setFloat("sliceWidth", sliceWidth);
nbt.setFloat("sliceThickness", sliceThickness);
nbt.setBoolean("piercing", piercing);
}
@Override
protected void readTypeSpecificNBT(NBTTagCompound nbt) {
this.sliceWidth = nbt.hasKey("sliceWidth") ? nbt.getFloat("sliceWidth") : 3.0f;
this.sliceThickness = nbt.hasKey("sliceThickness") ? nbt.getFloat("sliceThickness") : 0.15f;
this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing");
}
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getSlicerSpeed() { return homingData.speed; }
public void setSlicerSpeed(float speed) { homingData.speed = speed; }
public float getSliceWidth() { return sliceWidth; }
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 ====================
@SideOnly(Side.CLIENT)
@Override
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSlicerSpeed, this::setSlicerSpeed)
));
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
defs.add(FieldDef.section("ability.section.slicer"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.width", this::getSliceWidth, this::setSliceWidth).range(0.5f, 20.0f),
FieldDef.floatField("gui.thickness", this::getSliceThickness, this::setSliceThickness).range(0.05f, 2.0f)
));
defs.add(FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing)
.hover("ability.hover.piercing"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
}
}
@@ -1,12 +1,10 @@
package kamkeel.npcs.controllers.data.ability.type;
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.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import noppes.npcs.client.gui.builder.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
@@ -14,8 +12,8 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilitySweeper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.api.ability.type.IAbilitySweeper;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.List;
@@ -28,7 +26,7 @@ import java.util.List;
* All damage logic is handled by the EntityAbilitySweeper entity to ensure
* visual and damage hitbox alignment.
*/
public class AbilitySweeper extends Ability implements IAbilitySweeper {
public class AbilitySweeper extends AbilityEnergy implements IAbilitySweeper {
// Type-specific parameters
private float beamLength = 10.0f;
@@ -41,13 +39,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
private int numberOfRotations = 2; // How many full rotations before ability ends
private boolean lockOnTarget = false;
// Visual properties
private EnergyDisplayData colorData = new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.8f, 0.5f, 0.0f);
// Runtime state (transient)
private transient EntityAbilitySweeper activeEntity = null;
public AbilitySweeper() {
super(new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.8f, 0.5f, 0.0f));
this.typeId = "ability.cnpc.sweeper";
this.name = "Sweeper";
this.targetingMode = TargetingMode.AGGRO_TARGET;
@@ -83,7 +79,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
// Player: target is null — sweep rotates around caster's facing direction, lockOnTarget has no effect.
activeEntity = new EntityAbilitySweeper(caster.worldObj, caster, target,
beamLength, beamWidth, beamHeight,
colorData,
displayData,
sweepSpeed, numberOfRotations,
damage, damageInterval, piercing,
lockOnTarget);
@@ -93,9 +89,6 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
}
spawnAbilityEntity(activeEntity);
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
}
@Override
@@ -162,7 +155,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
nbt.setFloat("sweepSpeed", sweepSpeed);
nbt.setInteger("numberOfRotations", numberOfRotations);
nbt.setBoolean("lockOnTarget", lockOnTarget);
colorData.writeNBT(nbt);
writeEnergyNBT(nbt);
}
@Override
@@ -176,10 +169,10 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
this.sweepSpeed = nbt.getFloat("sweepSpeed");
this.numberOfRotations = nbt.getInteger("numberOfRotations");
this.lockOnTarget = nbt.getBoolean("lockOnTarget");
colorData.readNBT(nbt);
readEnergyNBT(nbt);
}
// Getters & Setters
// 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; }
@@ -198,14 +191,6 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
public void setNumberOfRotations(int numberOfRotations) { this.numberOfRotations = numberOfRotations; }
public boolean isLockOnTarget() { return lockOnTarget; }
public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; }
public int getInnerColor() { return colorData.innerColor; }
public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; }
public int getOuterColor() { return colorData.outerColor; }
public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; }
public float getOuterColorWidth() { return colorData.outerColorWidth; }
public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; }
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; }
@Override
public int getMaxPreviewDuration() {
@@ -216,7 +201,6 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval)
@@ -237,18 +221,10 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
FieldDef.boolField("ability.lockTarget", this::isLockOnTarget, this::setLockOnTarget)
.hover("ability.hover.lockTarget")
),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
// Visual tab
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled)
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
// Visual tab - colors + effects (from AbilityEnergy)
addEnergyVisualDefinitions(defs);
}
}
@@ -0,0 +1,189 @@
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.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.EnergyBarrierData;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
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;
import java.util.Arrays;
import java.util.List;
/**
* Energy Wall ability: Spawns a placeable flat energy barrier.
* Can float above ground (configurable height offset).
* Optional launching mode fires the wall forward with damage/knockback.
*/
public class AbilityWall extends AbilityEnergyBarrier {
private final EnergyPanelData panelData;
public AbilityWall() {
super(
new EnergyDisplayData(0x44FFCC, 0x22CCAA, true, 0.3f, 0.4f, 0.5f, 0.0f),
new EnergyBarrierData(80.0f, true, 160, true)
);
this.panelData = new EnergyPanelData(4.0f, 3.0f, 0.0f);
this.typeId = "ability.cnpc.wall";
this.name = "Wall";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.minRange = 2.0f;
this.cooldownTicks = 80;
this.windUpTicks = 20;
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpAnimationName = "";
this.activeAnimationName = "";
}
@Override
public boolean hasDamage() {
return panelData.launching; // Only has damage when launching
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityEnergyBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target) {
// Place wall between caster and target
double placeX, placeY, placeZ;
float yaw;
if (target != null) {
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist < 0.01) {
// Target at same position — fall through to look-direction placement
float yawRad = (float) Math.toRadians(caster.rotationYaw);
placeX = caster.posX + (-Math.sin(yawRad) * 3.0);
placeY = caster.posY;
placeZ = caster.posZ + (Math.cos(yawRad) * 3.0);
yaw = caster.rotationYaw;
} else {
double placeDist = Math.min(dist * 0.5, 5.0);
placeX = caster.posX + (dx / dist) * placeDist;
placeY = caster.posY;
placeZ = caster.posZ + (dz / dist) * placeDist;
yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI);
}
} else {
float yawRad = (float) Math.toRadians(caster.rotationYaw);
placeX = caster.posX + (-Math.sin(yawRad) * 3.0);
placeY = caster.posY;
placeZ = caster.posZ + (Math.cos(yawRad) * 3.0);
yaw = caster.rotationYaw;
}
EntityEnergyPanel.PanelMode mode = panelData.launching
? EntityEnergyPanel.PanelMode.LAUNCHED
: EntityEnergyPanel.PanelMode.PLACED;
EntityEnergyPanel panel = new EntityEnergyPanel(
caster.worldObj, caster,
placeX, placeY, placeZ, yaw, mode,
displayData.copy(), lightningData.copy(), barrierData.copy(), panelData.copy()
);
panel.setSourceAbility(this);
return panel;
}
@Override
public float getTelegraphRadius() {
return Math.max(panelData.panelWidth, panelData.panelHeight) * 0.5f;
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.static", a -> {
a.setName("Static Wall");
}),
new AbilityVariant("ability.variant.launched", a -> {
AbilityWall wall = (AbilityWall) a;
a.setName("Launched Wall");
wall.panelData.launching = true;
wall.panelData.launchSpeed = 0.6f;
wall.panelData.launchDamage = 10.0f;
wall.panelData.launchKnockback = 3.0f;
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeBarrierTypeNBT(NBTTagCompound nbt) {
panelData.writeNBT(nbt);
}
@Override
protected void readBarrierTypeNBT(NBTTagCompound nbt) {
panelData.readNBT(nbt);
}
// ==================== GETTERS & SETTERS ====================
public EnergyPanelData getPanelData() { return panelData; }
public float getPanelWidth() { return panelData.panelWidth; }
public void setPanelWidth(float width) { panelData.setPanelWidth(width); }
public float getPanelHeight() { return panelData.panelHeight; }
public void setPanelHeight(float height) { panelData.setPanelHeight(height); }
public float getHeightOffset() { return panelData.heightOffset; }
public void setHeightOffset(float offset) { panelData.heightOffset = offset; }
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 ====================
@SideOnly(Side.CLIENT)
@Override
protected void addBarrierTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.section("ability.section.panel"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.width", this::getPanelWidth, this::setPanelWidth).range(0.5f, 20.0f),
FieldDef.floatField("gui.height", this::getPanelHeight, this::setPanelHeight).range(0.5f, 20.0f)
));
defs.add(FieldDef.floatField("ability.heightOffset", this::getHeightOffset, this::setHeightOffset)
.min(Float.NEGATIVE_INFINITY));
defs.add(FieldDef.section("ability.section.launch"));
defs.add(FieldDef.boolField("gui.enabled", this::isLaunching, this::setLaunching)
.hover("ability.hover.launching"));
defs.add(FieldDef.row(
FieldDef.floatField("stats.speed", this::getLaunchSpeed, this::setLaunchSpeed)
.visibleWhen(this::isLaunching),
FieldDef.floatField("enchantment.damage", this::getLaunchDamage, this::setLaunchDamage)
.visibleWhen(this::isLaunching)
));
defs.add(FieldDef.floatField("ability.knockback", this::getLaunchKnockback, this::setLaunchKnockback)
.visibleWhen(this::isLaunching));
}
}
@@ -25,7 +25,7 @@ import java.util.List;
*
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityBeam extends EntityAbilityProjectile {
public class EntityAbilityBeam extends EntityEnergyProjectile {
// Beam shape properties
private float beamWidth = 0.3f;
@@ -19,7 +19,7 @@ import java.util.List;
*
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityDisc extends EntityAbilityProjectile {
public class EntityAbilityDisc extends EntityEnergyProjectile {
// Disc-specific movement propertie
@@ -21,7 +21,7 @@ import java.util.Set;
*
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityLaser extends EntityAbilityProjectile {
public class EntityAbilityLaser extends EntityEnergyProjectile {
// Laser-specific properties
private float laserWidth = 0.2f;
@@ -15,11 +15,11 @@ import java.util.List;
/**
* Orb projectile - spherical homing energy ball.
* Extends EntityAbilityProjectile for shared functionality.
* Extends EntityEnergyAbility for shared functionality.
*
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityOrb extends EntityAbilityProjectile {
public class EntityAbilityOrb extends EntityEnergyProjectile {
public EntityAbilityOrb(World world) {
super(world);
@@ -1,9 +1,7 @@
package kamkeel.npcs.entity;
import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
@@ -24,19 +22,16 @@ import java.util.Set;
* Sweeper ability entity - a rotating beam attached to the NPC.
* 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 Entity implements IEntityAdditionalSpawnData {
public class EntityAbilitySweeper extends EntityEnergyAbility {
// Beam properties
private float beamLength = 10.0f;
private float beamWidth = 0.3f; // Thin like beam trail
private float beamHeight = 0.8f;
private int innerColor = 0xFF6600;
private int outerColor = 0xFF0000;
private boolean outerColorEnabled = true;
private float outerColorWidth = 1.8f;
// Combat properties
private float damage = 5.0f;
@@ -52,18 +47,13 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
private float baseYaw = 0; // Starting yaw direction
private boolean lockOnTarget = false;
// Owner tracking
private int ownerEntityId = -1;
// Target tracking
private int targetEntityId = -1;
// Lifetime
private int maxTicks = 400;
private long deathWorldTime = -1;
// Preview mode
private boolean previewMode = false;
private EntityLivingBase previewOwner = null;
// Damage state
private transient int ticksSinceDamage = 0;
private transient Set<Integer> hitThisTick = new HashSet<>();
@@ -72,8 +62,6 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
super(world);
this.setSize(0.1f, 0.1f);
this.noClip = true;
this.isImmuneToFire = true;
this.ignoreFrustumCheck = true;
}
public EntityAbilitySweeper(World world, EntityLivingBase owner, EntityLivingBase target,
@@ -89,10 +77,7 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
this.beamLength = beamLength;
this.beamWidth = beamWidth;
this.beamHeight = beamHeight;
this.innerColor = displayData.innerColor;
this.outerColor = displayData.outerColor;
this.outerColorEnabled = displayData.outerColorEnabled;
this.outerColorWidth = displayData.outerColorWidth;
this.displayData = displayData;
this.sweepSpeed = sweepSpeed;
this.numberOfRotations = numberOfRotations;
this.damage = damage;
@@ -121,7 +106,7 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
@Override
protected void entityInit() {
// No DataWatcher needed
super.entityInit();
}
@Override
@@ -144,7 +129,7 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
return;
}
Entity owner = getOwner();
Entity owner = getOwnerEntity();
if (owner != null && owner.isDead) {
this.setDead();
return;
@@ -297,12 +282,6 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
this.previewOwner = owner;
}
private Entity getOwner() {
if (previewMode && previewOwner != null) return previewOwner;
if (ownerEntityId == -1) return null;
return worldObj.getEntityByID(ownerEntityId);
}
// ==================== RENDERING ====================
@Override
@@ -311,22 +290,6 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
return distance < 16384.0D; // 128 blocks squared
}
@Override
public boolean shouldRenderInPass(int pass) {
return pass == 1; // Translucent pass
}
@Override
public float getBrightness(float partialTicks) {
return 1.0f;
}
@Override
@SideOnly(Side.CLIENT)
public int getBrightnessForRender(float partialTicks) {
return 0xF000F0;
}
// ==================== GETTERS FOR RENDERER ====================
public float getBeamLength() {
@@ -341,22 +304,6 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
return beamHeight;
}
public int getInnerColor() {
return innerColor;
}
public int getOuterColor() {
return outerColor;
}
public boolean isOuterColorEnabled() {
return outerColorEnabled;
}
public float getOuterColorWidth() {
return outerColorWidth;
}
/**
* Get the full interpolated angle INCLUDING base yaw for rendering.
*/
@@ -382,18 +329,14 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
@Override
protected void readEntityFromNBT(NBTTagCompound nbt) {
readEnergyBaseNBT(nbt);
this.beamLength = nbt.getFloat("BeamLength");
this.beamWidth = nbt.getFloat("BeamWidth");
this.beamHeight = nbt.getFloat("BeamHeight");
this.innerColor = nbt.getInteger("InnerColor");
this.outerColor = nbt.getInteger("OuterColor");
this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled");
this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f;
this.sweepSpeed = nbt.getFloat("SweepSpeed");
this.numberOfRotations = nbt.getInteger("NumRotations");
this.completedRotations = nbt.getInteger("CompletedRotations");
this.maxTicks = nbt.getInteger("MaxTicks");
this.ownerEntityId = nbt.getInteger("OwnerId");
this.targetEntityId = nbt.getInteger("TargetId");
this.currentAngle = nbt.getFloat("CurrentAngle");
this.baseYaw = nbt.getFloat("BaseYaw");
@@ -406,18 +349,14 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
@Override
protected void writeEntityToNBT(NBTTagCompound nbt) {
writeEnergyBaseNBT(nbt);
nbt.setFloat("BeamLength", beamLength);
nbt.setFloat("BeamWidth", beamWidth);
nbt.setFloat("BeamHeight", beamHeight);
nbt.setInteger("InnerColor", innerColor);
nbt.setInteger("OuterColor", outerColor);
nbt.setBoolean("OuterColorEnabled", outerColorEnabled);
nbt.setFloat("OuterColorWidth", outerColorWidth);
nbt.setFloat("SweepSpeed", sweepSpeed);
nbt.setInteger("NumRotations", numberOfRotations);
nbt.setInteger("CompletedRotations", completedRotations);
nbt.setInteger("MaxTicks", maxTicks);
nbt.setInteger("OwnerId", ownerEntityId);
nbt.setInteger("TargetId", targetEntityId);
nbt.setFloat("CurrentAngle", currentAngle);
nbt.setFloat("BaseYaw", baseYaw);
@@ -427,53 +366,4 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
nbt.setBoolean("Piercing", piercing);
nbt.setBoolean("LockOnTarget", lockOnTarget);
}
// ==================== SPAWN DATA ====================
@Override
public void writeSpawnData(ByteBuf buffer) {
buffer.writeFloat(beamLength);
buffer.writeFloat(beamWidth);
buffer.writeFloat(beamHeight);
buffer.writeInt(innerColor);
buffer.writeInt(outerColor);
buffer.writeBoolean(outerColorEnabled);
buffer.writeFloat(outerColorWidth);
buffer.writeFloat(sweepSpeed);
buffer.writeInt(numberOfRotations);
buffer.writeInt(completedRotations);
buffer.writeInt(maxTicks);
buffer.writeInt(ownerEntityId);
buffer.writeInt(targetEntityId);
buffer.writeFloat(currentAngle);
buffer.writeFloat(baseYaw);
buffer.writeFloat(damage);
buffer.writeInt(damageInterval);
buffer.writeBoolean(piercing);
buffer.writeBoolean(lockOnTarget);
}
@Override
public void readSpawnData(ByteBuf buffer) {
this.beamLength = buffer.readFloat();
this.beamWidth = buffer.readFloat();
this.beamHeight = buffer.readFloat();
this.innerColor = buffer.readInt();
this.outerColor = buffer.readInt();
this.outerColorEnabled = buffer.readBoolean();
this.outerColorWidth = buffer.readFloat();
this.sweepSpeed = buffer.readFloat();
this.numberOfRotations = buffer.readInt();
this.completedRotations = buffer.readInt();
this.maxTicks = buffer.readInt();
this.ownerEntityId = buffer.readInt();
this.targetEntityId = buffer.readInt();
this.currentAngle = buffer.readFloat();
this.baseYaw = buffer.readFloat();
this.damage = buffer.readFloat();
this.damageInterval = buffer.readInt();
this.piercing = buffer.readBoolean();
this.lockOnTarget = buffer.readBoolean();
this.prevAngle = this.currentAngle;
}
}
@@ -6,7 +6,7 @@ import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
@@ -121,7 +121,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
private boolean particleGlow = true;
private long deathWorldTime = -1;
private List<AbilityEffect> effects = new ArrayList<>();
private List<AbilityPotionEffect> effects = new ArrayList<>();
// ═══════════════════════════════════════════════════════════════════
// TRAP-SPECIFIC PROPERTIES
@@ -194,7 +194,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
float particleDensity, float particleScale,
float animSpeed, float lightningDensity,
boolean visible,
List<AbilityEffect> effects) {
List<AbilityPotionEffect> effects) {
EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.TRAP, owner, x, y, z);
zone.shape = shape;
zone.radius = triggerRadius;
@@ -217,7 +217,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
zone.lightningDensity = lightningDensity;
zone.visible = visible;
if (effects != null) {
for (AbilityEffect e : effects) {
for (AbilityPotionEffect e : effects) {
zone.effects.add(e.copy());
}
}
@@ -236,7 +236,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
float zoneHeight,
float particleDensity, float particleScale,
float animSpeed, float lightningDensity,
List<AbilityEffect> effects) {
List<AbilityPotionEffect> effects) {
EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.HAZARD, owner, x, y, z);
zone.shape = shape;
zone.radius = radius;
@@ -255,7 +255,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
zone.animSpeed = animSpeed;
zone.lightningDensity = lightningDensity;
if (effects != null) {
for (AbilityEffect e : effects) {
for (AbilityPotionEffect e : effects) {
zone.effects.add(e.copy());
}
}
@@ -499,7 +499,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
private void applyEffects(EntityLivingBase entity) {
if (entity == null || effects.isEmpty()) return;
for (AbilityEffect effect : effects) {
for (AbilityPotionEffect effect : effects) {
effect.apply(entity);
}
}
@@ -668,7 +668,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
if (nbt.hasKey("Effects")) {
NBTTagList effectList = nbt.getTagList("Effects", 10);
for (int i = 0; i < effectList.tagCount(); i++) {
AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i));
AbilityPotionEffect effect = AbilityPotionEffect.fromNBT(effectList.getCompoundTagAt(i));
if (effect != null && effect.isValid()) {
effects.add(effect);
}
@@ -728,7 +728,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
// Effects
NBTTagList effectList = new NBTTagList();
for (AbilityEffect effect : effects) {
for (AbilityPotionEffect effect : effects) {
effectList.appendTag(effect.writeNBT());
}
nbt.setTag("Effects", effectList);
@@ -778,7 +778,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
// Effects
NBTTagCompound effectsNbt = new NBTTagCompound();
NBTTagList effectList = new NBTTagList();
for (AbilityEffect effect : effects) {
for (AbilityPotionEffect effect : effects) {
effectList.appendTag(effect.writeNBT());
}
effectsNbt.setTag("Effects", effectList);
@@ -832,7 +832,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
if (effectsNbt != null && effectsNbt.hasKey("Effects")) {
NBTTagList effectList = effectsNbt.getTagList("Effects", 10);
for (int i = 0; i < effectList.tagCount(); i++) {
AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i));
AbilityPotionEffect effect = AbilityPotionEffect.fromNBT(effectList.getCompoundTagAt(i));
if (effect != null && effect.isValid()) {
effects.add(effect);
}
@@ -0,0 +1,241 @@
package kamkeel.npcs.entity;
import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
/**
* Universal base class for all energy ability entities (Projectiles, Barriers, Sweeper).
* Provides shared visual data, owner tracking, charging state, and common entity properties.
*/
public abstract class EntityEnergyAbility extends Entity implements IEntityAdditionalSpawnData {
// ==================== VISUAL PROPERTIES ====================
protected EnergyDisplayData displayData = new EnergyDisplayData();
protected EnergyLightningData lightningData = new EnergyLightningData();
// Client-side lightning state (not saved to NBT)
@SideOnly(Side.CLIENT)
public transient Object lightningState; // Actually AttachedLightningRenderer.LightningState
// ==================== TRACKING ====================
protected int ownerEntityId = -1;
// ==================== STATE ====================
/** The ability that spawned this entity. Transient, not saved to NBT. */
protected transient Ability sourceAbility = null;
protected boolean previewMode = false;
protected EntityLivingBase previewOwner = null;
// ==================== CHARGING STATE ====================
protected boolean charging = false;
protected int chargeDuration = 0;
protected int chargeTick = 0;
protected static final int DW_CHARGING = 20;
public EntityEnergyAbility(World world) {
super(world);
this.isImmuneToFire = true;
this.ignoreFrustumCheck = true;
}
@Override
protected void entityInit() {
this.dataWatcher.addObject(DW_CHARGING, (byte) 0);
}
// ==================== CHARGING ====================
/**
* Check if entity is in charging state (synced via data watcher).
* In preview mode, uses local field since data watcher isn't synced.
*/
public boolean isCharging() {
if (previewMode) return this.charging;
return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1;
}
/**
* Set charging state (server only, synced to clients via data watcher).
*/
public void setCharging(boolean value) {
this.charging = value;
if (!worldObj.isRemote) {
this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0));
}
}
public float getChargeProgress() {
if (chargeDuration <= 0) return 1.0f;
return Math.min(1.0f, (float) chargeTick / chargeDuration);
}
public float getInterpolatedChargeProgress(float partialTicks) {
if (chargeDuration <= 0) return 1.0f;
float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration);
float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration);
return prevProgress + (currProgress - prevProgress) * partialTicks;
}
// ==================== OWNER ====================
/**
* Get the owner entity. In preview mode, returns the preview owner directly.
*/
public Entity getOwnerEntity() {
if (previewMode && previewOwner != null) {
return previewOwner;
}
if (ownerEntityId == -1) return null;
return worldObj.getEntityByID(ownerEntityId);
}
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; }
// ==================== PREVIEW MODE ====================
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 int getInnerColor() { return displayData.getInnerColor(); }
public void setInnerColor(int color) { displayData.setInnerColor(color); }
public float getInnerAlpha() { return displayData.getInnerAlpha(); }
public void setInnerAlpha(float alpha) { displayData.setInnerAlpha(alpha); }
public int getOuterColor() { return displayData.getOuterColor(); }
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 float getLightningDensity() { return lightningData.getLightningDensity(); }
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 ====================
@Override
public float getBrightness(float partialTicks) {
return 1.0f;
}
@Override
@SideOnly(Side.CLIENT)
public int getBrightnessForRender(float partialTicks) {
return 0xF000F0;
}
@Override
public boolean shouldRenderInPass(int pass) {
return pass == 1; // Translucent pass
}
// ==================== COLLISION SETTINGS ====================
@Override
public boolean canBePushed() {
return false;
}
@Override
protected boolean canTriggerWalking() {
return false;
}
@Override
public boolean isBurning() {
return false;
}
@SideOnly(Side.CLIENT)
public float getShadowSize() {
return 0.0f;
}
// ==================== NBT HELPERS ====================
/**
* Write shared energy base fields to NBT (owner, display, lightning).
* Call from subclass NBT writers before writing type-specific fields.
*/
protected void writeEnergyBaseNBT(NBTTagCompound nbt) {
nbt.setInteger("OwnerId", ownerEntityId);
displayData.writeNBT(nbt);
lightningData.writeNBT(nbt);
}
/**
* Read shared energy base fields from NBT (owner, display, lightning).
* Call from subclass NBT readers before reading type-specific fields.
*/
protected void readEnergyBaseNBT(NBTTagCompound nbt) {
this.ownerEntityId = nbt.getInteger("OwnerId");
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
}
// ==================== SPAWN DATA ====================
@Override
public void writeSpawnData(ByteBuf buffer) {
try {
NBTTagCompound compound = new NBTTagCompound();
this.writeEntityToNBT(compound);
cpw.mods.fml.common.network.ByteBufUtils.writeTag(buffer, compound);
} catch (Exception e) {
noppes.npcs.LogWriter.error("Error writing energy ability spawn data", e);
}
}
@Override
public void readSpawnData(ByteBuf buffer) {
try {
NBTTagCompound compound = cpw.mods.fml.common.network.ByteBufUtils.readTag(buffer);
if (compound != null) {
this.readEntityFromNBT(compound);
}
} catch (Exception e) {
noppes.npcs.LogWriter.error("Error reading energy ability spawn data", e);
}
}
}
@@ -0,0 +1,269 @@
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;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.EventHooks;
import noppes.npcs.entity.EntityNPCInterface;
/**
* Base class for energy barrier entities (Dome, Panel).
* Provides shared health, damage, hit flash, knockback target,
* and barrier lifecycle management.
* Extends EntityEnergyAbility for shared visual/owner/charging state.
*/
public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
// ==================== BARRIER PROPERTIES ====================
protected EnergyBarrierData barrierData = new EnergyBarrierData();
protected float currentHealth;
protected int ticksAlive = 0;
// ==================== DATA WATCHER INDICES ====================
protected static final int DW_HEALTH_PERCENT = 21;
protected static final int DW_HIT_FLASH = 22;
public EntityEnergyBarrier(World world) {
super(world);
this.noClip = true;
}
@Override
protected void entityInit() {
super.entityInit(); // DW_CHARGING = 20
this.dataWatcher.addObject(DW_HEALTH_PERCENT, 1.0f);
this.dataWatcher.addObject(DW_HIT_FLASH, (byte) 0);
}
// ==================== ABSTRACT METHODS ====================
/**
* Check if a projectile is incoming toward this barrier.
* Dome uses spherical check, Panel uses planar check.
*/
public abstract boolean isIncomingProjectile(EntityEnergyProjectile projectile);
/**
* Setup charging state. Called during windup phase.
*/
public abstract void setupCharging(int duration);
/**
* Finish charging — snap to full size and become active.
*/
public abstract void finishCharging();
/**
* Push entities away from the barrier surface.
*/
protected abstract void knockbackEntities();
// ==================== PROJECTILE HIT ====================
/**
* Apply damage to this barrier from a projectile.
* Returns true if the barrier absorbed the hit (projectile should be destroyed).
*/
public boolean onProjectileHit(EntityEnergyProjectile projectile, float baseDamage) {
// Get damage multiplier for this projectile type
String typeId = "";
if (projectile.getSourceAbility() != null) {
typeId = projectile.getSourceAbility().getTypeId();
}
float multiplier = barrierData.getMultiplier(typeId);
float damage = baseDamage * multiplier;
// Fire hit event (may cancel or modify damage)
if (!worldObj.isRemote) {
float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, projectile, damage);
if (eventDamage < 0) return false; // Event cancelled — don't block
damage = eventDamage;
}
triggerHitFlash();
if (!barrierData.useHealth) {
return true; // Duration-only mode: block but don't take damage
}
currentHealth -= damage;
syncHealthPercent();
if (currentHealth <= 0) {
onBarrierDestroyed();
this.setDead();
}
return true;
}
// ==================== MELEE DAMAGE ====================
@Override
public boolean canBeCollidedWith() {
return barrierData.meleeEnabled;
}
@Override
public boolean attackEntityFrom(DamageSource source, float amount) {
if (worldObj.isRemote || !barrierData.meleeEnabled) return false;
if (isCharging()) return false;
float damage = amount * barrierData.meleeDamageMultiplier;
// Fire hit event with null projectile (melee hit)
float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, null, damage);
if (eventDamage < 0) return false;
damage = eventDamage;
triggerHitFlash();
if (!barrierData.useHealth) return true;
currentHealth -= damage;
syncHealthPercent();
if (currentHealth <= 0) {
onBarrierDestroyed();
this.setDead();
}
return true;
}
// ==================== SHARED UPDATE LOGIC ====================
/**
* 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() {
if (!worldObj.isRemote) {
// Fire spawned event on first tick
if (ticksAlive == 1) {
EventHooks.onEnergyBarrierSpawned(this, ownerEntityId);
}
// Check owner death
if (ownerEntityId >= 0 && ticksAlive > 5) {
Entity owner = worldObj.getEntityByID(ownerEntityId);
if (owner != null) {
if (owner.isDead) {
onBarrierDestroyed();
this.setDead();
return true;
}
if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) {
onBarrierDestroyed();
this.setDead();
return true;
}
}
}
// Duration check
if (barrierData.useDuration && ticksAlive >= barrierData.durationTicks) {
onBarrierDestroyed();
this.setDead();
return true;
}
// Reset hit flash
if (getHitFlash() > 0) {
setHitFlash((byte) (getHitFlash() - 1));
}
// Fire tick event
EventHooks.onEnergyBarrierTick(this, ownerEntityId);
}
return false;
}
// ==================== HELPERS ====================
protected void onBarrierDestroyed() {
EventHooks.onEnergyBarrierDestroyed(this, ownerEntityId);
}
protected void syncHealthPercent() {
float percent = barrierData.useHealth && barrierData.maxHealth > 0
? Math.max(0, currentHealth / barrierData.maxHealth)
: 1.0f;
if (!worldObj.isRemote) {
this.dataWatcher.updateObject(DW_HEALTH_PERCENT, percent);
}
}
protected void triggerHitFlash() {
if (!worldObj.isRemote) {
setHitFlash((byte) 4);
}
}
protected void setHitFlash(byte value) {
this.dataWatcher.updateObject(DW_HIT_FLASH, value);
}
public byte getHitFlash() {
return this.dataWatcher.getWatchableObjectByte(DW_HIT_FLASH);
}
public float getHealthPercent() {
return this.dataWatcher.getWatchableObjectFloat(DW_HEALTH_PERCENT);
}
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;
}
}
// ==================== 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; }
// ==================== NBT HELPERS ====================
/**
* Write shared barrier base fields to NBT.
* Call from subclass writeEntityToNBT() before writing type-specific fields.
*/
protected void writeBarrierBaseNBT(NBTTagCompound nbt) {
writeEnergyBaseNBT(nbt);
nbt.setInteger("TicksAlive", ticksAlive);
nbt.setFloat("CurrentHealth", currentHealth);
nbt.setBoolean("Charging", charging);
nbt.setInteger("ChargeTick", chargeTick);
nbt.setInteger("ChargeDuration", chargeDuration);
barrierData.writeNBT(nbt);
}
/**
* Read shared barrier base fields from NBT.
* Call from subclass readEntityFromNBT() before reading type-specific fields.
*/
protected void readBarrierBaseNBT(NBTTagCompound nbt) {
readEnergyBaseNBT(nbt);
this.ticksAlive = nbt.getInteger("TicksAlive");
this.currentHealth = nbt.getFloat("CurrentHealth");
this.charging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
this.chargeTick = nbt.getInteger("ChargeTick");
this.chargeDuration = nbt.getInteger("ChargeDuration");
this.dataWatcher.updateObject(DW_CHARGING, (byte) (charging ? 1 : 0));
barrierData.readNBT(nbt);
}
}
@@ -0,0 +1,220 @@
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.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.world.World;
import java.util.List;
/**
* Energy Dome entity - a spherical barrier that blocks incoming energy projectiles.
* Centered on the caster's position at time of casting.
* Only blocks incoming attacks (not outgoing from allies inside).
* Extends EntityEnergyBarrier for shared barrier logic.
*/
public class EntityEnergyDome extends EntityEnergyBarrier {
// ==================== DOME-SPECIFIC PROPERTIES ====================
protected float domeRadius = 5.0f;
protected float targetDomeRadius = 5.0f;
protected boolean followCaster = false;
public EntityEnergyDome(World world) {
super(world);
this.setSize(1.0f, 1.0f); // Hitbox doesn't really matter, we use radius checks
}
public EntityEnergyDome(World world, EntityLivingBase owner, double x, double y, double z,
float domeRadius, EnergyDisplayData display, EnergyLightningData lightning,
EnergyBarrierData barrier) {
this(world);
this.setPosition(x, y, z);
this.ownerEntityId = owner.getEntityId();
this.domeRadius = domeRadius;
this.displayData = display;
this.lightningData = lightning;
this.barrierData = barrier;
this.currentHealth = barrier.maxHealth;
}
@Override
public void onUpdate() {
this.prevPosX = this.posX;
this.prevPosY = this.posY;
this.prevPosZ = this.posZ;
super.onUpdate();
// Follow caster: both sides for smooth interpolated rendering
if (followCaster) {
Entity owner = ownerEntityId >= 0 ? worldObj.getEntityByID(ownerEntityId) : null;
if (owner != null) {
this.setPosition(owner.posX, owner.posY, owner.posZ);
}
}
// Handle charging animation (both sides for smooth rendering)
if (isCharging()) {
chargeTick++;
float progress = getChargeProgress();
this.domeRadius = targetDomeRadius * progress;
return; // Don't tick duration/death during charging
}
ticksAlive++;
if (updateBarrierTick()) return;
// Knockback entities inside the dome
if (!worldObj.isRemote && barrierData.knockbackEnabled) {
knockbackEntities();
}
}
// ==================== INCOMING CHECK ====================
/**
* Check if a projectile at the given position is entering this dome from outside.
* Only blocks incoming projectiles (dot product check).
*/
@Override
public boolean isIncomingProjectile(EntityEnergyProjectile projectile) {
// Don't block during charging phase
if (isCharging()) return false;
// Don't block projectiles from the dome's owner
if (projectile.getOwnerEntityId() == this.ownerEntityId) return false;
// Don't block projectiles from same-faction NPCs
Entity owner = getOwnerEntity();
Entity projOwner = projectile.getOwnerEntity();
if (owner instanceof noppes.npcs.entity.EntityNPCInterface && projOwner instanceof noppes.npcs.entity.EntityNPCInterface) {
if (((noppes.npcs.entity.EntityNPCInterface) owner).faction.id == ((noppes.npcs.entity.EntityNPCInterface) projOwner).faction.id) {
return false;
}
}
double dx = projectile.posX - this.posX;
double dy = projectile.posY - this.posY;
double dz = projectile.posZ - this.posZ;
double distSq = dx * dx + dy * dy + dz * dz;
// Check if projectile is within dome radius
if (distSq > domeRadius * domeRadius) return false;
// Check if projectile is moving inward (dot product of velocity and position relative to center)
double dot = dx * projectile.motionX + dy * projectile.motionY + dz * projectile.motionZ;
// Negative dot = moving toward center = incoming
return dot < 0;
}
// ==================== CHARGING ====================
@Override
public void setupCharging(int duration) {
this.targetDomeRadius = this.domeRadius;
this.domeRadius = 0.01f;
this.chargeDuration = duration;
this.chargeTick = 0;
setCharging(true);
}
@Override
public void finishCharging() {
this.domeRadius = targetDomeRadius;
setCharging(false);
}
// ==================== KNOCKBACK ====================
/**
* Push entities away from the dome surface.
* Entities outside are pushed outward, entities inside are pushed inward (containment).
*/
@Override
@SuppressWarnings("unchecked")
protected void knockbackEntities() {
float margin = 1.0f;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
posX - domeRadius - margin, posY - domeRadius - margin, posZ - domeRadius - margin,
posX + domeRadius + margin, posY + domeRadius + margin, posZ + domeRadius + margin
);
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
for (EntityLivingBase ent : entities) {
if (ent.getEntityId() == ownerEntityId) continue;
if (!isKnockbackTarget(ent)) continue;
double dx = ent.posX - posX;
double dy = (ent.posY + ent.height * 0.5) - posY;
double dz = ent.posZ - posZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < 0.01) continue;
// Only affect entities near the dome surface
if (dist < domeRadius + margin && dist > domeRadius - margin) {
double pushStrength = barrierData.knockbackStrength * 0.5;
double pushDir = dist >= domeRadius ? 1.0 : -1.0; // Outside = outward, Inside = inward
ent.addVelocity(
(dx / dist) * pushStrength * pushDir,
0.05 * pushDir,
(dz / dist) * pushStrength * pushDir
);
ent.velocityChanged = true;
}
}
}
// ==================== BOUNDING BOX ====================
@Override
public AxisAlignedBB getBoundingBox() {
if (barrierData.meleeEnabled) {
return AxisAlignedBB.getBoundingBox(
posX - domeRadius, posY - domeRadius, posZ - domeRadius,
posX + domeRadius, posY + domeRadius, posZ + domeRadius
);
}
return null;
}
@Override
@SideOnly(Side.CLIENT)
public boolean isInRangeToRenderDist(double distance) {
double d = domeRadius * 8.0D;
d *= 64.0D;
return distance < d * d;
}
// ==================== 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; }
// ==================== NBT ====================
@Override
protected void readEntityFromNBT(NBTTagCompound nbt) {
readBarrierBaseNBT(nbt);
this.domeRadius = nbt.getFloat("DomeRadius");
this.targetDomeRadius = nbt.hasKey("TargetDomeRadius") ? nbt.getFloat("TargetDomeRadius") : domeRadius;
this.followCaster = nbt.hasKey("FollowCaster") && nbt.getBoolean("FollowCaster");
}
@Override
protected void writeEntityToNBT(NBTTagCompound nbt) {
writeBarrierBaseNBT(nbt);
nbt.setFloat("DomeRadius", domeRadius);
nbt.setFloat("TargetDomeRadius", targetDomeRadius);
nbt.setBoolean("FollowCaster", followCaster);
}
}
@@ -0,0 +1,373 @@
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 kamkeel.npcs.controllers.data.ability.data.EnergyPanelData;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.NpcDamageSource;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.List;
/**
* Energy Panel entity - a flat rectangular barrier used by Wall and Shield abilities.
* Supports three modes: PLACED (stationary), HELD (tracks caster), LAUNCHED (moves forward).
* Extends EntityEnergyBarrier for shared barrier logic.
*/
public class EntityEnergyPanel extends EntityEnergyBarrier {
public enum PanelMode {
PLACED, // Stationary wall
HELD, // Follows caster, rotates with caster look
LAUNCHED // Moves forward, deals damage/knockback on contact
}
// ==================== PANEL-SPECIFIC PROPERTIES ====================
protected EnergyPanelData panelData = new EnergyPanelData();
protected PanelMode mode = PanelMode.PLACED;
protected float panelYaw = 0.0f; // Rotation of the panel face (degrees)
// ==================== CHARGING (panel-specific targets) ====================
protected float targetPanelWidth;
protected float targetPanelHeight;
public EntityEnergyPanel(World world) {
super(world);
this.setSize(0.5f, 0.5f);
}
public EntityEnergyPanel(World world, EntityLivingBase owner, double x, double y, double z,
float yaw, PanelMode mode,
EnergyDisplayData display, EnergyLightningData lightning,
EnergyBarrierData barrier, EnergyPanelData panel) {
this(world);
this.setPosition(x, y + panel.heightOffset, z);
this.ownerEntityId = owner.getEntityId();
this.panelYaw = yaw;
this.mode = mode;
this.displayData = display;
this.lightningData = lightning;
this.barrierData = barrier;
this.panelData = panel;
this.currentHealth = barrier.maxHealth;
// For launched panels, set initial velocity
if (mode == PanelMode.LAUNCHED) {
float yawRad = (float) Math.toRadians(yaw);
this.motionX = -Math.sin(yawRad) * panel.launchSpeed;
this.motionY = 0;
this.motionZ = Math.cos(yawRad) * panel.launchSpeed;
}
}
@Override
public void onUpdate() {
this.prevPosX = this.posX;
this.prevPosY = this.posY;
this.prevPosZ = this.posZ;
super.onUpdate();
// Handle charging animation (both sides for smooth rendering)
if (isCharging()) {
chargeTick++;
float progress = getChargeProgress();
panelData.panelWidth = targetPanelWidth * progress;
panelData.panelHeight = targetPanelHeight * progress;
// During charging, held panels still track owner
if (mode == PanelMode.HELD) {
updateHeld();
}
return; // Don't tick duration/death during charging
}
ticksAlive++;
if (updateBarrierTick()) return;
// Knockback (not during launched mode)
if (!worldObj.isRemote && barrierData.knockbackEnabled && mode != PanelMode.LAUNCHED) {
knockbackEntities();
}
// Mode-specific updates
switch (mode) {
case HELD:
updateHeld();
break;
case LAUNCHED:
updateLaunched();
break;
case PLACED:
default:
// Stationary, no update needed
break;
}
}
private void updateHeld() {
Entity owner = getOwnerEntity();
if (owner == null) return;
// Update yaw BEFORE position calc to avoid 1-tick lag
this.panelYaw = owner.rotationYaw;
// Follow owner position
float frontDist = 1.5f;
float yawRad = (float) Math.toRadians(panelYaw);
double newX = owner.posX + (-Math.sin(yawRad) * frontDist);
double newY = owner.posY + panelData.heightOffset + (owner.height * 0.5f);
double newZ = owner.posZ + (Math.cos(yawRad) * frontDist);
this.setPosition(newX, newY, newZ);
}
private void updateLaunched() {
// Move forward on both sides for smooth client rendering
this.posX += motionX;
this.posY += motionY;
this.posZ += motionZ;
this.setPosition(posX, posY, posZ);
// Damage and knockback are server-only
if (!worldObj.isRemote) {
// Check entity collision for damage
float halfW = panelData.panelWidth * 0.5f;
float halfH = panelData.panelHeight * 0.5f;
float halfD = 0.5f; // Panel collision depth
float searchRadius = Math.max(halfW, halfD);
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
posX - searchRadius, posY - halfH, posZ - searchRadius,
posX + searchRadius, posY + halfH, posZ + searchRadius
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
Entity owner = getOwnerEntity();
for (EntityLivingBase target : entities) {
if (target == owner) continue;
if (owner instanceof EntityNPCInterface && target instanceof EntityNPCInterface) {
if (((EntityNPCInterface) owner).faction.id == ((EntityNPCInterface) target).faction.id) continue;
}
// Apply damage
if (panelData.launchDamage > 0) {
if (owner instanceof EntityNPCInterface) {
target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), panelData.launchDamage);
} else if (owner instanceof EntityPlayer) {
target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), panelData.launchDamage);
}
}
// Apply knockback
if (panelData.launchKnockback > 0) {
double dx = target.posX - posX;
double dz = target.posZ - posZ;
double len = Math.sqrt(dx * dx + dz * dz);
if (len > 0) {
target.addVelocity(
(dx / len) * panelData.launchKnockback * 0.5,
0.1,
(dz / len) * panelData.launchKnockback * 0.5
);
target.velocityChanged = true;
}
}
}
// Max lifetime check
if (ticksAlive > 200) {
this.setDead();
}
}
}
// ==================== INCOMING CHECK ====================
/**
* Check if a projectile hits this panel surface.
* Only blocks incoming projectiles (not from the panel's owner).
*/
@Override
public boolean isIncomingProjectile(EntityEnergyProjectile projectile) {
// Don't block during charging phase
if (isCharging()) return false;
if (projectile.getOwnerEntityId() == this.ownerEntityId) return false;
// Faction check
Entity owner = getOwnerEntity();
Entity projOwner = projectile.getOwnerEntity();
if (owner instanceof EntityNPCInterface && projOwner instanceof EntityNPCInterface) {
if (((EntityNPCInterface) owner).faction.id == ((EntityNPCInterface) projOwner).faction.id) {
return false;
}
}
// Check if projectile is within panel bounds
float halfW = panelData.panelWidth * 0.5f;
float halfH = panelData.panelHeight * 0.5f;
float panelThickness = 0.5f; // Collision thickness
// Transform projectile position to panel-local space
float yawRad = (float) Math.toRadians(panelYaw);
double relX = projectile.posX - this.posX;
double relY = projectile.posY - this.posY;
double relZ = projectile.posZ - this.posZ;
// Rotate into panel space (panel faces along -sin(yaw), cos(yaw))
double cos = Math.cos(yawRad);
double sin = Math.sin(yawRad);
double localForward = relX * (-sin) + relZ * cos; // Distance along panel normal
double localRight = relX * cos + relZ * sin; // Distance along panel width
// Check bounds
if (Math.abs(localForward) > panelThickness) return false;
if (Math.abs(localRight) > halfW) return false;
if (Math.abs(relY) > halfH) return false;
// Check incoming direction via dot product with panel normal
double normalX = -Math.sin(yawRad);
double normalZ = Math.cos(yawRad);
double dot = projectile.motionX * normalX + projectile.motionZ * normalZ;
// Determine which side of the panel the projectile is on
if (localForward > 0 && dot >= 0) return false; // Moving away from front side
if (localForward < 0 && dot <= 0) return false; // Moving away from back side
return true;
}
// ==================== CHARGING ====================
@Override
public void setupCharging(int duration) {
this.targetPanelWidth = panelData.panelWidth;
this.targetPanelHeight = panelData.panelHeight;
panelData.panelWidth = 0.01f;
panelData.panelHeight = 0.01f;
this.chargeDuration = duration;
this.chargeTick = 0;
setCharging(true);
}
@Override
public void finishCharging() {
panelData.panelWidth = targetPanelWidth;
panelData.panelHeight = targetPanelHeight;
setCharging(false);
}
// ==================== KNOCKBACK ====================
/**
* Push entities away from the panel surface.
*/
@Override
@SuppressWarnings("unchecked")
protected void knockbackEntities() {
float halfW = panelData.panelWidth * 0.5f;
float halfH = panelData.panelHeight * 0.5f;
float margin = 1.0f;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
posX - halfW - margin, posY - halfH - margin, posZ - halfW - margin,
posX + halfW + margin, posY + halfH + margin, posZ + halfW + margin
);
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
float yawRad = (float) Math.toRadians(panelYaw);
float normalX = -(float) Math.sin(yawRad);
float normalZ = (float) Math.cos(yawRad);
for (EntityLivingBase ent : entities) {
if (ent.getEntityId() == ownerEntityId) continue;
if (!isKnockbackTarget(ent)) continue;
// Transform entity position to panel-local space
double dx = ent.posX - posX;
double dy = (ent.posY + ent.height * 0.5) - posY;
double dz = ent.posZ - posZ;
// Check proximity to panel surface
float localForward = (float) (dx * normalX + dz * normalZ);
float localRight = (float) (dx * (-normalZ) + dz * normalX);
if (Math.abs(localForward) < margin && Math.abs(localRight) < halfW + margin && Math.abs(dy) < halfH + margin) {
// Push entity away from the panel face
float pushDir = localForward >= 0 ? 1.0f : -1.0f;
double pushStrength = barrierData.knockbackStrength * 0.5;
ent.addVelocity(
normalX * pushDir * pushStrength,
0.1,
normalZ * pushDir * pushStrength
);
ent.velocityChanged = true;
}
}
}
// ==================== BOUNDING BOX ====================
@Override
public AxisAlignedBB getBoundingBox() {
if (barrierData.meleeEnabled) {
float halfW = panelData.panelWidth * 0.5f;
float halfH = panelData.panelHeight * 0.5f;
float extent = Math.max(halfW, 0.5f);
return AxisAlignedBB.getBoundingBox(
posX - extent, posY - halfH, posZ - extent,
posX + extent, posY + halfH, posZ + extent
);
}
return null;
}
@Override
@SideOnly(Side.CLIENT)
public boolean isInRangeToRenderDist(double distance) {
double d = Math.max(panelData.panelWidth, panelData.panelHeight) * 8.0D;
d *= 64.0D;
return distance < d * d;
}
// ==================== GETTERS ====================
public PanelMode getMode() { return mode; }
public float getPanelYaw() { return panelYaw; }
public EnergyPanelData getPanelData() { return panelData; }
// ==================== NBT ====================
@Override
protected void readEntityFromNBT(NBTTagCompound nbt) {
readBarrierBaseNBT(nbt);
this.panelYaw = nbt.getFloat("PanelYaw");
int modeOrdinal = nbt.getInteger("PanelMode");
this.mode = (modeOrdinal >= 0 && modeOrdinal < PanelMode.values().length)
? PanelMode.values()[modeOrdinal] : PanelMode.PLACED;
panelData.readNBT(nbt);
this.targetPanelWidth = nbt.hasKey("TargetPanelWidth") ? nbt.getFloat("TargetPanelWidth") : panelData.panelWidth;
this.targetPanelHeight = nbt.hasKey("TargetPanelHeight") ? nbt.getFloat("TargetPanelHeight") : panelData.panelHeight;
}
@Override
protected void writeEntityToNBT(NBTTagCompound nbt) {
writeBarrierBaseNBT(nbt);
nbt.setFloat("PanelYaw", panelYaw);
nbt.setInteger("PanelMode", mode.ordinal());
nbt.setFloat("TargetPanelWidth", targetPanelWidth);
nbt.setFloat("TargetPanelHeight", targetPanelHeight);
panelData.writeNBT(nbt);
}
}
@@ -1,14 +1,10 @@
package kamkeel.npcs.entity;
import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
@@ -23,6 +19,9 @@ import net.minecraft.world.World;
import noppes.npcs.EventHooks;
import noppes.npcs.NpcDamageSource;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.controllers.PartyController;
import noppes.npcs.controllers.data.Party;
import noppes.npcs.controllers.data.PlayerData;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.scripted.NpcAPI;
@@ -35,20 +34,21 @@ import java.util.List;
import java.util.Map;
/**
* Base class for all ability projectiles (Orb, Disc, Beam, Laser).
* Provides common functionality for visuals, combat, effects, and interpolation.
* 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 EntityAbilityProjectile extends Entity implements IEntityAdditionalSpawnData {
public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// ==================== ACTIVE PROJECTILE TRACKING ====================
private static final Map<Integer, List<WeakReference<EntityAbilityProjectile>>> activeProjectiles = new HashMap<>();
private static final Map<Integer, List<WeakReference<EntityEnergyProjectile>>> activeProjectiles = new HashMap<>();
public static void trackProjectile(EntityAbilityProjectile projectile) {
public static void trackProjectile(EntityEnergyProjectile projectile) {
int ownerId = projectile.getOwnerEntityId();
if (ownerId < 0) return;
List<WeakReference<EntityAbilityProjectile>> refs = activeProjectiles.get(ownerId);
List<WeakReference<EntityEnergyProjectile>> refs = activeProjectiles.get(ownerId);
if (refs == null) {
refs = new ArrayList<>();
activeProjectiles.put(ownerId, refs);
@@ -56,13 +56,13 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
refs.add(new WeakReference<>(projectile));
}
public static List<EntityAbilityProjectile> getActiveProjectiles(int ownerEntityId) {
List<WeakReference<EntityAbilityProjectile>> refs = activeProjectiles.get(ownerEntityId);
public static List<EntityEnergyProjectile> getActiveProjectiles(int ownerEntityId) {
List<WeakReference<EntityEnergyProjectile>> refs = activeProjectiles.get(ownerEntityId);
if (refs == null) return Collections.emptyList();
List<EntityAbilityProjectile> result = new ArrayList<>();
Iterator<WeakReference<EntityAbilityProjectile>> it = refs.iterator();
List<EntityEnergyProjectile> result = new ArrayList<>();
Iterator<WeakReference<EntityEnergyProjectile>> it = refs.iterator();
while (it.hasNext()) {
EntityAbilityProjectile p = it.next().get();
EntityEnergyProjectile p = it.next().get();
if (p == null || p.isDead) {
it.remove();
} else {
@@ -80,17 +80,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
protected EnergyCombatData combatData = new EnergyCombatData();
// ==================== EFFECT PROPERTIES ====================
protected List<AbilityEffect> effects = new ArrayList<>();
// ==================== LIGHTNING EFFECT PROPERTIES ====================
protected EnergyLightningData lightningData = new EnergyLightningData();
// Client-side lightning state (not saved to NBT)
@cpw.mods.fml.relauncher.SideOnly(cpw.mods.fml.relauncher.Side.CLIENT)
public transient Object lightningState; // Actually AttachedLightningRenderer.LightningState
// ==================== DISPLAY PROPERTIES ====================
protected EnergyDisplayData displayData = new EnergyDisplayData();
protected List<AbilityPotionEffect> effects = new ArrayList<>();
// ==================== ANCHOR PROPERTIES ====================
protected EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
@@ -108,23 +98,13 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
// ==================== TRACKING ====================
protected double startX, startY, startZ;
protected int ownerEntityId = -1;
protected int targetEntityId = -1;
// ==================== STATE ====================
protected boolean hasHit = false;
protected boolean previewMode = false; // Client-side preview mode (no damage/effects)
protected EntityLivingBase previewOwner = null; // Direct reference for GUI preview (no world lookup)
/** The ability that spawned this projectile. Transient, not saved to NBT. */
protected transient Ability sourceAbility = null;
// ==================== CHARGING STATE ====================
protected boolean charging = false;
protected int chargeDuration = 40;
protected int chargeTick = 0;
// ==================== CHARGING STATE (projectile-specific) ====================
protected float targetSize = 1.0f;
protected static final int DW_CHARGING = 20;
// ==================== ROTATION INTERPOLATION ====================
public float prevRotationValX, prevRotationValY, prevRotationValZ;
@@ -139,12 +119,10 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
protected double interpTargetMotionX, interpTargetMotionY, interpTargetMotionZ;
protected int interpSteps;
public EntityAbilityProjectile(World world) {
public EntityEnergyProjectile(World world) {
super(world);
this.setSize(0.5f, 0.5f);
this.noClip = false;
this.isImmuneToFire = true;
this.ignoreFrustumCheck = true;
}
/**
@@ -177,11 +155,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
this.prevRenderSize = size;
}
@Override
protected void entityInit() {
this.dataWatcher.addObject(DW_CHARGING, (byte) 0);
}
@Override
@SideOnly(Side.CLIENT)
public boolean isInRangeToRenderDist(double distance) {
@@ -190,11 +163,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
return distance < d1 * d1;
}
@Override
public boolean shouldRenderInPass(int pass) {
return pass == 1; // Translucent pass
}
@Override
public void onUpdate() {
// Store previous values for interpolation
@@ -242,7 +210,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
}
// Set death time on first tick if not already set (handles chunk load/unload)
if (deathWorldTime < 0 && worldObj != null) {
// Skip during charging phase - lifetime starts when the entity actually fires
if (deathWorldTime < 0 && worldObj != null && !isCharging()) {
deathWorldTime = worldObj.getTotalWorldTime() + getMaxLifetime();
}
@@ -270,6 +239,13 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
}
}
// Check barrier collisions (server-side, non-preview, non-charging)
if (!previewMode && !worldObj.isRemote && !isCharging()) {
if (checkBarrierCollision()) {
return; // Projectile was absorbed by a barrier
}
}
// Subclass-specific update
updateProjectile();
@@ -310,6 +286,37 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
*/
protected abstract void updateProjectile();
// ==================== BARRIER COLLISION ====================
/**
* 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() {
// Search area for barriers - must cover large domes (up to 30 radius)
double searchRange = 32.0;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
posX - searchRange, posY - searchRange, posZ - searchRange,
posX + searchRange, posY + searchRange, posZ + searchRange
);
@SuppressWarnings("unchecked")
List<EntityEnergyBarrier> barriers = worldObj.getEntitiesWithinAABB(EntityEnergyBarrier.class, searchBox);
for (EntityEnergyBarrier barrier : barriers) {
if (barrier.isDead) continue;
if (barrier.isIncomingProjectile(this)) {
if (barrier.onProjectileHit(this, getDamage())) {
hasHit = true;
this.setDead();
return true;
}
}
}
return false;
}
// ==================== POSITION INTERPOLATION ====================
@Override
@@ -353,23 +360,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
}
}
// ==================== PREVIEW MODE ====================
/**
* Set preview mode for GUI display.
* In preview mode, no damage or world effects are applied.
*/
public void setPreviewMode(boolean preview) {
this.previewMode = preview;
}
/**
* Check if entity is in preview mode.
*/
public boolean isPreviewMode() {
return previewMode;
}
// ==================== DAMAGE & EFFECTS ====================
protected void applyDamage(EntityLivingBase target) {
@@ -430,7 +420,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
}
protected void applyEffects(EntityLivingBase target) {
for (AbilityEffect effect : effects) {
for (AbilityPotionEffect effect : effects) {
effect.apply(target);
}
}
@@ -438,25 +428,10 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
/**
* Set the effects list from the ability's configured effects.
*/
public void setEffects(List<AbilityEffect> effects) {
public void setEffects(List<AbilityPotionEffect> effects) {
this.effects = effects != null ? effects : new ArrayList<>();
}
/**
* Set the source ability that spawned this projectile.
* Used by external damage handlers to access ability-specific data.
*/
public void setSourceAbility(Ability ability) {
this.sourceAbility = ability;
}
/**
* Get the source ability that spawned this projectile.
*/
public Ability getSourceAbility() {
return sourceAbility;
}
protected void doExplosion() {
if (previewMode) return; // Skip explosion in preview mode
worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f);
@@ -513,38 +488,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
prevPosZ = newZ;
}
// ==================== CHARGING METHODS ====================
/**
* Check if entity is in charging state (synced via data watcher).
* In preview mode, uses local field since data watcher isn't synced.
*/
public boolean isCharging() {
if (previewMode) return this.charging;
return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1;
}
/**
* Set charging state (server only, synced to clients via data watcher).
*/
protected void setCharging(boolean value) {
this.charging = value;
if (!worldObj.isRemote) {
this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0));
}
}
public float getChargeProgress() {
if (chargeDuration <= 0) return 1.0f;
return Math.min(1.0f, (float) chargeTick / chargeDuration);
}
public float getInterpolatedChargeProgress(float partialTicks) {
if (chargeDuration <= 0) return 1.0f;
float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration);
float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration);
return prevProgress + (currProgress - prevProgress) * partialTicks;
}
// ==================== CHARGING METHODS (projectile-specific) ====================
/**
* Setup this entity in charging mode (for windup phase).
@@ -677,37 +621,12 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
// ==================== ENTITY HELPERS ====================
public Entity getOwnerEntity() {
// In preview mode, use direct reference (no world lookup)
if (previewMode && previewOwner != null) {
return previewOwner;
}
if (ownerEntityId == -1) return null;
return worldObj.getEntityByID(ownerEntityId);
}
public int getOwnerEntityId() {
return ownerEntityId;
}
public void setOwnerEntityId(int id) {
this.ownerEntityId = id;
}
public IEntity getOwner() {
if (previewMode) return null;
if (getOwnerEntity() == null) return null;
return NpcAPI.Instance().getIEntity(getOwnerEntity());
}
/**
* Set the preview owner for GUI preview mode.
* This allows anchor point calculations without world entity lookup.
*/
public void setPreviewOwner(EntityLivingBase owner) {
this.previewOwner = owner;
}
public int getTargetEntityId() {
return targetEntityId;
}
@@ -728,22 +647,48 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
/**
* Check if an entity should be ignored for collision.
* Ignores: the owner, same-faction NPCs, and other projectiles from the same caster.
* Ignores: the owner, same-faction NPCs, passive NPCs, NPCs friendly to player owner,
* party members (if friendly fire is off), and other projectiles from the same caster.
*/
protected boolean shouldIgnoreEntity(Entity entity) {
Entity owner = getOwnerEntity();
if (entity == owner) return true;
if (owner instanceof EntityNPCInterface && entity instanceof EntityNPCInterface) {
EntityNPCInterface ownerNpc = (EntityNPCInterface) owner;
EntityNPCInterface targetNpc = (EntityNPCInterface) entity;
if (ownerNpc.faction.id == targetNpc.faction.id) return true;
// Ignore other projectiles from the same caster (e.g. multi-projectile abilities)
if (entity instanceof EntityEnergyProjectile) {
EntityEnergyProjectile other = (EntityEnergyProjectile) entity;
if (other.ownerEntityId == this.ownerEntityId) return true;
}
// Ignore other projectiles from the same caster (e.g. multi-projectile abilities)
if (entity instanceof EntityAbilityProjectile) {
EntityAbilityProjectile other = (EntityAbilityProjectile) entity;
if (other.ownerEntityId == this.ownerEntityId) return true;
// NPC target checks
if (entity instanceof EntityNPCInterface) {
EntityNPCInterface targetNpc = (EntityNPCInterface) entity;
// Ignore passive NPCs
if (targetNpc.faction.isPassive) return true;
// NPC owner: ignore same-faction NPCs
if (owner instanceof EntityNPCInterface) {
EntityNPCInterface ownerNpc = (EntityNPCInterface) owner;
if (ownerNpc.faction.id == targetNpc.faction.id) return true;
}
// Player owner: ignore NPCs whose faction is friendly to the player
if (owner instanceof EntityPlayer) {
if (targetNpc.faction.isFriendlyToPlayer((EntityPlayer) owner)) return true;
}
}
// Party friendly fire check: player owner hitting another player
if (owner instanceof EntityPlayer && entity instanceof EntityPlayer) {
EntityPlayer ownerPlayer = (EntityPlayer) owner;
EntityPlayer targetPlayer = (EntityPlayer) entity;
PlayerData ownerData = PlayerData.get(ownerPlayer);
PlayerData targetData = PlayerData.get(targetPlayer);
if (ownerData.partyUUID != null && ownerData.partyUUID.equals(targetData.partyUUID)) {
Party party = PartyController.Instance().getParty(ownerData.partyUUID);
if (party != null && !party.friendlyFire()) return true;
}
}
return false;
@@ -768,24 +713,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
this.startZ = z;
}
public int getInnerColor() { return displayData.getInnerColor(); }
public void setInnerColor(int color) { displayData.setInnerColor(color); }
public int getOuterColor() { return displayData.getOuterColor(); }
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); }
// ==================== ROTATION GETTERS ====================
public float getInterpolatedRotationX(float partialTicks) {
@@ -804,20 +731,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks;
}
// ==================== LIGHTNING GETTERS & SETTERS ====================
public boolean hasLightningEffect() { return lightningData.isLightningEffect(); }
public void setLightningEffect(boolean enabled) { lightningData.setLightningEffect(enabled); }
public float getLightningDensity() { return lightningData.getLightningDensity(); }
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); }
// ==================== LIFESPAN GETTERS & SETTERS ====================
public float getMaxDistance() { return lifespanData.getMaxDistance(); }
@@ -879,19 +792,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
public boolean getHasHit() { return hasHit; }
// ==================== BRIGHTNESS ====================
@Override
public float getBrightness(float partialTicks) {
return 1.0f;
}
@Override
@SideOnly(Side.CLIENT)
public int getBrightnessForRender(float partialTicks) {
return 0xF000F0;
}
// ==================== NBT ====================
@Override
@@ -910,6 +810,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
* Read base projectile properties from NBT.
*/
protected void readBaseNBT(NBTTagCompound nbt) {
readEnergyBaseNBT(nbt);
this.size = nbt.getFloat("Size");
// Read effects list
@@ -917,7 +819,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
if (nbt.hasKey("Effects")) {
NBTTagList effectsList = nbt.getTagList("Effects", 10); // 10 = compound tag
for (int i = 0; i < effectsList.tagCount(); i++) {
effects.add(AbilityEffect.fromNBT(effectsList.getCompoundTagAt(i)));
effects.add(AbilityPotionEffect.fromNBT(effectsList.getCompoundTagAt(i)));
}
}
@@ -927,7 +829,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
this.startY = nbt.getDouble("StartY");
this.startZ = nbt.getDouble("StartZ");
this.ownerEntityId = nbt.getInteger("OwnerId");
this.targetEntityId = nbt.getInteger("TargetId");
this.motionX = nbt.getDouble("MotionX");
@@ -941,10 +842,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
anchorData.readNBT(nbt);
combatData.readNBT(nbt);
displayData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
lightningData.readNBT(nbt);
trajectoryData.readNBT(nbt);
}
@@ -952,12 +851,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
* Write base projectile properties to NBT.
*/
protected void writeBaseNBT(NBTTagCompound nbt) {
writeEnergyBaseNBT(nbt);
nbt.setFloat("Size", size);
// Write effects list
if (!effects.isEmpty()) {
NBTTagList effectsList = new NBTTagList();
for (AbilityEffect effect : effects) {
for (AbilityPotionEffect effect : effects) {
effectsList.appendTag(effect.writeNBT());
}
nbt.setTag("Effects", effectsList);
@@ -969,7 +870,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
nbt.setDouble("StartY", startY);
nbt.setDouble("StartZ", startZ);
nbt.setInteger("OwnerId", ownerEntityId);
nbt.setInteger("TargetId", targetEntityId);
nbt.setDouble("MotionX", motionX);
@@ -978,10 +878,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
anchorData.writeNBT(nbt);
combatData.writeNBT(nbt);
displayData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
lightningData.writeNBT(nbt);
trajectoryData.writeNBT(nbt);
}
@@ -995,55 +893,10 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
*/
protected abstract void writeProjectileNBT(NBTTagCompound nbt);
// ==================== SPAWN DATA ====================
@Override
public void writeSpawnData(ByteBuf buffer) {
try {
NBTTagCompound compound = new NBTTagCompound();
this.writeEntityToNBT(compound);
cpw.mods.fml.common.network.ByteBufUtils.writeTag(buffer, compound);
} catch (Exception e) {
noppes.npcs.LogWriter.error("Error writing ability projectile spawn data", e);
}
}
@Override
public void readSpawnData(ByteBuf buffer) {
try {
NBTTagCompound compound = cpw.mods.fml.common.network.ByteBufUtils.readTag(buffer);
if (compound != null) {
this.readEntityFromNBT(compound);
}
} catch (Exception e) {
noppes.npcs.LogWriter.error("Error reading ability projectile spawn data", e);
}
}
// ==================== COLLISION SETTINGS ====================
@Override
public boolean canBeCollidedWith() {
return false;
}
@Override
public boolean canBePushed() {
return false;
}
@Override
protected boolean canTriggerWalking() {
return false;
}
@Override
public boolean isBurning() {
return false;
}
@SideOnly(Side.CLIENT)
public float getShadowSize() {
return 0.0f;
}
}
@@ -0,0 +1,326 @@
package kamkeel.npcs.entity;
import kamkeel.npcs.controllers.data.ability.data.*;
import net.minecraft.entity.Entity;
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.ArrayList;
import java.util.List;
/**
* Energy Slicer entity - a thin, wide blade projectile that flies in a straight line.
* JJK Cleave/Dismantle inspired. Piercing by default (passes through entities).
* Extends EntityEnergyAbility to leverage the existing projectile infrastructure.
*/
public class EntityEnergySlicer extends EntityEnergyProjectile {
private float sliceWidth = 3.0f;
private float sliceThickness = 0.15f;
private boolean piercing = true;
private List<Integer> hitEntities = new ArrayList<>();
public EntityEnergySlicer(World world) {
super(world);
}
/**
* Full constructor with all parameters.
*/
public EntityEnergySlicer(World world, EntityLivingBase owner, EntityLivingBase target,
double x, double y, double z,
float sliceWidth, float sliceThickness,
EnergyDisplayData display, EnergyCombatData combat,
EnergyHomingData homing, EnergyLightningData lightning,
EnergyLifespanData lifespan, EnergyTrajectoryData trajectory,
boolean piercing) {
super(world);
initProjectile(owner, target, x, y, z, sliceWidth, display, combat, lightning, lifespan, trajectory);
this.homingData = homing;
this.sliceWidth = sliceWidth;
this.sliceThickness = sliceThickness;
this.piercing = piercing;
calculateInitialVelocity(owner, target, x, y, z);
}
// Targets stored during charging for grow animation
private float targetSliceWidth = 3.0f;
private float targetSliceThickness = 0.15f;
/**
* Setup charging with full target dimensions.
*/
public void setupSlicerCharging(EnergyAnchorData anchor, int chargeDuration, float width, float thickness) {
this.targetSliceWidth = width;
this.targetSliceThickness = thickness;
this.sliceWidth = 0.01f;
this.sliceThickness = 0.01f;
super.setupCharging(anchor, chargeDuration);
}
/**
* Setup preview mode.
*/
public void setupPreview(EntityLivingBase owner, float width, float thickness,
EnergyDisplayData display, EnergyLightningData lightning,
EnergyAnchorData anchor, int chargeDuration) {
this.setPreviewMode(true);
this.setPreviewOwner(owner);
this.displayData = display;
this.lightningData = lightning;
this.setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.targetSliceWidth = width;
this.targetSliceThickness = thickness;
this.sliceWidth = 0.01f;
this.sliceThickness = 0.01f;
this.size = 0.01f;
this.renderCurrentSize = 0.01f;
this.prevRenderSize = 0.01f;
Vec3 pos = kamkeel.npcs.util.AnchorPointHelper.calculateAnchorPosition(owner, anchorData);
this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
this.prevPosX = pos.xCoord;
this.prevPosY = pos.yCoord;
this.prevPosZ = pos.zCoord;
this.startX = pos.xCoord;
this.startY = pos.yCoord;
this.startZ = pos.zCoord;
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
}
/**
* Start preview firing.
*/
public void startPreviewFiring() {
setCharging(false);
startX = posX;
startY = posY;
startZ = posZ;
prevPosX = posX;
prevPosY = posY;
prevPosZ = posZ;
Entity owner = getOwnerEntity();
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
motionX = -Math.sin(yaw) * getSpeed();
motionY = 0;
motionZ = Math.cos(yaw) * getSpeed();
} else {
motionX = getSpeed();
motionY = 0;
motionZ = 0;
}
}
/**
* Start the slicer moving (exit charging mode).
*/
public void startMoving(EntityLivingBase target) {
setCharging(false);
snapToPlayerLookVector();
startX = posX;
startY = posY;
startZ = posZ;
Entity owner = getOwnerEntity();
if (target != null) {
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
double dz = target.posZ - posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
motionX = (dx / len) * getSpeed();
motionY = (dy / len) * getSpeed();
motionZ = (dz / len) * getSpeed();
}
} else if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
}
@Override
protected void updateRotation() {
// Slicer doesn't spin - it stays oriented perpendicular to travel direction
// No rotation update needed
}
@Override
protected void updateProjectile() {
if (isCharging()) {
updateCharging();
return;
}
if (previewMode) {
posX += motionX;
posY += motionY;
posZ += motionZ;
return;
}
if (worldObj.isRemote) {
handleClientInterpolation();
} else {
// No homing by default for slicer (straight line flight)
if (homingData.isHoming()) {
updateHoming();
}
checkBlockCollision();
checkEntityCollision();
this.moveEntity(motionX, motionY, motionZ);
}
// Wall collision
if (this.isCollidedHorizontally || this.isCollidedVertically) {
if (!worldObj.isRemote) {
hasHit = true;
if (isExplosive()) {
doExplosion();
}
}
this.setDead();
}
}
@Override
protected void updateCharging() {
chargeTick++;
float progress = getChargeProgress();
this.sliceWidth = targetSliceWidth * progress;
this.sliceThickness = targetSliceThickness * progress;
this.size = this.sliceWidth;
Entity owner = getOwnerEntity();
if (owner instanceof EntityLivingBase) {
Vec3 pos = kamkeel.npcs.util.AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData);
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
}
}
private void checkBlockCollision() {
if (piercing) return; // Piercing slicers pass through blocks
Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ);
Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ);
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
}
hasHit = true;
this.setDead();
}
}
private void checkEntityCollision() {
float halfW = sliceWidth * 0.5f;
float halfH = sliceThickness * 0.5f;
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
posX - halfW, posY - halfH, posZ - halfW,
posX + halfW, posY + halfH, posZ + halfW
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (EntityLivingBase entity : entities) {
if (shouldIgnoreEntity(entity)) continue;
if (hitEntities.contains(entity.getEntityId())) continue; // Already hit this entity
if (piercing) {
// Pierce: damage but keep flying
applyDamage(entity);
hitEntities.add(entity.getEntityId());
} else {
// Non-piercing: damage and die
hasHit = true;
if (isExplosive()) {
doExplosion();
} else {
applyDamage(entity);
}
this.setDead();
return;
}
}
}
/**
* Get the yaw angle from motion vector for orientation.
*/
public float getTravelYaw() {
if (isCharging()) {
Entity owner = previewMode ? previewOwner : getOwnerEntity();
if (owner != null) return owner.rotationYaw;
}
double speed = Math.sqrt(motionX * motionX + motionZ * motionZ);
if (speed > 0.001) {
return (float) (Math.atan2(-motionX, motionZ) * 180.0 / Math.PI);
}
return 0.0f;
}
/**
* Get the pitch angle from motion vector.
*/
public float getTravelPitch() {
double horizSpeed = Math.sqrt(motionX * motionX + motionZ * motionZ);
if (horizSpeed > 0.001 || Math.abs(motionY) > 0.001) {
return (float) (-Math.atan2(motionY, horizSpeed) * 180.0 / Math.PI);
}
return 0.0f;
}
// ==================== GETTERS ====================
public float getSliceWidth() { return sliceWidth; }
public void setSliceWidth(float width) { this.sliceWidth = width; }
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 ====================
@Override
protected void readProjectileNBT(NBTTagCompound nbt) {
readChargingNBT(nbt);
this.sliceWidth = nbt.hasKey("SliceWidth") ? nbt.getFloat("SliceWidth") : 3.0f;
this.sliceThickness = nbt.hasKey("SliceThickness") ? nbt.getFloat("SliceThickness") : 0.15f;
this.piercing = !nbt.hasKey("Piercing") || nbt.getBoolean("Piercing");
this.targetSliceWidth = nbt.hasKey("TargetSliceWidth") ? nbt.getFloat("TargetSliceWidth") : sliceWidth;
this.targetSliceThickness = nbt.hasKey("TargetSliceThickness") ? nbt.getFloat("TargetSliceThickness") : sliceThickness;
}
@Override
protected void writeProjectileNBT(NBTTagCompound nbt) {
writeChargingNBT(nbt);
nbt.setFloat("SliceWidth", sliceWidth);
nbt.setFloat("SliceThickness", sliceThickness);
nbt.setBoolean("Piercing", piercing);
nbt.setFloat("TargetSliceWidth", targetSliceWidth);
nbt.setFloat("TargetSliceThickness", targetSliceThickness);
}
}
@@ -20,18 +20,18 @@ import net.minecraft.nbt.NBTTagCompound;
import java.io.IOException;
/**
* Request packet to get a specific custom ability by UUID.
* Request packet to get a specific custom ability by name.
*/
public final class CustomAbilityGetPacket extends AbstractPacket {
public static String packetName = "Request|CustomAbilityGet";
private String uuid;
private String name;
public CustomAbilityGetPacket() {
}
public CustomAbilityGetPacket(String uuid) {
this.uuid = uuid;
public CustomAbilityGetPacket(String name) {
this.name = name;
}
@Override
@@ -47,7 +47,7 @@ public final class CustomAbilityGetPacket extends AbstractPacket {
@SideOnly(Side.CLIENT)
@Override
public void sendData(ByteBuf out) throws IOException {
ByteBufUtils.writeString(out, uuid);
ByteBufUtils.writeString(out, name);
}
@Override
@@ -58,12 +58,10 @@ public final class CustomAbilityGetPacket extends AbstractPacket {
if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player))
return;
String id = ByteBufUtils.readString(in);
Ability ability = AbilityController.Instance.getCustomAbility(id);
String abilityName = ByteBufUtils.readString(in);
Ability ability = AbilityController.Instance.getCustomAbility(abilityName);
if (ability != null) {
NBTTagCompound compound = ability.writeNBT();
// Include the UUID so the client can track it
compound.setString("_uuid", id);
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
}
}
@@ -19,18 +19,18 @@ import noppes.npcs.NoppesUtilServer;
import java.io.IOException;
/**
* Request packet to remove a custom ability by UUID.
* Request packet to remove a custom ability by name.
*/
public final class CustomAbilityRemovePacket extends AbstractPacket {
public static String packetName = "Request|CustomAbilityRemove";
private String uuid;
private String name;
public CustomAbilityRemovePacket() {
}
public CustomAbilityRemovePacket(String uuid) {
this.uuid = uuid;
public CustomAbilityRemovePacket(String name) {
this.name = name;
}
@Override
@@ -51,7 +51,7 @@ public final class CustomAbilityRemovePacket extends AbstractPacket {
@SideOnly(Side.CLIENT)
@Override
public void sendData(ByteBuf out) throws IOException {
ByteBufUtils.writeString(out, uuid);
ByteBufUtils.writeString(out, name);
}
@Override
@@ -71,7 +71,6 @@ public final class CustomAbilitySavePacket extends AbstractPacket {
AbilityController.Instance.saveCustomAbility(ability);
NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player);
NBTTagCompound responseNbt = ability.writeNBT();
responseNbt.setString("_uuid", ability.getId());
GuiDataPacket.sendGuiData((EntityPlayerMP) player, responseNbt);
}
}
+31 -1
View File
@@ -2,11 +2,13 @@ package kamkeel.npcs.util;
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.function.Consumer;
import java.util.function.Supplier;
public class Register<T> {
@@ -36,6 +38,7 @@ public class Register<T> {
}
public static class Abilities extends Register<Ability> {
protected final Map<String, Map<String, Supplier<AbilityVariant>>> variantEntries = new LinkedHashMap<>();
protected final Map<String, String> uniqueNames = new LinkedHashMap<>();
protected final Set<String> typeOnly = new HashSet<>();
@@ -64,6 +67,23 @@ public class Register<T> {
return factory.get();
}
public AbilityVariant registerVariant(String typeId, String variantName, String group, Consumer<Ability> configurator) {
registerVariant(typeId, variantName, () -> new AbilityVariant(variantName, group, configurator));
return new AbilityVariant(variantName, group, configurator);
}
public AbilityVariant registerVariant(String typeId, String factoryName, Supplier<AbilityVariant> factory) {
if (!variantEntries.containsKey(typeId)) {
variantEntries.put(typeId, new HashMap<>());
}
Map<String, Supplier<AbilityVariant>> type = variantEntries.get(typeId);
String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_");
type.put(name, factory);
return factory.get();
}
public void register() {
for (Map.Entry<String, Supplier<Ability>> entry : entries.entrySet()) {
AbilityController.Instance.registerType(entry.getKey(), entry.getValue());
@@ -71,6 +91,14 @@ public class Register<T> {
AbilityController.Instance.registerAbility(uniqueNames.get(entry.getKey()), entry.getValue().get());
}
}
for (Map.Entry<String, Map<String, Supplier<AbilityVariant>>> entry : variantEntries.entrySet()) {
String typeId = entry.getKey();
Map<String, Supplier<AbilityVariant>> map = entry.getValue();
for (Map.Entry<String, Supplier<AbilityVariant>> innerEntry : map.entrySet()) {
AbilityController.Instance.registerVariant(typeId, innerEntry.getValue().get());
}
}
}
public static Register.Abilities create(String namespace, String displayName) {
@@ -117,7 +145,9 @@ public class Register<T> {
try {
String path = "/assets/" + namespace + "/" + animationsPath;
for (Map.Entry<String, Supplier<Animation>> entry : entries.entrySet()) {
String name = entry.getKey().substring(entry.getKey().indexOf(namespace + ".") + 1);
String prefix = registryKey + "." + namespace + ".";
String key = entry.getKey();
String name = key.substring(prefix.length());
AnimationController.Instance.loadBuiltInAnimation(modClass, path, name);
}
} catch (Exception e) {
@@ -0,0 +1,478 @@
package noppes.npcs;
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.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.List;
/**
* Shared base class for NPC and Player ability data management.
* Contains the common ability lifecycle logic: tick, phase transitions,
* animation/telegraph/sound management, cooldown state, and position/rotation locking.
* <p>
* Subclasses must implement abstract methods for entity-specific behavior:
* event firing, rotation/position application, telegraph packets, and cooldown rolling.
*
* @see DataAbilities - NPC implementation
* @see noppes.npcs.controllers.data.PlayerAbilityData - Player implementation
*/
public abstract class AbstractDataAbilities {
// ═══════════════════════════════════════════════════════════════════
// SHARED RUNTIME STATE (not saved to NBT)
// ═══════════════════════════════════════════════════════════════════
/** Currently executing ability (null if none) */
protected Ability currentAbility;
/** World time when cooldown ends */
protected long cooldownEndTime = 0;
/** Rotation lock state */
protected boolean rotationLocked = false;
protected float lockedYaw = 0;
protected float lockedPitch = 0;
/** Position lock state */
protected boolean positionLocked = false;
protected double lockedPosX = 0;
protected double lockedPosY = 0;
protected double lockedPosZ = 0;
// ═══════════════════════════════════════════════════════════════════
// ABSTRACT METHODS - Subclasses must implement
// ═══════════════════════════════════════════════════════════════════
/** Get the entity this ability data belongs to (NPC or Player). */
protected abstract EntityLivingBase getEntity();
/** Get the current target for ability execution. */
protected abstract EntityLivingBase getTarget();
/** Get the current world time. */
protected abstract long getWorldTime();
// -- Event Firing --
/** 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. */
protected abstract boolean fireExecuteEvent(Ability ability, EntityLivingBase target);
/** Fire complete event. */
protected abstract void fireCompleteEvent(Ability ability, EntityLivingBase target);
/** Fire interrupt event. */
protected abstract void fireInterruptEvent(Ability ability, EntityLivingBase target,
DamageSource source, float damage);
// -- Telegraph --
/** Spawn telegraphs for an ability. */
protected abstract void spawnTelegraph(Ability ability, EntityLivingBase target);
/** Remove telegraphs for an ability. */
protected abstract void removeTelegraph(Ability ability);
// -- Animation/Sound Data Access --
/** Set animation data on the entity. */
protected abstract void setAnimationData(Animation animation);
/** Clear animation data on the entity. */
protected abstract void clearAnimationData();
/** 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. */
protected abstract void captureLockedRotation();
// -- Completion --
/**
* Roll cooldown after ability completes.
* NPC: random(min, max) + ability offset. Player: ability's base cooldownTicks.
*/
protected abstract void rollCooldown(Ability ability);
/** Additional completion logic (NPC clears lastTarget, Player clears key/target and syncs). */
protected abstract void onAbilityComplete();
// -- Hooks (optional overrides) --
/** 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) {}
/** Additional lock releases in BURST_DELAY. NPC releases hitScan. */
protected void onBurstDelayReleaseLocks() {}
// ═══════════════════════════════════════════════════════════════════
// SHARED ANIMATION METHODS
// ═══════════════════════════════════════════════════════════════════
/**
* Play an animation on the entity.
*/
public void playAbilityAnimation(Animation animation) {
if (animation == null) return;
if (AnimationController.Instance == null) return;
setAnimationData(animation);
}
public void playAbilityAnimation(int animation) {
if (animation < 0) return;
if (AnimationController.Instance == null) return;
if (AnimationController.Instance.get(animation) == null) return;
playAbilityAnimation((Animation) AnimationController.Instance.get(animation));
}
public void playAbilityAnimation(String animation) {
if (animation.isEmpty()) return;
if (AnimationController.Instance == null) return;
if (AnimationController.Instance.get(animation, true) == null) return;
playAbilityAnimation((Animation) AnimationController.Instance.get(animation, true));
}
/**
* Stop any currently playing ability animation.
*/
protected void stopAbilityAnimation() {
clearAnimationData();
}
// ═══════════════════════════════════════════════════════════════════
// STATE QUERIES
// ═══════════════════════════════════════════════════════════════════
/** Check if an ability is currently executing. */
public boolean isExecutingAbility() {
return currentAbility != null && currentAbility.isExecuting();
}
/** Get the currently executing ability. */
public Ability getCurrentAbility() {
return currentAbility;
}
// ═══════════════════════════════════════════════════════════════════
// COOLDOWN MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
/** Check if on cooldown. */
public boolean isOnCooldown() {
return getWorldTime() < cooldownEndTime;
}
/** Get remaining cooldown ticks. */
public long getRemainingCooldown() {
long remaining = cooldownEndTime - getWorldTime();
return remaining > 0 ? remaining : 0;
}
/** Reset cooldown (allow immediate ability use). */
public void resetCooldown() {
cooldownEndTime = 0;
}
// ═══════════════════════════════════════════════════════════════════
// POSITION LOCKING
// ═══════════════════════════════════════════════════════════════════
/**
* Capture current position to lock entity in place.
*/
protected void captureLockedPosition() {
EntityLivingBase entity = getEntity();
lockedPosX = entity.posX;
lockedPosY = entity.posY;
lockedPosZ = entity.posZ;
positionLocked = true;
onPositionLockChanged(true);
}
/**
* Release position lock.
*/
protected void releaseLockedPosition() {
positionLocked = false;
onPositionLockChanged(false);
}
/** Hook for subclass to react to position lock state changes (e.g., NPC sets data watcher flag). */
protected void onPositionLockChanged(boolean locked) {}
// ═══════════════════════════════════════════════════════════════════
// ROTATION LOCKING
// ═══════════════════════════════════════════════════════════════════
/**
* Release rotation control.
*/
protected void releaseRotationControl() {
rotationLocked = false;
onRotationLockChanged(false);
}
/** 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
// ═══════════════════════════════════════════════════════════════════
/**
* Tick the currently executing ability. Handles all phase transitions,
* telegraph/animation/sound lifecycle, and burst management.
* <p>
* This is the core shared logic between DataAbilities and PlayerAbilityData.
* Subclass-specific behavior is injected via abstract methods and hooks.
*/
protected void tickCurrentAbility() {
EntityLivingBase entity = getEntity();
EntityLivingBase target = getTarget();
AbilityPhase oldPhase = currentAbility.getPhase();
// Tick advances time and possibly changes phase
boolean phaseChanged = currentAbility.tick();
// Fire tick event
fireTickEvent(currentAbility, target);
// Fire extender tick hook (e.g., per-tick resource drain)
if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, entity, target,
currentAbility.getPhase(), currentAbility.getCurrentTick())) {
interruptCurrentAbility(null, 0);
return;
}
// Handle phase-specific logic
switch (currentAbility.getPhase()) {
case WINDUP:
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
// Burst replay: re-enter windup - set up locks, telegraph, sound, animation
if (currentAbility.isRotationLockedDuringWindup()) {
captureLockedRotation();
}
if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) {
captureLockedPosition();
}
spawnTelegraph(currentAbility, target);
playAbilitySound(currentAbility.getWindUpSound());
playAbilityAnimation(currentAbility.getWindUpAnimation());
}
currentAbility.onWindUpTick(entity, target, currentAbility.getCurrentTick());
break;
case ACTIVE:
if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) {
// Just entered ACTIVE phase - lock telegraph positions
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
telegraph.lockPosition();
}
// Remove telegraph unless the ability keeps it during active phase
if (!currentAbility.keepTelegraphDuringActive()) {
removeTelegraph(currentAbility);
}
// Handle rotation control transition from WINDUP to ACTIVE
if (currentAbility.isRotationLockedDuringActive()) {
if (!rotationLocked) {
captureLockedRotation();
}
} else if (rotationLocked) {
releaseRotationControl();
}
// Handle position lock transition from WINDUP to ACTIVE
if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) {
if (!positionLocked) {
captureLockedPosition();
}
} else if (positionLocked) {
releaseLockedPosition();
}
// Play active sound and animation
if (!currentAbility.keepTelegraphDuringActive()) {
playAbilitySound(currentAbility.getActiveSound());
}
playAbilityAnimation(currentAbility.getActiveAnimation());
// Pre-execute hook (NPC: snap to face target for hit scan)
onPreExecute(currentAbility, target);
// Fire execute event (cancelable)
if (fireExecuteEvent(currentAbility, target)) {
return; // Cancelled
}
// Call onExecute
currentAbility.onExecute(entity, target);
// Check if ability completed during onExecute
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(target);
return;
}
}
currentAbility.onActiveTick(entity, target, currentAbility.getCurrentTick());
// Check if ability completed during onActiveTick
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(target);
return;
}
// Auto-complete for burst overlap mode
if (currentAbility.isBurstEnabled()
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
currentAbility.signalCompletion();
}
// Check if ability entered burst delay
if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) {
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
onBurstDelayReleaseLocks();
}
break;
case BURST_DELAY:
// Free movement and rotation during burst delay
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
break;
case DAZED:
// Dazed phase - just wait for phase transition
break;
case IDLE:
// Ability completed (reached via tick() from DAZED)
handleAbilityCompletion(target);
break;
}
// Post-phase hook (NPC: hit scan + movement control)
onPostPhaseTick(currentAbility, target);
}
// ═══════════════════════════════════════════════════════════════════
// ABILITY COMPLETION
// ═══════════════════════════════════════════════════════════════════
/**
* Handle ability completion. Called when ability phase becomes IDLE.
*/
protected void handleAbilityCompletion(EntityLivingBase target) {
if (currentAbility == null) return;
// Remove any remaining telegraphs
removeTelegraph(currentAbility);
// Fire extender complete hook
AbilityController.Instance.fireOnAbilityComplete(currentAbility, getEntity(), target, false);
// Call onComplete callback
currentAbility.onComplete(getEntity(), target);
// Fire complete event
fireCompleteEvent(currentAbility, target);
// Release locks
releaseRotationControl();
releaseLockedPosition();
// Subclass-specific completion (cooldown, cleanup, sync)
rollCooldown(currentAbility);
stopAbilityAnimation();
onAbilityComplete();
}
// ═══════════════════════════════════════════════════════════════════
// EXECUTE IMMEDIATE
// ═══════════════════════════════════════════════════════════════════
/**
* Execute an ability immediately (no windup). Called when windUpTicks is 0.
*/
protected void executeImmediate(Ability ability, EntityLivingBase target) {
// Play active sound and animation
playAbilitySound(ability.getActiveSound());
playAbilityAnimation(ability.getActiveAnimation());
// Fire execute event (cancelable)
if (fireExecuteEvent(ability, target)) {
return; // Cancelled
}
// Call onExecute
ability.onExecute(getEntity(), target);
// Check if ability completed during onExecute
if (ability.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(target);
}
}
// ═══════════════════════════════════════════════════════════════════
// INTERRUPTION
// ═══════════════════════════════════════════════════════════════════
/**
* Interrupt the currently executing ability.
* Shared interrupt logic: remove telegraph, play dazed animation,
* release locks, fire events, call ability.interrupt().
*/
public void interruptCurrentAbility(DamageSource source, float damage) {
if (currentAbility != null && currentAbility.isExecuting()) {
// Remove telegraph
removeTelegraph(currentAbility);
// Fire extender complete hook (interrupted)
AbilityController.Instance.fireOnAbilityComplete(currentAbility, getEntity(), getTarget(), true);
// Fire interrupt event
fireInterruptEvent(currentAbility, getTarget(), source, damage);
// Call ability callbacks
currentAbility.onInterrupt(getEntity(), source, damage);
currentAbility.interrupt();
// Play dazed animation
stopAbilityAnimation();
playAbilityAnimation(currentAbility.getDazedAnimation());
// Release locks
releaseRotationControl();
releaseLockedPosition();
// Let subclass handle cleanup
onInterruptComplete();
}
}
/**
* Hook called after interrupt completes. Subclass handles clearing state and syncing.
* NPC: keeps currentAbility (ticks through DAZED). Player: clears immediately.
*/
protected void onInterruptComplete() {}
}
+7 -1
View File
@@ -92,6 +92,9 @@ 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;
@@ -294,6 +297,9 @@ public class CustomNpcs {
registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 64, 3, true);
registerNewEntity(EntityAbilitySweeper.class, "abilitysweeper", 64, 3, true);
registerNewEntity(EntityAbilityZone.class, "abilityzone", 64, 3, true);
registerNewEntity(EntityEnergyDome.class, "energydome", 64, 3, true);
registerNewEntity(EntityEnergyPanel.class, "energypanel", 64, 3, true);
registerNewEntity(EntityEnergySlicer.class, "energyslicer", 64, 3, true);
new RecipeController();
@@ -338,6 +344,7 @@ public class CustomNpcs {
clientJaninoCompiler = null;
Server = event.getServer();
AnimationController.Instance.load();
ChunkController.Instance.clear();
FactionController.getInstance().load();
MagicController.getInstance().load();
@@ -347,7 +354,6 @@ public class CustomNpcs {
new GlobalDataController();
new SpawnController();
new LinkedNpcController();
new AnimationController();
AbilityController.Instance.load();
TelegraphController.init();
+193 -493
View File
@@ -12,22 +12,20 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.DamageSource;
import noppes.npcs.controllers.AnimationController;
import noppes.npcs.controllers.data.Animation;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.scripted.NpcAPI;
import noppes.npcs.scripted.event.AbilityEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Manages NPC abilities - storage, selection, execution, and cooldowns.
* Follows the DataStats/DataAI pattern for NPC data management.
* Extends AbstractDataAbilities for shared ability lifecycle logic.
*/
public class DataAbilities {
public class DataAbilities extends AbstractDataAbilities {
private final EntityNPCInterface npc;
private final Random random = new Random();
@@ -57,19 +55,9 @@ public class DataAbilities {
public int maxCooldown = 200;
// ═══════════════════════════════════════════════════════════════════
// RUNTIME STATE (not saved)
// NPC-SPECIFIC RUNTIME STATE (not saved)
// ═══════════════════════════════════════════════════════════════════
/**
* Currently executing ability (null if none)
*/
private transient Ability currentAbility;
/**
* World time when NPC cooldown ends (can select next ability)
*/
private transient long cooldownEndTime = 0;
/**
* Last target used for ability execution
*/
@@ -81,33 +69,26 @@ public class DataAbilities {
private transient List<Long> recentHitTimes = new ArrayList<>();
/**
* Locked rotation values for ACTIVE phase when movement is locked.
* Stored when entering ACTIVE phase, applied every tick to prevent rotation changes.
* NPC-specific locked rotation fields (yawHead, renderYawOffset).
* Base class handles lockedYaw and lockedPitch.
*/
private transient boolean rotationLocked = false;
private transient float lockedYaw = 0;
private transient float lockedYawHead = 0;
private transient float lockedRenderYawOffset = 0;
private transient float lockedPitch = 0;
/**
* Locked position values for movement lock.
* Stored when entering a locked phase, applied after super.onLivingUpdate()
* to override any position changes from AI/physics.
*/
private transient boolean positionLocked = false;
private transient double lockedPosX = 0;
private transient double lockedPosY = 0;
private transient double lockedPosZ = 0;
/**
* Hit scan state - forces NPC to face target.
* Set during tickCurrentAbility(), applied after super.onLivingUpdate()
* Set during onPostPhaseTick(), applied after super.onLivingUpdate()
* to override AI look helper rotation.
*/
private transient boolean hitScanActive = false;
private transient EntityLivingBase hitScanTarget = null;
/** 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 */
private static final int POSITION_LOCKED_FLAG = 32;
// ═══════════════════════════════════════════════════════════════════
// CONSTRUCTOR
// ═══════════════════════════════════════════════════════════════════
@@ -116,6 +97,170 @@ public class DataAbilities {
this.npc = npc;
}
// ═══════════════════════════════════════════════════════════════════
// ABSTRACT METHOD IMPLEMENTATIONS
// ═══════════════════════════════════════════════════════════════════
@Override
protected EntityLivingBase getEntity() {
return npc;
}
@Override
protected EntityLivingBase getTarget() {
return lastTarget != null ? lastTarget : npc.getAttackTarget();
}
@Override
protected long getWorldTime() {
return npc.worldObj.getTotalWorldTime();
}
@Override
protected void fireTickEvent(Ability ability, EntityLivingBase target) {
AbilityEvent.TickEvent event = new AbilityEvent.TickEvent(
npc.wrappedNPC, ability, target,
ability.getPhase().ordinal(), ability.getCurrentTick());
NpcAPI.EVENT_BUS.post(event);
}
@Override
protected boolean fireExecuteEvent(Ability ability, EntityLivingBase target) {
AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent(
npc.wrappedNPC, ability, target);
return NpcAPI.EVENT_BUS.post(executeEvent);
}
@Override
protected void fireCompleteEvent(Ability ability, EntityLivingBase target) {
AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent(
npc.wrappedNPC, ability, target);
NpcAPI.EVENT_BUS.post(completeEvent);
}
@Override
protected void fireInterruptEvent(Ability ability, EntityLivingBase target,
DamageSource source, float damage) {
AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent(
npc.wrappedNPC, ability, target, source, damage);
NpcAPI.EVENT_BUS.post(interruptEvent);
}
@Override
protected void spawnTelegraph(Ability ability, EntityLivingBase target) {
List<TelegraphInstance> telegraphs = ability.createTelegraphs(npc, target);
if (!telegraphs.isEmpty()) {
ability.setTelegraphInstances(telegraphs);
for (TelegraphInstance telegraph : telegraphs) {
TelegraphSpawnPacket.sendToTracking(telegraph, npc);
}
}
}
@Override
protected void removeTelegraph(Ability ability) {
List<TelegraphInstance> telegraphs = ability.getTelegraphInstances();
for (TelegraphInstance telegraph : telegraphs) {
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), npc);
}
ability.setTelegraphInstances(null);
}
@Override
protected void setAnimationData(Animation animation) {
npc.display.animationData.setEnabled(true);
npc.display.animationData.setAnimation(animation);
npc.display.animationData.updateClient();
}
@Override
protected void clearAnimationData() {
npc.display.animationData.setAnimation(null);
npc.display.animationData.updateClient();
}
@Override
protected void playAbilitySound(String sound) {
if (sound != null && !sound.isEmpty()) {
npc.worldObj.playSoundAtEntity(npc, sound, 1.0f, 1.0f);
}
}
@Override
protected void captureLockedRotation() {
lockedYaw = npc.rotationYaw;
lockedYawHead = npc.rotationYawHead;
lockedRenderYawOffset = npc.renderYawOffset;
lockedPitch = npc.rotationPitch;
rotationLocked = true;
npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG);
}
@Override
protected void rollCooldown(Ability ability) {
int baseCooldown = minCooldown;
if (maxCooldown > minCooldown) {
baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1);
}
cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown;
// Add ability-specific cooldown offset
cooldownEndTime += ability.getCooldownTicks();
}
@Override
protected void onAbilityComplete() {
currentAbility = null;
lastTarget = null;
}
// ═══════════════════════════════════════════════════════════════════
// HOOK OVERRIDES
// ═══════════════════════════════════════════════════════════════════
@Override
protected void onPreExecute(Ability ability, EntityLivingBase target) {
// Snap NPC to face target before execute so startMoving() reads correct rotation
if (ability.isHitScanForCurrentPhase() && target != null) {
faceTarget(target);
}
}
@Override
protected void onPostPhaseTick(Ability ability, EntityLivingBase target) {
// 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) {
enableHitScan(target);
} else if (hitScanActive) {
releaseRotationControl();
}
// Apply movement control if ability is still executing
if (ability != null && ability.isExecuting()) {
applyMovementControl();
}
}
@Override
protected void onBurstDelayReleaseLocks() {
if (hitScanActive) releaseRotationControl();
}
@Override
protected void onPositionLockChanged(boolean locked) {
npc.setBoolFlag(locked, POSITION_LOCKED_FLAG);
}
@Override
protected void onRotationLockChanged(boolean locked) {
if (!locked) {
hitScanActive = false;
hitScanTarget = null;
}
npc.setBoolFlag(locked, ROTATION_CONTROLLED_FLAG);
}
// ═══════════════════════════════════════════════════════════════════
// TICK - Called from EntityNPCInterface.onLivingUpdate()
// ═══════════════════════════════════════════════════════════════════
@@ -152,173 +297,15 @@ public class DataAbilities {
if (positionLocked) releaseLockedPosition();
}
// Tick current ability if executing
// Tick current ability if executing (shared logic from base class)
if (currentAbility != null && currentAbility.isExecuting()) {
tickCurrentAbility();
}
}
/**
* Tick the currently executing ability.
*/
private void tickCurrentAbility() {
EntityLivingBase target = lastTarget != null ? lastTarget : npc.getAttackTarget();
AbilityPhase oldPhase = currentAbility.getPhase();
// Tick advances time and possibly changes phase
boolean phaseChanged = currentAbility.tick();
// Fire tick event for scripts
fireTickEvent(currentAbility, target);
// Fire extender tick hook (e.g., per-tick resource drain)
if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, npc, target,
currentAbility.getPhase(), currentAbility.getCurrentTick())) {
interruptCurrentAbility(null, 0);
return;
}
// Handle phase-specific logic
switch (currentAbility.getPhase()) {
case WINDUP:
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
// Burst replay: re-enter windup - set up telegraph, sound, animation, locks
if (currentAbility.isRotationLockedDuringWindup()) {
captureLockedRotation();
}
if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) {
captureLockedPosition();
}
spawnTelegraph(currentAbility, target);
playAbilitySound(currentAbility.getWindUpSound());
playAbilityAnimation(currentAbility.getWindUpAnimation());
}
currentAbility.onWindUpTick(npc, target, currentAbility.getCurrentTick());
break;
case ACTIVE:
if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) {
// Just entered ACTIVE phase - lock telegraph positions if they were following
// This commits the ability to its current target position
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
telegraph.lockPosition();
}
// Remove telegraph unless the ability keeps it during active phase
// (e.g., sweeping attacks where the telegraph shows the ongoing danger zone)
if (!currentAbility.keepTelegraphDuringActive()) {
removeTelegraph(currentAbility);
}
// Handle rotation control transition from WINDUP to ACTIVE
if (currentAbility.isRotationLockedDuringActive()) {
if (!rotationLocked) {
captureLockedRotation();
}
} else if (rotationLocked) {
releaseRotationControl();
}
// Handle position lock transition from WINDUP to ACTIVE
if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) {
if (!positionLocked) {
captureLockedPosition();
}
} else if (positionLocked) {
releaseLockedPosition();
}
// Play active sound and animation
// Abilities that keep telegraph during active manage their own sound timing
if (!currentAbility.keepTelegraphDuringActive()) {
playAbilitySound(currentAbility.getActiveSound());
}
playAbilityAnimation(currentAbility.getActiveAnimation());
// Snap NPC to face target before execute so startMoving() reads correct rotation
if (currentAbility.isHitScanForCurrentPhase() && target != null) {
faceTarget(target);
}
// Fire execute event (cancelable)
AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent(
npc.wrappedNPC, currentAbility, target);
if (NpcAPI.EVENT_BUS.post(executeEvent)) {
// Event was cancelled - skip execution but continue to recovery
return;
}
// Call onExecute
currentAbility.onExecute(npc, target);
// Check if ability completed during onExecute (signalCompletion was called)
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(target);
return;
}
}
currentAbility.onActiveTick(npc, target, currentAbility.getCurrentTick());
// Check if ability completed during onActiveTick (signalCompletion was called)
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(target);
return;
}
// Auto-complete for burst overlap mode (entities fly independently)
// This allows entity-spawning abilities to immediately chain next burst
// without each type needing to check burst state in their onActiveTick()
// Wait until all staggered projectiles have been fired before auto-completing
if (currentAbility.isBurstEnabled()
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
currentAbility.signalCompletion();
}
// Check if ability entered burst delay during onActiveTick or overlap auto-complete
if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) {
// Release all locks - free movement/rotation during delay
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
if (hitScanActive) releaseRotationControl();
}
break;
case BURST_DELAY:
// Free movement and rotation during burst delay
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
break;
case DAZED:
// Dazed phase - NPC cannot attack, just wait for daze to end
break;
case IDLE:
// Ability completed (reached via tick() phase transition from DAZED)
handleAbilityCompletion(target);
break;
}
// Update hit scan state - actual facing is deferred to applyRotationControl()
// which runs AFTER super.onLivingUpdate() to override AI look helper
if (currentAbility != null && currentAbility.isExecuting()
&& currentAbility.isHitScanForCurrentPhase() && target != null) {
enableHitScan(target);
} else if (hitScanActive) {
releaseRotationControl();
}
// Apply movement control if ability is still executing
if (currentAbility != null && currentAbility.isExecuting()) {
applyMovementControl();
}
}
/**
* Apply movement control based on lock movement settings.
* Called every tick during ability execution.
* Called every tick during ability execution via onPostPhaseTick().
*/
private void applyMovementControl() {
if (currentAbility == null) return;
@@ -337,61 +324,6 @@ public class DataAbilities {
}
}
/**
* Handle ability completion - called when ability phase becomes IDLE.
* This can happen from:
* - signalCompletion() called in onExecute() or onActiveTick()
* - tick() transitioning from DAZED to IDLE
*/
private void handleAbilityCompletion(EntityLivingBase target) {
if (currentAbility == null) return;
// Remove any remaining telegraphs (for abilities that keep telegraph during active)
removeTelegraph(currentAbility);
// Fire extender complete hook
AbilityController.Instance.fireOnAbilityComplete(currentAbility, npc, target, false);
// Call onComplete callback
currentAbility.onComplete(npc, target);
// Fire complete event
AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent(
npc.wrappedNPC, currentAbility, target);
NpcAPI.EVENT_BUS.post(completeEvent);
// Release locks before cleanup
releaseRotationControl();
releaseLockedPosition();
// Clean up and roll cooldown (this sets currentAbility to null)
onAbilityComplete();
}
/**
* Called when an ability completes.
* Calculates cooldown as: random(minCooldown, maxCooldown) + ability's cooldown offset
*/
private void onAbilityComplete() {
if (currentAbility != null) {
// Stop any ability animation
stopAbilityAnimation();
// Release locks
releaseRotationControl();
releaseLockedPosition();
// Calculate cooldown: random(min, max) + ability offset
int abilityCooldownOffset = currentAbility.getCooldownTicks();
rollCooldown();
// Add ability-specific cooldown offset
cooldownEndTime += abilityCooldownOffset;
currentAbility = null;
lastTarget = null;
}
}
// ═══════════════════════════════════════════════════════════════════
// ABILITY SELECTION - Called from CombatHandler
// ═══════════════════════════════════════════════════════════════════
@@ -521,7 +453,13 @@ public class DataAbilities {
ability.start(target);
if (ability.getPhase() == AbilityPhase.ACTIVE) {
// Windup was 0 — skip telegraph/windup and go straight to 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
@@ -539,137 +477,6 @@ public class DataAbilities {
return true;
}
/**
* Execute an ability immediately (no windup).
* Called when windUpTicks is 0.
*/
private void executeImmediate(Ability ability, EntityLivingBase target) {
// Lock rotation if rotation is locked during ACTIVE phase
if (ability.isRotationLockedDuringActive()) {
captureLockedRotation();
}
if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) {
captureLockedPosition();
}
// Play active sound and animation
playAbilitySound(ability.getActiveSound());
playAbilityAnimation(ability.getActiveAnimation());
// Fire execute event (cancelable)
AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent(
npc.wrappedNPC, ability, target);
if (NpcAPI.EVENT_BUS.post(executeEvent)) {
return;
}
// Call onExecute
ability.onExecute(npc, target);
// Check if ability completed during onExecute
if (ability.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(target);
}
}
/**
* Play a sound at the NPC's location.
*/
private void playAbilitySound(String sound) {
if (sound != null && !sound.isEmpty()) {
npc.worldObj.playSoundAtEntity(npc, sound, 1.0f, 1.0f);
}
}
/**
* Play an animation on the NPC by ID.
* Public so abilities can trigger animations directly if needed.
*
* @param animation The animation to play, or null if none
*/
public void playAbilityAnimation(Animation animation) {
if (animation == null) return;
if (AnimationController.Instance == null) return;
npc.display.animationData.setEnabled(true);
npc.display.animationData.setAnimation(animation);
npc.display.animationData.updateClient();
}
public void playAbilityAnimation(int animation) {
if (animation < 0) return;
if (AnimationController.Instance == null) return;
if (AnimationController.Instance.get(animation) == null) return;
playAbilityAnimation((Animation) AnimationController.Instance.get(animation));
}
public void playAbilityAnimation(String animation) {
if (animation.isEmpty()) return;
if (AnimationController.Instance == null) return;
if (AnimationController.Instance.get(animation, true) == null) return;
playAbilityAnimation((Animation) AnimationController.Instance.get(animation, true));
}
/**
* Stop any currently playing ability animation.
*/
private void stopAbilityAnimation() {
npc.display.animationData.setAnimation(null);
npc.display.animationData.updateClient();
}
/**
* Spawn and send telegraphs for an ability.
*/
private void spawnTelegraph(Ability ability, EntityLivingBase target) {
List<TelegraphInstance> telegraphs = ability.createTelegraphs(npc, target);
if (!telegraphs.isEmpty()) {
ability.setTelegraphInstances(telegraphs);
for (TelegraphInstance telegraph : telegraphs) {
TelegraphSpawnPacket.sendToTracking(telegraph, npc);
}
}
}
/**
* Remove telegraphs when ability ends or is interrupted.
*/
private void removeTelegraph(Ability ability) {
List<TelegraphInstance> telegraphs = ability.getTelegraphInstances();
for (TelegraphInstance telegraph : telegraphs) {
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), npc);
}
ability.setTelegraphInstances(null);
}
// ═══════════════════════════════════════════════════════════════════
// COOLDOWN MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
/**
* Check if NPC is on cooldown (cannot use any ability).
*/
public boolean isOnCooldown() {
return npc.worldObj.getTotalWorldTime() < cooldownEndTime;
}
/**
* Get remaining cooldown ticks.
*/
public long getRemainingCooldown() {
long remaining = cooldownEndTime - npc.worldObj.getTotalWorldTime();
return remaining > 0 ? remaining : 0;
}
/**
* Reset cooldown (allow immediate ability use).
*/
public void resetCooldown() {
cooldownEndTime = 0;
}
// ═══════════════════════════════════════════════════════════════════
// INTERRUPTION - Called from CombatHandler on damage
// ═══════════════════════════════════════════════════════════════════
@@ -748,39 +555,6 @@ public class DataAbilities {
return count;
}
/**
* Interrupt the currently executing ability.
* If interrupted during WINDUP, the ability will transition to DAZED phase.
* When DAZED completes, the ability will move to IDLE and onAbilityComplete() is called.
*/
public void interruptCurrentAbility(DamageSource source, float damage) {
if (currentAbility != null) {
// Remove telegraph early
removeTelegraph(currentAbility);
// Stop any ability animation
stopAbilityAnimation();
// Release locks
releaseRotationControl();
releaseLockedPosition();
// Fire extender complete hook (interrupted)
AbilityController.Instance.fireOnAbilityComplete(currentAbility, npc, lastTarget, true);
// Fire interrupt event
AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent(
npc.wrappedNPC, currentAbility, lastTarget, source, damage);
NpcAPI.EVENT_BUS.post(interruptEvent);
currentAbility.onInterrupt(npc, source, damage);
currentAbility.interrupt(); // Transitions to DAZED if interrupted during WINDUP
// Don't clear currentAbility - let it tick through DAZED phase
// When DAZED ends and phase becomes IDLE, onAbilityComplete() will be called
}
}
/**
* Force stop the current ability (no interrupt event).
*/
@@ -849,16 +623,6 @@ public class DataAbilities {
// SCRIPT EVENTS
// ═══════════════════════════════════════════════════════════════════
/**
* Fire the ability tick event for scripts.
*/
private void fireTickEvent(Ability ability, EntityLivingBase target) {
AbilityEvent.TickEvent event = new AbilityEvent.TickEvent(
npc.wrappedNPC, ability, target,
ability.getPhase().ordinal(), ability.getCurrentTick());
NpcAPI.EVENT_BUS.post(event);
}
/**
* Fire an ability hit event. Called by abilities when they hit an entity.
* Returns null if the event was cancelled, otherwise returns the (possibly modified) event.
@@ -902,7 +666,7 @@ public class DataAbilities {
stopCurrentAbility();
// Roll cooldown so NPC doesn't immediately attack after reset
rollCooldown();
rollCooldownOnReset();
// Reset execution state on all resolved abilities
for (Ability ability : getAbilities()) {
@@ -911,10 +675,10 @@ public class DataAbilities {
}
/**
* Roll a new cooldown using the min/max range.
* Called after ability completes and on reset.
* Roll a new cooldown using the min/max range (without ability offset).
* Called on reset when no ability is involved.
*/
private void rollCooldown() {
private void rollCooldownOnReset() {
int baseCooldown = minCooldown;
if (maxCooldown > minCooldown) {
baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1);
@@ -950,7 +714,7 @@ public class DataAbilities {
abilitySlots.add(AbilitySlot.inline(ability));
}
/** Add a reference ability by key (built-in name or custom UUID). */
/** Add a reference ability by key (built-in name or custom ability name). */
public void addAbilityReference(String key) {
abilitySlots.add(AbilitySlot.reference(key));
}
@@ -1002,23 +766,9 @@ public class DataAbilities {
}
// ═══════════════════════════════════════════════════════════════════
// STATE QUERIES
// STATE QUERIES (NPC-specific)
// ═══════════════════════════════════════════════════════════════════
/**
* Check if NPC is currently executing an ability.
*/
public boolean isExecutingAbility() {
return currentAbility != null && currentAbility.isExecuting();
}
/**
* Get the currently executing ability.
*/
public Ability getCurrentAbility() {
return currentAbility;
}
/**
* Check if ability is controlling NPC movement (AI pathfinding should be blocked).
* Called by AI tasks to determine if they should try to path.
@@ -1086,35 +836,9 @@ public class DataAbilities {
}
// ═══════════════════════════════════════════════════════════════════
// ROTATION CONTROL
// ROTATION CONTROL (NPC-specific: hit scan + 4-field rotation)
// ═══════════════════════════════════════════════════════════════════
/** Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 */
private static final int ROTATION_CONTROLLED_FLAG = 16;
/**
* Capture current rotation values to lock NPC's look direction.
* Called when entering a LOCKED rotation phase.
*/
private void captureLockedRotation() {
lockedYaw = npc.rotationYaw;
lockedYawHead = npc.rotationYawHead;
lockedRenderYawOffset = npc.renderYawOffset;
lockedPitch = npc.rotationPitch;
rotationLocked = true;
npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG);
}
/**
* Release rotation control (both locked and hit scan).
*/
private void releaseRotationControl() {
rotationLocked = false;
hitScanActive = false;
hitScanTarget = null;
npc.setBoolFlag(false, ROTATION_CONTROLLED_FLAG);
}
/**
* Enable hit scan tracking for the given target.
*/
@@ -1195,33 +919,9 @@ public class DataAbilities {
}
// ═══════════════════════════════════════════════════════════════════
// POSITION LOCKING
// POSITION LOCKING (NPC-specific: data watcher + client-side logic)
// ═══════════════════════════════════════════════════════════════════
/** Bit flag for position lock in data watcher slot 15 */
private static final int POSITION_LOCKED_FLAG = 32;
/**
* Capture current position to lock NPC in place.
* Called when entering a phase with movement lock.
*/
private void captureLockedPosition() {
lockedPosX = npc.posX;
lockedPosY = npc.posY;
lockedPosZ = npc.posZ;
positionLocked = true;
npc.setBoolFlag(true, POSITION_LOCKED_FLAG);
}
/**
* Release the position lock.
* Called when leaving the locked phase or ability completes.
*/
private void releaseLockedPosition() {
positionLocked = false;
npc.setBoolFlag(false, POSITION_LOCKED_FLAG);
}
/**
* Apply locked position values to the NPC.
* Called AFTER super.onLivingUpdate() in EntityNPCInterface to override
+54 -6
View File
@@ -1,7 +1,7 @@
package noppes.npcs;
import cpw.mods.fml.common.eventhandler.Event;
import kamkeel.npcs.entity.EntityAbilityProjectile;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.network.packets.data.AchievementPacket;
import net.minecraft.command.ICommandSender;
import net.minecraft.entity.Entity;
@@ -455,7 +455,7 @@ public class EventHooks {
// ==================== ENERGY PROJECTILE EVENTS ====================
public static void onEnergyProjectileFired(EntityAbilityProjectile projectile) {
public static void onEnergyProjectileFired(EntityEnergyProjectile projectile) {
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
EnergyProjectileEvent.FiredEvent event = new EnergyProjectileEvent.FiredEvent(wrapped);
ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_FIRED, event);
@@ -473,7 +473,7 @@ public class EventHooks {
NpcAPI.EVENT_BUS.post(event);
}
public static void onEnergyProjectileTick(EntityAbilityProjectile projectile) {
public static void onEnergyProjectileTick(EntityEnergyProjectile projectile) {
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
EnergyProjectileEvent.UpdateEvent event = new EnergyProjectileEvent.UpdateEvent(wrapped);
ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_TICK, event);
@@ -494,7 +494,7 @@ public class EventHooks {
/**
* Fires the entity impact event. Returns the (possibly modified) damage, or -1 if cancelled.
*/
public static float onEnergyProjectileEntityImpact(EntityAbilityProjectile projectile, EntityLivingBase target, float damage) {
public static float onEnergyProjectileEntityImpact(EntityEnergyProjectile projectile, EntityLivingBase target, float damage) {
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
IEntity wrappedTarget = NpcAPI.Instance().getIEntity(target);
EnergyProjectileEvent.EntityImpactEvent event = new EnergyProjectileEvent.EntityImpactEvent(wrapped, wrappedTarget, damage);
@@ -518,7 +518,7 @@ public class EventHooks {
return event.getDamage();
}
public static void onEnergyProjectileBlockImpact(EntityAbilityProjectile projectile, int blockX, int blockY, int blockZ) {
public static void onEnergyProjectileBlockImpact(EntityEnergyProjectile projectile, int blockX, int blockY, int blockZ) {
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
IPos pos = NpcAPI.Instance().getIPos(blockX, blockY, blockZ);
IWorld world = NpcAPI.Instance().getIWorld(projectile.worldObj);
@@ -539,7 +539,7 @@ public class EventHooks {
NpcAPI.EVENT_BUS.post(event);
}
public static void onEnergyProjectileExpired(EntityAbilityProjectile projectile) {
public static void onEnergyProjectileExpired(EntityEnergyProjectile projectile) {
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
EnergyProjectileEvent.ExpiredEvent event = new EnergyProjectileEvent.ExpiredEvent(wrapped);
ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_EXPIRED, event);
@@ -557,6 +557,54 @@ public class EventHooks {
NpcAPI.EVENT_BUS.post(event);
}
// ==================== ENERGY BARRIER EVENTS ====================
private static void dispatchBarrierEvent(net.minecraft.entity.Entity barrierEntity, int ownerEntityId,
noppes.npcs.scripted.event.EnergyBarrierEvent event, EnumScriptType type) {
ScriptController.Instance.globalNpcScripts.callScript(type, event);
net.minecraft.entity.Entity owner = barrierEntity.worldObj.getEntityByID(ownerEntityId);
if (owner instanceof EntityNPCInterface) {
((EntityNPCInterface) owner).script.callScript(type, event);
} else if (owner instanceof EntityPlayer) {
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner);
if (handler != null) {
handler.callScript(type, event);
}
}
NpcAPI.EVENT_BUS.post(event);
}
public static void onEnergyBarrierSpawned(net.minecraft.entity.Entity barrierEntity, int ownerEntityId) {
noppes.npcs.api.entity.IEnergyBarrier wrapped = (noppes.npcs.api.entity.IEnergyBarrier) NpcAPI.Instance().getIEntity(barrierEntity);
noppes.npcs.scripted.event.EnergyBarrierEvent.SpawnedEvent event = new noppes.npcs.scripted.event.EnergyBarrierEvent.SpawnedEvent(wrapped);
dispatchBarrierEvent(barrierEntity, ownerEntityId, event, EnumScriptType.ENERGY_BARRIER_SPAWNED);
}
public static void onEnergyBarrierTick(net.minecraft.entity.Entity barrierEntity, int ownerEntityId) {
noppes.npcs.api.entity.IEnergyBarrier wrapped = (noppes.npcs.api.entity.IEnergyBarrier) NpcAPI.Instance().getIEntity(barrierEntity);
noppes.npcs.scripted.event.EnergyBarrierEvent.UpdateEvent event = new noppes.npcs.scripted.event.EnergyBarrierEvent.UpdateEvent(wrapped);
dispatchBarrierEvent(barrierEntity, ownerEntityId, event, EnumScriptType.ENERGY_BARRIER_TICK);
}
public static float onEnergyBarrierHit(net.minecraft.entity.Entity barrierEntity, int ownerEntityId,
EntityEnergyProjectile projectile, float damage) {
noppes.npcs.api.entity.IEnergyBarrier wrappedBarrier = (noppes.npcs.api.entity.IEnergyBarrier) NpcAPI.Instance().getIEntity(barrierEntity);
noppes.npcs.api.entity.IEnergyProjectile wrappedProjectile = projectile != null
? (noppes.npcs.api.entity.IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile) : null;
noppes.npcs.scripted.event.EnergyBarrierEvent.HitEvent event = new noppes.npcs.scripted.event.EnergyBarrierEvent.HitEvent(wrappedBarrier, wrappedProjectile, damage);
dispatchBarrierEvent(barrierEntity, ownerEntityId, event, EnumScriptType.ENERGY_BARRIER_HIT);
if (event.isCanceled()) return -1;
return event.getDamage();
}
public static void onEnergyBarrierDestroyed(net.minecraft.entity.Entity barrierEntity, int ownerEntityId) {
noppes.npcs.api.entity.IEnergyBarrier wrapped = (noppes.npcs.api.entity.IEnergyBarrier) NpcAPI.Instance().getIEntity(barrierEntity);
noppes.npcs.scripted.event.EnergyBarrierEvent.DestroyedEvent event = new noppes.npcs.scripted.event.EnergyBarrierEvent.DestroyedEvent(wrapped);
dispatchBarrierEvent(barrierEntity, ownerEntityId, event, EnumScriptType.ENERGY_BARRIER_DESTROYED);
}
public static void onPlayerInit(PlayerDataScript handler, IPlayer player) {
PlayerEvent.InitEvent event = new PlayerEvent.InitEvent(player);
handler.callScript(EnumScriptType.INIT, event);
@@ -781,8 +781,9 @@ public class NoppesUtilServer {
if (controller != null) {
int index = 0;
for (String typeId : controller.getTypes()) {
// Exclude built-in types from the NPC type list
if (controller.isBuiltInType(typeId)) continue;
if (!controller.isAllowedByNPC(typeId) && !controller.isAllowedByBoth(typeId))
continue;
map.put(typeId, index++);
}
}
@@ -809,13 +810,10 @@ public class NoppesUtilServer {
kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance;
if (controller != null) {
int index = 0;
for (String uuid : controller.getCustomAbilityIds()) {
Ability ability = controller.getCustomAbility(uuid);
for (String name : controller.getCustomAbilityNames()) {
Ability ability = controller.getCustomAbility(name);
if (ability == null || !ability.getAllowedBy().allowsNpc()) continue;
String name = controller.getCustomAbilityName(uuid);
if (name == null || name.isEmpty()) name = uuid;
map.put(name + "\t" + uuid, index++);
map.put(name, index++);
}
}
sendScrollData(player, map, EnumScrollData.CUSTOM_ABILITIES);
@@ -154,6 +154,9 @@ 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;
@@ -161,6 +164,9 @@ 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;
@@ -275,6 +281,9 @@ public class ClientProxy extends CommonProxy {
RenderingRegistry.registerEntityRenderingHandler(EntityAbilityBeam.class, new RenderAbilityBeam());
RenderingRegistry.registerEntityRenderingHandler(EntityAbilitySweeper.class, new RenderAbilitySweeper());
RenderingRegistry.registerEntityRenderingHandler(EntityAbilityZone.class, new RenderAbilityZone());
RenderingRegistry.registerEntityRenderingHandler(EntityEnergyDome.class, new RenderEnergyDome());
RenderingRegistry.registerEntityRenderingHandler(EntityEnergyPanel.class, new RenderEnergyPanel());
RenderingRegistry.registerEntityRenderingHandler(EntityEnergySlicer.class, new RenderEnergySlicer());
RenderingRegistry.registerEntityRenderingHandler(EntityCustomNpc.class, new RenderCustomNpc());
@@ -21,7 +21,6 @@ import noppes.npcs.constants.EnumScrollData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.Vector;
/**
@@ -33,10 +32,8 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL
private final SubGuiAbilityConfig parentConfig;
private final GuiNPCAbilities parentAbilities;
private String selectedDisplayName = null;
private String selectedUuid = null;
private String selectedName = null;
/** Maps display name -> UUID for custom abilities. */
private final HashMap<String, String> displayToUuid = new HashMap<>();
private HashMap<String, Integer> rawData = new HashMap<>();
private GuiCustomScroll scroll;
@@ -97,15 +94,15 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL
addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.load"));
addButton(new GuiNpcButton(2, guiLeft + 115, y, 95, 20, "gui.cancel"));
getButton(0).setEnabled(selectedUuid != null);
getButton(0).setEnabled(selectedName != null);
}
private List<String> getFilteredList() {
if (search.isEmpty()) {
return new ArrayList<>(displayToUuid.keySet());
return new ArrayList<>(rawData.keySet());
}
List<String> list = new ArrayList<>();
for (String name : displayToUuid.keySet()) {
for (String name : rawData.keySet()) {
if (name.toLowerCase().contains(search.toLowerCase())) {
list.add(name);
}
@@ -117,7 +114,7 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
if (id == 0 && selectedUuid != null) {
if (id == 0 && selectedName != null) {
if (parentAbilities != null) {
// From NPC abilities GUI - ask clone/reference
setSubGui(new SubGuiAbilityLoadMode());
@@ -125,7 +122,7 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL
// From config GUI - always clone
waitingForLoad = true;
pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE;
PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid));
PacketClient.sendClient(new CustomAbilityGetPacket(selectedName));
}
} else if (id == 2) {
close();
@@ -152,16 +149,16 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL
if (mode < 0) return; // cancelled
if (mode == SubGuiAbilityLoadMode.MODE_REFERENCE) {
// Reference mode - just pass the reference ID
if (parentAbilities != null && selectedUuid != null) {
parentAbilities.loadAbilityReference(selectedUuid);
// Reference mode - just pass the reference name
if (parentAbilities != null && selectedName != null) {
parentAbilities.loadAbilityReference(selectedName);
}
close();
} else {
// Clone mode - request full data
waitingForLoad = true;
pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE;
PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid));
PacketClient.sendClient(new CustomAbilityGetPacket(selectedName));
}
}
}
@@ -170,9 +167,7 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL
public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) {
if (scroll.id == 0) {
selectedDisplayName = scroll.getSelected();
if (selectedDisplayName != null) {
selectedUuid = displayToUuid.get(selectedDisplayName);
}
selectedName = selectedDisplayName;
initGui();
}
}
@@ -181,15 +176,13 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
if (scroll.id == 0 && selection != null && !selection.isEmpty()) {
selectedDisplayName = selection;
selectedUuid = displayToUuid.get(selectedDisplayName);
if (selectedUuid != null) {
if (parentAbilities != null) {
setSubGui(new SubGuiAbilityLoadMode());
} else {
waitingForLoad = true;
pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE;
PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid));
}
selectedName = selection;
if (parentAbilities != null) {
setSubGui(new SubGuiAbilityLoadMode());
} else {
waitingForLoad = true;
pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE;
PacketClient.sendClient(new CustomAbilityGetPacket(selectedName));
}
}
}
@@ -197,22 +190,10 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL
@Override
public void setData(Vector<String> list, HashMap<String, Integer> data, EnumScrollData type) {
if (type == EnumScrollData.CUSTOM_ABILITIES) {
displayToUuid.clear();
rawData = data;
for (String key : data.keySet()) {
// key format: "displayName\tUUID"
int tabIndex = key.indexOf('\t');
if (tabIndex > 0) {
String displayName = key.substring(0, tabIndex);
String uuid = key.substring(tabIndex + 1);
displayToUuid.put(displayName, uuid);
} else {
displayToUuid.put(key, key);
}
}
if (scroll != null) {
scroll.setList(getFilteredList());
if (selectedDisplayName != null && displayToUuid.containsKey(selectedDisplayName)) {
if (selectedDisplayName != null && rawData.containsKey(selectedDisplayName)) {
scroll.setSelected(selectedDisplayName);
}
}
@@ -226,11 +207,6 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL
waitingForLoad = false;
Ability loaded = AbilityController.Instance.fromNBT(compound);
if (loaded != null) {
if (pendingLoadMode == SubGuiAbilityLoadMode.MODE_CLONE) {
// Give it a new UUID so it's a unique inline copy
loaded.setId(UUID.randomUUID().toString());
}
if (parentConfig != null) {
parentConfig.loadAbility(loaded);
} else if (parentAbilities != null) {
@@ -58,8 +58,6 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS
allDisplayNameToTypeId.clear();
String[] types = AbilityController.Instance.getTypes();
for (String typeId : types) {
// Exclude built-in types — they cannot be created as custom presets
if (AbilityController.Instance.isBuiltInType(typeId)) continue;
String displayName = I18n.format(typeId);
allDisplayNameToTypeId.put(displayName, typeId);
}
@@ -284,12 +284,12 @@ public class GuiFieldBuilder {
String btnText = def.getButtonLabel();
int btnW;
if (def.hasClearAction()) {
btnW = contentRight - 22 - fieldX;
GuiNpcButton clearBtn = new GuiNpcButton(clearId, contentRight - 20, y, 20, 20, "X");
btnW = fieldW - 22;
GuiNpcButton clearBtn = new GuiNpcButton(clearId, fieldX + fieldW - 20, y, 20, 20, "X");
sw.addButton(clearBtn);
clearFieldMap.put(clearId, def);
} else {
btnW = contentRight - fieldX;
btnW = fieldW;
}
GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, btnW, 20, btnText);
Integer textColor = def.getButtonTextColor();
@@ -3,7 +3,6 @@ package noppes.npcs.client.gui.global;
import kamkeel.npcs.client.renderer.TelegraphRenderer;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import java.util.UUID;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket;
@@ -62,10 +61,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
private GuiCustomScroll scroll;
private HashMap<String, Integer> customData = new HashMap<>();
private HashMap<String, Integer> builtInData = new HashMap<>();
/** Maps display name -> UUID for custom abilities */
private final HashMap<String, String> displayToUuid = new HashMap<>();
private String selected = null;
private String selectedUuid = null;
private String search = "";
private Ability selectedAbility = null;
@@ -117,7 +113,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
// Remove button — only for custom view
addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove"));
getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected));
getButton(1).setEnabled(selected != null && !selected.isEmpty() && customData.containsKey(selected));
}
// Edit button — hidden when viewing built-in
@@ -178,29 +174,17 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
}
private List<String> getSearchList() {
if (showingBuiltIn) {
if (search.isEmpty()) {
return new ArrayList<>(builtInData.keySet());
}
List<String> list = new ArrayList<>();
for (String name : builtInData.keySet()) {
if (name.toLowerCase().contains(search.toLowerCase())) {
list.add(name);
}
}
return list;
} else {
if (search.isEmpty()) {
return new ArrayList<>(displayToUuid.keySet());
}
List<String> list = new ArrayList<>();
for (String name : displayToUuid.keySet()) {
if (name.toLowerCase().contains(search.toLowerCase())) {
list.add(name);
}
}
return list;
HashMap<String, Integer> data = getCurrentData();
if (search.isEmpty()) {
return new ArrayList<>(data.keySet());
}
List<String> list = new ArrayList<>();
for (String name : data.keySet()) {
if (name.toLowerCase().contains(search.toLowerCase())) {
list.add(name);
}
}
return list;
}
@Override
@@ -260,7 +244,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
if (id == 10) {
showingBuiltIn = !showingBuiltIn;
selected = null;
selectedUuid = null;
selectedAbility = null;
currentIsBuiltIn = false;
search = "";
@@ -276,7 +259,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
if (id == 2 && !showingBuiltIn) {
// Add — open type selection
setSubGui(new SubGuiAbilityTypeSelect());
} else if (id == 1 && !showingBuiltIn && selected != null && selectedUuid != null) {
} else if (id == 1 && !showingBuiltIn && selected != null) {
GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1);
displayGuiScreen(guiyesno);
} else if (id == 100 && !showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) {
@@ -325,17 +308,11 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
selected = newSelection;
if (showingBuiltIn) {
// Fetch built-in ability by display name
selectedUuid = null;
currentIsBuiltIn = true;
PacketClient.sendClient(new BuiltInAbilityGetPacket(selected));
} else {
// Fetch custom ability by UUID
selectedUuid = displayToUuid.get(selected);
currentIsBuiltIn = false;
if (selectedUuid != null) {
PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid));
}
PacketClient.sendClient(new CustomAbilityGetPacket(selected));
}
}
}
@@ -357,21 +334,9 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
if (type == EnumScrollData.CUSTOM_ABILITIES) {
String prevSelected = scroll != null ? scroll.getSelected() : null;
this.customData = data;
displayToUuid.clear();
for (String key : data.keySet()) {
// key format: "displayName\tUUID"
int tabIndex = key.indexOf('\t');
if (tabIndex > 0) {
String displayName = key.substring(0, tabIndex);
String uuid = key.substring(tabIndex + 1);
displayToUuid.put(displayName, uuid);
} else {
displayToUuid.put(key, key);
}
}
if (!showingBuiltIn && scroll != null) {
scroll.setList(getSearchList());
if (prevSelected != null && displayToUuid.containsKey(prevSelected)) {
if (prevSelected != null && customData.containsKey(prevSelected)) {
scroll.setSelected(prevSelected);
}
}
@@ -422,7 +387,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
Ability newAbility = AbilityController.Instance.create(pendingTypeId);
if (newAbility != null) {
variantGui.getVariants().get(idx).apply(newAbility);
newAbility.setId(UUID.randomUUID().toString());
if (hasDuplicateName(newAbility)) {
pendingSaveAbility = newAbility;
pendingNewCreation = true;
@@ -450,7 +414,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
if (variants.size() == 1) {
variants.get(0).apply(newAbility);
}
newAbility.setId(UUID.randomUUID().toString());
if (hasDuplicateName(newAbility)) {
pendingSaveAbility = newAbility;
pendingNewCreation = true;
@@ -504,7 +467,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
private void openConfig(Ability ability) {
selectedAbility = ability;
selected = ability.getName();
selectedUuid = ability.getId();
previewExecutor.stop();
setSubGui(ability.createConfigGui(this));
}
@@ -512,12 +474,9 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
private boolean hasDuplicateName(Ability ability) {
String name = ability.getName();
if (name == null || name.isEmpty()) return false;
for (java.util.Map.Entry<String, String> entry : displayToUuid.entrySet()) {
if (entry.getKey().equals(name) && !entry.getValue().equals(ability.getId())) {
return true;
}
}
return false;
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);
}
@Override
@@ -525,11 +484,10 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
NoppesUtil.openGUI(player, this);
if (!result) return;
if (id == 1 && selected != null && selectedUuid != null) {
PacketClient.sendClient(new CustomAbilityRemovePacket(selectedUuid));
if (id == 1 && selected != null) {
PacketClient.sendClient(new CustomAbilityRemovePacket(selected));
scroll.clear();
selected = null;
selectedUuid = null;
selectedAbility = null;
initGui();
}
@@ -131,7 +131,13 @@ public enum EnumScriptType {
ENERGY_PROJECTILE_TICK("energyProjectileTick"),
ENERGY_PROJECTILE_ENTITY_IMPACT("energyProjectileEntityImpact"),
ENERGY_PROJECTILE_BLOCK_IMPACT("energyProjectileBlockImpact"),
ENERGY_PROJECTILE_EXPIRED("energyProjectileExpired");
ENERGY_PROJECTILE_EXPIRED("energyProjectileExpired"),
// Energy Barrier Events
ENERGY_BARRIER_SPAWNED("energyBarrierSpawned"),
ENERGY_BARRIER_TICK("energyBarrierTick"),
ENERGY_BARRIER_HIT("energyBarrierHit"),
ENERGY_BARRIER_DESTROYED("energyBarrierDestroyed");
public String function;
@@ -1,5 +1,6 @@
package noppes.npcs.controllers;
import kamkeel.npcs.util.Register;
import net.minecraft.nbt.CompressedStreamTools;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
@@ -28,11 +29,7 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.*;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
@@ -45,14 +42,15 @@ public class AnimationController implements IAnimationHandler {
private static final String BUILTIN_ANIMATIONS_PATH = "/assets/customnpcs/animations";
private static final String BUILTIN_ANIMATIONS_RESOURCE = "assets/customnpcs/animations";
public static AnimationController Instance;
public static AnimationController Instance = new AnimationController();
private int lastUsedID = 0;
public LinkedHashMap<String, Register.Animations> registeredAnimations = new LinkedHashMap<>();
public AnimationController() {
Instance = this;
bootOrder = new HashMap<>();
animations = new HashMap<>();
load();
}
public static AnimationController getInstance() {
@@ -65,6 +63,7 @@ public class AnimationController implements IAnimationHandler {
builtInAnimations = new HashMap<>();
LogWriter.info("Loading animations...");
loadBuiltInAnimations();
loadRegisteredBuiltIns();
readAnimationMap();
loadAnimations();
LogWriter.info("Done loading animations.");
@@ -124,6 +123,7 @@ public class AnimationController implements IAnimationHandler {
String resourcePath = path + "/" + animName + ".json";
try (InputStream stream = modClass.getResourceAsStream(resourcePath)) {
if (stream == null) {
LogWriter.error("Animation not found: " + animName.toLowerCase());
return;
}
@@ -144,6 +144,7 @@ public class AnimationController implements IAnimationHandler {
// Store with lowercase key for case-insensitive lookup
builtInAnimations.put(animName.toLowerCase(), animation);
LogWriter.info("Registered animation: " + animName.toLowerCase());
}
}
@@ -199,6 +200,16 @@ public class AnimationController implements IAnimationHandler {
saveAnimationMap();
}
public void addAnimationRegister(String namespace, Register.Animations register) {
registeredAnimations.put(namespace, register);
}
private void loadRegisteredBuiltIns() {
for (Register.Animations register : registeredAnimations.values()) {
register.register();
}
}
private File getDir() {
return new File(CustomNpcs.getWorldSaveDirectory(), "animations");
}
@@ -3,7 +3,6 @@ package noppes.npcs.controllers.data;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
import kamkeel.npcs.controllers.data.ability.UserType;
import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
@@ -15,6 +14,7 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.util.DamageSource;
import noppes.npcs.AbstractDataAbilities;
import noppes.npcs.LogWriter;
import noppes.npcs.EventHooks;
import noppes.npcs.api.ability.IPlayerAbilityData;
@@ -35,6 +35,7 @@ import java.util.List;
/**
* Manages player ability data - unlocked abilities, selection, cooldowns, and execution.
* Players reference abilities by key (built-in keys or preset names), never customized copies.
* Extends AbstractDataAbilities for shared ability lifecycle logic.
* <p>
* Key differences from NPC DataAbilities:
* - Universal cooldown (shared across all abilities)
@@ -42,7 +43,7 @@ import java.util.List;
* - Conditions requiring a target are skipped
* - Uses ability's base cooldownTicks only
*/
public class PlayerAbilityData implements IPlayerAbilityData {
public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerAbilityData {
private final PlayerData playerData;
@@ -69,19 +70,9 @@ public class PlayerAbilityData implements IPlayerAbilityData {
private boolean playingAbilityAnimation = false;
// ═══════════════════════════════════════════════════════════════════
// RUNTIME STATE (not saved)
// PLAYER-SPECIFIC RUNTIME STATE (not saved)
// ═══════════════════════════════════════════════════════════════════
/**
* Universal cooldown end time (shared across all abilities).
*/
private transient long cooldownEndTime = 0;
/**
* Currently executing ability instance (null if none).
*/
private transient Ability currentAbility;
/**
* Current ability's key (for cooldown tracking after completion).
*/
@@ -92,21 +83,6 @@ public class PlayerAbilityData implements IPlayerAbilityData {
*/
private transient EntityLivingBase currentTarget;
/**
* Rotation lock state - freezes player yaw/pitch during ability phases.
*/
private transient boolean rotationLocked = false;
private transient float lockedYaw = 0;
private transient float lockedPitch = 0;
/**
* Position lock state - freezes player position during ability phases.
*/
private transient boolean positionLocked = false;
private transient double lockedPosX = 0;
private transient double lockedPosY = 0;
private transient double lockedPosZ = 0;
/**
* Last synced state flags byte for change detection (avoids spamming packets).
*/
@@ -120,6 +96,160 @@ public class PlayerAbilityData implements IPlayerAbilityData {
this.playerData = playerData;
}
// ═══════════════════════════════════════════════════════════════════
// ABSTRACT METHOD IMPLEMENTATIONS
// ═══════════════════════════════════════════════════════════════════
@Override
protected EntityLivingBase getEntity() {
return playerData.player;
}
@Override
protected EntityLivingBase getTarget() {
return currentTarget;
}
@Override
protected long getWorldTime() {
EntityPlayer player = playerData.player;
return player != null ? player.worldObj.getTotalWorldTime() : 0;
}
@Override
protected void fireTickEvent(Ability ability, EntityLivingBase target) {
EntityPlayer player = playerData.player;
if (ScriptController.Instance == null || player == null) return;
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
if (handler == null) return;
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
PlayerAbilityEvent.TickEvent event = new PlayerAbilityEvent.TickEvent(
iPlayer, ability, target, ability.getPhase().ordinal(), ability.getCurrentTick());
EventHooks.onPlayerAbilityTick(handler, event);
}
@Override
protected boolean fireExecuteEvent(Ability ability, EntityLivingBase target) {
EntityPlayer player = playerData.player;
if (ScriptController.Instance == null || player == null) return false;
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
if (handler == null) return false;
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
PlayerAbilityEvent.ExecuteEvent event = new PlayerAbilityEvent.ExecuteEvent(iPlayer, ability, target);
return EventHooks.onPlayerAbilityExecute(handler, event);
}
@Override
protected void fireCompleteEvent(Ability ability, EntityLivingBase target) {
EntityPlayer player = playerData.player;
if (ScriptController.Instance == null || player == null) return;
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
if (handler == null) return;
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
PlayerAbilityEvent.CompleteEvent event = new PlayerAbilityEvent.CompleteEvent(iPlayer, ability, target);
EventHooks.onPlayerAbilityComplete(handler, event);
}
@Override
protected void fireInterruptEvent(Ability ability, EntityLivingBase target,
DamageSource source, float damage) {
EntityPlayer player = playerData.player;
if (ScriptController.Instance == null || player == null) return;
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
if (handler == null) return;
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
PlayerAbilityEvent.InterruptEvent event = new PlayerAbilityEvent.InterruptEvent(
iPlayer, ability, target, source, damage);
EventHooks.onPlayerAbilityInterrupt(handler, event);
}
@Override
protected void spawnTelegraph(Ability ability, EntityLivingBase target) {
EntityPlayer player = playerData.player;
List<TelegraphInstance> telegraphs = ability.createTelegraphs(player, target);
if (!telegraphs.isEmpty()) {
ability.setTelegraphInstances(telegraphs);
if (player instanceof EntityPlayerMP) {
for (TelegraphInstance telegraph : telegraphs) {
TelegraphSpawnPacket.sendToTracking(telegraph, player);
}
}
}
}
@Override
protected void removeTelegraph(Ability ability) {
EntityPlayer player = playerData.player;
List<TelegraphInstance> telegraphs = ability.getTelegraphInstances();
for (TelegraphInstance telegraph : telegraphs) {
if (player instanceof EntityPlayerMP) {
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player);
}
}
ability.setTelegraphInstances(null);
}
@Override
protected void setAnimationData(Animation animation) {
playerData.animationData.setEnabled(true);
playerData.animationData.setAnimation(animation);
playerData.animationData.updateClient();
playingAbilityAnimation = true;
}
@Override
protected void clearAnimationData() {
playerData.animationData.setAnimation(null);
playerData.animationData.updateClient();
playingAbilityAnimation = false;
}
@Override
protected void playAbilitySound(String sound) {
if (sound != null && !sound.isEmpty()) {
EntityPlayer player = playerData.player;
if (player != null) {
player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f);
}
}
}
@Override
protected void captureLockedRotation() {
EntityPlayer player = playerData.player;
lockedYaw = player.rotationYaw;
lockedPitch = player.rotationPitch;
rotationLocked = true;
}
@Override
protected void rollCooldown(Ability ability) {
if (!ability.isIgnoreCooldown()) {
cooldownEndTime = getWorldTime() + ability.getCooldownTicks();
}
}
@Override
protected void onAbilityComplete() {
currentAbility = null;
currentAbilityKey = null;
currentTarget = null;
syncAbilityStateClear(playerData.player);
}
// ═══════════════════════════════════════════════════════════════════
// HOOK OVERRIDES
// ═══════════════════════════════════════════════════════════════════
@Override
protected void onInterruptComplete() {
// Player clears state immediately (unlike NPC which ticks through DAZED)
currentAbility = null;
currentAbilityKey = null;
currentTarget = null;
syncAbilityStateClear(playerData.player);
}
// ═══════════════════════════════════════════════════════════════════
// TICK - Called from player tick handler
// ═══════════════════════════════════════════════════════════════════
@@ -134,7 +264,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
}
if (currentAbility != null && currentAbility.isExecuting()) {
tickCurrentAbility(player);
tickCurrentAbility();
// Apply rotation and position locks after ability tick
applyRotationControl(player);
@@ -152,160 +282,6 @@ public class PlayerAbilityData implements IPlayerAbilityData {
}
}
/**
* Tick the currently executing ability.
*/
private void tickCurrentAbility(EntityPlayer player) {
AbilityPhase oldPhase = currentAbility.getPhase();
// Tick advances time and possibly changes phase
boolean phaseChanged = currentAbility.tick();
// Fire tick event for scripts
firePlayerTickEvent(player, currentAbility, currentTarget);
// Fire extender tick hook (e.g., per-tick resource drain)
if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, player, currentTarget,
currentAbility.getPhase(), currentAbility.getCurrentTick())) {
interruptCurrentAbility();
return;
}
switch (currentAbility.getPhase()) {
case WINDUP:
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
// Burst replay: re-enter windup - set up locks
if (currentAbility.isRotationLockedDuringWindup()) {
captureLockedRotation(player);
}
if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) {
captureLockedPosition(player);
}
spawnTelegraph(currentAbility, player, null);
playAbilitySound(player, currentAbility.getWindUpSound());
playAbilityAnimation(currentAbility.getWindUpAnimation());
}
currentAbility.onWindUpTick(player, currentTarget, currentAbility.getCurrentTick());
break;
case ACTIVE:
if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) {
// Just entered ACTIVE phase - lock all telegraph positions
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
telegraph.lockPosition();
}
// Remove telegraph unless the ability keeps it during active phase
if (!currentAbility.keepTelegraphDuringActive()) {
removeTelegraph(currentAbility, player);
}
// Handle rotation control transition from WINDUP to ACTIVE
if (currentAbility.isRotationLockedDuringActive()) {
if (!rotationLocked) {
captureLockedRotation(player);
}
} else if (rotationLocked) {
releaseRotationControl();
}
// Handle position lock transition from WINDUP to ACTIVE
if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) {
if (!positionLocked) {
captureLockedPosition(player);
}
} else if (positionLocked) {
releaseLockedPosition();
}
// Play active sound and animation
playAbilitySound(player, currentAbility.getActiveSound());
playAbilityAnimation(currentAbility.getActiveAnimation());
// Fire execute event (cancelable)
if (firePlayerExecuteEvent(player, currentAbility, currentTarget)) {
return; // Cancelled
}
// Call onExecute
currentAbility.onExecute(player, currentTarget);
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(player);
return;
}
}
currentAbility.onActiveTick(player, currentTarget, currentAbility.getCurrentTick());
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(player);
return;
}
// Auto-complete for burst overlap mode (entities fly independently)
if (currentAbility.isBurstEnabled()
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
currentAbility.signalCompletion();
}
// Release locks during burst delay
if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) {
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
}
break;
case BURST_DELAY:
// Free movement and rotation during burst delay
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
break;
case DAZED:
// Player is dazed - just wait
break;
case IDLE:
handleAbilityCompletion(player);
break;
}
}
/**
* Handle ability completion.
*/
private void handleAbilityCompletion(EntityPlayer player) {
if (currentAbility == null) return;
// Fire extender complete hook
AbilityController.Instance.fireOnAbilityComplete(currentAbility, player, currentTarget, false);
currentAbility.onComplete(player, currentTarget);
// Fire complete event
firePlayerCompleteEvent(player, currentAbility, currentTarget);
// Release all locks
releaseRotationControl();
releaseLockedPosition();
// Apply universal cooldown (using base cooldownTicks only, not min/max random)
if (!currentAbility.isIgnoreCooldown()) {
cooldownEndTime = player.worldObj.getTotalWorldTime() + currentAbility.getCooldownTicks();
}
// Stop animation
stopAbilityAnimation();
currentAbility = null;
currentAbilityKey = null;
currentTarget = null;
// Send cleared state to client
syncAbilityStateClear(player);
}
// ═══════════════════════════════════════════════════════════════════
// ABILITY ACTIVATION - Called on Special Key press
// ═══════════════════════════════════════════════════════════════════
@@ -348,7 +324,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
if (!ability.getAllowedBy().allowsPlayer()) return false;
// Check universal cooldown
if (!ability.isIgnoreCooldown() && isOnCooldown(player)) return false;
if (!ability.isIgnoreCooldown() && isOnCooldown()) return false;
// Check conditions (skip target-requiring ones)
if (!ability.checkConditionsForPlayer(player)) return false;
@@ -370,53 +346,30 @@ public class PlayerAbilityData implements IPlayerAbilityData {
ability.start(null);
if (ability.getPhase() == AbilityPhase.ACTIVE) {
// Windup was 0 — skip telegraph/windup and go straight to active
// Windup was 0 — capture locks for immediate active phase, then execute
if (ability.isRotationLockedDuringActive()) {
captureLockedRotation(player);
captureLockedRotation();
}
if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) {
captureLockedPosition(player);
captureLockedPosition();
}
executeImmediate(ability, player);
executeImmediate(ability, currentTarget);
} else {
// Normal windup flow
if (ability.isRotationLockedDuringWindup()) {
captureLockedRotation(player);
captureLockedRotation();
}
if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) {
captureLockedPosition(player);
captureLockedPosition();
}
spawnTelegraph(ability, player, null);
playAbilitySound(player, ability.getWindUpSound());
spawnTelegraph(ability, null);
playAbilitySound(ability.getWindUpSound());
playAbilityAnimation(ability.getWindUpAnimation());
}
return true;
}
/**
* Execute an ability immediately (no windup).
* Called when windUpTicks is 0.
*/
private void executeImmediate(Ability ability, EntityPlayer player) {
// Play active sound and animation
playAbilitySound(player, ability.getActiveSound());
playAbilityAnimation(ability.getActiveAnimation());
// Fire execute event (cancelable)
if (firePlayerExecuteEvent(player, ability, currentTarget)) {
return; // Cancelled
}
// Call onExecute
ability.onExecute(player, currentTarget);
// Check if ability completed during onExecute
if (ability.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(player);
}
}
// ═══════════════════════════════════════════════════════════════════
// CLIENT SYNC
// ═══════════════════════════════════════════════════════════════════
@@ -508,9 +461,12 @@ public class PlayerAbilityData implements IPlayerAbilityData {
}
// ═══════════════════════════════════════════════════════════════════
// COOLDOWNS
// COOLDOWNS (Player-specific overloads)
// ═══════════════════════════════════════════════════════════════════
/**
* Check if on cooldown using player reference.
*/
public boolean isOnCooldown(EntityPlayer player) {
return player.worldObj.getTotalWorldTime() < cooldownEndTime;
}
@@ -543,6 +499,10 @@ public class PlayerAbilityData implements IPlayerAbilityData {
cooldownEndTime = 0;
}
// ═══════════════════════════════════════════════════════════════════
// IPlayerAbilityData INTERFACE
// ═══════════════════════════════════════════════════════════════════
@Override
public boolean activateAbility() {
return activateAbility(playerData.player);
@@ -553,40 +513,36 @@ public class PlayerAbilityData implements IPlayerAbilityData {
return activateAbility(playerData.player, key);
}
// ═══════════════════════════════════════════════════════════════════
// STATE QUERIES
// ═══════════════════════════════════════════════════════════════════
public boolean isExecutingAbility() {
return currentAbility != null && currentAbility.isExecuting();
@Override
public boolean isOnCooldown() {
EntityPlayer player = playerData.player;
if (player == null) return false;
return isOnCooldown(player);
}
public Ability getCurrentAbility() {
return currentAbility;
@Override
public boolean isOnCooldown(String key) {
EntityPlayer player = playerData.player;
if (player == null) return false;
return isOnCooldown(key, player);
}
@Override
public void resetCooldown() {
cooldownEndTime = 0;
}
// ═══════════════════════════════════════════════════════════════════
// INTERRUPTION (convenience overload)
// ═══════════════════════════════════════════════════════════════════
public void interruptCurrentAbility() {
interruptCurrentAbility(null, 0);
}
public void interruptCurrentAbility(DamageSource source, float damage) {
if (currentAbility != null && currentAbility.isExecuting()) {
// Fire extender complete hook (interrupted)
AbilityController.Instance.fireOnAbilityComplete(currentAbility, playerData.player, currentTarget, true);
// Fire interrupt event
firePlayerInterruptEvent(playerData.player, currentAbility, currentTarget, source, damage);
currentAbility.interrupt();
stopAbilityAnimation();
releaseRotationControl();
releaseLockedPosition();
currentAbility = null;
currentAbilityKey = null;
currentTarget = null;
syncAbilityStateClear(playerData.player);
}
}
// ═══════════════════════════════════════════════════════════════════
// DAMAGE HANDLING
// ═══════════════════════════════════════════════════════════════════
/**
* Handle damage taken while an ability is executing.
@@ -635,24 +591,9 @@ public class PlayerAbilityData implements IPlayerAbilityData {
return amount;
}
@Override
public boolean isOnCooldown() {
EntityPlayer player = playerData.player;
if (player == null) return false;
return isOnCooldown(player);
}
@Override
public boolean isOnCooldown(String key) {
EntityPlayer player = playerData.player;
if (player == null) return false;
return isOnCooldown(key, player);
}
@Override
public void resetCooldown() {
cooldownEndTime = 0;
}
// ═══════════════════════════════════════════════════════════════════
// STATE QUERIES (Player-specific)
// ═══════════════════════════════════════════════════════════════════
/**
* Check if the player's movement should be locked due to an executing ability.
@@ -663,26 +604,26 @@ public class PlayerAbilityData implements IPlayerAbilityData {
&& currentAbility.isMovementLockedForCurrentPhase();
}
// ═══════════════════════════════════════════════════════════════════
// ROTATION CONTROL
// ═══════════════════════════════════════════════════════════════════
/**
* Capture current rotation values to lock player's look direction.
* Returns true if an ability-driven animation is currently playing.
* Used by PlayerData.onLogin() to detect orphaned ability animations.
*/
private void captureLockedRotation(EntityPlayer player) {
lockedYaw = player.rotationYaw;
lockedPitch = player.rotationPitch;
rotationLocked = true;
public boolean isPlayingAbilityAnimation() {
return playingAbilityAnimation;
}
/**
* Release rotation lock.
* Clears an orphaned ability animation (e.g. after relog when the ability is gone).
* Stops the animation and resets the tracking flag.
*/
private void releaseRotationControl() {
rotationLocked = false;
public void clearOrphanedAbilityAnimation() {
stopAbilityAnimation();
}
// ═══════════════════════════════════════════════════════════════════
// ROTATION CONTROL (Player-specific application)
// ═══════════════════════════════════════════════════════════════════
/**
* Apply rotation control on the server side.
* Enforces locked rotation values on the player entity.
@@ -702,26 +643,9 @@ public class PlayerAbilityData implements IPlayerAbilityData {
}
// ═══════════════════════════════════════════════════════════════════
// POSITION LOCKING
// POSITION LOCKING (Player-specific application)
// ═══════════════════════════════════════════════════════════════════
/**
* Capture current position to lock player in place.
*/
private void captureLockedPosition(EntityPlayer player) {
lockedPosX = player.posX;
lockedPosY = player.posY;
lockedPosZ = player.posZ;
positionLocked = true;
}
/**
* Release position lock.
*/
private void releaseLockedPosition() {
positionLocked = false;
}
/**
* Apply position lock on the server side.
* Snaps player back to locked position and zeroes motion.
@@ -729,12 +653,23 @@ public class PlayerAbilityData implements IPlayerAbilityData {
private void applyPositionLock(EntityPlayer player) {
if (!positionLocked) return;
player.setPosition(lockedPosX, lockedPosY, lockedPosZ);
player.prevPosX = lockedPosX;
player.prevPosY = lockedPosY;
player.prevPosZ = lockedPosZ;
boolean flying = AbilityController.Instance != null
&& AbilityController.Instance.isPlayerFlying(player);
if (flying) {
// Flying players: lock X/Z only, preserve Y freedom
player.setPosition(lockedPosX, player.posY, lockedPosZ);
player.prevPosX = lockedPosX;
player.prevPosZ = lockedPosZ;
} else {
// Grounded players: full position lock including Y
player.setPosition(lockedPosX, lockedPosY, lockedPosZ);
player.prevPosX = lockedPosX;
player.prevPosY = lockedPosY;
player.prevPosZ = lockedPosZ;
player.motionY = 0;
}
player.motionX = 0;
player.motionY = 0; // Full vertical freeze for position lock
player.motionZ = 0;
}
@@ -788,89 +723,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
}
// ═══════════════════════════════════════════════════════════════════
// TELEGRAPH
// ═══════════════════════════════════════════════════════════════════
private void spawnTelegraph(Ability ability, EntityPlayer player, EntityLivingBase target) {
List<TelegraphInstance> telegraphs = ability.createTelegraphs(player, target);
if (!telegraphs.isEmpty()) {
ability.setTelegraphInstances(telegraphs);
if (player instanceof EntityPlayerMP) {
for (TelegraphInstance telegraph : telegraphs) {
TelegraphSpawnPacket.sendToTracking(telegraph, player);
}
}
}
}
private void removeTelegraph(Ability ability, EntityPlayer player) {
List<TelegraphInstance> telegraphs = ability.getTelegraphInstances();
for (TelegraphInstance telegraph : telegraphs) {
if (player instanceof EntityPlayerMP) {
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player);
}
}
ability.setTelegraphInstances(null);
}
// ═══════════════════════════════════════════════════════════════════
// ANIMATION
// ═══════════════════════════════════════════════════════════════════
public void playAbilityAnimation(Animation animation) {
if (animation == null || AnimationController.Instance == null) return;
playerData.animationData.setEnabled(true);
playerData.animationData.setAnimation(animation);
playerData.animationData.updateClient();
playingAbilityAnimation = true;
}
public void playAbilityAnimation(int animation) {
if (animation < 0) return;
if (AnimationController.Instance == null) return;
if (AnimationController.Instance.get(animation) == null) return;
playAbilityAnimation((Animation) AnimationController.Instance.get(animation));
}
public void playAbilityAnimation(String animation) {
if (animation.isEmpty()) return;
if (AnimationController.Instance == null) return;
if (AnimationController.Instance.get(animation, true) == null) return;
playAbilityAnimation((Animation) AnimationController.Instance.get(animation, true));
}
private void stopAbilityAnimation() {
playerData.animationData.setAnimation(null);
playerData.animationData.updateClient();
playingAbilityAnimation = false;
}
/**
* Returns true if an ability-driven animation is currently playing.
* Used by PlayerData.onLogin() to detect orphaned ability animations.
*/
public boolean isPlayingAbilityAnimation() {
return playingAbilityAnimation;
}
/**
* Clears an orphaned ability animation (e.g. after relog when the ability is gone).
* Stops the animation and resets the tracking flag.
*/
public void clearOrphanedAbilityAnimation() {
stopAbilityAnimation();
}
private void playAbilitySound(EntityPlayer player, String sound) {
if (sound != null && !sound.isEmpty()) {
player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f);
}
}
// ═══════════════════════════════════════════════════════════════════
// SCRIPT EVENTS
// SCRIPT EVENTS (Player-specific: start event only, others via abstract)
// ═══════════════════════════════════════════════════════════════════
/**
@@ -885,57 +738,6 @@ public class PlayerAbilityData implements IPlayerAbilityData {
return EventHooks.onPlayerAbilityStart(handler, event);
}
/**
* Fire the ability execute event. Returns true if cancelled.
*/
private boolean firePlayerExecuteEvent(EntityPlayer player, Ability ability, EntityLivingBase target) {
if (ScriptController.Instance == null) return false;
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
if (handler == null) return false;
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
PlayerAbilityEvent.ExecuteEvent event = new PlayerAbilityEvent.ExecuteEvent(iPlayer, ability, target);
return EventHooks.onPlayerAbilityExecute(handler, event);
}
/**
* Fire the ability tick event.
*/
private void firePlayerTickEvent(EntityPlayer player, Ability ability, EntityLivingBase target) {
if (ScriptController.Instance == null) return;
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
if (handler == null) return;
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
PlayerAbilityEvent.TickEvent event = new PlayerAbilityEvent.TickEvent(
iPlayer, ability, target, ability.getPhase().ordinal(), ability.getCurrentTick());
EventHooks.onPlayerAbilityTick(handler, event);
}
/**
* Fire the ability interrupt event.
*/
private void firePlayerInterruptEvent(EntityPlayer player, Ability ability, EntityLivingBase target,
DamageSource source, float damage) {
if (ScriptController.Instance == null) return;
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
if (handler == null) return;
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
PlayerAbilityEvent.InterruptEvent event = new PlayerAbilityEvent.InterruptEvent(
iPlayer, ability, target, source, damage);
EventHooks.onPlayerAbilityInterrupt(handler, event);
}
/**
* Fire the ability complete event.
*/
private void firePlayerCompleteEvent(EntityPlayer player, Ability ability, EntityLivingBase target) {
if (ScriptController.Instance == null) return;
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
if (handler == null) return;
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
PlayerAbilityEvent.CompleteEvent event = new PlayerAbilityEvent.CompleteEvent(iPlayer, ability, target);
EventHooks.onPlayerAbilityComplete(handler, event);
}
// ═══════════════════════════════════════════════════════════════════
// NBT SERIALIZATION
// ═══════════════════════════════════════════════════════════════════
@@ -11,6 +11,7 @@ import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import net.minecraft.block.Block;
import net.minecraft.command.CommandHandler;
import net.minecraft.entity.Entity;
@@ -127,6 +128,7 @@ import noppes.npcs.scripted.entity.ScriptEnergyBeam;
import noppes.npcs.scripted.entity.ScriptEnergyDisc;
import noppes.npcs.scripted.entity.ScriptEnergyLaser;
import noppes.npcs.scripted.entity.ScriptEnergyOrb;
import noppes.npcs.scripted.entity.ScriptEnergyProjectile;
import noppes.npcs.scripted.entity.ScriptEntity;
import noppes.npcs.scripted.entity.ScriptEntityItem;
import noppes.npcs.scripted.entity.ScriptFishHook;
@@ -470,6 +472,14 @@ public class NpcAPI extends AbstractNpcAPI {
data = new ScriptEntityData(new ScriptEnergyDisc<>((EntityAbilityDisc) entity));
else if (entity instanceof EntityAbilityLaser)
data = new ScriptEntityData(new ScriptEnergyLaser<>((EntityAbilityLaser) entity));
else if (entity instanceof kamkeel.npcs.entity.EntityEnergySlicer)
data = new ScriptEntityData(new noppes.npcs.scripted.entity.ScriptEnergySlicer<>((kamkeel.npcs.entity.EntityEnergySlicer) entity));
else if (entity instanceof EntityEnergyProjectile)
data = new ScriptEntityData(new ScriptEnergyProjectile((EntityEnergyProjectile) entity));
else if (entity instanceof kamkeel.npcs.entity.EntityEnergyDome)
data = new ScriptEntityData(new noppes.npcs.scripted.entity.ScriptEnergyDome<>((kamkeel.npcs.entity.EntityEnergyDome) entity));
else if (entity instanceof kamkeel.npcs.entity.EntityEnergyPanel)
data = new ScriptEntityData(new noppes.npcs.scripted.entity.ScriptEnergyPanel<>((kamkeel.npcs.entity.EntityEnergyPanel) entity));
else
data = new ScriptEntityData(new ScriptEntity<>(entity));
entity.registerExtendedProperties("ScriptedObject", data);
@@ -19,4 +19,8 @@ public class EntityType {
public static final int ENERGY_BEAM = 15;
public static final int ENERGY_DISC = 16;
public static final int ENERGY_LASER = 17;
public static final int ENERGY_SLICER = 18;
public static final int ENERGY_BARRIER = 19;
public static final int ENERGY_DOME = 20;
public static final int ENERGY_PANEL = 21;
}
@@ -0,0 +1,68 @@
package noppes.npcs.scripted.entity;
import kamkeel.npcs.entity.EntityEnergyAbility;
import net.minecraft.entity.Entity;
import noppes.npcs.api.entity.IEnergyAbility;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.scripted.NpcAPI;
/**
* Base script wrapper for all energy ability entities.
* Implements shared display, lightning, owner, and charging methods.
*/
public abstract class ScriptEnergyAbility<T extends EntityEnergyAbility> extends ScriptEntity<T> implements IEnergyAbility {
public ScriptEnergyAbility(T entity) {
super(entity);
}
// ==================== OWNER ====================
public int getOwnerEntityId() {
return entity.getOwnerEntityId();
}
public IEntity getOwner() {
Entity owner = entity.getOwnerEntity();
return owner != null ? NpcAPI.Instance().getIEntity(owner) : null;
}
// ==================== DISPLAY ====================
public int getInnerColor() { return entity.getInnerColor(); }
public void setInnerColor(int color) { entity.setInnerColor(color); }
public float getInnerAlpha() { return entity.getInnerAlpha(); }
public void setInnerAlpha(float alpha) { entity.setInnerAlpha(alpha); }
public int getOuterColor() { return entity.getOuterColor(); }
public void setOuterColor(int color) { entity.setOuterColor(color); }
public boolean isOuterColorEnabled() { return entity.isOuterColorEnabled(); }
public void setOuterColorEnabled(boolean enabled) { entity.setOuterColorEnabled(enabled); }
public float getOuterColorWidth() { return entity.getOuterColorWidth(); }
public void setOuterColorWidth(float width) { entity.setOuterColorWidth(width); }
public float getOuterColorAlpha() { return entity.getOuterColorAlpha(); }
public void setOuterColorAlpha(float alpha) { entity.setOuterColorAlpha(alpha); }
// ==================== LIGHTNING ====================
public boolean hasLightningEffect() { return entity.hasLightningEffect(); }
public void setLightningEffect(boolean enabled) { entity.setLightningEffect(enabled); }
public float getLightningDensity() { return entity.getLightningDensity(); }
public void setLightningDensity(float density) { entity.setLightningDensity(density); }
public float getLightningRadius() { return entity.getLightningRadius(); }
public void setLightningRadius(float radius) { entity.setLightningRadius(radius); }
public int getLightningFadeTime() { return entity.getLightningFadeTime(); }
public void setLightningFadeTime(int ticks) { entity.setLightningFadeTime(ticks); }
// ==================== STATE ====================
public boolean isCharging() { return entity.isCharging(); }
public float getChargeProgress() { return entity.getChargeProgress(); }
}
@@ -0,0 +1,99 @@
package noppes.npcs.scripted.entity;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyDome;
import noppes.npcs.api.entity.IEnergyBarrier;
import noppes.npcs.scripted.constants.EntityType;
/**
* Base script wrapper for energy barrier entities (Dome, Panel).
* All shared methods delegate directly to EntityEnergyBarrier base.
*/
public class ScriptEnergyBarrier<T extends EntityEnergyBarrier> extends ScriptEnergyAbility<T> implements IEnergyBarrier {
public ScriptEnergyBarrier(T entity) {
super(entity);
}
@Override
public int getType() {
return EntityType.ENERGY_BARRIER;
}
@Override
public boolean typeOf(int type) {
return type == EntityType.ENERGY_BARRIER || super.typeOf(type);
}
// ==================== HEALTH ====================
public float getCurrentHealth() {
return entity.getCurrentHealth();
}
public void setCurrentHealth(float health) {
entity.setCurrentHealth(health);
}
public float getHealthPercent() {
return entity.getHealthPercent();
}
public float getMaxHealth() {
return entity.getBarrierData().maxHealth;
}
public void setMaxHealth(float maxHealth) {
entity.getBarrierData().setMaxHealth(maxHealth);
}
public boolean isUseHealth() {
return entity.getBarrierData().useHealth;
}
public void setUseHealth(boolean useHealth) {
entity.getBarrierData().useHealth = useHealth;
}
// ==================== DURATION ====================
public int getDuration() {
return entity.getBarrierData().durationTicks;
}
public void setDuration(int ticks) {
entity.getBarrierData().setDurationTicks(ticks);
}
public boolean isUseDuration() {
return entity.getBarrierData().useDuration;
}
public void setUseDuration(boolean useDuration) {
entity.getBarrierData().useDuration = useDuration;
}
// ==================== STATE (BARRIER-SPECIFIC) ====================
public int getTicksAlive() {
return entity.getTicksAlive();
}
// ==================== BARRIER ====================
public float getDefaultMultiplier() { return entity.getBarrierData().defaultMultiplier; }
public void setDefaultMultiplier(float multiplier) { entity.getBarrierData().defaultMultiplier = multiplier; }
public boolean isKnockbackEnabled() { return entity.getBarrierData().knockbackEnabled; }
public void setKnockbackEnabled(boolean enabled) { entity.getBarrierData().knockbackEnabled = enabled; }
public float getKnockbackStrength() { return entity.getBarrierData().knockbackStrength; }
public void setKnockbackStrength(float strength) { entity.getBarrierData().knockbackStrength = strength; }
public int getKnockbackTarget() { return entity.getBarrierData().knockbackTarget; }
public void setKnockbackTarget(int target) { entity.getBarrierData().knockbackTarget = target; }
public int getBarrierType() {
return entity instanceof EntityEnergyDome ? 0 : 1;
}
}
@@ -0,0 +1,32 @@
package noppes.npcs.scripted.entity;
import kamkeel.npcs.entity.EntityEnergyDome;
import noppes.npcs.api.entity.IEnergyDome;
import noppes.npcs.scripted.constants.EntityType;
public class ScriptEnergyDome<T extends EntityEnergyDome> extends ScriptEnergyBarrier<T> implements IEnergyDome {
public ScriptEnergyDome(T entity) {
super(entity);
}
@Override
public int getType() {
return EntityType.ENERGY_DOME;
}
@Override
public boolean typeOf(int type) {
return type == EntityType.ENERGY_DOME || super.typeOf(type);
}
@Override
public int getBarrierType() {
return 0;
}
// ==================== DOME-SPECIFIC ====================
public float getDomeRadius() { return entity.getDomeRadius(); }
public void setDomeRadius(float radius) { entity.setDomeRadius(radius); }
}
@@ -0,0 +1,41 @@
package noppes.npcs.scripted.entity;
import kamkeel.npcs.entity.EntityEnergyPanel;
import noppes.npcs.api.entity.IEnergyPanel;
import noppes.npcs.scripted.constants.EntityType;
public class ScriptEnergyPanel<T extends EntityEnergyPanel> extends ScriptEnergyBarrier<T> implements IEnergyPanel {
public ScriptEnergyPanel(T entity) {
super(entity);
}
@Override
public int getType() {
return EntityType.ENERGY_PANEL;
}
@Override
public boolean typeOf(int type) {
return type == EntityType.ENERGY_PANEL || super.typeOf(type);
}
@Override
public int getBarrierType() {
return 1;
}
// ==================== PANEL-SPECIFIC ====================
public float getPanelWidth() { return entity.getPanelData().panelWidth; }
public void setPanelWidth(float width) { entity.getPanelData().setPanelWidth(width); }
public float getPanelHeight() { return entity.getPanelData().panelHeight; }
public void setPanelHeight(float height) { entity.getPanelData().setPanelHeight(height); }
public float getPanelYaw() { return entity.getPanelYaw(); }
public int getPanelMode() { return entity.getMode().ordinal(); }
public boolean isLaunched() { return entity.getMode() == EntityEnergyPanel.PanelMode.LAUNCHED; }
}
@@ -1,21 +1,18 @@
package noppes.npcs.scripted.entity;
import kamkeel.npcs.entity.EntityAbilityProjectile;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import noppes.npcs.api.entity.IEnergyProjectile;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.EventHooks;
import noppes.npcs.scripted.NpcAPI;
import noppes.npcs.scripted.constants.EntityType;
public class ScriptEnergyProjectile<T extends EntityAbilityProjectile> extends ScriptEntity<T> implements IEnergyProjectile {
protected T entity;
public class ScriptEnergyProjectile<T extends EntityEnergyProjectile> extends ScriptEnergyAbility<T> implements IEnergyProjectile {
public ScriptEnergyProjectile(T entity) {
super(entity);
this.entity = entity;
}
// ==================== TYPE ====================
@@ -36,14 +33,6 @@ public class ScriptEnergyProjectile<T extends EntityAbilityProjectile> extends S
// ==================== OWNER & TARGET ====================
public int getOwnerEntityId() {
return entity.getOwnerEntityId();
}
public IEntity getOwner() {
return entity.getOwner();
}
public void setOwner(IEntity owner) {
if (owner != null) {
entity.setOwnerEntityId(owner.getMCEntity().getEntityId());
@@ -76,22 +65,7 @@ public class ScriptEnergyProjectile<T extends EntityAbilityProjectile> extends S
entity.setProjectileSize(size);
}
// ==================== DISPLAY ====================
public int getInnerColor() { return entity.getInnerColor(); }
public void setInnerColor(int color) { entity.setInnerColor(color); }
public int getOuterColor() { return entity.getOuterColor(); }
public void setOuterColor(int color) { entity.setOuterColor(color); }
public boolean isOuterColorEnabled() { return entity.isOuterColorEnabled(); }
public void setOuterColorEnabled(boolean enabled) { entity.setOuterColorEnabled(enabled); }
public float getOuterColorWidth() { return entity.getOuterColorWidth(); }
public void setOuterColorWidth(float width) { entity.setOuterColorWidth(width); }
public float getOuterColorAlpha() { return entity.getOuterColorAlpha(); }
public void setOuterColorAlpha(float alpha) { entity.setOuterColorAlpha(alpha); }
// ==================== DISPLAY (PROJECTILE-SPECIFIC) ====================
public float getRotationSpeed() { return entity.getRotationSpeed(); }
public void setRotationSpeed(float speed) { entity.setRotationSpeed(speed); }
@@ -103,20 +77,6 @@ public class ScriptEnergyProjectile<T extends EntityAbilityProjectile> extends S
public float getInterpolatedRotationZ(float partialTicks) { return entity.getInterpolatedRotationZ(partialTicks); }
public float getInterpolatedSize(float partialTicks) { return entity.getInterpolatedSize(partialTicks); }
// ==================== LIGHTNING ====================
public boolean hasLightningEffect() { return entity.hasLightningEffect(); }
public void setLightningEffect(boolean enabled) { entity.setLightningEffect(enabled); }
public float getLightningDensity() { return entity.getLightningDensity(); }
public void setLightningDensity(float density) { entity.setLightningDensity(density); }
public float getLightningRadius() { return entity.getLightningRadius(); }
public void setLightningRadius(float radius) { entity.setLightningRadius(radius); }
public int getLightningFadeTime() { return entity.getLightningFadeTime(); }
public void setLightningFadeTime(int ticks) { entity.setLightningFadeTime(ticks); }
// ==================== LIFESPAN ====================
public float getMaxDistance() { return entity.getMaxDistance(); }
@@ -175,8 +135,6 @@ public class ScriptEnergyProjectile<T extends EntityAbilityProjectile> extends S
// ==================== STATE ====================
public boolean hasHit() { return entity.getHasHit(); }
public boolean isCharging() { return entity.isCharging(); }
public float getChargeProgress() { return entity.getChargeProgress(); }
// ==================== FIRE ====================
@@ -0,0 +1,67 @@
package noppes.npcs.scripted.entity;
import kamkeel.npcs.entity.EntityEnergySlicer;
import net.minecraft.entity.EntityLivingBase;
import noppes.npcs.api.entity.IEnergySlicer;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.scripted.constants.EntityType;
public class ScriptEnergySlicer<T extends EntityEnergySlicer> extends ScriptEnergyProjectile<T> implements IEnergySlicer {
public ScriptEnergySlicer(T entity) {
super(entity);
}
@Override
public int getType() {
return EntityType.ENERGY_SLICER;
}
@Override
public boolean typeOf(int type) {
return type == EntityType.ENERGY_SLICER || super.typeOf(type);
}
@Override
public int getEnergyType() {
return 4;
}
// ==================== SLICER-SPECIFIC ====================
public float getSliceWidth() { return entity.getSliceWidth(); }
public void setSliceWidth(float width) { entity.setSliceWidth(width); }
public float getSliceThickness() { return entity.getSliceThickness(); }
public void setSliceThickness(float thickness) { entity.setSliceThickness(thickness); }
public boolean isPiercing() { return entity.isPiercing(); }
public void setPiercing(boolean piercing) { entity.setPiercing(piercing); }
// ==================== FIRE ====================
@Override
public void fire(IEntity target) {
ensureSpawned();
EntityLivingBase mcTarget = null;
if (target != null && target.getMCEntity() instanceof EntityLivingBase) {
mcTarget = (EntityLivingBase) target.getMCEntity();
}
entity.startMoving(mcTarget);
}
@Override
public void fire(double x, double y, double z) {
ensureSpawned();
double dx = x - entity.posX;
double dy = y - entity.posY;
double dz = z - entity.posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
entity.motionX = (dx / len) * entity.getSpeed();
entity.motionY = (dy / len) * entity.getSpeed();
entity.motionZ = (dz / len) * entity.getSpeed();
}
entity.startMoving(null);
}
}
@@ -4,7 +4,7 @@ import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityProjectile;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import net.minecraft.block.material.Material;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityList;
@@ -532,7 +532,7 @@ public class ScriptEntity<T extends Entity> implements IEntity {
else if (type == EntityType.NPC)
cls = EntityNPCInterface.class;
else if (type == EntityType.ENERGY_PROJECTILE)
cls = EntityAbilityProjectile.class;
cls = EntityEnergyProjectile.class;
else if (type == EntityType.ENERGY_ORB)
cls = EntityAbilityOrb.class;
else if (type == EntityType.ENERGY_BEAM)
@@ -29,7 +29,7 @@ import noppes.npcs.constants.EnumMovingType;
import noppes.npcs.constants.EnumNavType;
import noppes.npcs.constants.EnumRoleType;
import noppes.npcs.constants.EnumStandingType;
import kamkeel.npcs.entity.EntityAbilityProjectile;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import noppes.npcs.controllers.FactionController;
import noppes.npcs.controllers.data.DialogOption;
import noppes.npcs.controllers.data.Line;
@@ -1467,7 +1467,7 @@ public class ScriptNpc<T extends EntityNPCInterface> extends ScriptLiving<T> imp
}
public IEnergyProjectile[] getActiveEnergyProjectiles() {
java.util.List<EntityAbilityProjectile> entities = EntityAbilityProjectile.getActiveProjectiles(npc.getEntityId());
java.util.List<EntityEnergyProjectile> entities = EntityEnergyProjectile.getActiveProjectiles(npc.getEntityId());
IEnergyProjectile[] result = new IEnergyProjectile[entities.size()];
for (int i = 0; i < entities.size(); i++) {
result[i] = (IEnergyProjectile) NpcAPI.Instance().getIEntity(entities.get(i));
@@ -1,7 +1,7 @@
package noppes.npcs.scripted.entity;
import kamkeel.npcs.controllers.AttributeController;
import kamkeel.npcs.entity.EntityAbilityProjectile;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.packets.data.AchievementPacket;
import kamkeel.npcs.network.packets.data.ChatAlertPacket;
@@ -937,7 +937,7 @@ public class ScriptPlayer<T extends EntityPlayerMP> extends ScriptLivingBase<T>
}
public IEnergyProjectile[] getActiveEnergyProjectiles() {
List<EntityAbilityProjectile> entities = EntityAbilityProjectile.getActiveProjectiles(player.getEntityId());
List<EntityEnergyProjectile> entities = EntityEnergyProjectile.getActiveProjectiles(player.getEntityId());
IEnergyProjectile[] result = new IEnergyProjectile[entities.size()];
for (int i = 0; i < entities.size(); i++) {
result[i] = (IEnergyProjectile) NpcAPI.Instance().getIEntity(entities.get(i));
@@ -0,0 +1,98 @@
package noppes.npcs.scripted.event;
import cpw.mods.fml.common.eventhandler.Cancelable;
import noppes.npcs.api.entity.IEnergyBarrier;
import noppes.npcs.api.entity.IEnergyProjectile;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.event.IEnergyBarrierEvent;
import noppes.npcs.constants.EnumScriptType;
public class EnergyBarrierEvent extends CustomNPCsEvent implements IEnergyBarrierEvent {
public final IEnergyBarrier barrier;
public final IEntity owner;
public EnergyBarrierEvent(IEnergyBarrier barrier) {
this.barrier = barrier;
this.owner = barrier.getOwner();
}
@Override
public IEnergyBarrier getBarrier() {
return barrier;
}
@Override
public IEntity getOwner() {
return owner;
}
@Override
public String getHookName() {
return "energyBarrierEvent";
}
public static class SpawnedEvent extends EnergyBarrierEvent implements IEnergyBarrierEvent.SpawnedEvent {
public SpawnedEvent(IEnergyBarrier barrier) {
super(barrier);
}
@Override
public String getHookName() {
return EnumScriptType.ENERGY_BARRIER_SPAWNED.function;
}
}
public static class UpdateEvent extends EnergyBarrierEvent implements IEnergyBarrierEvent.UpdateEvent {
public UpdateEvent(IEnergyBarrier barrier) {
super(barrier);
}
@Override
public String getHookName() {
return EnumScriptType.ENERGY_BARRIER_TICK.function;
}
}
@Cancelable
public static class HitEvent extends EnergyBarrierEvent implements IEnergyBarrierEvent.HitEvent {
private final IEnergyProjectile projectile;
private float damage;
public HitEvent(IEnergyBarrier barrier, IEnergyProjectile projectile, float damage) {
super(barrier);
this.projectile = projectile;
this.damage = damage;
}
@Override
public IEnergyProjectile getProjectile() {
return projectile;
}
@Override
public float getDamage() {
return damage;
}
@Override
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public String getHookName() {
return EnumScriptType.ENERGY_BARRIER_HIT.function;
}
}
public static class DestroyedEvent extends EnergyBarrierEvent implements IEnergyBarrierEvent.DestroyedEvent {
public DestroyedEvent(IEnergyBarrier barrier) {
super(barrier);
}
@Override
public String getHookName() {
return EnumScriptType.ENERGY_BARRIER_DESTROYED.function;
}
}
}
@@ -36,7 +36,7 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData {
if (!ability.getAllowedBy().allowsPlayer()) {
return; // Ability is NPC-only
}
// Use canonical key (registry key for built-in, UUID for custom)
// Use canonical key (registry key for built-in, name for custom)
String canonicalKey = ability.getId() != null ? ability.getId() : key;
data.unlockAbility(canonicalKey);
}
@@ -1694,6 +1694,7 @@ ability.activeSound=Active Sound
ability.counterSound=Counter Sound
ability.windUpAnimation=Wind Up Animation
ability.activeAnimation=Active Animation
ability.dazedAnimation=Dazed Animation
ability.counterAnimation=Counter Animation
ability.showTelegraph=Show Telegraph
ability.windUpColor=Wind Up Color
@@ -1804,6 +1805,10 @@ ability.innerRadius=Inner Radius
# Projectile
ability.projectileType=Projectile Type
# Heavy Hit
ability.hitDelayTicks=Hit Delay
ability.activeDisplayTicks=End Delay
# Orb Settings
ability.homing=Homing
ability.explosive=Explosive
@@ -1987,6 +1992,46 @@ ability.hover.canCounter=Enable counterattack when blocking damage
ability.hover.counterType=Flat = fixed damage, Percent = % of blocked damage
ability.hover.affectsCaster=Hazard can damage the NPC that created it
ability.hover.sweepMode=Swipe = single arc attack, Spin = continuous rotation
ability.hover.launching=When enabled, wall launches forward dealing damage and knockback
# Energy Barrier Abilities
ability.cnpc.dome=Dome
ability.cnpc.wall=Wall
ability.cnpc.shield=Shield
ability.cnpc.slicer=Slicer
ability.section.barrier=Barrier
ability.section.knockback=Knockback
ability.section.panel=Panel
ability.section.launch=Launch
ability.section.slicer=Slicer
ability.domeRadius=Dome Radius
ability.useHealth=Use Health
ability.maxHealth=Max Health
ability.useDuration=Use Duration
ability.defaultMultiplier=Default Multiplier
ability.innerAlpha=Inner Alpha
ability.heightOffset=Height Offset
ability.knockbackEnabled=Knockback
ability.knockbackStrength=Knockback Strength
ability.knockbackTarget=Target
ability.section.melee=Melee
ability.meleeEnabled=Melee Attackable
ability.meleeDamageMultiplier=Melee Multiplier
ability.followCaster=Follow Caster
ability.hover.followCaster=Dome stays centered on the caster as they move
ability.variant.small=Small
ability.variant.large=Large
ability.variant.static=Static
ability.variant.launched=Launched
gui.both=Both
gui.players=Players
# Conditions
condition.edit=Edit Condition