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..f6a22e6ed --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyDome.java @@ -0,0 +1,153 @@ +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.EntityEnergyDome; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the Energy Dome entity as a translucent blocky hemisphere. + * Built from cube panels forming a dome shape - voxel-blocky aesthetic. + * Inner layer more opaque, outer layer translucent glow. + */ +@SideOnly(Side.CLIENT) +public class RenderEnergyDome extends RenderAbilityProjectile { + + @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 + if (dome.hasLightningEffect()) { + renderAttachedLightning(dome, radius); + } + + // Hit flash effect + float flashAlpha = 0.0f; + if (dome.getHitFlash() > 0) { + flashAlpha = dome.getHitFlash() / 4.0f * 0.3f; + } + + float innerScale = 0.95f; + + // Render outer dome (translucent) + if (dome.isOuterColorEnabled()) { + float outerScale = 1.0f + dome.getOuterColorWidth() * 0.1f; + float outerAlpha = dome.getOuterColorAlpha() * healthPercent; + GL11.glDepthMask(false); + renderBlockyDome(dome.getOuterColor(), outerAlpha + flashAlpha, radius * outerScale, 8); + GL11.glDepthMask(true); + } + + // Render inner dome (more opaque) + float innerAlpha = (0.5f + 0.3f * healthPercent); + renderBlockyDome(dome.getInnerColor(), innerAlpha + flashAlpha, radius * innerScale, 8); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * Render a dome made of blocky quad panels - voxel style. + * Uses latitude/longitude segments to form a hemisphere of flat panels. + */ + private void renderBlockyDome(int color, float alpha, float radius, int segments) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + Tessellator tess = Tessellator.instance; + int latSegments = segments / 2; // Half sphere + + for (int lat = 0; lat < latSegments; lat++) { + float theta1 = (float) (Math.PI * 0.5 * lat / latSegments); + float theta2 = (float) (Math.PI * 0.5 * (lat + 1) / latSegments); + + 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; + + // Normal pointing outward + 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.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + 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(); + } + } + + // Bottom ring (ground level circle) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha * 0.5f); + tess.setNormal(0, -1, 0); + 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) * radius; + float z1 = (float) Math.sin(phi1) * radius; + float x2 = (float) Math.cos(phi2) * radius; + float z2 = (float) Math.sin(phi2) * radius; + + tess.addVertex(0, 0, 0); + tess.addVertex(x1, 0, z1); + tess.addVertex(x2, 0, z2); + tess.addVertex(0, 0, 0); + } + tess.draw(); + } + + /** + * Render lightning arcs on the dome surface. + */ + private void renderAttachedLightning(EntityEnergyDome dome, float radius) { + AttachedLightningRenderer.LightningState state = getLightningState(dome); + float density = dome.getLightningDensity(); + float lightningRadius = radius + dome.getLightningRadius(); + int outerColor = dome.getOuterColor(); + int innerColor = dome.getInnerColor(); + int fadeTime = dome.getLightningFadeTime(); + + state.update(density, lightningRadius, outerColor, innerColor, fadeTime); + state.render(); + } + + private AttachedLightningRenderer.LightningState getLightningState(EntityEnergyDome dome) { + if (dome.lightningState == null) { + dome.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) dome.lightningState; + } +} 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..25352ec54 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyPanel.java @@ -0,0 +1,171 @@ +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.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 RenderAbilityProjectile { + + @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); + + // Rotate panel to face the correct direction + GL11.glRotatef(-panelYaw, 0.0f, 1.0f, 0.0f); + + // Lightning (before rotation for uniform spread) + if (panel.hasLightningEffect()) { + renderAttachedLightning(panel, Math.max(width, height) * 0.5f); + } + + // Hit flash + float flashAlpha = 0.0f; + if (panel.getHitFlash() > 0) { + flashAlpha = panel.getHitFlash() / 4.0f * 0.3f; + } + + // 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 (more opaque core) + float innerAlpha = (0.6f + 0.3f * healthPercent); + renderPanel(panel.getInnerColor(), innerAlpha + flashAlpha, + 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). + */ + 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; + + // Front face (z+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + 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); + tess.draw(); + + // Back face (z-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + 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); + tess.draw(); + + // Top edge (y+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + 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); + tess.draw(); + + // Bottom edge (y-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + 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); + tess.draw(); + + // Right edge (x+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + 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(); + + // Left edge (x-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + 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(); + } + + /** + * Render lightning arcs on the panel surface. + */ + private void renderAttachedLightning(EntityEnergyPanel panel, float size) { + AttachedLightningRenderer.LightningState state = getLightningState(panel); + float density = panel.getLightningDensity(); + float radius = size + panel.getLightningRadius(); + int outerColor = panel.getOuterColor(); + int innerColor = panel.getInnerColor(); + int fadeTime = panel.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + private AttachedLightningRenderer.LightningState getLightningState(EntityEnergyPanel panel) { + if (panel.lightningState == null) { + panel.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) panel.lightningState; + } +} 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..ad48254ab --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergySlicer.java @@ -0,0 +1,163 @@ +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.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 RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityEnergySlicer slicer = (EntityEnergySlicer) entity; + + float size = slicer.getInterpolatedSize(partialTicks); + float width = slicer.getSliceWidth(); + float thickness = slicer.getSliceThickness(); + + // Scale with interpolated size for charging grow effect + float sizeRatio = size > 0.01f ? size / Math.max(0.01f, slicer.getSliceWidth()) : 0.01f; + if (sizeRatio > 1.0f) sizeRatio = 1.0f; + + setupRenderState(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Lightning before rotation + if (slicer.hasLightningEffect()) { + renderAttachedLightning(slicer, width * 0.5f * sizeRatio); + } + + // 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); + + GL11.glPushMatrix(); + GL11.glScalef(sizeRatio, sizeRatio, sizeRatio); + + 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) + renderBlade(slicer.getInnerColor(), 1.0f, + width * 0.5f * innerScale, thickness, 0.03f); + + GL11.glPopMatrix(); + 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). + */ + 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; + + // Front face (z+) + tess.startDrawingQuads(); + 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); + tess.draw(); + + // Back face (z-) + tess.startDrawingQuads(); + 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); + tess.draw(); + + // Top edge (y+) + tess.startDrawingQuads(); + 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); + tess.draw(); + + // Bottom edge (y-) + tess.startDrawingQuads(); + 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); + tess.draw(); + + // Right edge (x+) + tess.startDrawingQuads(); + 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); + tess.draw(); + + // Left edge (x-) + tess.startDrawingQuads(); + 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); + tess.draw(); + } + + /** + * Render lightning arcs on the slicer. + */ + private void renderAttachedLightning(EntityEnergySlicer slicer, float size) { + AttachedLightningRenderer.LightningState state = getLightningState(slicer); + float density = slicer.getLightningDensity(); + float radius = size + slicer.getLightningRadius(); + int outerColor = slicer.getOuterColor(); + int innerColor = slicer.getInnerColor(); + int fadeTime = slicer.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + private AttachedLightningRenderer.LightningState getLightningState(EntityEnergySlicer slicer) { + if (slicer.lightningState == null) { + slicer.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) slicer.lightningState; + } +} 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..7d6cca477 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -99,6 +99,11 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:hazard", AbilityHazard::new); registerType("cnpc:trap", AbilityTrap::new); + + registerType("cnpc:energy_dome", AbilityEnergyDome::new); + registerType("cnpc:energy_wall", AbilityEnergyWall::new); + registerType("cnpc:energy_shield", AbilityEnergyShield::new); + registerType("cnpc:energy_slicer", AbilityEnergySlicer::new); } // ═══════════════════════════════════════════════════════════════════════════ 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..be1c3254a --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyBarrierData.java @@ -0,0 +1,115 @@ +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 { + public float maxHealth = 100.0f; + public boolean useHealth = true; + public int durationTicks = 200; + public boolean useDuration = true; + public float defaultMultiplier = 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; } + + // ==================== 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); + + 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.getFloat("barrierMaxHealth"); + useHealth = nbt.getBoolean("barrierUseHealth"); + durationTicks = nbt.getInteger("barrierDuration"); + useDuration = nbt.getBoolean("barrierUseDuration"); + defaultMultiplier = nbt.hasKey("barrierDefaultMult") ? nbt.getFloat("barrierDefaultMult") : 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.damageMultipliers = new HashMap<>(damageMultipliers); + 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..87f68de1d --- /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.getFloat("panelHeightOffset"); + launching = 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/type/AbilityEnergyDome.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyDome.java new file mode 100644 index 000000000..0fdac9328 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyDome.java @@ -0,0 +1,122 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.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.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 AbilityEnergyDome extends AbstractEnergyBarrierAbility { + + private float domeRadius = 5.0f; + + public AbilityEnergyDome() { + super( + new EnergyDisplayData(0x44CCFF, 0x2288FF, true, 0.3f, 0.35f, 0.0f), + new EnergyBarrierData(100.0f, true, 200, true) + ); + this.typeId = "ability.cnpc.energy_dome"; + this.name = "Energy Dome"; + this.targetingMode = TargetingMode.SELF; + this.maxRange = 0; + this.minRange = 0; + this.cooldownTicks = 100; + this.windUpTicks = 30; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.RING; + this.showTelegraph = true; + this.windUpAnimationName = "Ability_EnergyDome_Windup"; + this.activeAnimationName = "Ability_EnergyDome_Active"; + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected Entity 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.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 -> { + AbilityEnergyDome dome = (AbilityEnergyDome) a; + a.setName("Small Energy Dome"); + dome.setDomeRadius(3.0f); + dome.setBarrierMaxHealth(60.0f); + }), + new AbilityVariant("ability.variant.large", a -> { + AbilityEnergyDome dome = (AbilityEnergyDome) a; + a.setName("Large Energy 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); + } + + @Override + protected void readBarrierTypeNBT(NBTTagCompound nbt) { + this.domeRadius = nbt.hasKey("domeRadius") ? nbt.getFloat("domeRadius") : 5.0f; + } + + // ==================== GETTERS & SETTERS ==================== + + public float getDomeRadius() { return domeRadius; } + public void setDomeRadius(float radius) { this.domeRadius = Math.max(1.0f, radius); } + + // ==================== 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)); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyShield.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyShield.java new file mode 100644 index 000000000..9bcff686e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyShield.java @@ -0,0 +1,144 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.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.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 AbilityEnergyShield extends AbstractEnergyBarrierAbility { + + private float shieldWidth = 2.5f; + private float shieldHeight = 2.5f; + + public AbilityEnergyShield() { + super( + new EnergyDisplayData(0xFFDD44, 0xFFAA00, true, 0.3f, 0.4f, 0.0f), + new EnergyBarrierData(60.0f, true, 120, true) + ); + this.typeId = "ability.cnpc.energy_shield"; + this.name = "Energy Shield"; + this.targetingMode = TargetingMode.SELF; + this.maxRange = 0; + this.minRange = 0; + this.cooldownTicks = 60; + this.windUpTicks = 10; + this.lockMovement = LockMovementType.NO; + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + this.windUpAnimationName = "Ability_EnergyShield_Windup"; + this.activeAnimationName = "Ability_EnergyShield_Active"; + this.allowedBy = UserType.BOTH; + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected Entity 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 -> { + AbilityEnergyShield shield = (AbilityEnergyShield) a; + a.setName("Small Energy Shield"); + shield.setShieldWidth(1.5f); + shield.setShieldHeight(1.5f); + shield.setBarrierMaxHealth(40.0f); + }), + new AbilityVariant("ability.variant.large", a -> { + AbilityEnergyShield shield = (AbilityEnergyShield) a; + a.setName("Large Energy 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/AbilityEnergySlicer.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergySlicer.java new file mode 100644 index 000000000..195311049 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergySlicer.java @@ -0,0 +1,176 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.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 AbstractEnergyProjectileAbility to leverage the existing projectile infrastructure. + */ +public class AbilityEnergySlicer extends AbstractEnergyProjectileAbility { + + private float sliceWidth = 3.0f; + private float sliceThickness = 0.15f; + private boolean piercing = true; + + public AbilityEnergySlicer() { + 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.energy_slicer"; + this.name = "Energy 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 = "Ability_EnergySlicer_Windup"; + this.activeAnimationName = "Ability_EnergySlicer_Active"; + } + + // ==================== 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("Energy Slicer"); + }), + new AbilityVariant("ability.variant.dual", a -> { + AbilityEnergySlicer slicer = (AbilityEnergySlicer) a; + a.setName("Double Slicer"); + slicer.setProjectileCount(2); + slicer.setFireDelay(5); + a.setWindUpAnimationName("Ability_EnergySlicerDual_Windup"); + a.setActiveAnimationName("Ability_EnergySlicerDual_Active"); + }) + ); + } + + // ==================== 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/AbilityEnergyWall.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyWall.java new file mode 100644 index 000000000..9007c1dba --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyWall.java @@ -0,0 +1,179 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.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.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 AbilityEnergyWall extends AbstractEnergyBarrierAbility { + + private final EnergyPanelData panelData; + + public AbilityEnergyWall() { + super( + new EnergyDisplayData(0x44FFCC, 0x22CCAA, true, 0.3f, 0.4f, 0.0f), + new EnergyBarrierData(80.0f, true, 160, true) + ); + this.panelData = new EnergyPanelData(4.0f, 3.0f, 0.0f); + this.typeId = "ability.cnpc.energy_wall"; + this.name = "Energy 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 = "Ability_EnergyWall_Windup"; + this.activeAnimationName = "Ability_EnergyWall_Active"; + } + + @Override + public boolean hasDamage() { + return panelData.launching; // Only has damage when launching + } + + // ==================== ABSTRACT IMPLEMENTATIONS ==================== + + @Override + protected Entity 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); + 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 -> { + AbilityEnergyWall wall = (AbilityEnergyWall) 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/controllers/data/ability/type/AbstractEnergyBarrierAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyBarrierAbility.java new file mode 100644 index 000000000..f95e76d47 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyBarrierAbility.java @@ -0,0 +1,224 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +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.telegraph.TelegraphType; +import net.minecraft.entity.Entity; +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; +import java.util.Map; + +/** + * Abstract base for energy barrier abilities (Dome, Wall, Shield). + * Handles shared visual data, barrier configuration, and lifecycle management. + */ +public abstract class AbstractEnergyBarrierAbility extends Ability { + + protected EnergyDisplayData displayData; + protected EnergyLightningData lightningData; + protected final EnergyBarrierData barrierData; + + // Runtime entity tracking + protected transient Entity barrierEntity; + + protected AbstractEnergyBarrierAbility(EnergyDisplayData displayData, EnergyBarrierData barrierData) { + this.displayData = displayData; + this.barrierData = barrierData; + this.lightningData = new EnergyLightningData(); + } + + // ==================== ABSTRACT METHODS ==================== + + /** + * Create and spawn the barrier entity during execution. + */ + protected abstract Entity 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 onExecute(EntityLivingBase caster, EntityLivingBase target) { + 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); + displayData.writeNBT(nbt); + lightningData.writeNBT(nbt); + barrierData.writeNBT(nbt); + } + + @Override + public final void readTypeNBT(NBTTagCompound nbt) { + readBarrierTypeNBT(nbt); + displayData.readNBT(nbt); + lightningData.readNBT(nbt); + barrierData.readNBT(nbt); + } + + // ==================== API GETTERS & SETTERS ==================== + + // Display data + public int getInnerColor() { return displayData.innerColor; } + public void setInnerColor(int color) { displayData.innerColor = color; } + + 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; } + + // 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; } + + // 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); } + + // ==================== 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)); + + // Visual tab + defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual")); + defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab("ability.tab.visual")); + defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual")); + defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .range(0, 1).visibleWhen(this::isOuterColorEnabled) + ).tab("ability.tab.visual")); + + defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual")); + defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab("ability.tab.visual")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .visibleWhen(this::hasLightningEffect).range(0.01f, 100f), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .range(0.1f, 100f).visibleWhen(this::hasLightningEffect) + ).tab("ability.tab.visual")); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 99c66edaa..630bf9c84 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -270,6 +270,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 +317,52 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA */ protected abstract void updateProjectile(); + // ==================== BARRIER COLLISION ==================== + + /** + * Check for collision with energy barrier entities (Dome, Panel). + * Barriers only block incoming projectiles from enemies. + * @return true if projectile was absorbed (caller should stop processing) + */ + protected boolean checkBarrierCollision() { + // Search area for barriers - check a generous range + double searchRange = 10.0; + AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( + posX - searchRange, posY - searchRange, posZ - searchRange, + posX + searchRange, posY + searchRange, posZ + searchRange + ); + + // Check domes + @SuppressWarnings("unchecked") + List domes = worldObj.getEntitiesWithinAABB(EntityEnergyDome.class, searchBox); + for (EntityEnergyDome dome : domes) { + if (dome.isDead) continue; + if (dome.isIncomingProjectile(this)) { + if (dome.onProjectileHit(this, getDamage())) { + hasHit = true; + this.setDead(); + return true; + } + } + } + + // Check panels + @SuppressWarnings("unchecked") + List panels = worldObj.getEntitiesWithinAABB(EntityEnergyPanel.class, searchBox); + for (EntityEnergyPanel panel : panels) { + if (panel.isDead) continue; + if (panel.isIncomingProjectile(this)) { + if (panel.onProjectileHit(this, getDamage())) { + hasHit = true; + this.setDead(); + return true; + } + } + } + + return false; + } + // ==================== POSITION INTERPOLATION ==================== @Override 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..851de3687 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java @@ -0,0 +1,314 @@ +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.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 noppes.npcs.entity.EntityNPCInterface; + +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). + */ +public class EntityEnergyDome extends Entity implements IEntityAdditionalSpawnData { + + // ==================== VISUAL PROPERTIES ==================== + protected EnergyDisplayData displayData = new EnergyDisplayData(); + protected EnergyLightningData lightningData = new EnergyLightningData(); + + // ==================== BARRIER PROPERTIES ==================== + protected EnergyBarrierData barrierData = new EnergyBarrierData(); + protected float currentHealth; + protected float domeRadius = 5.0f; + + // ==================== TRACKING ==================== + protected int ownerEntityId = -1; + protected int ticksAlive = 0; + + // ==================== STATE ==================== + protected transient Ability sourceAbility = null; + + // ==================== DATA WATCHER INDICES ==================== + private static final int DW_HEALTH_PERCENT = 20; + private static final int DW_HIT_FLASH = 21; + + // ==================== CLIENT STATE ==================== + @SideOnly(Side.CLIENT) + public transient Object lightningState; + + public EntityEnergyDome(World world) { + super(world); + this.setSize(1.0f, 1.0f); // Hitbox doesn't really matter, we use radius checks + this.noClip = true; + this.isImmuneToFire = true; + this.ignoreFrustumCheck = true; + } + + 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 + protected void entityInit() { + this.dataWatcher.addObject(DW_HEALTH_PERCENT, 1.0f); + this.dataWatcher.addObject(DW_HIT_FLASH, (byte) 0); + } + + @Override + public void onUpdate() { + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + + super.onUpdate(); + ticksAlive++; + + if (!worldObj.isRemote) { + // Check owner death + if (ownerEntityId >= 0 && ticksAlive > 5) { + Entity owner = worldObj.getEntityByID(ownerEntityId); + if (owner != null) { + if (owner.isDead) { this.setDead(); return; } + if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) { + this.setDead(); return; + } + } + } + + // Duration check + if (barrierData.useDuration && ticksAlive >= barrierData.durationTicks) { + this.setDead(); + return; + } + + // Reset hit flash + if (getHitFlash() > 0) { + setHitFlash((byte) (getHitFlash() - 1)); + } + } + } + + /** + * Apply damage to this dome from a projectile. + * Returns true if the dome absorbed the hit (projectile should be destroyed). + */ + public boolean onProjectileHit(EntityAbilityProjectile projectile, float baseDamage) { + if (!barrierData.useHealth) { + // Duration-only mode: block but don't take damage + triggerHitFlash(); + return true; + } + + // 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; + + currentHealth -= damage; + syncHealthPercent(); + triggerHitFlash(); + + if (currentHealth <= 0) { + this.setDead(); + } + + return true; + } + + /** + * Check if a projectile at the given position is entering this dome from outside. + * Only blocks incoming projectiles (dot product check). + */ + public boolean isIncomingProjectile(EntityAbilityProjectile projectile) { + // 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 EntityNPCInterface && projOwner instanceof EntityNPCInterface) { + if (((EntityNPCInterface) owner).faction.id == ((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; + } + + // ==================== HELPERS ==================== + + private 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); + } + } + + private void triggerHitFlash() { + if (!worldObj.isRemote) { + setHitFlash((byte) 4); + } + } + + private 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); + } + + public Entity getOwnerEntity() { + if (ownerEntityId == -1) return null; + return worldObj.getEntityByID(ownerEntityId); + } + + public int getOwnerEntityId() { return ownerEntityId; } + public float getDomeRadius() { return domeRadius; } + public EnergyDisplayData getDisplayData() { return displayData; } + public EnergyLightningData getLightningData() { return lightningData; } + public EnergyBarrierData getBarrierData() { return barrierData; } + public float getCurrentHealth() { return currentHealth; } + + public void setSourceAbility(Ability ability) { this.sourceAbility = ability; } + public Ability getSourceAbility() { return sourceAbility; } + + // ==================== VISUAL GETTERS ==================== + + public int getInnerColor() { return displayData.innerColor; } + public int getOuterColor() { return displayData.outerColor; } + public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; } + public float getOuterColorWidth() { return displayData.outerColorWidth; } + public float getOuterColorAlpha() { return displayData.outerColorAlpha; } + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public int getLightningFadeTime() { return lightningData.lightningFadeTime; } + + // ==================== 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; } + + @Override + @SideOnly(Side.CLIENT) + public boolean isInRangeToRenderDist(double distance) { + double d = domeRadius * 8.0D; + d *= 64.0D; + return distance < d * d; + } + + // ==================== 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; } + + // ==================== NBT ==================== + + @Override + protected void readEntityFromNBT(NBTTagCompound nbt) { + this.domeRadius = nbt.getFloat("DomeRadius"); + this.ownerEntityId = nbt.getInteger("OwnerId"); + this.ticksAlive = nbt.getInteger("TicksAlive"); + this.currentHealth = nbt.getFloat("CurrentHealth"); + displayData.readNBT(nbt); + lightningData.readNBT(nbt); + barrierData.readNBT(nbt); + } + + @Override + protected void writeEntityToNBT(NBTTagCompound nbt) { + nbt.setFloat("DomeRadius", domeRadius); + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setInteger("TicksAlive", ticksAlive); + nbt.setFloat("CurrentHealth", currentHealth); + displayData.writeNBT(nbt); + lightningData.writeNBT(nbt); + barrierData.writeNBT(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 dome 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 dome spawn data", e); + } + } +} 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..ebc75bb0c --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java @@ -0,0 +1,447 @@ +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.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.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 net.minecraft.entity.player.EntityPlayer; + +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). + */ +public class EntityEnergyPanel extends Entity implements IEntityAdditionalSpawnData { + + public enum PanelMode { + PLACED, // Stationary wall + HELD, // Follows caster, rotates with caster look + LAUNCHED // Moves forward, deals damage/knockback on contact + } + + // ==================== VISUAL PROPERTIES ==================== + protected EnergyDisplayData displayData = new EnergyDisplayData(); + protected EnergyLightningData lightningData = new EnergyLightningData(); + + // ==================== BARRIER PROPERTIES ==================== + protected EnergyBarrierData barrierData = new EnergyBarrierData(); + protected EnergyPanelData panelData = new EnergyPanelData(); + protected float currentHealth; + + // ==================== PANEL STATE ==================== + protected PanelMode mode = PanelMode.PLACED; + protected float panelYaw = 0.0f; // Rotation of the panel face (degrees) + + // ==================== TRACKING ==================== + protected int ownerEntityId = -1; + protected int ticksAlive = 0; + + // ==================== STATE ==================== + protected transient Ability sourceAbility = null; + + // ==================== DATA WATCHER INDICES ==================== + private static final int DW_HEALTH_PERCENT = 20; + private static final int DW_HIT_FLASH = 21; + + // ==================== CLIENT STATE ==================== + @SideOnly(Side.CLIENT) + public transient Object lightningState; + + public EntityEnergyPanel(World world) { + super(world); + this.setSize(0.5f, 0.5f); + this.noClip = true; + this.isImmuneToFire = true; + this.ignoreFrustumCheck = true; + } + + 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 + protected void entityInit() { + this.dataWatcher.addObject(DW_HEALTH_PERCENT, 1.0f); + this.dataWatcher.addObject(DW_HIT_FLASH, (byte) 0); + } + + @Override + public void onUpdate() { + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + + super.onUpdate(); + ticksAlive++; + + if (!worldObj.isRemote) { + // Check owner death + if (ownerEntityId >= 0 && ticksAlive > 5) { + Entity owner = worldObj.getEntityByID(ownerEntityId); + if (owner != null) { + if (owner.isDead) { this.setDead(); return; } + if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) { + this.setDead(); return; + } + } + } + + // Duration check + if (barrierData.useDuration && ticksAlive >= barrierData.durationTicks) { + this.setDead(); + return; + } + + // Reset hit flash + if (getHitFlash() > 0) { + setHitFlash((byte) (getHitFlash() - 1)); + } + } + + // 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; + + // Follow owner position + float frontDist = 1.5f; + float yawRad = (float) Math.toRadians(owner.rotationYaw); + 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); + this.panelYaw = owner.rotationYaw; + } + + private void updateLaunched() { + if (worldObj.isRemote) return; + + // Move forward + this.posX += motionX; + this.posY += motionY; + this.posZ += motionZ; + this.setPosition(posX, posY, posZ); + + // Check entity collision for damage + float halfW = panelData.panelWidth * 0.5f; + float halfH = panelData.panelHeight * 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); + 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 distance check (30 blocks) + double distSq = (posX - prevPosX) * (posX - prevPosX) + (posZ - prevPosZ) * (posZ - prevPosZ); + if (ticksAlive > 200) { + this.setDead(); + } + } + + /** + * Apply damage to this panel from a projectile. + */ + public boolean onProjectileHit(EntityAbilityProjectile projectile, float baseDamage) { + if (!barrierData.useHealth) { + triggerHitFlash(); + return true; + } + + String typeId = ""; + if (projectile.getSourceAbility() != null) { + typeId = projectile.getSourceAbility().getTypeId(); + } + float multiplier = barrierData.getMultiplier(typeId); + float damage = baseDamage * multiplier; + + currentHealth -= damage; + syncHealthPercent(); + triggerHitFlash(); + + if (currentHealth <= 0) { + this.setDead(); + } + + return true; + } + + /** + * Check if a projectile hits this panel surface. + * Only blocks incoming projectiles (not from the panel's owner). + */ + public boolean isIncomingProjectile(EntityAbilityProjectile projectile) { + 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 (dot product with panel normal) + double normalX = -Math.sin(yawRad); + double normalZ = Math.cos(yawRad); + double dot = projectile.motionX * normalX + projectile.motionZ * normalZ; + + // If the projectile is on the owner's side, it must be moving toward the panel + // (negative dot means moving into the panel from the front side) + return true; // If within bounds, it's a hit regardless of direction + } + + // ==================== HELPERS ==================== + + private 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); + } + } + + private void triggerHitFlash() { + if (!worldObj.isRemote) { + setHitFlash((byte) 4); + } + } + + private 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); + } + + public Entity getOwnerEntity() { + if (ownerEntityId == -1) return null; + return worldObj.getEntityByID(ownerEntityId); + } + + // ==================== GETTERS ==================== + + public int getOwnerEntityId() { return ownerEntityId; } + public PanelMode getMode() { return mode; } + public float getPanelYaw() { return panelYaw; } + public EnergyDisplayData getDisplayData() { return displayData; } + public EnergyLightningData getLightningData() { return lightningData; } + public EnergyBarrierData getBarrierData() { return barrierData; } + public EnergyPanelData getPanelData() { return panelData; } + public float getCurrentHealth() { return currentHealth; } + + public void setSourceAbility(Ability ability) { this.sourceAbility = ability; } + public Ability getSourceAbility() { return sourceAbility; } + + // Visual getters + public int getInnerColor() { return displayData.innerColor; } + public int getOuterColor() { return displayData.outerColor; } + public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; } + public float getOuterColorWidth() { return displayData.outerColorWidth; } + public float getOuterColorAlpha() { return displayData.outerColorAlpha; } + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public int getLightningFadeTime() { return lightningData.lightningFadeTime; } + + // ==================== 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; } + + @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; + } + + // ==================== 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; } + + // ==================== NBT ==================== + + @Override + protected void readEntityFromNBT(NBTTagCompound nbt) { + this.ownerEntityId = nbt.getInteger("OwnerId"); + this.ticksAlive = nbt.getInteger("TicksAlive"); + this.currentHealth = nbt.getFloat("CurrentHealth"); + this.panelYaw = nbt.getFloat("PanelYaw"); + this.mode = PanelMode.values()[nbt.getInteger("PanelMode")]; + displayData.readNBT(nbt); + lightningData.readNBT(nbt); + barrierData.readNBT(nbt); + panelData.readNBT(nbt); + } + + @Override + protected void writeEntityToNBT(NBTTagCompound nbt) { + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setInteger("TicksAlive", ticksAlive); + nbt.setFloat("CurrentHealth", currentHealth); + nbt.setFloat("PanelYaw", panelYaw); + nbt.setInteger("PanelMode", mode.ordinal()); + displayData.writeNBT(nbt); + lightningData.writeNBT(nbt); + barrierData.writeNBT(nbt); + panelData.writeNBT(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 panel 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 panel spawn data", e); + } + } +} 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..5fccfe2e4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java @@ -0,0 +1,337 @@ +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 EntityAbilityProjectile to leverage the existing projectile infrastructure. + */ +public class EntityEnergySlicer extends EntityAbilityProjectile { + + 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); + } + + /** + * Setup in charging mode (windup phase). + */ + public void setupCharging(EnergyAnchorData anchor, int chargeDuration, float width, float thickness) { + this.sliceWidth = 0.01f; + this.sliceThickness = 0.01f; + super.setupCharging(anchor, chargeDuration); + // Store target dimensions + this.targetSize = width; // Re-use base field for width + } + + // 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/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index caf86c7b5..ed51c9d6c 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(); diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 2970d835a..28e028afe 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -152,6 +152,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; @@ -159,6 +162,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; @@ -273,6 +279,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());