From 6c7c72b0f987059c6eeeeb661134864201b2e100 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 26 Jan 2026 18:38:43 -0500 Subject: [PATCH] Lightning, Orb and Beam Improvements. Outer Glow Configurations for Abilities --- .gitignore | 1 + .../client/renderer/RenderAbilityBeam.java | 271 ++++++++++++- .../client/renderer/RenderAbilityDisc.java | 45 ++- .../client/renderer/RenderAbilityLaser.java | 84 +++- .../client/renderer/RenderAbilityOrb.java | 113 +++--- .../client/renderer/RenderAbilityOrbCube.java | 56 --- .../client/renderer/TelegraphRenderer.java | 12 +- .../lightning/AttachedLightningRenderer.java | 281 +++++++++++++ .../renderer/lightning/LightningBolt.java | 295 ++++++++++++++ .../renderer/lightning/LightningHandler.java | 202 ++++++++++ .../client/renderer/lightning/Quaternion.java | 85 ++++ .../npcs/client/renderer/lightning/Vec3d.java | 184 +++++++++ .../data/ability/type/AbilityDisc.java | 24 +- .../data/ability/type/AbilityEnergyBeam.java | 82 +++- .../data/ability/type/AbilityLaserShot.java | 24 +- .../data/ability/type/AbilityOrb.java | 53 ++- .../data/telegraph/TelegraphInstance.java | 56 ++- .../npcs/entity/EntityAbilityBeam.java | 381 +++++++++++++++++- .../npcs/entity/EntityAbilityDisc.java | 29 +- .../npcs/entity/EntityAbilityLaser.java | 28 +- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 28 +- .../npcs/entity/EntityAbilityProjectile.java | 71 +++- .../java/noppes/npcs/client/ClientProxy.java | 9 +- .../noppes/npcs/client/ClientTickHandler.java | 4 + .../advanced/ability/SubGuiAbilityDisc.java | 32 ++ .../ability/SubGuiAbilityEnergyBeam.java | 32 ++ .../ability/SubGuiAbilityLaserShot.java | 32 ++ .../advanced/ability/SubGuiAbilityOrb.java | 62 ++- .../assets/customnpcs/lang/en_US.lang | 3 + 29 files changed, 2372 insertions(+), 207 deletions(-) delete mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java diff --git a/.gitignore b/.gitignore index d24cc7cf7..908eae39d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ out run src/main/resources/assets/customnpcs/api df/ +tmp/ diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java index 204be9d7e..554b07e31 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -2,6 +2,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; import kamkeel.npcs.entity.EntityAbilityBeam; import net.minecraft.client.renderer.Tessellator; import net.minecraft.entity.Entity; @@ -31,6 +32,13 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { setupRenderState(); + // Handle charging state (windup phase) - render only growing orb + if (beam.isCharging()) { + renderChargingOrb(beam, x, y, z, partialTicks); + restoreRenderState(); + return; + } + // Get trail points (these are RELATIVE to origin) List trail = beam.getTrailPoints(); if (trail.isEmpty()) { @@ -60,11 +68,17 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { GL11.glPushMatrix(); GL11.glTranslated(renderOriginX, renderOriginY, renderOriginZ); - // Render tail orb at origin (0,0,0) - static, no rotation - renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled()); + // Render tail orb at origin (0,0,0) - only in anchored mode + if (beam.shouldRenderTailOrb()) { + renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } - // Render trail segments with smooth connections - renderSmoothTrail(trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + // Render trail segments with smooth connections (with fading for non-anchored) + if (beam.hasFadingTrail()) { + renderFadingTrail(beam, trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } else { + renderSmoothTrail(trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } // Render head at interpolated offset position renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, partialTicks); @@ -74,27 +88,84 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { restoreRenderState(); } + /** + * Render the charging orb during windup phase. + * Grows from 0 to headSize based on charge progress. + */ + private void renderChargingOrb(EntityAbilityBeam beam, double x, double y, double z, float partialTicks) { + float headSize = beam.getHeadSize(); + float chargeProgress = beam.getInterpolatedChargeProgress(partialTicks); + float size = headSize * chargeProgress; + + if (size <= 0.01f) return; + + int innerColor = beam.getInnerColor(); + int outerColor = beam.getOuterColor(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = beam.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.08f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning during charging if enabled + if (beam.hasLightningEffect()) { + renderAttachedLightning(beam, scale); + } + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + if (beam.isOuterColorEnabled()) { + float outerScale = innerScale + beam.getOuterColorWidth(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(outerColor, 0.5f, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } + /** * Render the tail orb at the origin (where the beam emanates from). * This orb does not rotate - it's a stable anchor point. */ - private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled) { + private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth) { GL11.glPushMatrix(); // Already at origin (0,0,0) + // Inner scale defines the core size + float innerScale = 0.6f; + // Render outer glow only if enabled + // outerColorWidth is an additive offset from inner size if (outerColorEnabled) { + float outerScale = innerScale + outerColorWidth; GL11.glDepthMask(false); GL11.glPushMatrix(); - GL11.glScalef(size, size, size); + GL11.glScalef(size * outerScale, size * outerScale, size * outerScale); renderCube(outerColor, 0.4f, 0.5f); GL11.glPopMatrix(); GL11.glDepthMask(true); } - // Render inner core (smaller, solid) + // Render inner core (solid) GL11.glPushMatrix(); - GL11.glScalef(size * 0.6f, size * 0.6f, size * 0.6f); + GL11.glScalef(size * innerScale, size * innerScale, size * innerScale); renderCube(innerColor, 1.0f, 0.5f); GL11.glPopMatrix(); @@ -115,15 +186,153 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { // At connection points, average the perpendiculars for smooth transitions List perpFrames = computePerpendiculars(trail); + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + // Render outer glow trail (wider, translucent) - only if enabled + // outerColorWidth is an additive offset from inner width if (outerColorEnabled) { + float outerWidth = innerWidth + outerColorWidth * width; GL11.glDepthMask(false); - renderTrailTube(trail, perpFrames, width * outerColorWidth, outerColor, 0.3f, true); + renderTrailTube(trail, perpFrames, outerWidth, outerColor, 0.3f, true); GL11.glDepthMask(true); } // Render inner core trail - renderTrailTube(trail, perpFrames, width * 0.6f, innerColor, 0.9f, false); + renderTrailTube(trail, perpFrames, innerWidth, innerColor, 0.9f, false); + } + + /** + * Render trail with fading effect (comet style) for non-anchored beams. + * Older trail points fade out based on their age. + */ + private void renderFadingTrail(EntityAbilityBeam beam, List trail, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { + if (trail.size() < 2) return; + + List ages = beam.getTrailPointAges(); + int fadeTime = beam.getTrailFadeTime(); + + // Pre-compute perpendicular frames + List perpFrames = computePerpendiculars(trail); + + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + + // Render outer glow trail with fading + if (outerColorEnabled) { + float outerWidth = innerWidth + outerColorWidth * width; + GL11.glDepthMask(false); + renderFadingTrailTube(trail, perpFrames, ages, fadeTime, outerWidth, outerColor, 0.3f); + GL11.glDepthMask(true); + } + + // Render inner core trail with fading + renderFadingTrailTube(trail, perpFrames, ages, fadeTime, innerWidth, innerColor, 0.9f); + } + + /** + * Render trail tube with age-based fading. + */ + private void renderFadingTrailTube(List trail, List perpFrames, List ages, + int fadeTime, float width, int color, float baseAlpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + float halfWidth = width * 0.5f; + int size = trail.size(); + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + + for (int i = 0; i < size - 1; i++) { + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double[] frame1 = perpFrames.get(i); + double[] frame2 = perpFrames.get(i + 1); + + // Calculate alpha based on age (older = more faded) + float age1 = i < ages.size() ? ages.get(i) : 0; + float age2 = (i + 1) < ages.size() ? ages.get(i + 1) : 0; + float fade1 = 1.0f - Math.min(1.0f, age1 / fadeTime); + float fade2 = 1.0f - Math.min(1.0f, age2 / fadeTime); + float alpha1 = baseAlpha * fade1; + float alpha2 = baseAlpha * fade2; + + // Skip fully faded segments + if (alpha1 <= 0.01f && alpha2 <= 0.01f) continue; + + // Calculate 4 corners at start + double s1x = p1.xCoord - frame1[0] * halfWidth - frame1[3] * halfWidth; + double s1y = p1.yCoord - frame1[1] * halfWidth - frame1[4] * halfWidth; + double s1z = p1.zCoord - frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s2x = p1.xCoord + frame1[0] * halfWidth - frame1[3] * halfWidth; + double s2y = p1.yCoord + frame1[1] * halfWidth - frame1[4] * halfWidth; + double s2z = p1.zCoord + frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s3x = p1.xCoord + frame1[0] * halfWidth + frame1[3] * halfWidth; + double s3y = p1.yCoord + frame1[1] * halfWidth + frame1[4] * halfWidth; + double s3z = p1.zCoord + frame1[2] * halfWidth + frame1[5] * halfWidth; + + double s4x = p1.xCoord - frame1[0] * halfWidth + frame1[3] * halfWidth; + double s4y = p1.yCoord - frame1[1] * halfWidth + frame1[4] * halfWidth; + double s4z = p1.zCoord - frame1[2] * halfWidth + frame1[5] * halfWidth; + + // Calculate 4 corners at end + double e1x = p2.xCoord - frame2[0] * halfWidth - frame2[3] * halfWidth; + double e1y = p2.yCoord - frame2[1] * halfWidth - frame2[4] * halfWidth; + double e1z = p2.zCoord - frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e2x = p2.xCoord + frame2[0] * halfWidth - frame2[3] * halfWidth; + double e2y = p2.yCoord + frame2[1] * halfWidth - frame2[4] * halfWidth; + double e2z = p2.zCoord + frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e3x = p2.xCoord + frame2[0] * halfWidth + frame2[3] * halfWidth; + double e3y = p2.yCoord + frame2[1] * halfWidth + frame2[4] * halfWidth; + double e3z = p2.zCoord + frame2[2] * halfWidth + frame2[5] * halfWidth; + + double e4x = p2.xCoord - frame2[0] * halfWidth + frame2[3] * halfWidth; + double e4y = p2.yCoord - frame2[1] * halfWidth + frame2[4] * halfWidth; + double e4z = p2.zCoord - frame2[2] * halfWidth + frame2[5] * halfWidth; + + // Bottom face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + } + + tess.draw(); } /** @@ -328,6 +537,11 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.1f; float scale = size * (1.0f + scaleModifier); + // Render lightning BEFORE rotation so it crackles in all directions (only on head) + if (beam.hasLightningEffect()) { + renderAttachedLightning(beam, scale); + } + // Rotation GL11.glRotatef(beam.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); GL11.glRotatef(beam.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); @@ -336,19 +550,54 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { GL11.glPushMatrix(); GL11.glScalef(scale, scale, scale); + // Inner scale defines the core size + float innerScale = 0.6f; + // Render outer glow only if enabled + // outerColorWidth is an additive offset from inner size if (beam.isOuterColorEnabled()) { + float outerScale = innerScale + beam.getOuterColorWidth(); GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); renderCube(outerColor, 0.5f, 0.5f); + GL11.glPopMatrix(); GL11.glDepthMask(true); } // Render inner core - float innerScale = 0.6f; GL11.glScalef(innerScale, innerScale, innerScale); renderCube(innerColor, 1.0f, 0.5f); GL11.glPopMatrix(); GL11.glPopMatrix(); } + + /** + * Render fading lightning arcs attached to the beam head (in local space). + */ + private void renderAttachedLightning(EntityAbilityBeam beam, float headScale) { + AttachedLightningRenderer.LightningState state = getLightningState(beam); + + float density = beam.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * headScale * 0.5f; + float radius = innerRadius + beam.getLightningRadius() * headScale; + int outerColor = beam.getOuterColor(); + int innerColor = beam.getInnerColor(); + int fadeTime = beam.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityBeam beam) { + if (beam.lightningState == null) { + beam.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) beam.lightningState; + } } diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java index 13de38b50..fdd80689a 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -2,6 +2,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; import kamkeel.npcs.entity.EntityAbilityDisc; import net.minecraft.entity.Entity; import org.lwjgl.opengl.GL11; @@ -21,6 +22,7 @@ public class RenderAbilityDisc extends RenderAbilityProjectile { setupRenderState(); + GL11.glPushMatrix(); GL11.glTranslated(x, y, z); // Get interpolated values @@ -33,29 +35,66 @@ public class RenderAbilityDisc extends RenderAbilityProjectile { float scaleModifier = (float) Math.sin(pulseTime * 0.1f) * 0.03f; float scale = 1.0f + scaleModifier; + // Render lightning BEFORE rotation so it crackles in all directions + if (disc.hasLightningEffect()) { + renderAttachedLightning(disc, radius * scale); + } + GL11.glPushMatrix(); GL11.glScalef(scale, scale, scale); // Apply ONLY Y-axis rotation for flat spin GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + // Inner scale defines the core size + float innerScale = 0.6f; + // Render outer square (translucent) - only if enabled + // outerColorWidth is an additive offset from inner size if (disc.isOuterColorEnabled()) { - float outerScale = disc.getOuterColorWidth() / 1.8f; // Normalize to default width + float outerScale = innerScale + disc.getOuterColorWidth(); GL11.glDepthMask(false); renderFlatSquare(disc.getOuterColor(), 0.5f, radius * outerScale, thickness); GL11.glDepthMask(true); } - // Render inner square (solid, smaller) - float innerScale = 0.6f; + // Render inner square (solid) renderFlatSquare(disc.getInnerColor(), 1.0f, radius * innerScale, thickness * 1.2f); + GL11.glPopMatrix(); GL11.glPopMatrix(); restoreRenderState(); } + /** + * Render fading lightning arcs attached to the disc (in local space). + */ + private void renderAttachedLightning(EntityAbilityDisc disc, float discSize) { + AttachedLightningRenderer.LightningState state = getLightningState(disc); + + float density = disc.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * discSize * 0.5f; + float radius = innerRadius + disc.getLightningRadius() * discSize; + int outerColor = disc.getOuterColor(); + int innerColor = disc.getInnerColor(); + int fadeTime = disc.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityDisc disc) { + if (disc.lightningState == null) { + disc.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) disc.lightningState; + } + /** * Render a flat square (thin cube) centered at origin. * The square lies flat on the XZ plane with thin Y height. diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java index fce85d2db..bf4df79eb 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -2,6 +2,7 @@ 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.EntityAbilityLaser; import net.minecraft.client.renderer.Tessellator; import net.minecraft.entity.Entity; @@ -84,32 +85,103 @@ public class RenderAbilityLaser extends RenderAbilityProjectile { double vertY = dz * horzX - dx * horzZ; double vertZ = dx * horzY - dy * horzX; + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + // Render outer rectangle (larger, outer color, translucent) - only if enabled + // outerColorWidth is an additive offset from inner width if (laser.isOuterColorEnabled()) { - float outerScale = laser.getOuterColorWidth(); + float outerWidth = innerWidth + laser.getOuterColorWidth() * width; GL11.glDepthMask(false); renderBeamRectangle(renderStartX, renderStartY, renderStartZ, renderEndX, renderEndY, renderEndZ, horzX, horzY, horzZ, vertX, vertY, vertZ, - width * outerScale, laser.getOuterColor(), alpha * 0.4f); + outerWidth, laser.getOuterColor(), alpha * 0.4f); - // Render middle layer + // Render middle layer (halfway between inner and outer) + float midWidth = innerWidth + (outerWidth - innerWidth) * 0.5f; renderBeamRectangle(renderStartX, renderStartY, renderStartZ, renderEndX, renderEndY, renderEndZ, horzX, horzY, horzZ, vertX, vertY, vertZ, - width * 1.3f, laser.getOuterColor(), alpha * 0.7f); + midWidth, laser.getOuterColor(), alpha * 0.7f); GL11.glDepthMask(true); } - // Render inner core (smaller, inner color, solid) + // Render inner core (solid) renderBeamRectangle(renderStartX, renderStartY, renderStartZ, renderEndX, renderEndY, renderEndZ, horzX, horzY, horzZ, vertX, vertY, vertZ, - width * 0.6f, laser.getInnerColor(), alpha); + innerWidth, laser.getInnerColor(), alpha); + + // Render lightning along the laser beam + if (laser.hasLightningEffect() && laser.getCurrentLength() > 0.1f) { + renderLaserLightning(laser, renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, width); + } restoreRenderState(); } + /** + * Render lightning effects along the laser beam at multiple points. + */ + private void renderLaserLightning(EntityAbilityLaser laser, + double startX, double startY, double startZ, + double endX, double endY, double endZ, float width) { + AttachedLightningRenderer.LightningState state = getLightningState(laser); + + float density = laser.getLightningDensity(); + // Lightning radius extends outward from inner width + float innerRadius = width * 0.6f * 0.5f; + float radius = innerRadius + laser.getLightningRadius() * width; + int outerColor = laser.getOuterColor(); + int innerColor = laser.getInnerColor(); + int fadeTime = laser.getLightningFadeTime(); + + // Render lightning at multiple points along the laser + // Start, middle, and end points + double midX = (startX + endX) * 0.5; + double midY = (startY + endY) * 0.5; + double midZ = (startZ + endZ) * 0.5; + + GL11.glPushMatrix(); + GL11.glTranslated(midX, midY, midZ); + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + GL11.glPopMatrix(); + + // Also render at the tip + GL11.glPushMatrix(); + GL11.glTranslated(endX, endY, endZ); + // Use a secondary state for the tip + AttachedLightningRenderer.LightningState tipState = getTipLightningState(laser); + tipState.update(density * 0.7f, radius * 0.8f, outerColor, innerColor, fadeTime); + tipState.render(); + GL11.glPopMatrix(); + } + + /** + * Get or create the main lightning state for the laser. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityLaser laser) { + if (laser.lightningState == null) { + laser.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) laser.lightningState; + } + + /** + * Get or create the tip lightning state for the laser. + * Stored as a secondary object in lightningState2. + */ + private AttachedLightningRenderer.LightningState getTipLightningState(EntityAbilityLaser laser) { + // Use a simple approach - store in a transient field + // Since lightningState is already used, we'll create a new state each time + // This works because we're updating and rendering immediately + return new AttachedLightningRenderer.LightningState(); + } + /** * Render a beam as a 3D rectangular prism (box along a line). */ diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java index 39048648d..e2e6f8234 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -2,57 +2,29 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.client.renderer.quadric.SphereRenderer; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; import kamkeel.npcs.entity.EntityAbilityOrb; -import net.minecraft.client.renderer.entity.Render; import net.minecraft.entity.Entity; -import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; /** - * Renders the AbilityOrb entity as a glowing sphere with inner and outer layers. - * - * Design inspired by LouisXIV's energy rendering system. + * Renders the AbilityOrb entity as a cube with inner and outer layers. + * Minecraft-style blocky appearance using Tessellator. */ @SideOnly(Side.CLIENT) -public class RenderAbilityOrb extends Render { +public class RenderAbilityOrb extends RenderAbilityProjectile { - // White texture for solid color rendering - private static final ResourceLocation WHITE_TEXTURE = new ResourceLocation("customnpcs", "textures/entity/white.png"); - - // Static sphere renderer - shared across all orb renders - private static final SphereRenderer sphere = new SphereRenderer(0.5f, 16); - - public RenderAbilityOrb() { - this.shadowSize = 0.0f; - } + private static final int LIGHTNING_FADE_TICKS = 6; @Override public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { EntityAbilityOrb orb = (EntityAbilityOrb) entity; + setupRenderState(); + GL11.glPushMatrix(); - - // Disable culling, enable rescale normal - GL11.glDisable(GL11.GL_CULL_FACE); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_ALPHA_TEST); - - // Enable blending for transparency - GL11.glEnable(GL11.GL_BLEND); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - GL11.glAlphaFunc(GL11.GL_GREATER, 0.003921569F); - - // Disable lighting for self-illumination - GL11.glDisable(GL11.GL_LIGHTING); - - // Translate to render position GL11.glTranslated(x, y, z); - // Bind texture - this.bindTexture(WHITE_TEXTURE); - // Get interpolated size for smooth rendering float size = orb.getInterpolatedSize(partialTicks); @@ -61,51 +33,74 @@ public class RenderAbilityOrb extends Render { float scaleModifier = (float) Math.sin(pulseTime * 0.15f) * 0.05f; float scale = size * (1.0f + scaleModifier); - GL11.glPushMatrix(); - GL11.glScalef(scale, scale, scale); + // Render lightning BEFORE rotation so it crackles in all directions + if (orb.hasLightningEffect()) { + renderAttachedLightning(orb, scale); + } // Use entity's interpolated rotation for smooth spinning GL11.glRotatef(orb.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); - // Render outer sphere (translucent) - only if enabled + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer cube (translucent) - only if enabled + // outerColorWidth is an additive offset from inner size if (orb.isOuterColorEnabled()) { + float outerScale = innerScale + orb.getOuterColorWidth(); GL11.glDepthMask(false); - this.bindTexture(WHITE_TEXTURE); - float outerScale = orb.getOuterColorWidth() / 1.8f; // Normalize to default width GL11.glPushMatrix(); GL11.glScalef(outerScale, outerScale, outerScale); - renderSphere(orb.getOuterColor(), 0.6f); + renderCube(orb.getOuterColor(), 0.5f, 0.5f); GL11.glPopMatrix(); GL11.glDepthMask(true); } - // Render inner sphere (more opaque) - float innerScale = 0.75f; - GL11.glPushMatrix(); + // Render inner cube (solid) GL11.glScalef(innerScale, innerScale, innerScale); - this.bindTexture(WHITE_TEXTURE); - renderSphere(orb.getInnerColor(), 1.0f); - GL11.glPopMatrix(); + renderCube(orb.getInnerColor(), 1.0f, 0.5f); GL11.glPopMatrix(); - - // Restore GL state - GL11.glEnable(GL11.GL_LIGHTING); GL11.glPopMatrix(); + + restoreRenderState(); } - private void renderSphere(int color, float alpha) { - // Set sphere properties and render (rotation already applied to matrix) - sphere.setScale(1.0f); - sphere.setAlpha(alpha); - sphere.setColor(color); - sphere.render(0); + /** + * Render fading lightning arcs attached to the orb (in local space). + * Arcs persist for a few ticks and fade out, while staying attached to the orb. + */ + private void renderAttachedLightning(EntityAbilityOrb orb, float orbScale) { + // Get or create the lightning state for this entity + AttachedLightningRenderer.LightningState state = getLightningState(orb); + + float density = orb.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * orbScale * 0.5f; // Half size of inner cube + float radius = innerRadius + orb.getLightningRadius() * orbScale; + int outerColor = orb.getOuterColor(); + int innerColor = orb.getInnerColor(); + int fadeTime = orb.getLightningFadeTime(); + + // Update: age existing arcs, spawn new ones based on density + state.update(density, radius, outerColor, innerColor, fadeTime); + + // Render all active arcs in local space (they move with the orb) + state.render(); } - @Override - protected ResourceLocation getEntityTexture(Entity entity) { - return WHITE_TEXTURE; + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityOrb orb) { + if (orb.lightningState == null) { + orb.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) orb.lightningState; } } diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java deleted file mode 100644 index 29656e597..000000000 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java +++ /dev/null @@ -1,56 +0,0 @@ -package kamkeel.npcs.client.renderer; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.entity.EntityAbilityOrb; -import net.minecraft.entity.Entity; -import org.lwjgl.opengl.GL11; - -/** - * Renders the AbilityOrb entity as a cube with inner and outer layers. - * Minecraft-style blocky appearance using Tessellator. - * - * Design inspired by LouisXIV's energy rendering system. - */ -@SideOnly(Side.CLIENT) -public class RenderAbilityOrbCube extends RenderAbilityProjectile { - - @Override - public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { - EntityAbilityOrb orb = (EntityAbilityOrb) entity; - - setupRenderState(); - - GL11.glTranslated(x, y, z); - - // Get interpolated size for smooth rendering - float size = orb.getInterpolatedSize(partialTicks); - - // Pulsing scale effect - float pulseTime = entity.ticksExisted + partialTicks; - float scaleModifier = (float) Math.sin(pulseTime * 0.15f) * 0.05f; - float scale = size * (1.0f + scaleModifier); - - // Use entity's interpolated rotation for smooth spinning - GL11.glRotatef(orb.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); - GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); - GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); - - GL11.glPushMatrix(); - GL11.glScalef(scale, scale, scale); - - // Render outer cube (translucent) - GL11.glDepthMask(false); - renderCube(orb.getOuterColor(), 0.5f, 0.5f); - GL11.glDepthMask(true); - - // Render inner cube (solid, smaller) - float innerScale = 0.6f; - GL11.glScalef(innerScale, innerScale, innerScale); - renderCube(orb.getInnerColor(), 1.0f, 0.5f); - - GL11.glPopMatrix(); - - restoreRenderState(); - } -} diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java index 0cac8805e..5f4e815b7 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -76,10 +76,10 @@ public class TelegraphRenderer { return; } - // Calculate render position relative to camera - double renderX = instance.getX() - playerX; - double renderY = instance.getY() - playerY + telegraph.getHeightOffset(); - double renderZ = instance.getZ() - playerZ; + // Calculate render position relative to camera using interpolated positions for smooth movement + double renderX = instance.getInterpolatedX(partialTicks) - playerX; + double renderY = instance.getInterpolatedY(partialTicks) - playerY + telegraph.getHeightOffset(); + double renderZ = instance.getInterpolatedZ(partialTicks) - playerZ; // Get animated color int color = instance.getAnimatedColor(partialTicks); @@ -91,8 +91,8 @@ public class TelegraphRenderer { GL11.glPushMatrix(); GL11.glTranslated(renderX, renderY, renderZ); - // Rotate for directional shapes - GL11.glRotatef(-instance.getYaw(), 0, 1, 0); + // Rotate for directional shapes using interpolated yaw for smooth rotation + GL11.glRotatef(-instance.getInterpolatedYaw(partialTicks), 0, 1, 0); switch (telegraph.getType()) { case CIRCLE: diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java new file mode 100644 index 000000000..4fa28cda8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java @@ -0,0 +1,281 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.opengl.GL11; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +/** + * Renders lightning arcs attached to an entity (in local space). + * Supports both instant arcs and fading persistent bolts that move with the entity. + */ +public class AttachedLightningRenderer { + + private static final Random rand = new Random(); + + /** + * Data for a single lightning bolt with fading support. + */ + public static class LightningArc { + public List points; + public int age; + public int maxAge; + public int outerColor; + public int innerColor; + + public LightningArc(List points, int maxAge, int outerColor, int innerColor) { + this.points = points; + this.age = 0; + this.maxAge = maxAge; + this.outerColor = outerColor; + this.innerColor = innerColor; + } + + public float getAlphaMultiplier() { + // Fade out over lifetime + return 1.0f - ((float) age / (float) maxAge); + } + + public boolean isDead() { + return age >= maxAge; + } + + public void tick() { + age++; + } + } + + /** + * Storage for persistent lightning arcs per entity. + * Call updateAndRender() each frame from the entity's renderer. + */ + public static class LightningState { + private final List arcs = new ArrayList<>(); + private float spawnAccumulator = 0; + + /** + * Update arc ages, remove dead arcs, spawn new ones based on density. + * + * @param density Arcs per tick (can be fractional) + * @param radius Max arc distance from center + * @param outerColor Outer glow color + * @param innerColor Inner core color + * @param maxAge How many ticks arcs live (for fading) + */ + public void update(float density, float radius, int outerColor, int innerColor, int maxAge) { + // Age existing arcs and remove dead ones + Iterator iter = arcs.iterator(); + while (iter.hasNext()) { + LightningArc arc = iter.next(); + arc.tick(); + if (arc.isDead()) { + iter.remove(); + } + } + + // Spawn new arcs based on density + spawnAccumulator += density; + while (spawnAccumulator >= 1.0f) { + spawnAccumulator -= 1.0f; + arcs.add(createArc(radius, outerColor, innerColor, maxAge)); + } + } + + /** + * Render all active arcs in local space. + * Call this after glTranslated to entity position. + */ + public void render() { + if (arcs.isEmpty()) return; + + // Setup GL state + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDepthMask(false); + + Tessellator tess = Tessellator.instance; + + for (LightningArc arc : arcs) { + float alphaMult = arc.getAlphaMultiplier(); + + // Render outer glow + renderBoltPath(tess, arc.points, arc.outerColor, 0.025f, 0.35f * alphaMult); + + // Render inner core + renderBoltPath(tess, arc.points, arc.innerColor, 0.012f, 0.7f * alphaMult); + } + + GL11.glDepthMask(true); + GL11.glPopAttrib(); + } + + public int getArcCount() { + return arcs.size(); + } + } + + /** + * Create a new lightning arc from center to a random point. + */ + private static LightningArc createArc(float radius, int outerColor, int innerColor, int maxAge) { + // Random end point on sphere + double theta = rand.nextDouble() * Math.PI * 2; + double phi = Math.acos(2 * rand.nextDouble() - 1); + double r = radius * (0.5 + rand.nextDouble() * 0.5); + + double endX = r * Math.sin(phi) * Math.cos(theta); + double endY = r * Math.sin(phi) * Math.sin(theta); + double endZ = r * Math.cos(phi); + + int segments = 4 + rand.nextInt(3); + float displacement = radius * 0.25f; + + List points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement); + + return new LightningArc(points, maxAge, outerColor, innerColor); + } + + /** + * Generate a jagged lightning path between two points. + */ + private static List generateLightningPath(double x1, double y1, double z1, + double x2, double y2, double z2, + int segments, float displacement) { + List points = new ArrayList<>(); + points.add(new double[]{x1, y1, z1}); + + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + + // Create perpendicular vectors for displacement + Vec3d dir = new Vec3d(dx, dy, dz); + Vec3d perp1 = dir.copy().perpendicular().normalize(); + Vec3d perp2 = dir.copy().crossProduct(perp1).normalize(); + + for (int i = 1; i < segments; i++) { + double t = (double) i / segments; + + double px = x1 + dx * t; + double py = y1 + dy * t; + double pz = z1 + dz * t; + + // Displacement strongest in middle + float strength = (float) (1.0 - Math.abs(t - 0.5) * 2.0) * displacement; + double offset1 = (rand.nextDouble() - 0.5) * 2 * strength; + double offset2 = (rand.nextDouble() - 0.5) * 2 * strength; + + px += perp1.x * offset1 + perp2.x * offset2; + py += perp1.y * offset1 + perp2.y * offset2; + pz += perp1.z * offset1 + perp2.z * offset2; + + points.add(new double[]{px, py, pz}); + } + + points.add(new double[]{x2, y2, z2}); + return points; + } + + /** + * Render the lightning path as quads. + */ + private static void renderBoltPath(Tessellator tess, List points, int color, float width, float alpha) { + if (alpha <= 0.01f) return; + + Color c = new Color(color); + int r = c.getRed(); + int g = c.getGreen(); + int b = c.getBlue(); + int a = Math.min(255, Math.max(0, (int) (alpha * 255))); + + for (int i = 0; i < points.size() - 1; i++) { + double[] p1 = points.get(i); + double[] p2 = points.get(i + 1); + + double segDx = p2[0] - p1[0]; + double segDy = p2[1] - p1[1]; + double segDz = p2[2] - p1[2]; + + Vec3d segDir = new Vec3d(segDx, segDy, segDz); + Vec3d perp = segDir.copy().perpendicular().normalize().multiply(width); + + // First quad + tess.startDrawingQuads(); + tess.setBrightness(0xF000F0); + tess.setColorRGBA(r, g, b, a); + + tess.addVertex(p1[0] - perp.x, p1[1] - perp.y, p1[2] - perp.z); + tess.addVertex(p1[0] + perp.x, p1[1] + perp.y, p1[2] + perp.z); + tess.addVertex(p2[0] + perp.x, p2[1] + perp.y, p2[2] + perp.z); + tess.addVertex(p2[0] - perp.x, p2[1] - perp.y, p2[2] - perp.z); + + tess.draw(); + + // Second quad (perpendicular for 3D look) + Vec3d perp2 = segDir.copy().crossProduct(perp).normalize().multiply(width); + + tess.startDrawingQuads(); + tess.setBrightness(0xF000F0); + tess.setColorRGBA(r, g, b, a); + + tess.addVertex(p1[0] - perp2.x, p1[1] - perp2.y, p1[2] - perp2.z); + tess.addVertex(p1[0] + perp2.x, p1[1] + perp2.y, p1[2] + perp2.z); + tess.addVertex(p2[0] + perp2.x, p2[1] + perp2.y, p2[2] + perp2.z); + tess.addVertex(p2[0] - perp2.x, p2[1] - perp2.y, p2[2] - perp2.z); + + tess.draw(); + } + } + + // ==================== INSTANT RENDERING (no persistence) ==================== + + /** + * Render a single instant lightning arc (no fading, drawn this frame only). + */ + public static void renderInstantArc(float radius, int outerColor, int innerColor) { + double theta = rand.nextDouble() * Math.PI * 2; + double phi = Math.acos(2 * rand.nextDouble() - 1); + double r = radius * (0.5 + rand.nextDouble() * 0.5); + + double endX = r * Math.sin(phi) * Math.cos(theta); + double endY = r * Math.sin(phi) * Math.sin(theta); + double endZ = r * Math.cos(phi); + + int segments = 4 + rand.nextInt(3); + float displacement = radius * 0.25f; + + List points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement); + + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDepthMask(false); + + Tessellator tess = Tessellator.instance; + renderBoltPath(tess, points, outerColor, 0.025f, 0.35f); + renderBoltPath(tess, points, innerColor, 0.012f, 0.7f); + + GL11.glDepthMask(true); + GL11.glPopAttrib(); + } + + /** + * Render multiple instant arcs. + */ + public static void renderCracklingLightning(int count, float radius, int outerColor, int innerColor) { + for (int i = 0; i < count; i++) { + renderInstantArc(radius, outerColor, innerColor); + } + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java new file mode 100644 index 000000000..6cf6e7d3a --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java @@ -0,0 +1,295 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.world.World; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Lightning bolt data model with fractal segment generation. + * Adapted from Botania's LightningBolt by Vazkii/ChickenBones. + */ +public class LightningBolt { + + public static ConcurrentLinkedQueue boltList = new ConcurrentLinkedQueue<>(); + + public ArrayList segments = new ArrayList<>(); + public Vec3d start; + public Vec3d end; + + public double length; + public int numSegments0; + private int numSplits; + private boolean finalized; + private Random rand; + public long seed; + + public int particleAge; + public int particleMaxAge; + public boolean isDead; + + private World world; + private HashMap splitParents = new HashMap<>(); + + public float speed = 1.5F; + public static final int FADE_TIME = 4; // Short lifespan for projectile-attached lightning + + public int colorOuter; + public int colorInner; + + public LightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, long seed, int colorOuter, int colorInner) { + this.world = world; + this.seed = seed; + this.rand = new Random(seed); + + this.start = start; + this.end = end; + this.speed = ticksPerMeter; + + this.colorOuter = colorOuter; + this.colorInner = colorInner; + + this.numSegments0 = 1; + this.length = end.copy().subtract(start).mag(); + this.particleMaxAge = FADE_TIME + rand.nextInt(FADE_TIME) - FADE_TIME / 2; + this.particleAge = -(int) (length * speed); + + segments.add(new Segment(start, end)); + } + + /** + * Apply default fractal splitting for natural-looking lightning. + */ + public void defaultFractal() { + fractal(2, length / 1.5, 0.7F, 0.7F, 45); + fractal(2, length / 4, 0.5F, 0.8F, 50); + fractal(2, length / 15, 0.5F, 0.9F, 55); + fractal(2, length / 30, 0.5F, 1.0F, 60); + fractal(2, length / 60, 0, 0, 0); + fractal(2, length / 100, 0, 0, 0); + fractal(2, length / 400, 0, 0, 0); + } + + /** + * Simplified fractal for short-lived projectile lightning. + * Less detail but faster and more appropriate for fast-moving objects. + */ + public void simpleFractal() { + fractal(2, length / 2.0, 0.4F, 0.6F, 35); + fractal(2, length / 6, 0.3F, 0.7F, 40); + fractal(2, length / 20, 0, 0, 0); + } + + /** + * Apply fractal splitting to create branching lightning. + * + * @param splits Number of splits per segment + * @param amount Displacement amount + * @param splitChance Probability of branching (0-1) + * @param splitLength Length multiplier for branches + * @param splitAngle Angle for branch splits in degrees + */ + public void fractal(int splits, double amount, double splitChance, double splitLength, double splitAngle) { + if (finalized) return; + + ArrayList oldSegments = segments; + segments = new ArrayList<>(); + + Segment prev = null; + + for (Segment segment : oldSegments) { + prev = segment.prev; + + Vec3d subSegment = segment.diff.copy().multiply(1.0 / splits); + + BoltPoint[] newPoints = new BoltPoint[splits + 1]; + + Vec3d startPoint = segment.startPoint.point; + newPoints[0] = segment.startPoint; + newPoints[splits] = segment.endPoint; + + for (int i = 1; i < splits; i++) { + Vec3d randOff = segment.diff.copy().perpendicular().normalize() + .rotate(rand.nextFloat() * 360, segment.diff); + randOff.multiply((rand.nextFloat() - 0.5F) * amount * 2); + + Vec3d basePoint = startPoint.copy().add(subSegment.copy().multiply(i)); + + newPoints[i] = new BoltPoint(basePoint, randOff); + } + + for (int i = 0; i < splits; i++) { + Segment next = new Segment(newPoints[i], newPoints[i + 1], segment.light, + segment.segmentNo * splits + i, segment.splitNo); + next.prev = prev; + if (prev != null) { + prev.next = next; + } + + if (i != 0 && rand.nextFloat() < splitChance) { + Vec3d splitRot = next.diff.copy().xCrossProduct() + .rotate(rand.nextFloat() * 360, next.diff); + Vec3d diff = next.diff.copy() + .rotate((rand.nextFloat() * 0.66F + 0.33F) * splitAngle, splitRot) + .multiply(splitLength); + + numSplits++; + splitParents.put(numSplits, next.splitNo); + + Segment split = new Segment(newPoints[i], + new BoltPoint(newPoints[i + 1].basePoint, newPoints[i + 1].offsetVec.copy().add(diff)), + segment.light / 2F, next.segmentNo, numSplits); + split.prev = prev; + + segments.add(split); + } + + prev = next; + segments.add(next); + } + + if (segment.next != null) { + segment.next.prev = prev; + } + } + + numSegments0 *= splits; + } + + /** + * Finalize the bolt after all fractal passes. + */ + public void finalizeBolt() { + if (finalized) return; + finalized = true; + + calculateEndDiffs(); + Collections.sort(segments, new SegmentLightSorter()); + } + + private void calculateEndDiffs() { + Collections.sort(segments, new SegmentSorter()); + + for (Segment segment : segments) { + segment.calcEndDiffs(); + } + } + + public void onUpdate() { + particleAge++; + if (particleAge >= particleMaxAge) { + isDead = true; + } + } + + /** + * Update all active lightning bolts. Called from client tick handler. + */ + public static void updateAll() { + for (Iterator iterator = boltList.iterator(); iterator.hasNext(); ) { + LightningBolt bolt = iterator.next(); + bolt.onUpdate(); + if (bolt.isDead) { + iterator.remove(); + } + } + } + + // ==================== INNER CLASSES ==================== + + public class BoltPoint { + public Vec3d point; + public Vec3d basePoint; + public Vec3d offsetVec; + + public BoltPoint(Vec3d basePoint, Vec3d offsetVec) { + this.point = basePoint.copy().add(offsetVec); + this.basePoint = basePoint; + this.offsetVec = offsetVec; + } + } + + public class Segment { + public BoltPoint startPoint; + public BoltPoint endPoint; + + public Vec3d diff; + public Segment prev; + public Segment next; + + public Vec3d nextDiff; + public Vec3d prevDiff; + + public float sinPrev; + public float sinNext; + public float light; + + public int segmentNo; + public int splitNo; + + public Segment(BoltPoint start, BoltPoint end, float light, int segmentNo, int splitNo) { + this.startPoint = start; + this.endPoint = end; + this.light = light; + this.segmentNo = segmentNo; + this.splitNo = splitNo; + calcDiff(); + } + + public Segment(Vec3d start, Vec3d end) { + this(new BoltPoint(start, new Vec3d(0, 0, 0)), + new BoltPoint(end, new Vec3d(0, 0, 0)), 1, 0, 0); + } + + public void calcDiff() { + diff = endPoint.point.copy().subtract(startPoint.point); + } + + public void calcEndDiffs() { + if (prev != null) { + Vec3d prevDiffNorm = prev.diff.copy().normalize(); + Vec3d thisDiffNorm = diff.copy().normalize(); + + prevDiff = thisDiffNorm.copy().add(prevDiffNorm).normalize(); + sinPrev = (float) Math.sin(thisDiffNorm.angle(prevDiffNorm.multiply(-1)) / 2); + } else { + prevDiff = diff.copy().normalize(); + sinPrev = 1; + } + + if (next != null) { + Vec3d nextDiffNorm = next.diff.copy().normalize(); + Vec3d thisDiffNorm = diff.copy().normalize(); + + nextDiff = thisDiffNorm.add(nextDiffNorm).normalize(); + sinNext = (float) Math.sin(thisDiffNorm.angle(nextDiffNorm.multiply(-1)) / 2); + } else { + nextDiff = diff.copy().normalize(); + sinNext = 1; + } + } + } + + public class SegmentSorter implements Comparator { + @Override + public int compare(Segment o1, Segment o2) { + int comp = Integer.compare(o1.splitNo, o2.splitNo); + if (comp == 0) { + return Integer.compare(o1.segmentNo, o2.segmentNo); + } + return comp; + } + } + + public class SegmentLightSorter implements Comparator { + @Override + public int compare(Segment o1, Segment o2) { + return Float.compare(o2.light, o1.light); + } + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java new file mode 100644 index 000000000..1951666e4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java @@ -0,0 +1,202 @@ +package kamkeel.npcs.client.renderer.lightning; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.LightningBolt.Segment; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; +import net.minecraftforge.client.event.RenderWorldLastEvent; +import org.lwjgl.opengl.GL11; + +import java.awt.Color; + +/** + * Handles rendering of lightning bolts in the world. + * Adapted from Botania's LightningHandler by Vazkii/ChickenBones. + * + * Register this handler with: MinecraftForge.EVENT_BUS.register(new LightningHandler()); + */ +@SideOnly(Side.CLIENT) +public class LightningHandler { + + private static final ResourceLocation TEXTURE_OUTER = new ResourceLocation("customnpcs", "textures/effects/lightning_outer.png"); + private static final ResourceLocation TEXTURE_INNER = new ResourceLocation("customnpcs", "textures/effects/lightning_inner.png"); + + public static int lightningCount = 0; + + private static Vec3d getRelativeViewVector(Vec3d pos) { + Entity renderEntity = Minecraft.getMinecraft().renderViewEntity; + return new Vec3d( + (float) renderEntity.posX - pos.x, + (float) renderEntity.posY + renderEntity.getEyeHeight() - pos.y, + (float) renderEntity.posZ - pos.z + ); + } + + @SubscribeEvent + public void onRenderWorldLast(RenderWorldLastEvent event) { + float partialTicks = event.partialTicks; + Entity entity = Minecraft.getMinecraft().thePlayer; + TextureManager textureManager = Minecraft.getMinecraft().renderEngine; + + double interpPosX = entity.lastTickPosX + (entity.posX - entity.lastTickPosX) * partialTicks; + double interpPosY = entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * partialTicks; + double interpPosZ = entity.lastTickPosZ + (entity.posZ - entity.lastTickPosZ) * partialTicks; + + if (LightningBolt.boltList.isEmpty()) { + return; + } + + GL11.glPushMatrix(); + GL11.glTranslated(-interpPosX, -interpPosY, -interpPosZ); + + Tessellator tessellator = Tessellator.instance; + + GL11.glDepthMask(false); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + + lightningCount = 0; + + // Render outer layer (translucent glow) + for (LightningBolt bolt : LightningBolt.boltList) { + renderBolt(bolt, tessellator, partialTicks, false); + } + + // Render inner layer (bright core) + for (LightningBolt bolt : LightningBolt.boltList) { + renderBolt(bolt, tessellator, partialTicks, true); + } + + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + GL11.glDepthMask(true); + + GL11.glPopMatrix(); + } + + private void renderBolt(LightningBolt bolt, Tessellator tessellator, float partialTicks, boolean inner) { + lightningCount++; + + float boltAge = bolt.particleAge < 0 ? 0 : (float) bolt.particleAge / (float) bolt.particleMaxAge; + float mainAlpha; + if (!inner) { + mainAlpha = (1 - boltAge) * 0.4F; + } else { + mainAlpha = 1 - boltAge * 0.5F; + } + + int expandTime = (int) (bolt.length * bolt.speed); + + int renderStart = (int) ((expandTime / 2 - bolt.particleMaxAge + bolt.particleAge) / (float) (expandTime / 2) * bolt.numSegments0); + int renderEnd = (int) ((bolt.particleAge + expandTime) / (float) expandTime * bolt.numSegments0); + + for (Segment segment : bolt.segments) { + if (segment.segmentNo < renderStart || segment.segmentNo > renderEnd) { + continue; + } + + Vec3d playerVec = getRelativeViewVector(segment.startPoint.point).multiply(-1); + + double width = 0.008F * (playerVec.mag() / 8 + 1) * (1 + segment.light) * 0.4F; + + Vec3d diff1 = playerVec.copy().crossProduct(segment.prevDiff).normalize().multiply(width / segment.sinPrev); + Vec3d diff2 = playerVec.copy().crossProduct(segment.nextDiff).normalize().multiply(width / segment.sinNext); + + Vec3d startVec = segment.startPoint.point; + Vec3d endVec = segment.endPoint.point; + + int color = inner ? bolt.colorInner : bolt.colorOuter; + Color c = new Color(color); + int alpha = (int) (mainAlpha * segment.light * c.getAlpha()); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(endVec.x - diff2.x, endVec.y - diff2.y, endVec.z - diff2.z); + tessellator.addVertex(startVec.x - diff1.x, startVec.y - diff1.y, startVec.z - diff1.z); + tessellator.addVertex(startVec.x + diff1.x, startVec.y + diff1.y, startVec.z + diff1.z); + tessellator.addVertex(endVec.x + diff2.x, endVec.y + diff2.y, endVec.z + diff2.z); + + tessellator.draw(); + + // Rounded end caps + if (segment.next == null) { + Vec3d roundEnd = segment.endPoint.point.copy().add(segment.diff.copy().normalize().multiply(width)); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(roundEnd.x - diff2.x, roundEnd.y - diff2.y, roundEnd.z - diff2.z); + tessellator.addVertex(endVec.x - diff2.x, endVec.y - diff2.y, endVec.z - diff2.z); + tessellator.addVertex(endVec.x + diff2.x, endVec.y + diff2.y, endVec.z + diff2.z); + tessellator.addVertex(roundEnd.x + diff2.x, roundEnd.y + diff2.y, roundEnd.z + diff2.z); + + tessellator.draw(); + } + + if (segment.prev == null) { + Vec3d roundEnd = segment.startPoint.point.copy().subtract(segment.diff.copy().normalize().multiply(width)); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(startVec.x - diff1.x, startVec.y - diff1.y, startVec.z - diff1.z); + tessellator.addVertex(roundEnd.x - diff1.x, roundEnd.y - diff1.y, roundEnd.z - diff1.z); + tessellator.addVertex(roundEnd.x + diff1.x, roundEnd.y + diff1.y, roundEnd.z + diff1.z); + tessellator.addVertex(startVec.x + diff1.x, startVec.y + diff1.y, startVec.z + diff1.z); + + tessellator.draw(); + } + } + } + + /** + * Spawn a lightning bolt between two points. + * + * @param world The world + * @param start Start position + * @param end End position + * @param ticksPerMeter Speed of bolt expansion + * @param seed Random seed for consistent appearance + * @param colorOuter Outer glow color (ARGB) + * @param colorInner Inner core color (ARGB) + */ + public static void spawnLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, long seed, int colorOuter, int colorInner) { + LightningBolt bolt = new LightningBolt(world, start, end, ticksPerMeter, seed, colorOuter, colorInner); + bolt.defaultFractal(); + bolt.finalizeBolt(); + LightningBolt.boltList.add(bolt); + } + + /** + * Spawn a lightning bolt with automatic seed. + */ + public static void spawnLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, int colorOuter, int colorInner) { + spawnLightningBolt(world, start, end, ticksPerMeter, System.nanoTime(), colorOuter, colorInner); + } + + /** + * Spawn a simple lightning bolt for projectiles - less detail, faster. + */ + public static void spawnSimpleLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, int colorOuter, int colorInner) { + LightningBolt bolt = new LightningBolt(world, start, end, ticksPerMeter, System.nanoTime(), colorOuter, colorInner); + bolt.simpleFractal(); + bolt.finalizeBolt(); + LightningBolt.boltList.add(bolt); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java new file mode 100644 index 000000000..cda0307b7 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java @@ -0,0 +1,85 @@ +package kamkeel.npcs.client.renderer.lightning; + +/** + * Quaternion class for axis-angle rotations in lightning calculations. + * Adapted from Botania's Quat by ChickenBones. + */ +public class Quaternion { + public double x; + public double y; + public double z; + public double s; + + public Quaternion() { + s = 1.0; + x = 0.0; + y = 0.0; + z = 0.0; + } + + public Quaternion(Quaternion quat) { + x = quat.x; + y = quat.y; + z = quat.z; + s = quat.s; + } + + public Quaternion(double s, double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + this.s = s; + } + + public static Quaternion aroundAxis(double ax, double ay, double az, double angle) { + angle *= 0.5; + double sinAngle = Math.sin(angle); + return new Quaternion(Math.cos(angle), ax * sinAngle, ay * sinAngle, az * sinAngle); + } + + public static Quaternion aroundAxis(Vec3d axis, double angle) { + return aroundAxis(axis.x, axis.y, axis.z, angle); + } + + public void multiply(Quaternion quat) { + double d = s * quat.s - x * quat.x - y * quat.y - z * quat.z; + double d1 = s * quat.x + x * quat.s - y * quat.z + z * quat.y; + double d2 = s * quat.y + x * quat.z + y * quat.s - z * quat.x; + double d3 = s * quat.z - x * quat.y + y * quat.x + z * quat.s; + s = d; + x = d1; + y = d2; + z = d3; + } + + public double mag() { + return Math.sqrt(x * x + y * y + z * z + s * s); + } + + public void normalize() { + double d = mag(); + if (d == 0.0) { + return; + } + d = 1.0 / d; + x *= d; + y *= d; + z *= d; + s *= d; + } + + public void rotate(Vec3d vec) { + double d = -x * vec.x - y * vec.y - z * vec.z; + double d1 = s * vec.x + y * vec.z - z * vec.y; + double d2 = s * vec.y - x * vec.z + z * vec.x; + double d3 = s * vec.z + x * vec.y - y * vec.x; + vec.x = d1 * s - d * x - d2 * z + d3 * y; + vec.y = d2 * s - d * y + d1 * z - d3 * x; + vec.z = d3 * s - d * z - d1 * y + d2 * x; + } + + @Override + public String toString() { + return "Quaternion(" + s + ", " + x + ", " + y + ", " + z + ")"; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java new file mode 100644 index 000000000..018e55bdb --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java @@ -0,0 +1,184 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.entity.Entity; +import net.minecraft.util.Vec3; +import org.lwjgl.opengl.GL11; + +/** + * 3D Vector helper class for lightning calculations. + * Adapted from Botania's Vector3 by ChickenBones. + */ +public class Vec3d { + public double x; + public double y; + public double z; + + public Vec3d() { + } + + public Vec3d(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vec3d(Vec3d vec) { + this.x = vec.x; + this.y = vec.y; + this.z = vec.z; + } + + public Vec3d(Vec3 vec) { + this.x = vec.xCoord; + this.y = vec.yCoord; + this.z = vec.zCoord; + } + + public Vec3d copy() { + return new Vec3d(this); + } + + public static Vec3d fromEntity(Entity e) { + return new Vec3d(e.posX, e.posY, e.posZ); + } + + public static Vec3d fromEntityCenter(Entity e) { + return new Vec3d(e.posX, e.posY - e.yOffset + e.height / 2, e.posZ); + } + + public Vec3d set(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + public Vec3d set(Vec3d vec) { + this.x = vec.x; + this.y = vec.y; + this.z = vec.z; + return this; + } + + public double dotProduct(Vec3d vec) { + double d = vec.x * x + vec.y * y + vec.z * z; + if (d > 1 && d < 1.00001) d = 1; + else if (d < -1 && d > -1.00001) d = -1; + return d; + } + + public Vec3d crossProduct(Vec3d vec) { + double d = y * vec.z - z * vec.y; + double d1 = z * vec.x - x * vec.z; + double d2 = x * vec.y - y * vec.x; + x = d; + y = d1; + z = d2; + return this; + } + + public Vec3d add(double x, double y, double z) { + this.x += x; + this.y += y; + this.z += z; + return this; + } + + public Vec3d add(Vec3d vec) { + this.x += vec.x; + this.y += vec.y; + this.z += vec.z; + return this; + } + + public Vec3d subtract(Vec3d vec) { + this.x -= vec.x; + this.y -= vec.y; + this.z -= vec.z; + return this; + } + + public Vec3d multiply(double d) { + this.x *= d; + this.y *= d; + this.z *= d; + return this; + } + + public double mag() { + return Math.sqrt(x * x + y * y + z * z); + } + + public double magSquared() { + return x * x + y * y + z * z; + } + + public Vec3d normalize() { + double d = mag(); + if (d != 0) { + multiply(1 / d); + } + return this; + } + + public Vec3d perpendicular() { + if (z == 0) { + return zCrossProduct(); + } + return xCrossProduct(); + } + + public Vec3d xCrossProduct() { + double d = z; + double d1 = -y; + x = 0; + y = d; + z = d1; + return this; + } + + public Vec3d zCrossProduct() { + double d = y; + double d1 = -x; + x = d; + y = d1; + z = 0; + return this; + } + + public Vec3 toVec3() { + return Vec3.createVectorHelper(x, y, z); + } + + public double angle(Vec3d vec) { + return Math.acos(copy().normalize().dotProduct(vec.copy().normalize())); + } + + public Vec3d negate() { + x = -x; + y = -y; + z = -z; + return this; + } + + public Vec3d rotate(double angle, Vec3d axis) { + Quaternion.aroundAxis(axis.copy().normalize(), angle).rotate(this); + return this; + } + + public void glVertex() { + GL11.glVertex3d(x, y, z); + } + + @Override + public String toString() { + return "Vec3d(" + x + ", " + y + ", " + z + ")"; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Vec3d)) return false; + Vec3d v = (Vec3d) o; + return x == v.x && y == v.y && z == v.z; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 0db44ea8a..5aa34a969 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -57,10 +57,15 @@ public class AbilityDisc extends Ability { // Visual properties private int innerColor = 0xFFFFFF; private int outerColor = 0xFF8800; - private float outerColorWidth = 1.8f; + private float outerColorWidth = 0.4f; // Additive offset from inner size private boolean outerColorEnabled = true; private float rotationSpeed = 5.0f; + // Lightning effect properties + private boolean lightningEffect = false; + private float lightningDensity = 0.15f; + private float lightningRadius = 0.5f; + public AbilityDisc() { this.typeId = "ability.cnpc.disc"; this.name = "Disc"; @@ -113,7 +118,8 @@ public class AbilityDisc extends Ability { boomerang, boomerangDelay, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius ); world.spawnEntityInWorld(disc); @@ -184,6 +190,9 @@ public class AbilityDisc extends Ability { nbt.setFloat("outerColorWidth", outerColorWidth); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setFloat("rotationSpeed", rotationSpeed); + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); } @Override @@ -209,9 +218,12 @@ public class AbilityDisc extends Ability { this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF8800; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 5.0f; + this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; } // Getters & Setters @@ -263,4 +275,10 @@ public class AbilityDisc extends Ability { public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } public float getRotationSpeed() { return rotationSpeed; } public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } + public boolean hasLightningEffect() { return lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 58a224c64..9a5345435 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -8,6 +8,7 @@ import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityBeam; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -56,10 +57,18 @@ public class AbilityEnergyBeam extends Ability { // Visual properties private int innerColor = 0xFFFFFF; private int outerColor = 0x00AAFF; - private float outerColorWidth = 1.8f; + private float outerColorWidth = 0.4f; // Additive offset from inner size private boolean outerColorEnabled = true; private float rotationSpeed = 6.0f; + // Lightning effect properties (only on head) + private boolean lightningEffect = false; + private float lightningDensity = 0.15f; + private float lightningRadius = 0.5f; + + // Transient state for beam entity (used during windup charging) + private transient int beamEntityId = -1; + public AbilityEnergyBeam() { this.typeId = "ability.cnpc.beam"; this.name = "Beam"; @@ -99,22 +108,43 @@ public class AbilityEnergyBeam extends Ability { public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { if (world.isRemote) return; - double spawnX = npc.posX; - double spawnY = npc.posY + npc.getEyeHeight() * 0.7; - double spawnZ = npc.posZ; + // Find and start firing the beam that was spawned during windup + if (beamEntityId != -1) { + Entity entity = world.getEntityByID(beamEntityId); + if (entity instanceof EntityAbilityBeam) { + EntityAbilityBeam beam = (EntityAbilityBeam) entity; + beam.startFiring(target); + } + beamEntityId = -1; + } + } - EntityAbilityBeam beam = new EntityAbilityBeam( - world, npc, target, - spawnX, spawnY, spawnZ, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, - damage, knockback, knockbackUp, - speed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime - ); + @Override + public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; - world.spawnEntityInWorld(beam); + // Spawn beam in charging mode on first tick of windup + if (tick == 1) { + float offsetDist = 1.0f; + + // Create beam in charging mode - follows NPC's look direction during windup + EntityAbilityBeam beam = EntityAbilityBeam.createCharging( + world, npc, target, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, + lockMovement, // Anchored mode + windUpTicks, // Charge duration = windup duration + offsetDist // Offset distance in front of NPC + ); + + world.spawnEntityInWorld(beam); + beamEntityId = beam.getEntityId(); + } } @Override @@ -127,6 +157,14 @@ public class AbilityEnergyBeam extends Ability { @Override public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + // Despawn charging beam if interrupted during windup + if (beamEntityId != -1) { + Entity entity = npc.worldObj.getEntityByID(beamEntityId); + if (entity != null) { + entity.setDead(); + } + beamEntityId = -1; + } } @Override @@ -180,6 +218,9 @@ public class AbilityEnergyBeam extends Ability { nbt.setFloat("outerColorWidth", outerColorWidth); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setFloat("rotationSpeed", rotationSpeed); + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); } @Override @@ -203,9 +244,12 @@ public class AbilityEnergyBeam extends Ability { this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x00AAFF; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 6.0f; + this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; } // Getters & Setters @@ -253,4 +297,10 @@ public class AbilityEnergyBeam extends Ability { public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } public float getRotationSpeed() { return rotationSpeed; } public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } + public boolean hasLightningEffect() { return lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 64a3da8ad..77df6ff35 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -46,9 +46,14 @@ public class AbilityLaserShot extends Ability { // Visual properties private int innerColor = 0xFFFFFF; private int outerColor = 0xFF0000; - private float outerColorWidth = 1.8f; + private float outerColorWidth = 0.4f; // Additive offset from inner width private boolean outerColorEnabled = true; + // Lightning effect properties + private boolean lightningEffect = false; + private float lightningDensity = 0.15f; + private float lightningRadius = 0.5f; + public AbilityLaserShot() { this.typeId = "ability.cnpc.laser_shot"; this.name = "Laser Shot"; @@ -110,7 +115,8 @@ public class AbilityLaserShot extends Ability { expansionSpeed, lingerTicks, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius ); world.spawnEntityInWorld(laser); @@ -148,6 +154,9 @@ public class AbilityLaserShot extends Ability { nbt.setInteger("outerColor", outerColor); nbt.setFloat("outerColorWidth", outerColorWidth); nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); } @Override @@ -168,8 +177,11 @@ public class AbilityLaserShot extends Ability { this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; } // Getters & Setters @@ -209,4 +221,10 @@ public class AbilityLaserShot extends Ability { public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } public boolean isOuterColorEnabled() { return outerColorEnabled; } public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } + public boolean hasLightningEffect() { return lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 7b6f34b68..358b82896 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -53,10 +53,16 @@ public class AbilityOrb extends Ability { // Visual properties private int innerColor = 0xFFFFFF; // White core private int outerColor = 0x8888FF; // Light blue glow - private float outerColorWidth = 1.8f; // Multiplier for outer glow size + private float outerColorWidth = 0.4f; // Additive offset from inner size private boolean outerColorEnabled = true; // Whether to render outer glow private float rotationSpeed = 4.0f; // Degrees per tick + // Lightning effect properties + private boolean lightningEffect = false; + private float lightningDensity = 0.15f; // Bolts spawned per tick (0.15 = ~15% chance per frame) + private float lightningRadius = 0.5f; // Max distance lightning arcs - scales with orbSize + private int lightningFadeTime = 6; // Ticks before lightning fades out + public AbilityOrb() { this.typeId = "ability.cnpc.orb"; this.name = "Orb"; @@ -110,7 +116,8 @@ public class AbilityOrb extends Ability { orbSpeed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime ); world.spawnEntityInWorld(orb); @@ -181,6 +188,10 @@ public class AbilityOrb extends Ability { nbt.setFloat("outerColorWidth", outerColorWidth); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setFloat("rotationSpeed", rotationSpeed); + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); + nbt.setInteger("lightningFadeTime", lightningFadeTime); } @Override @@ -203,9 +214,13 @@ public class AbilityOrb extends Ability { this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; + this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + this.lightningFadeTime = nbt.hasKey("lightningFadeTime") ? nbt.getInteger("lightningFadeTime") : 6; } // Getters & Setters @@ -376,4 +391,36 @@ public class AbilityOrb extends Ability { public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } + + public boolean hasLightningEffect() { + return lightningEffect; + } + + public void setLightningEffect(boolean lightningEffect) { + this.lightningEffect = lightningEffect; + } + + public float getLightningDensity() { + return lightningDensity; + } + + public void setLightningDensity(float lightningDensity) { + this.lightningDensity = lightningDensity; + } + + public float getLightningRadius() { + return lightningRadius; + } + + public void setLightningRadius(float lightningRadius) { + this.lightningRadius = lightningRadius; + } + + public int getLightningFadeTime() { + return lightningFadeTime; + } + + public void setLightningFadeTime(int lightningFadeTime) { + this.lightningFadeTime = lightningFadeTime; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java index 71d63f9c6..a5d2f0d95 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java @@ -17,12 +17,18 @@ public class TelegraphInstance { private String instanceId; private Telegraph telegraph; - // Position + // Position (current) private double x; private double y; private double z; private float yaw; + // Position (previous tick - for interpolation) + private double prevX; + private double prevY; + private double prevZ; + private float prevYaw; + // Entity tracking private int entityIdToFollow = -1; private int casterEntityId = -1; @@ -50,6 +56,11 @@ public class TelegraphInstance { this.y = y; this.z = z; this.yaw = yaw; + // Initialize prev positions to current for first frame + this.prevX = x; + this.prevY = y; + this.prevZ = z; + this.prevYaw = yaw; this.remainingTicks = telegraph.getDurationTicks(); this.totalTicks = telegraph.getDurationTicks(); } @@ -69,6 +80,12 @@ public class TelegraphInstance { return false; } + // Save previous position for interpolation + this.prevX = this.x; + this.prevY = this.y; + this.prevZ = this.z; + this.prevYaw = this.yaw; + remainingTicks--; // Update position if following an entity @@ -144,6 +161,43 @@ public class TelegraphInstance { return (alpha << 24) | (r << 16) | (g << 8) | b; } + // ═══════════════════════════════════════════════════════════════════ + // INTERPOLATED POSITION (for smooth rendering) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Get interpolated X position for smooth rendering. + */ + public double getInterpolatedX(float partialTicks) { + return prevX + (x - prevX) * partialTicks; + } + + /** + * Get interpolated Y position for smooth rendering. + */ + public double getInterpolatedY(float partialTicks) { + return prevY + (y - prevY) * partialTicks; + } + + /** + * Get interpolated Z position for smooth rendering. + */ + public double getInterpolatedZ(float partialTicks) { + return prevZ + (z - prevZ) * partialTicks; + } + + /** + * Get interpolated yaw for smooth rendering. + * Handles angle wrapping for smooth rotation. + */ + public float getInterpolatedYaw(float partialTicks) { + // Handle angle wrapping to prevent spinning the wrong way + float diff = yaw - prevYaw; + while (diff > 180.0f) diff -= 360.0f; + while (diff < -180.0f) diff += 360.0f; + return prevYaw + diff * partialTicks; + } + // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index b0c3579e7..ab62c4ca9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -47,6 +47,23 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { // Origin stays fixed (or follows owner) private boolean attachedToOwner = true; + // Whether to render tail orb (only when anchored) + private boolean renderTailOrb = true; + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private float chargeOffsetDistance = 1.0f; + + // Trail fading for non-anchored beams (comet effect) + private boolean fadeTrail = false; + private int trailFadeTime = 20; // Ticks for trail to fully fade + private List trailPointAges = new ArrayList<>(); + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + // Debug logging private static final boolean DEBUG_LOGGING = true; @@ -54,8 +71,32 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { super(world); } + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + /** - * Full constructor with all parameters. + * Check if beam is in charging state (synced via data watcher). + */ + public boolean isCharging() { + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor with all parameters (no lightning). */ public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, @@ -66,15 +107,62 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + this(world, owner, target, x, y, z, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + false, 0.15f, 0.5f); + } + + /** + * Full constructor with all parameters including lightning. + */ + public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float beamWidth, float headSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius) { + this(world, owner, target, x, y, z, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, + true); // Default to anchored mode + } + + /** + * Full constructor with all parameters including lightning and anchored mode. + * @param anchoredMode If true, origin follows owner and tail orb is rendered. + * If false, beam is free-moving with trailing length (no tail orb). + */ + public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float beamWidth, float headSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, + boolean anchoredMode) { super(world); - // Initialize base properties + // Initialize base properties with lightning initProjectile(owner, target, x, y, z, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime); + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, 6); // Beam-specific properties this.speed = speed; @@ -84,6 +172,10 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.beamWidth = beamWidth; this.headSize = headSize; + // Anchored mode controls whether origin follows owner and tail orb is rendered + this.attachedToOwner = anchoredMode; + this.renderTailOrb = anchoredMode; + // Initialize head offset at origin (0,0,0 relative) this.headOffsetX = 0; this.headOffsetY = 0; @@ -119,6 +211,118 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } } + /** + * Create a beam in charging mode (for windup phase). + * The beam will grow from 0 to headSize over chargeDuration ticks. + * Position follows the owner's look direction. + */ + public static EntityAbilityBeam createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, + float beamWidth, float headSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, + boolean anchoredMode, int chargeDuration, float chargeOffsetDistance) { + // Calculate initial position in front of owner's look direction + float yaw = (float) Math.toRadians(owner.rotationYawHead); + double spawnX = owner.posX - Math.sin(yaw) * chargeOffsetDistance; + double spawnY = owner.posY + owner.getEyeHeight() * 0.7; + double spawnZ = owner.posZ + Math.cos(yaw) * chargeOffsetDistance; + + EntityAbilityBeam beam = new EntityAbilityBeam( + world, owner, target, + spawnX, spawnY, spawnZ, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, + anchoredMode); + + // Set charging state (uses data watcher for client sync) + beam.setCharging(true); + beam.chargeDuration = chargeDuration; + beam.chargeTick = 0; + beam.chargeOffsetDistance = chargeOffsetDistance; + + // Non-anchored beams have fading trail (comet effect) + beam.fadeTrail = !anchoredMode; + + // Clear motion while charging + beam.motionX = 0; + beam.motionY = 0; + beam.motionZ = 0; + + return beam; + } + + /** + * Start the beam firing (exit charging mode). + * Called by ability when windup ends. + * + * For anchored beams: origin follows owner, head starts at charged position + * For non-anchored beams: origin fixed at charged position, head starts there + */ + public void startFiring(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Origin (tail) stays at the charged position - where the orb was + // This is the same for both anchored and non-anchored modes + startX = posX; + startY = posY; + startZ = posZ; + + // Head starts at the origin (tail position) + headOffsetX = 0; + headOffsetY = 0; + headOffsetZ = 0; + prevHeadOffsetX = 0; + prevHeadOffsetY = 0; + prevHeadOffsetZ = 0; + + // Origin no longer follows owner after firing starts + // (for anchored mode, tail is fixed in space; for non-anchored, there's no tail) + attachedToOwner = false; + + // Initialize trail with just the origin point + trailPoints.clear(); + trailPointAges.clear(); + trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + if (fadeTrail) trailPointAges.add(0); + + // Calculate velocity toward target (head starts at origin = startX/Y/Z) + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - startX; + double dy = (target.posY + target.height * 0.5) - startY; + double dz = target.posZ - startZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } 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) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + + if (DEBUG_LOGGING && !worldObj.isRemote) { + LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ + + ") headOffset=(" + headOffsetX + "," + headOffsetY + "," + headOffsetZ + + ") motion=(" + motionX + "," + motionY + "," + motionZ + ")"); + } + } + @Override protected boolean checkMaxDistance() { // Check distance using head offset (distance from origin) @@ -136,19 +340,41 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { @Override protected void updateProjectile() { + // Handle charging state (windup phase) - use isCharging() for synced value + if (isCharging()) { + updateCharging(); + return; + } + // Store previous head offset for interpolation prevHeadOffsetX = headOffsetX; prevHeadOffsetY = headOffsetY; prevHeadOffsetZ = headOffsetZ; + // Age trail points for fading effect + if (fadeTrail) { + ageTrailPoints(); + } + if (worldObj.isRemote) { - // Client: interpolate entity position, derive head from offset + // Client: interpolate entity position handleClientInterpolation(); - // Client also updates head offset to match motion for smooth movement - headOffsetX += motionX; - headOffsetY += motionY; - headOffsetZ += motionZ; + // Client: update origin if attached to owner (mirrors server logic) + if (attachedToOwner) { + Entity owner = getOwner(); + if (owner != null) { + startX = owner.posX; + startY = owner.posY + owner.getEyeHeight() * 0.7; + startZ = owner.posZ; + } + } + + // Client: derive head offset from entity position and origin + // Entity position = head world position, so headOffset = pos - origin + headOffsetX = posX - startX; + headOffsetY = posY - startY; + headOffsetZ = posZ - startZ; // Add trail point on client addTrailPoint(); @@ -166,7 +392,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { Entity owner = getOwner(); if (owner != null) { startX = owner.posX; - startY = owner.posY + owner.height * 0.7; + startY = owner.posY + owner.getEyeHeight() * 0.7; startZ = owner.posZ; } } @@ -201,10 +427,66 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } } + /** + * Update during charging state - follow owner's look direction. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Follow owner's head/look direction (rotationYawHead for NPCs/players) + float yaw; + if (owner instanceof EntityLivingBase) { + yaw = (float) Math.toRadians(((EntityLivingBase) owner).rotationYawHead); + } else { + yaw = (float) Math.toRadians(owner.rotationYaw); + } + + double offsetX = -Math.sin(yaw) * chargeOffsetDistance; + double offsetZ = Math.cos(yaw) * chargeOffsetDistance; + + double newX = owner.posX + offsetX; + double newY = owner.posY + owner.getEyeHeight() * 0.7; + double newZ = owner.posZ + offsetZ; + + setPosition(newX, newY, newZ); + + // Also update origin for when firing starts + startX = newX; + startY = newY; + startZ = newZ; + } + + /** + * Age trail points and remove old ones (for comet effect). + */ + private void ageTrailPoints() { + // Age all trail points + for (int i = 0; i < trailPointAges.size(); i++) { + trailPointAges.set(i, trailPointAges.get(i) + 1); + } + + // Remove trail points that have fully faded + while (!trailPointAges.isEmpty() && trailPointAges.get(0) >= trailFadeTime) { + trailPointAges.remove(0); + if (!trailPoints.isEmpty()) { + trailPoints.remove(0); + } + } + } + private void addTrailPoint() { // Trail points are RELATIVE to origin if (trailPoints.isEmpty()) { trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + if (fadeTrail) { + trailPointAges.add(0); + } return; } @@ -217,10 +499,15 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { if (dist >= MIN_POINT_DISTANCE) { trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + if (fadeTrail) { + trailPointAges.add(0); + } - // Limit trail length - while (trailPoints.size() > MAX_TRAIL_POINTS) { - trailPoints.remove(0); + // Limit trail length (only if not using fading - fading handles its own cleanup) + if (!fadeTrail) { + while (trailPoints.size() > MAX_TRAIL_POINTS) { + trailPoints.remove(0); + } } } } @@ -369,6 +656,59 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { return startZ; } + /** + * Whether the tail orb should be rendered (only true in anchored mode). + */ + public boolean shouldRenderTailOrb() { + return renderTailOrb; + } + + /** + * Whether the beam is attached to its owner (anchored mode). + */ + public boolean isAttachedToOwner() { + return attachedToOwner; + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + /** + * Whether trail should fade (comet effect for non-anchored beams). + */ + public boolean hasFadingTrail() { + return fadeTrail; + } + + /** + * Get trail point ages for fading calculation. + */ + public List getTrailPointAges() { + return trailPointAges; + } + + /** + * Get trail fade time in ticks. + */ + public int getTrailFadeTime() { + return trailFadeTime; + } + // ==================== NBT ==================== @Override @@ -386,6 +726,16 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.prevHeadOffsetY = this.headOffsetY; this.prevHeadOffsetZ = this.headOffsetZ; this.attachedToOwner = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner"); + this.renderTailOrb = !nbt.hasKey("RenderTailOrb") || nbt.getBoolean("RenderTailOrb"); + // Read charging state and sync to data watcher + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; + this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail"); + this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20; // Read trail points (relative to origin) trailPoints.clear(); @@ -414,6 +764,13 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { nbt.setDouble("HeadOffsetY", headOffsetY); nbt.setDouble("HeadOffsetZ", headOffsetZ); nbt.setBoolean("AttachedToOwner", attachedToOwner); + nbt.setBoolean("RenderTailOrb", renderTailOrb); + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); + nbt.setBoolean("FadeTrail", fadeTrail); + nbt.setInteger("TrailFadeTime", trailFadeTime); // Write trail points NBTTagList trailList = new NBTTagList(); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index ef513f80d..c0127286f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -40,7 +40,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { } /** - * Full constructor with all parameters. + * Full constructor with all parameters (no lightning). */ public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, @@ -52,15 +52,38 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + this(world, owner, target, x, y, z, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + boomerang, boomerangDelay, explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + false, 0.15f, 0.5f); + } + + /** + * Full constructor with all parameters including lightning. + */ + public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float discRadius, float discThickness, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean boomerang, int boomerangDelay, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius) { super(world); - // Initialize base properties (use discRadius as size for base) + // Initialize base properties with lightning (use discRadius as size for base) initProjectile(owner, target, x, y, z, discRadius, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime); + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, 6); // Disc-specific properties this.speed = speed; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 78668377a..7ad1f0d45 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -45,7 +45,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } /** - * Full constructor with all parameters. + * Full constructor with all parameters (no lightning). */ public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, @@ -56,15 +56,37 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + this(world, owner, target, x, y, z, + laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, + damage, knockback, knockbackUp, expansionSpeed, lingerTicks, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + false, 0.15f, 0.5f); + } + + /** + * Full constructor with all parameters including lightning. + */ + public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float laserWidth, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, + float damage, float knockback, float knockbackUp, + float expansionSpeed, int lingerTicks, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius) { super(world); - // Initialize base properties + // Initialize base properties with lightning initProjectile(owner, target, x, y, z, laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, 0.0f, // No rotation for laser damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime); + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, 6); // Laser-specific properties this.laserWidth = laserWidth; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index de447333b..38e3741f9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -41,15 +41,39 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + this(world, owner, target, x, y, z, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + false, 0.15f, 0.5f, 6); // Default: no lightning, low density, small radius, 6 tick fade + } + + /** + * Full constructor with lightning effect support. + */ + public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float orbSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime) { super(world); - // Initialize base properties + // Initialize base properties with lightning initProjectile(owner, target, x, y, z, orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime); + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime); // Orb-specific properties this.speed = speed; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index eed12d10b..32c5796bd 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -30,7 +30,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected int innerColor = 0xFFFFFF; protected int outerColor = 0x8888FF; protected boolean outerColorEnabled = true; - protected float outerColorWidth = 1.8f; + protected float outerColorWidth = 0.4f; // Additive offset from inner size protected float rotationSpeed = 4.0f; // ==================== COMBAT PROPERTIES ==================== @@ -48,6 +48,16 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected float explosionRadius = 3.0f; protected float explosionDamageFalloff = 0.5f; + // ==================== LIGHTNING EFFECT PROPERTIES ==================== + protected boolean lightningEffect = false; + protected float lightningDensity = 0.15f; // Bolts spawned per tick (0.15 = ~15% chance per frame) + protected float lightningRadius = 0.5f; // Max distance lightning can arc from center + protected int lightningFadeTime = 6; // Ticks before lightning fades out + + // Client-side lightning state (not saved to NBT) + @cpw.mods.fml.relauncher.SideOnly(cpw.mods.fml.relauncher.Side.CLIENT) + public transient Object lightningState; // Actually AttachedLightningRenderer.LightningState + // ==================== LIFESPAN ==================== protected float maxDistance = 30.0f; protected int maxLifetime = 200; @@ -93,6 +103,27 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + initProjectile(owner, target, x, y, z, + size, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + false, 0.15f, 0.5f, 6); // Default: no lightning, low density, small radius, 6 tick fade + } + + /** + * Initialize common properties with lightning effect support. + */ + protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float size, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime) { this.setPosition(x, y, z); this.startX = x; this.startY = y; @@ -129,6 +160,12 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.maxLifetime = maxLifetime; // deathWorldTime will be set on first tick when world is available + // Lightning + this.lightningEffect = lightningEffect; + this.lightningDensity = lightningDensity; + this.lightningRadius = lightningRadius; + this.lightningFadeTime = lightningFadeTime; + // Initialize render size this.renderCurrentSize = size; this.prevRenderSize = size; @@ -417,6 +454,24 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks; } + // ==================== LIGHTNING GETTERS ==================== + + public boolean hasLightningEffect() { + return lightningEffect; + } + + public float getLightningDensity() { + return lightningDensity; + } + + public float getLightningRadius() { + return lightningRadius; + } + + public int getLightningFadeTime() { + return lightningFadeTime; + } + // ==================== BRIGHTNESS ==================== @Override @@ -452,7 +507,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.innerColor = nbt.getInteger("InnerColor"); this.outerColor = nbt.getInteger("OuterColor"); this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled"); - this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 0.4f; this.rotationSpeed = nbt.hasKey("RotationSpeed") ? nbt.getFloat("RotationSpeed") : 4.0f; this.damage = nbt.getFloat("Damage"); @@ -482,6 +537,12 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.motionY = nbt.getDouble("MotionY"); this.motionZ = nbt.getDouble("MotionZ"); + // Lightning + this.lightningEffect = nbt.getBoolean("LightningEffect"); + this.lightningDensity = nbt.hasKey("LightningDensity") ? nbt.getFloat("LightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("LightningRadius") ? nbt.getFloat("LightningRadius") : 0.5f; + this.lightningFadeTime = nbt.hasKey("LightningFadeTime") ? nbt.getInteger("LightningFadeTime") : 6; + if (this.worldObj != null && this.worldObj.isRemote) { this.renderCurrentSize = this.size; this.prevRenderSize = this.size; @@ -525,6 +586,12 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA nbt.setDouble("MotionX", motionX); nbt.setDouble("MotionY", motionY); nbt.setDouble("MotionZ", motionZ); + + // Lightning + nbt.setBoolean("LightningEffect", lightningEffect); + nbt.setFloat("LightningDensity", lightningDensity); + nbt.setFloat("LightningRadius", lightningRadius); + nbt.setInteger("LightningFadeTime", lightningFadeTime); } /** diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 534963e38..9a0044191 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -144,11 +144,12 @@ import noppes.npcs.client.renderer.RenderNpcCrystal; import noppes.npcs.client.renderer.RenderNpcDragon; import noppes.npcs.client.renderer.RenderNpcSlime; import noppes.npcs.client.renderer.RenderProjectile; -import kamkeel.npcs.client.renderer.RenderAbilityOrbCube; +import kamkeel.npcs.client.renderer.RenderAbilityOrb; import kamkeel.npcs.client.renderer.RenderAbilityDisc; import kamkeel.npcs.client.renderer.RenderAbilityLaser; import kamkeel.npcs.client.renderer.RenderAbilityBeam; import kamkeel.npcs.client.renderer.RenderAbilitySweeper; +import kamkeel.npcs.client.renderer.lightning.LightningHandler; import kamkeel.npcs.entity.EntityAbilityOrb; import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.entity.EntityAbilityLaser; @@ -261,8 +262,7 @@ public class ClientProxy extends CommonProxy { RenderingRegistry.registerEntityRenderingHandler(EntityNpcDragon.class, new RenderNpcDragon(new ModelNpcDragon(0.0F), 0.5F)); RenderingRegistry.registerEntityRenderingHandler(EntityNpcSlime.class, new RenderNpcSlime(new ModelNpcSlime(16), new ModelNpcSlime(0), 0.25F)); RenderingRegistry.registerEntityRenderingHandler(EntityProjectile.class, new RenderProjectile()); - // RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrb()); - RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrbCube()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrb()); RenderingRegistry.registerEntityRenderingHandler(EntityAbilityDisc.class, new RenderAbilityDisc()); RenderingRegistry.registerEntityRenderingHandler(EntityAbilityLaser.class, new RenderAbilityLaser()); RenderingRegistry.registerEntityRenderingHandler(EntityAbilityBeam.class, new RenderAbilityBeam()); @@ -340,6 +340,9 @@ public class ClientProxy extends CommonProxy { TelegraphManager.initClient(); MinecraftForge.EVENT_BUS.register(new TelegraphRenderer()); + // Lightning effect rendering system + MinecraftForge.EVENT_BUS.register(new LightningHandler()); + if (ConfigClient.InventoryGuiEnabled) { MinecraftForge.EVENT_BUS.register(new TabRegistry()); if (TabRegistry.getTabList().isEmpty()) { diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 6142de16c..203cef3e2 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -5,6 +5,7 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; +import kamkeel.npcs.client.renderer.lightning.LightningBolt; import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; @@ -115,6 +116,9 @@ public class ClientTickHandler { if (TelegraphManager.ClientInstance != null) { TelegraphManager.ClientInstance.tick(mc.theWorld); } + + // Update lightning bolts for ability effects + LightningBolt.updateAll(); if (Minecraft.getMinecraft().thePlayer != null && (prevWidth != mc.displayWidth || prevHeight != mc.displayHeight)) { prevWidth = mc.displayWidth; prevHeight = mc.displayHeight; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index ec80ed5d6..373752fa6 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -126,6 +126,28 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { // Row 3: Rotation Speed addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); addTextField(createFloatField(204, fieldX, y, 50, disc.getRotationSpeed())); + + y += 24; + + // Row 4: Lightning Effect Enabled + addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); + addButton(new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.hasLightningEffect() ? 1 : 0)); + + // Only show Lightning settings if Lightning is enabled + if (disc.hasLightningEffect()) { + y += 24; + + // Row 5: Density + Radius + addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(disc.getLightningDensity())); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(disc.getLightningRadius())); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } } @Override @@ -160,6 +182,10 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { disc.setOuterColorEnabled(value == 1); initGui(); break; + case 205: + disc.setLightningEffect(value == 1); + initGui(); + break; } } @@ -222,6 +248,12 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { case 204: disc.setRotationSpeed(parseFloat(field, disc.getRotationSpeed())); break; + case 206: + disc.setLightningDensity(parseFloat(field, disc.getLightningDensity())); + break; + case 207: + disc.setLightningRadius(parseFloat(field, disc.getLightningRadius())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index afb89ffed..ad15bb576 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -127,6 +127,28 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { // Row 3: Rotation Speed addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); addTextField(createFloatField(204, fieldX, y, 50, beam.getRotationSpeed())); + + y += 24; + + // Row 4: Lightning Effect Enabled (only affects head) + addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); + addButton(new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.hasLightningEffect() ? 1 : 0)); + + // Only show Lightning settings if Lightning is enabled + if (beam.hasLightningEffect()) { + y += 24; + + // Row 5: Density + Radius + addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(beam.getLightningDensity())); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(beam.getLightningRadius())); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } } @Override @@ -158,6 +180,10 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { beam.setOuterColorEnabled(value == 1); initGui(); break; + case 205: + beam.setLightningEffect(value == 1); + initGui(); + break; } } @@ -223,6 +249,12 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { case 204: beam.setRotationSpeed(parseFloat(field, beam.getRotationSpeed())); break; + case 206: + beam.setLightningDensity(parseFloat(field, beam.getLightningDensity())); + break; + case 207: + beam.setLightningRadius(parseFloat(field, beam.getLightningRadius())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java index 47385f7cb..f83e93baf 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -111,6 +111,28 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, laser.getOuterColorWidth()); widthField.setEnabled(laser.isOuterColorEnabled()); addTextField(widthField); + + y += 24; + + // Row 3: Lightning Effect Enabled + addLabel(new GuiNpcLabel(204, "ability.lightning", labelX, y + 5)); + addButton(new GuiNpcButton(204, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.hasLightningEffect() ? 1 : 0)); + + // Only show Lightning settings if Lightning is enabled + if (laser.hasLightningEffect()) { + y += 24; + + // Row 4: Density + Radius + addLabel(new GuiNpcLabel(205, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(205, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(laser.getLightningDensity())); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(206, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(206, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(laser.getLightningRadius())); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } } @Override @@ -139,6 +161,10 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { laser.setOuterColorEnabled(value == 1); initGui(); break; + case 204: + laser.setLightningEffect(value == 1); + initGui(); + break; } } @@ -165,6 +191,12 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { case 203: laser.setOuterColorWidth(parseFloat(field, laser.getOuterColorWidth())); break; + case 205: + laser.setLightningDensity(parseFloat(field, laser.getLightningDensity())); + break; + case 206: + laser.setLightningRadius(parseFloat(field, laser.getLightningRadius())); + break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 9f5ac4caf..42dd6a773 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -87,36 +87,58 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 260; - // Row 1: Inner Color + Outer Color + // Row 1: Inner Color + Rotation Speed addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", orb.getInnerColor() & 0xFFFFFF); GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); innerColorBtn.setTextColor(orb.getInnerColor() & 0xFFFFFF); addButton(innerColorBtn); - addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); - String outerHex = String.format("%06X", orb.getOuterColor() & 0xFFFFFF); - GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); - outerColorBtn.setTextColor(orb.getOuterColor() & 0xFFFFFF); - outerColorBtn.setEnabled(orb.isOuterColorEnabled()); - addButton(outerColorBtn); + addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", col2LabelX, y + 5)); + addTextField(createFloatField(204, col2FieldX, y, 55, orb.getRotationSpeed())); y += 24; - // Row 2: Outer Color Enabled + Outer Color Width + // Row 2: Outer Glow Enabled addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isOuterColorEnabled() ? 1 : 0)); - addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); - GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, orb.getOuterColorWidth()); - widthField.setEnabled(orb.isOuterColorEnabled()); - addTextField(widthField); + // Only show Outer Color and Width if Outer Glow is enabled + if (orb.isOuterColorEnabled()) { + y += 24; + + // Row 3: Outer Color + Outer Width + addLabel(new GuiNpcLabel(201, "ability.outerColor", labelX, y + 5)); + String outerHex = String.format("%06X", orb.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, fieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(orb.getOuterColor() & 0xFFFFFF); + addButton(outerColorBtn); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + addTextField(createFloatField(203, col2FieldX, y, 55, orb.getOuterColorWidth())); + } y += 24; - // Row 3: Rotation Speed - addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); - addTextField(createFloatField(204, fieldX, y, 50, orb.getRotationSpeed())); + // Row 4: Lightning Effect Enabled + addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); + addButton(new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.hasLightningEffect() ? 1 : 0)); + + // Only show Lightning settings if Lightning is enabled + if (orb.hasLightningEffect()) { + y += 24; + + // Row 5: Density + Radius + addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(orb.getLightningDensity())); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(orb.getLightningRadius())); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } } @Override @@ -148,6 +170,10 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { orb.setOuterColorEnabled(value == 1); initGui(); break; + case 205: + orb.setLightningEffect(value == 1); + initGui(); + break; } } @@ -207,6 +233,12 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { case 204: orb.setRotationSpeed(parseFloat(field, orb.getRotationSpeed())); break; + case 206: + orb.setLightningDensity(parseFloat(field, orb.getLightningDensity())); + break; + case 207: + orb.setLightningRadius(parseFloat(field, orb.getLightningRadius())); + break; } } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index f3bba0c1d..b7e33a029 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1686,6 +1686,9 @@ ability.innerColor=Inner Color ability.outerColor=Outer Color ability.outerEnabled=Outer Glow ability.outerWidth=Outer Width +ability.lightning=Enable Lightning +ability.lightningDensity=Density +ability.lightningRadius=Radius ability.noVisualSettings=No visual settings for this ability type # Laser Shot Settings