diff --git a/src/api b/src/api index efa8721d3..22bce4b2a 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit efa8721d3658b45146d58d6731a0b83c4860f02e +Subproject commit 22bce4b2acc9cf9a120a7fdddbb62cc4e567c796 diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java index 4912e4b72..725e440e5 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -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; - } } diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java index 8cb8cdaad..76ffc6cfb 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -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. diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java index e70e5c6ba..e26f33489 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -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. diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java index 048b3642f..a3adf71a0 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -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; - } } diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java similarity index 84% rename from src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java rename to src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java index 96e8e7b08..f4aa9c318 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyBarrier.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyBarrier.java new file mode 100644 index 000000000..6f4e1e89f --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyBarrier.java @@ -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; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyDome.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyDome.java new file mode 100644 index 000000000..ecdba16eb --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyDome.java @@ -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 +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyPanel.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyPanel.java new file mode 100644 index 000000000..279c6902d --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyPanel.java @@ -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(); + } + +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergySlicer.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergySlicer.java new file mode 100644 index 000000000..b5ee31568 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergySlicer.java @@ -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(); + } + +} diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index 90d20ea1e..10df3cdbe 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -33,20 +33,19 @@ public class AbilityCommand extends CommandKamkeelBase { desc = "List all custom abilities" ) public void list(ICommandSender sender, String[] args) { - Set uuids = AbilityController.Instance.getCustomAbilityIds(); - if (uuids.isEmpty()) { + Set 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 = "" + desc = "Delete a custom ability by name", + usage = "" ) public void delete(ICommandSender sender, String[] args) { if (args.length < 1) { - sendError(sender, "Usage: /kam ability delete "); + sendError(sender, "Usage: /kam ability delete "); 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 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 11ceffea8..891558b96 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -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 effects = new ArrayList<>(); + protected List 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 getEffects() { + public List getEffects() { return effects; } - public void setEffects(List effects) { + public void setEffects(List 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); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index c492cf50b..406d66c12 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -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 extenders = new ArrayList<>(); private final List> 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 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 getCustomAbilityIds() { + public Set getCustomAbilityNames() { return new LinkedHashSet<>(customAbilities.keySet()); } - public String getCustomAbilityName(String uuid) { - Ability a = customAbilities.get(uuid); - return a != null ? a.getName() : null; - } - public Map 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); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPotionEffect.java similarity index 87% rename from src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java rename to src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPotionEffect.java index e2c1da87d..cb2984946 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPotionEffect.java @@ -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; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java index 4849e4dd4..ab8446114 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java @@ -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(); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyBarrierData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyBarrierData.java new file mode 100644 index 000000000..2d65b6228 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyBarrierData.java @@ -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 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 getDamageMultipliers() { + return damageMultipliers; + } + + public void setDamageMultipliers(Map 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 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 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; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java index 559be77f4..415c5796c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java @@ -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; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyPanelData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyPanelData.java new file mode 100644 index 000000000..1fa8586fe --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyPanelData.java @@ -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; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java index 219479614..d41418d91 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java @@ -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 effects = (List) def.getValue(); + List effects = (List) 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 effects = (List) def.getValue(); + List effects = (List) 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 effects = (List) def.getValue(); + List effects = (List) def.getValue(); if (effects != null && meta[0] < effects.size()) { if (meta[1] == 1) { // duration field effects.get(meta[0]).setDurationTicks(field.getInteger()); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java index 6e419b943..be7b29aa6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java @@ -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> getter, Consumer> setter) { + public static FieldDef effectsListField(String label, Supplier> getter, Consumer> setter) { return FieldDef.custom(label, FieldType.EFFECTS_LIST, () -> getter.get(), - v -> setter.accept((List) v)); + v -> setter.accept((List) v)); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index dc1b075e7..331ed0de4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -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) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 367b534c7..7c85826a2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -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 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) )); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 62471c051..a8782122f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -1,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: diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java index 0d4b0ce8f..b75e41c3e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -1,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; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java similarity index 97% rename from src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java rename to src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java index 45772eedb..66e5ef4ac 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java @@ -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 implements IAbilityEnergyBeam { +public class AbilityBeam extends AbilityEnergyProjectile implements IAbilityEnergyBeam { private float beamWidth = 0.4f; private float headSize = 0.6f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java similarity index 98% rename from src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java rename to src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java index 5a542ce7d..cd36eeab3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java @@ -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 implements IAbilityDisc { +public class AbilityDisc extends AbilityEnergyProjectile implements IAbilityDisc { private float discRadius = 1.0f; private float discThickness = 0.2f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java new file mode 100644 index 000000000..b6bb57e96 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDome.java @@ -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 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 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")); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java new file mode 100644 index 000000000..3e0747160 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergy.java @@ -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 defs) { + addEnergyColorDefinitions(defs); + addEnergyEffectDefinitions(defs); + } + + /** + * Add the colors section to the visual tab. + */ + @SideOnly(Side.CLIENT) + protected void addEnergyColorDefinitions(List 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 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 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")); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyBarrier.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyBarrier.java new file mode 100644 index 000000000..7909f5a56 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyBarrier.java @@ -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 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 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); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java similarity index 83% rename from src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java rename to src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index f3ee572c2..d9ab67646 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -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 The entity type this ability spawns */ -public abstract class AbstractEnergyProjectileAbility - extends Ability implements IAbilityEnergyProjectile { +public abstract class AbilityEnergyProjectile extends AbilityEnergy implements IAbilityEnergyProjectile { protected static final int MAX_PROJECTILES = 8; @@ -39,9 +37,7 @@ public abstract class AbstractEnergyProjectileAbility 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 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 implements IAbilityLaserShot { +public class AbilityLaserShot extends AbilityEnergyProjectile implements IAbilityLaserShot { private float laserWidth = 0.3f; private float expansionSpeed = 3.0f; @@ -73,7 +73,7 @@ public class AbilityLaserShot extends AbstractEnergyProjectileAbility implements IAbilityOrb { +public class AbilityOrb extends AbilityEnergyProjectile implements IAbilityOrb { private float orbSize = 1.0f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java new file mode 100644 index 000000000..3cb407ef3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityShield.java @@ -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 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 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) + )); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java new file mode 100644 index 000000000..c79822f52 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java @@ -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 { + + 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 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 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)); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java similarity index 81% rename from src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java rename to src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java index c7cc9a1fa..67f9643d3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java @@ -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 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); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java new file mode 100644 index 000000000..f6bc037a4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java @@ -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 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 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)); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index db57660e4..4b6b6e18d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index dd499f122..72ebe29ae 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -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 diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 5adabc4b6..391d9466c 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 2eb5c0084..61cbe7a0b 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -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); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index 6f0068d26..f8b0aaac1 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -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 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; - } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java index 34156e362..11eaade79 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -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 effects = new ArrayList<>(); + private List 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 effects) { + List 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 effects) { + List 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); } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java new file mode 100644 index 000000000..c98fae96a --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java @@ -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); + } + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java new file mode 100644 index 000000000..c295f586d --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java @@ -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); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java new file mode 100644 index 000000000..d0a2a8a96 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java @@ -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 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); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java new file mode 100644 index 000000000..909e0dda8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java @@ -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 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 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); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java similarity index 75% rename from src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java rename to src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index 99c66edaa..8f58fada7 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -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>> activeProjectiles = new HashMap<>(); + private static final Map>> activeProjectiles = new HashMap<>(); - public static void trackProjectile(EntityAbilityProjectile projectile) { + public static void trackProjectile(EntityEnergyProjectile projectile) { int ownerId = projectile.getOwnerEntityId(); if (ownerId < 0) return; - List> refs = activeProjectiles.get(ownerId); + List> 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 getActiveProjectiles(int ownerEntityId) { - List> refs = activeProjectiles.get(ownerEntityId); + public static List getActiveProjectiles(int ownerEntityId) { + List> refs = activeProjectiles.get(ownerEntityId); if (refs == null) return Collections.emptyList(); - List result = new ArrayList<>(); - Iterator> it = refs.iterator(); + List result = new ArrayList<>(); + Iterator> 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 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 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 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 effects) { + public void setEffects(List 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; - } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java new file mode 100644 index 000000000..c76fc7bad --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java @@ -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 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 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); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java index d95051a66..a4757a20c 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java @@ -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); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java index 3ef040700..487d5f346 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java @@ -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 diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java index d5ca1c556..f42ae5ada 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java @@ -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); } } diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index e1cbfaf79..bd2ef983c 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -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 { @@ -36,6 +38,7 @@ public class Register { } public static class Abilities extends Register { + protected final Map>> variantEntries = new LinkedHashMap<>(); protected final Map uniqueNames = new LinkedHashMap<>(); protected final Set typeOnly = new HashSet<>(); @@ -64,6 +67,23 @@ public class Register { return factory.get(); } + public AbilityVariant registerVariant(String typeId, String variantName, String group, Consumer configurator) { + registerVariant(typeId, variantName, () -> new AbilityVariant(variantName, group, configurator)); + return new AbilityVariant(variantName, group, configurator); + } + + public AbilityVariant registerVariant(String typeId, String factoryName, Supplier factory) { + if (!variantEntries.containsKey(typeId)) { + variantEntries.put(typeId, new HashMap<>()); + } + + Map> 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> entry : entries.entrySet()) { AbilityController.Instance.registerType(entry.getKey(), entry.getValue()); @@ -71,6 +91,14 @@ public class Register { AbilityController.Instance.registerAbility(uniqueNames.get(entry.getKey()), entry.getValue().get()); } } + + for (Map.Entry>> entry : variantEntries.entrySet()) { + String typeId = entry.getKey(); + Map> map = entry.getValue(); + for (Map.Entry> 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 { try { String path = "/assets/" + namespace + "/" + animationsPath; for (Map.Entry> 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) { diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java new file mode 100644 index 000000000..774e9661e --- /dev/null +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -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. + *

+ * 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. + *

+ * 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() {} +} diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index caf86c7b5..892c3648f 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -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(); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 78692162f..cb1e5104d 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -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 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 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 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 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 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 diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 75b615671..2fefb23ac 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -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); diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index b54b4b245..bbb19c97d 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -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); diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index d4c729e02..0790e3dd3 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -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()); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index 103d446fc..905faa1f4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -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 displayToUuid = new HashMap<>(); private HashMap 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 getFilteredList() { if (search.isEmpty()) { - return new ArrayList<>(displayToUuid.keySet()); + return new ArrayList<>(rawData.keySet()); } List 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 list, HashMap 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) { diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java index cce48bd32..5d810862f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java @@ -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); } diff --git a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java index b1f123956..1ef3d52e5 100644 --- a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java +++ b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java @@ -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(); diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index 66e8ed46c..d016e7c77 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -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 customData = new HashMap<>(); private HashMap builtInData = new HashMap<>(); - /** Maps display name -> UUID for custom abilities */ - private final HashMap 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 getSearchList() { - if (showingBuiltIn) { - if (search.isEmpty()) { - return new ArrayList<>(builtInData.keySet()); - } - List 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 list = new ArrayList<>(); - for (String name : displayToUuid.keySet()) { - if (name.toLowerCase().contains(search.toLowerCase())) { - list.add(name); - } - } - return list; + HashMap data = getCurrentData(); + if (search.isEmpty()) { + return new ArrayList<>(data.keySet()); } + List 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 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(); } diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java index da4e8f136..9e2588103 100644 --- a/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -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; diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index b4203b452..e7c29f324 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -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 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"); } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 60386b8bf..f5ffb7028 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -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. *

* 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 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 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 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 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 // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 24f271f33..02ec0f4b3 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -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); diff --git a/src/main/java/noppes/npcs/scripted/constants/EntityType.java b/src/main/java/noppes/npcs/scripted/constants/EntityType.java index 355173646..62cd482fa 100644 --- a/src/main/java/noppes/npcs/scripted/constants/EntityType.java +++ b/src/main/java/noppes/npcs/scripted/constants/EntityType.java @@ -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; } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java new file mode 100644 index 000000000..b84c2bf4b --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java @@ -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 extends ScriptEntity 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(); } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java new file mode 100644 index 000000000..9455b28cd --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java @@ -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 extends ScriptEnergyAbility 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; + } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDome.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDome.java new file mode 100644 index 000000000..8d331c170 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDome.java @@ -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 extends ScriptEnergyBarrier 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); } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyPanel.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyPanel.java new file mode 100644 index 000000000..5da93dc4a --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyPanel.java @@ -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 extends ScriptEnergyBarrier 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; } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java index f614b8122..9da13936f 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java @@ -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 extends ScriptEntity implements IEnergyProjectile { - protected T entity; +public class ScriptEnergyProjectile extends ScriptEnergyAbility implements IEnergyProjectile { public ScriptEnergyProjectile(T entity) { super(entity); - this.entity = entity; } // ==================== TYPE ==================== @@ -36,14 +33,6 @@ public class ScriptEnergyProjectile 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 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 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 extends S // ==================== STATE ==================== public boolean hasHit() { return entity.getHasHit(); } - public boolean isCharging() { return entity.isCharging(); } - public float getChargeProgress() { return entity.getChargeProgress(); } // ==================== FIRE ==================== diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java new file mode 100644 index 000000000..270266123 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java @@ -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 extends ScriptEnergyProjectile 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); + } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEntity.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEntity.java index 65b2866af..c5b2c6a6e 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEntity.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEntity.java @@ -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 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) diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java index 2379c2035..78134a3fc 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java @@ -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 extends ScriptLiving imp } public IEnergyProjectile[] getActiveEnergyProjectiles() { - java.util.List entities = EntityAbilityProjectile.getActiveProjectiles(npc.getEntityId()); + java.util.List 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)); diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 3608b9d1e..6caca7419 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -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 extends ScriptLivingBase } public IEnergyProjectile[] getActiveEnergyProjectiles() { - List entities = EntityAbilityProjectile.getActiveProjectiles(player.getEntityId()); + List 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)); diff --git a/src/main/java/noppes/npcs/scripted/event/EnergyBarrierEvent.java b/src/main/java/noppes/npcs/scripted/event/EnergyBarrierEvent.java new file mode 100644 index 000000000..a64948a6c --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/event/EnergyBarrierEvent.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java index 42ed3372e..c1de83878 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -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); } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index eaaf48a45..347997210 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -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