diff --git a/src/api b/src/api index 9fbd1ecef..4e9642531 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 9fbd1eceffaa32f088676ff89d13fe3ca4ba1155 +Subproject commit 4e96425314a9fd2d3e48f68cff5fc7ee69f87213 diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergy.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergy.java index ce5d59669..5891b5742 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergy.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergy.java @@ -81,70 +81,95 @@ public abstract class RenderEnergy extends Render { * Render a cube centered at origin with given color and alpha. */ protected void renderCube(int color, float alpha, float halfSize) { - float[] rgb = extractRGB(color); - float r = rgb[0], g = rgb[1], b = rgb[2]; - Tessellator tess = Tessellator.instance; + float r = ((color >> 16) & 0xFF) / 255.0f; + float g = ((color >> 8) & 0xFF) / 255.0f; + float b = (color & 0xFF) / 255.0f; - // Front face (z+) tess.startDrawingQuads(); tess.setColorRGBA_F(r, g, b, alpha); + addCubeVertices(tess, 0, 0, 0, halfSize); + tess.draw(); + } + + // ==================== BATCHED CUBE RENDERING ==================== + + /** + * Begin a batched cube draw call. All cubes added via {@link #addBatchedCube} + * will be drawn in a single tessellator pass when {@link #endCubeBatch} is called. + */ + protected void beginCubeBatch() { + Tessellator.instance.startDrawingQuads(); + } + + /** + * Add a cube to the current batch at the given position with per-cube color/alpha. + * Must be called between {@link #beginCubeBatch} and {@link #endCubeBatch}. + */ + protected void addBatchedCube(float px, float py, float pz, float halfSize, + float r, float g, float b, float alpha) { + Tessellator tess = Tessellator.instance; + tess.setColorRGBA_F(r, g, b, alpha); + addCubeVertices(tess, px, py, pz, halfSize); + } + + /** + * End the batched cube draw call, flushing all queued cubes in one draw. + */ + protected void endCubeBatch() { + Tessellator.instance.draw(); + } + + /** + * Add the 24 vertices (6 faces) of a cube centered at (px,py,pz) to the tessellator. + * Color must be set before calling. Used by both single and batched cube rendering. + */ + private static void addCubeVertices(Tessellator tess, float px, float py, float pz, float h) { + float x0 = px - h, x1 = px + h; + float y0 = py - h, y1 = py + h; + float z0 = pz - h, z1 = pz + h; + + // Front (z+) tess.setNormal(0, 0, 1); - tess.addVertex(-halfSize, -halfSize, halfSize); - tess.addVertex(halfSize, -halfSize, halfSize); - tess.addVertex(halfSize, halfSize, halfSize); - tess.addVertex(-halfSize, halfSize, halfSize); - tess.draw(); + tess.addVertex(x0, y0, z1); + tess.addVertex(x1, y0, z1); + tess.addVertex(x1, y1, z1); + tess.addVertex(x0, y1, z1); - // Back face (z-) - tess.startDrawingQuads(); - tess.setColorRGBA_F(r, g, b, alpha); + // Back (z-) tess.setNormal(0, 0, -1); - tess.addVertex(halfSize, -halfSize, -halfSize); - tess.addVertex(-halfSize, -halfSize, -halfSize); - tess.addVertex(-halfSize, halfSize, -halfSize); - tess.addVertex(halfSize, halfSize, -halfSize); - tess.draw(); + tess.addVertex(x1, y0, z0); + tess.addVertex(x0, y0, z0); + tess.addVertex(x0, y1, z0); + tess.addVertex(x1, y1, z0); - // Top face (y+) - tess.startDrawingQuads(); - tess.setColorRGBA_F(r, g, b, alpha); + // Top (y+) tess.setNormal(0, 1, 0); - tess.addVertex(-halfSize, halfSize, halfSize); - tess.addVertex(halfSize, halfSize, halfSize); - tess.addVertex(halfSize, halfSize, -halfSize); - tess.addVertex(-halfSize, halfSize, -halfSize); - tess.draw(); + tess.addVertex(x0, y1, z1); + tess.addVertex(x1, y1, z1); + tess.addVertex(x1, y1, z0); + tess.addVertex(x0, y1, z0); - // Bottom face (y-) - tess.startDrawingQuads(); - tess.setColorRGBA_F(r, g, b, alpha); + // Bottom (y-) tess.setNormal(0, -1, 0); - tess.addVertex(-halfSize, -halfSize, -halfSize); - tess.addVertex(halfSize, -halfSize, -halfSize); - tess.addVertex(halfSize, -halfSize, halfSize); - tess.addVertex(-halfSize, -halfSize, halfSize); - tess.draw(); + tess.addVertex(x0, y0, z0); + tess.addVertex(x1, y0, z0); + tess.addVertex(x1, y0, z1); + tess.addVertex(x0, y0, z1); - // Right face (x+) - tess.startDrawingQuads(); - tess.setColorRGBA_F(r, g, b, alpha); + // Right (x+) tess.setNormal(1, 0, 0); - tess.addVertex(halfSize, -halfSize, halfSize); - tess.addVertex(halfSize, -halfSize, -halfSize); - tess.addVertex(halfSize, halfSize, -halfSize); - tess.addVertex(halfSize, halfSize, halfSize); - tess.draw(); + tess.addVertex(x1, y0, z1); + tess.addVertex(x1, y0, z0); + tess.addVertex(x1, y1, z0); + tess.addVertex(x1, y1, z1); - // Left face (x-) - tess.startDrawingQuads(); - tess.setColorRGBA_F(r, g, b, alpha); + // Left (x-) tess.setNormal(-1, 0, 0); - tess.addVertex(-halfSize, -halfSize, -halfSize); - tess.addVertex(-halfSize, -halfSize, halfSize); - tess.addVertex(-halfSize, halfSize, halfSize); - tess.addVertex(-halfSize, halfSize, -halfSize); - tess.draw(); + tess.addVertex(x0, y0, z0); + tess.addVertex(x0, y0, z1); + tess.addVertex(x0, y1, z1); + tess.addVertex(x0, y1, z0); } /** diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyExplosion.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyExplosion.java index 02388621a..bb1f94cab 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyExplosion.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyExplosion.java @@ -10,6 +10,8 @@ import org.lwjgl.opengl.GL11; /** * Voxel-style renderer for {@link EntityEnergyExplosion}. * Draws expanding cubic shells with a short fade-out. + * Uses batched cube rendering for performance — all cubes of each layer + * are drawn in a single tessellator pass instead of per-cube draw calls. */ @SideOnly(Side.CLIENT) public class RenderEnergyExplosion extends RenderEnergy { @@ -55,44 +57,73 @@ public class RenderEnergyExplosion extends RenderEnergy { int steps = Math.max(2, Math.min(6, (int) Math.ceil(radius * 0.65f) + 1)); float cell = radius / steps; float cubeHalf = Math.max(0.04f, cell * 0.22f * (0.90f - life * 0.25f)); + float outerScale = hasOuter ? 1.0f + explosion.getOuterColorWidth() * 0.35f : 1.0f; long seed = explosion.getRenderSeed(); + // Pre-extract RGB for batched rendering + float outerR = ((outerColor >> 16) & 0xFF) / 255.0f; + float outerG = ((outerColor >> 8) & 0xFF) / 255.0f; + float outerB = (outerColor & 0xFF) / 255.0f; + float innerR = ((innerColor >> 16) & 0xFF) / 255.0f; + float innerG = ((innerColor >> 8) & 0xFF) / 255.0f; + float innerB = (innerColor & 0xFF) / 255.0f; + GL11.glDepthMask(false); + + // Smoke layer — single batched pass renderSmokeVoxelShell(radius, life, seed); - for (int ix = -steps; ix <= steps; ix++) { - for (int iy = -steps; iy <= steps; iy++) { - for (int iz = -steps; iz <= steps; iz++) { - int edge = Math.max(Math.abs(ix), Math.max(Math.abs(iy), Math.abs(iz))); - if (edge != steps) continue; - if (!shouldRenderVoxel(ix, iy, iz, steps, seed, life)) continue; + // Outer color layer — single batched pass + if (hasOuter) { + float outerHalf = cubeHalf * outerScale; + float oa = outerAlpha * fade; + beginCubeBatch(); + for (int ix = -steps; ix <= steps; ix++) { + for (int iy = -steps; iy <= steps; iy++) { + for (int iz = -steps; iz <= steps; iz++) { + int edge = Math.max(Math.abs(ix), Math.max(Math.abs(iy), Math.abs(iz))); + if (edge != steps) continue; + if (!shouldRenderVoxel(ix, iy, iz, steps, seed, life)) continue; - float px = ix * cell + jitter(ix, iy, iz, seed, 1) * cell * 0.18f; - float py = iy * cell + jitter(ix, iy, iz, seed, 2) * cell * 0.18f; - float pz = iz * cell + jitter(ix, iy, iz, seed, 3) * cell * 0.18f; - - GL11.glPushMatrix(); - GL11.glTranslatef(px, py, pz); - - if (hasOuter) { - GL11.glPushMatrix(); - float outerScale = 1.0f + explosion.getOuterColorWidth() * 0.35f; - GL11.glScalef(outerScale, outerScale, outerScale); - renderCube(outerColor, outerAlpha * fade, cubeHalf); - GL11.glPopMatrix(); + float px = ix * cell + jitter(ix, iy, iz, seed, 1) * cell * 0.18f; + float py = iy * cell + jitter(ix, iy, iz, seed, 2) * cell * 0.18f; + float pz = iz * cell + jitter(ix, iy, iz, seed, 3) * cell * 0.18f; + addBatchedCube(px, py, pz, outerHalf, outerR, outerG, outerB, oa); } - - renderCube(innerColor, innerAlpha * fade, cubeHalf * 0.9f); - GL11.glPopMatrix(); } } + endCubeBatch(); } + + // Inner color layer — single batched pass + { + float innerHalf = cubeHalf * 0.9f; + float ia = innerAlpha * fade; + beginCubeBatch(); + for (int ix = -steps; ix <= steps; ix++) { + for (int iy = -steps; iy <= steps; iy++) { + for (int iz = -steps; iz <= steps; iz++) { + int edge = Math.max(Math.abs(ix), Math.max(Math.abs(iy), Math.abs(iz))); + if (edge != steps) continue; + if (!shouldRenderVoxel(ix, iy, iz, steps, seed, life)) continue; + + float px = ix * cell + jitter(ix, iy, iz, seed, 1) * cell * 0.18f; + float py = iy * cell + jitter(ix, iy, iz, seed, 2) * cell * 0.18f; + float pz = iz * cell + jitter(ix, iy, iz, seed, 3) * cell * 0.18f; + addBatchedCube(px, py, pz, innerHalf, innerR, innerG, innerB, ia); + } + } + } + endCubeBatch(); + } + renderCoreFlash(innerColor, innerAlpha, radius, cubeHalf, life); GL11.glDepthMask(true); } /** * Adds large translucent white/gray cubes to fake a smoky volumetric expansion. + * All smoke cubes are batched into a single draw call. */ private void renderSmokeVoxelShell(float radius, float life, long seed) { int smokeSteps = Math.max(2, Math.min(5, (int) Math.ceil(radius * 0.5f) + 1)); @@ -101,6 +132,8 @@ public class RenderEnergyExplosion extends RenderEnergy { float smokeCubeHalf = Math.max(0.07f, smokeCell * 0.24f * (0.95f - life * 0.25f)); float smokeAlpha = Math.min(MAX_SMOKE_CUBE_ALPHA, (0.15f + (1.0f - life) * 0.55f)); + // White = 1,1,1 Gray (0xDCDCDC) = 0.863,0.863,0.863 + beginCubeBatch(); for (int ix = -smokeSteps; ix <= smokeSteps; ix++) { for (int iy = -smokeSteps; iy <= smokeSteps; iy++) { for (int iz = -smokeSteps; iz <= smokeSteps; iz++) { @@ -115,16 +148,14 @@ public class RenderEnergyExplosion extends RenderEnergy { float py = iy * smokeCell + jitter(ix, iy, iz, seed ^ 0x2299aa11L, 2) * smokeJitter; float pz = iz * smokeCell + jitter(ix, iy, iz, seed ^ 0x2299aa11L, 3) * smokeJitter; - int smokeColor = ((h >>> 9) & 1) == 0 ? 0xFFFFFF : 0xDCDCDC; + boolean isWhite = ((h >>> 9) & 1) == 0; + float gray = isWhite ? 1.0f : 0.863f; float alpha = smokeAlpha * (0.78f + (((h >>> 10) & 15) / 15.0f) * 0.22f); - - GL11.glPushMatrix(); - GL11.glTranslatef(px, py, pz); - renderCube(smokeColor, alpha, smokeCubeHalf); - GL11.glPopMatrix(); + addBatchedCube(px, py, pz, smokeCubeHalf, gray, gray, gray, alpha); } } } + endCubeBatch(); } /** @@ -138,11 +169,8 @@ public class RenderEnergyExplosion extends RenderEnergy { float whiteAlpha = Math.min(1.0f, 0.25f + flash * 0.95f); float innerFlashAlpha = Math.min(1.0f, innerAlpha * (0.45f + flash * 0.95f)); - GL11.glPushMatrix(); renderCube(0xFFFFFF, whiteAlpha, coreHalf * 1.45f); - GL11.glScalef(0.82f, 0.82f, 0.82f); - renderCube(innerColor, innerFlashAlpha, coreHalf); - GL11.glPopMatrix(); + renderCube(innerColor, innerFlashAlpha, coreHalf * 0.82f); } private float saturate(float value) { diff --git a/src/main/java/kamkeel/npcs/controllers/EnergyController.java b/src/main/java/kamkeel/npcs/controllers/EnergyController.java new file mode 100644 index 000000000..ffd25c400 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/EnergyController.java @@ -0,0 +1,209 @@ +package kamkeel.npcs.controllers; + +import kamkeel.npcs.controllers.data.energy.IEnergyHandler; +import kamkeel.npcs.entity.*; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.World; +import noppes.npcs.api.IEnergyController; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.*; +import noppes.npcs.api.entity.IEnergyExplosion; +import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.scripted.entity.ScriptEnergyExplosion; + +import java.util.ArrayList; +import java.util.List; + +/** + * Central controller for script-created energy entities and handler registration. + * Provides factory methods for creating all energy entity types and routes + * damage through registered handlers when entities have customDamageData + * but no sourceAbility. + */ +public class EnergyController implements IEnergyController { + + public static EnergyController Instance = new EnergyController(); + + private final List handlers = new ArrayList<>(); + + // ═══════════════════════════════════════════════════════════════════ + // HANDLER REGISTRATION + // ═══════════════════════════════════════════════════════════════════ + + public void registerHandler(IEnergyHandler handler) { + handlers.add(handler); + } + + public List getHandlers() { + return handlers; + } + + // ═══════════════════════════════════════════════════════════════════ + // DAMAGE ROUTING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Fire onEnergyDamage on all handlers. Chain of responsibility — first true wins. + */ + public boolean fireOnEnergyDamage(Entity energyEntity, EntityLivingBase owner, + EntityLivingBase target, float damage, + float knockback, float knockbackUp, + double kbDirX, double kbDirZ, + float damageMultiplier, + NBTTagCompound damageData) { + for (IEnergyHandler handler : handlers) { + if (handler.onEnergyDamage(energyEntity, owner, target, damage, + knockback, knockbackUp, kbDirX, kbDirZ, damageMultiplier, damageData)) { + return true; + } + } + return false; + } + + /** + * Fire modifyEnergyDamage on all handlers. Cumulative — each handler's output feeds the next. + */ + public float fireModifyEnergyDamage(Entity energyEntity, EntityLivingBase owner, + float baseDamage, NBTTagCompound damageData) { + float damage = baseDamage; + for (IEnergyHandler handler : handlers) { + damage = handler.modifyEnergyDamage(energyEntity, owner, damage, damageData); + } + return damage; + } + + // ═══════════════════════════════════════════════════════════════════ + // INTERNAL FACTORY METHODS (MC types) + // ═══════════════════════════════════════════════════════════════════ + + public EntityAbilityOrb createOrbInternal(World world, Entity owner, double x, double y, double z, float size) { + EntityAbilityOrb entity = new EntityAbilityOrb(world); + entity.setPosition(x, y, z); + entity.setStartPosition(x, y, z); + entity.setOwnerEntityId(owner.getEntityId()); + entity.setProjectileSize(size); + return entity; + } + + public EntityAbilityBeam createBeamInternal(World world, Entity owner, double x, double y, double z, + float beamWidth, float headSize) { + EntityAbilityBeam entity = new EntityAbilityBeam(world); + entity.setPosition(x, y, z); + entity.setStartPosition(x, y, z); + entity.setOwnerEntityId(owner.getEntityId()); + entity.setBeamWidth(beamWidth); + entity.setHeadSize(headSize); + return entity; + } + + public EntityAbilityDisc createDiscInternal(World world, Entity owner, double x, double y, double z, + float radius, float thickness) { + EntityAbilityDisc entity = new EntityAbilityDisc(world); + entity.setPosition(x, y, z); + entity.setStartPosition(x, y, z); + entity.setOwnerEntityId(owner.getEntityId()); + entity.setDiscRadius(radius); + entity.setDiscThickness(thickness); + return entity; + } + + public EntityAbilityLaser createLaserInternal(World world, Entity owner, double x, double y, double z, + float laserWidth) { + EntityAbilityLaser entity = new EntityAbilityLaser(world); + entity.setPosition(x, y, z); + entity.setStartPosition(x, y, z); + entity.setOwnerEntityId(owner.getEntityId()); + entity.setLaserWidth(laserWidth); + return entity; + } + + public EntityAbilityZone createHazardInternal(World world, Entity owner, double x, double y, double z) { + EntityAbilityZone entity = new EntityAbilityZone(world); + entity.initAsHazard(owner, x, y, z); + return entity; + } + + public EntityAbilityZone createTrapInternal(World world, Entity owner, double x, double y, double z) { + EntityAbilityZone entity = new EntityAbilityZone(world); + entity.initAsTrap(owner, x, y, z); + return entity; + } + + public EntityEnergySweeper createSweeperInternal(World world, Entity owner, double x, double y, double z) { + EntityEnergySweeper entity = new EntityEnergySweeper(world); + entity.setPosition(x, y, z); + entity.setOwnerEntityId(owner.getEntityId()); + return entity; + } + + public EntityEnergyPanel createPanelInternal(World world, Entity owner, double x, double y, double z) { + EntityEnergyPanel entity = new EntityEnergyPanel(world); + entity.setPosition(x, y, z); + entity.setOwnerEntityId(owner.getEntityId()); + return entity; + } + + public EntityEnergyExplosion createExplosionInternal(World world, Entity owner, double x, double y, double z, float radius) { + return new EntityEnergyExplosion(world, owner, x, y, z, radius); + } + + // ═══════════════════════════════════════════════════════════════════ + // IEnergyController (Script API methods) + // ═══════════════════════════════════════════════════════════════════ + + @Override + public IEnergyOrb createOrb(IWorld world, IEntity owner, double x, double y, double z, float size) { + EntityAbilityOrb entity = createOrbInternal((World) world.getMCWorld(), owner.getMCEntity(), x, y, z, size); + return (IEnergyOrb) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyBeam createBeam(IWorld world, IEntity owner, double x, double y, double z, float beamWidth, float headSize) { + EntityAbilityBeam entity = createBeamInternal((World) world.getMCWorld(), owner.getMCEntity(), x, y, z, beamWidth, headSize); + return (IEnergyBeam) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyDisc createDisc(IWorld world, IEntity owner, double x, double y, double z, float radius, float thickness) { + EntityAbilityDisc entity = createDiscInternal((World) world.getMCWorld(), owner.getMCEntity(), x, y, z, radius, thickness); + return (IEnergyDisc) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyLaser createLaser(IWorld world, IEntity owner, double x, double y, double z, float laserWidth) { + EntityAbilityLaser entity = createLaserInternal((World) world.getMCWorld(), owner.getMCEntity(), x, y, z, laserWidth); + return (IEnergyLaser) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyZone createHazard(IWorld world, IEntity owner, double x, double y, double z) { + EntityAbilityZone entity = createHazardInternal((World) world.getMCWorld(), owner.getMCEntity(), x, y, z); + return (IEnergyZone) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyZone createTrap(IWorld world, IEntity owner, double x, double y, double z) { + EntityAbilityZone entity = createTrapInternal((World) world.getMCWorld(), owner.getMCEntity(), x, y, z); + return (IEnergyZone) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergySweeper createSweeper(IWorld world, IEntity owner, double x, double y, double z) { + EntityEnergySweeper entity = createSweeperInternal((World) world.getMCWorld(), owner.getMCEntity(), x, y, z); + return (IEnergySweeper) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyPanel createPanel(IWorld world, IEntity owner, double x, double y, double z) { + EntityEnergyPanel entity = createPanelInternal((World) world.getMCWorld(), owner.getMCEntity(), x, y, z); + return (IEnergyPanel) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyExplosion createExplosion(IWorld world, IEntity owner, double x, double y, double z, float radius) { + EntityEnergyExplosion entity = createExplosionInternal((World) world.getMCWorld(), owner.getMCEntity(), x, y, z, radius); + return new ScriptEnergyExplosion<>(entity); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 270d4ede7..dc0de02fa 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -22,6 +22,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityEnergyBarrier; import kamkeel.npcs.entity.EntityEnergyDome; import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.util.AttributeAttackUtil; import kamkeel.npcs.util.FileNameHelper; import net.minecraft.block.Block; import net.minecraft.entity.Entity; @@ -37,6 +38,7 @@ import net.minecraft.util.StatCollector; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.NpcDamageSource; +import noppes.npcs.config.ConfigMain; import noppes.npcs.api.INbt; import noppes.npcs.api.ability.IAbility; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -49,11 +51,18 @@ import noppes.npcs.controllers.data.AbilityScript; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.EventHooks; import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicData; +import noppes.npcs.controllers.data.MagicEntry; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.client.gui.advanced.SubGuiAbilityMagic; import noppes.npcs.scripted.event.AbilityEvent; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; public abstract class Ability implements IAbility, IAbilityAction { @@ -91,6 +100,7 @@ public abstract class Ability implements IAbility, IAbilityAction { protected LockMode lockMovement = LockMode.WINDUP; protected RotationMode rotationMode = RotationMode.FREE; protected LockMode rotationPhase = LockMode.WINDUP_AND_ACTIVE; + protected float trackDelay = 0; // 0 = instant tracking (legacy). Higher = slower lerp toward target. protected int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color @@ -116,6 +126,9 @@ public abstract class Ability implements IAbility, IAbilityAction { // Custom data for external mods protected NBTTagCompound customData = new NBTTagCompound(); + // Magic data — defines magic types and damage splits for this ability + protected MagicData magicData = new MagicData(); + // User type restriction protected UserType allowedBy = UserType.BOTH; @@ -280,6 +293,35 @@ public abstract class Ability implements IAbility, IAbilityAction { return true; } + /** + * Returns true if this ability type uses magic data. + * By default, any ability that deals damage also has magic. + * Override in subclasses like Barrier that need magic but don't deal direct damage. + */ + public boolean hasMagic() { + return hasDamage(); + } + + // ── Display methods for inventory preview ────────────────────── + + /** Base damage for preview display. Override in damaging types. */ + public float getDisplayDamage() { return 0; } + + /** True if getDisplayDamage() is damage-per-second (e.g., Hazard). */ + public boolean isDisplayDamageDPS() { return false; } + + /** Base barrier health for preview display. Override in barrier types. */ + public float getDisplayBarrierHealth() { return 0; } + + /** True if this ability reflects projectiles. */ + public boolean isDisplayReflect() { return false; } + + /** Reflection strength percentage (0-100). */ + public float getDisplayReflectStrength() { return 0; } + + /** True if this ability absorbs projectiles. */ + public boolean isDisplayAbsorbing() { return false; } + /** * Override to provide variant templates for this ability type. * When a user creates this ability type, they will be shown a selection dialog @@ -471,13 +513,22 @@ public abstract class Ability implements IAbility, IAbilityAction { this, caster, hitEntity, damage, knockback, knockbackUp, knockbackDirX, knockbackDirZ, 1.0f); if (!handled) { + float finalDamage = damage; + + // Apply magic pipeline for player and NPC casters + MagicData resolved = resolveMagicData(caster); + if (resolved != null && !resolved.isEmpty()) { + finalDamage = AttributeAttackUtil.calculateAbilityDamage( + caster, hitEntity, damage, resolved); + } + // Default damage path if (caster instanceof EntityNPCInterface) { - hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), damage); + hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), finalDamage); } else if (caster instanceof EntityPlayer) { - hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), damage); + hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), finalDamage); } else { - hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), damage); + hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), finalDamage); } } } finally { @@ -687,6 +738,11 @@ public abstract class Ability implements IAbility, IAbilityAction { .hover("ability.hover.rotationPhase") .visibleWhen(() -> this.rotationMode != RotationMode.FREE) ).tab("General")); + defs.add(FieldDef.floatField("ability.trackDelay", this::getTrackDelay, this::setTrackDelay) + .range(0.0f, 20.0f) + .hover("ability.hover.trackDelay") + .visibleWhen(() -> this.rotationMode == RotationMode.TRACK) + .tab("General")); defs.add(FieldDef.row( FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) .hover("ability.hover.interruptible") @@ -731,6 +787,41 @@ public abstract class Ability implements IAbility, IAbilityAction { } } + // ── Magic section ──────────────────────────────────────────── + if (hasMagic()) { + defs.add(FieldDef.section("ability.section.magic").tab("General")); + + defs.add(FieldDef.labelField("ability.magic.note", () -> { + if (magicData.isEmpty()) { + return StatCollector.translateToLocal("ability.magic.usesCaster"); + } + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : magicData.getMagics().entrySet()) { + Magic magic = MagicController.getInstance().getMagic(entry.getKey()); + if (magic != null) { + if (sb.length() > 0) sb.append(", "); + sb.append(magic.getDisplayName()).append(": ") + .append(Math.round(entry.getValue().split * 100)).append("%"); + } + } + return sb.toString(); + }).tab("General")); + + defs.add(FieldDef.subGuiField("ability.magic.editor", + () -> new SubGuiAbilityMagic(magicData.copy()), + gui -> { + MagicData result = ((SubGuiAbilityMagic) gui).magicData; + magicData.getMagics().clear(); + for (Map.Entry e : result.getMagics().entrySet()) { + magicData.getMagics().put(e.getKey(), e.getValue()); + } + }) + .buttonLabel(() -> magicData.isEmpty() + ? StatCollector.translateToLocal("ability.magic.usesCaster") + : magicData.getMagics().size() + " Magic(s)") + .tab("General")); + } + // ── Target tab ─────────────────────────────────────────────── defs.add(FieldDef.row( FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)).range(0, 100), @@ -1388,6 +1479,7 @@ public abstract class Ability implements IAbility, IAbilityAction { nbt.setInteger("lockMovement", lockMovement.ordinal()); nbt.setInteger("rotationMode", rotationMode.ordinal()); nbt.setInteger("rotationPhase", rotationPhase.ordinal()); + nbt.setFloat("trackDelay", trackDelay); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); @@ -1403,6 +1495,7 @@ public abstract class Ability implements IAbility, IAbilityAction { nbt.setString("telegraphType", telegraphType.name()); nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset); nbt.setTag("customData", (NBTTagCompound) customData.copy()); + magicData.writeToNBT(nbt); nbt.setInteger("allowedBy", allowedBy.ordinal()); nbt.setBoolean("ignoreCooldown", ignoreCooldown); nbt.setBoolean("perAbilityCooldown", perAbilityCooldown); @@ -1482,6 +1575,7 @@ public abstract class Ability implements IAbility, IAbilityAction { lockMovement = LockMode.fromOrdinal(nbt.getInteger("lockMovement")); rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode")); rotationPhase = LockMode.fromOrdinal(nbt.getInteger("rotationPhase")); + trackDelay = nbt.hasKey("trackDelay") ? nbt.getFloat("trackDelay") : 0; windUpColor = nbt.hasKey("windUpColor") ? nbt.getInteger("windUpColor") : 0x80FF4400; activeColor = nbt.hasKey("activeColor") ? nbt.getInteger("activeColor") : 0xC0FF0000; windUpSound = nbt.getString("windUpSound"); @@ -1500,6 +1594,7 @@ public abstract class Ability implements IAbility, IAbilityAction { } telegraphHeightOffset = nbt.getFloat("telegraphHeightOffset"); customData = (NBTTagCompound) nbt.getCompoundTag("customData").copy(); + magicData.readToNBT(nbt); allowedBy = UserType.fromOrdinal(nbt.getInteger("allowedBy")); ignoreCooldown = nbt.getBoolean("ignoreCooldown"); perAbilityCooldown = nbt.getBoolean("perAbilityCooldown"); @@ -1817,6 +1912,14 @@ public abstract class Ability implements IAbility, IAbilityAction { this.rotationPhase = rotationPhase; } + public float getTrackDelay() { + return trackDelay; + } + + public void setTrackDelay(float trackDelay) { + this.trackDelay = trackDelay; + } + /** * API method: Get lock movement type as integer. * @@ -1877,6 +1980,25 @@ public abstract class Ability implements IAbility, IAbilityAction { this.rotationPhase = LockMode.fromOrdinal(type); } + /** + * API method: Get track delay for TRACK rotation mode. + * 0 = instant tracking (legacy). Higher = slower lerp toward target. + */ + @Override + public float getTrackDelayValue() { + return trackDelay; + } + + /** + * API method: Set track delay for TRACK rotation mode. + * + * @param delay 0 = instant tracking. Higher = slower lerp toward target. + */ + @Override + public void setTrackDelayValue(float delay) { + this.trackDelay = Math.max(0, delay); + } + /** * Check if movement (pathfinding/motion) should be locked during WINDUP phase. */ @@ -2170,6 +2292,35 @@ public abstract class Ability implements IAbility, IAbilityAction { return customData; } + public MagicData getMagicData() { + return magicData; + } + + /** + * Resolve magic data for this ability + caster combination. + * If the ability has its own magic data, use that (override). + * Otherwise, fall back to the caster's magic data (player or NPC). + * + * @return Resolved MagicData, or null if neither has magic + */ + public MagicData resolveMagicData(EntityLivingBase caster) { + if (!this.magicData.isEmpty()) { + return this.magicData; + } + if (caster instanceof EntityPlayer) { + PlayerData data = PlayerData.get((EntityPlayer) caster); + if (data != null && data.magicData != null && !data.magicData.isEmpty()) { + return data.magicData; + } + } else if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + if (npc.stats != null && npc.stats.magicData != null && !npc.stats.magicData.isEmpty()) { + return npc.stats.magicData; + } + } + return null; + } + public List getConditions() { return conditions; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 4ad6811d5..c88d63c30 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -276,6 +276,9 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public float getKnockback() { return knockback; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 2fdc8fd48..8c6fecc4c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -375,6 +375,9 @@ public class AbilityCutter extends Ability implements IAbilityCutter { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public float getKnockback() { return knockback; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index a37df738e..47f9156a8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -80,6 +80,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { zoneHeight, particleDensity, particleScale, animSpeed, lightningDensity, getEffects()); + entity.setSourceAbility(this); applyVisualToEntity(entity); @@ -126,6 +127,12 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { this.damagePerSecond = damagePerSecond; } + @Override + public float getDisplayDamage() { return damagePerSecond; } + + @Override + public boolean isDisplayDamageDPS() { return true; } + public int getDamageInterval() { return damageInterval; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 2e5de3006..56458b1ab 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -174,6 +174,9 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public float getKnockback() { return knockback; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 2c9945299..104209eeb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -293,6 +293,9 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public float getSpeed() { return speed; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index a41da923c..c5e860c09 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -197,6 +197,9 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public boolean isAoe() { return aoe; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index d7a0f4cd6..0a5c134f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -751,6 +751,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public float getRadius() { return radius; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 448a431af..d22150c9e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -536,6 +536,9 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public float getDamageRadius() { return damageRadius; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 1ed357d94..5e508069a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -86,6 +86,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { particleDensity, particleScale, animSpeed, lightningDensity, visible, getEffects()); + entity.setSourceAbility(this); applyVisualToEntity(entity); @@ -269,6 +270,9 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public float getDamageRadius() { return damageRadius; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index 03c54ef71..8f9b3f3a0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -9,6 +9,7 @@ import kamkeel.npcs.controllers.data.ability.enums.TargetFilter; import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; @@ -23,7 +24,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; /** * Vortex ability: Pulls targets toward the caster. @@ -39,8 +39,8 @@ public class AbilityVortex extends Ability implements IAbilityVortex { private boolean damageOnPull = false; private float pullDamage = 0.0f; - // Runtime state - private transient Map pulledEntities; + // Runtime state — keyed by entity ID (not UUID, since cloned NPCs share UUIDs) + private transient Map pulledEntities; private transient boolean pullComplete = false; private transient int ticksSincePullDamage = 0; @@ -56,7 +56,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } } - private Map getPulledEntities() { + private Map getPulledEntities() { if (pulledEntities == null) { pulledEntities = new HashMap<>(); } @@ -115,7 +115,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { double dist = caster.getDistanceToEntity(entity); if (dist <= pullRadius) { - getPulledEntities().put(entity.getUniqueID(), new PullState(entity)); + getPulledEntities().put(entity.getEntityId(), new PullState(entity)); } } } else { @@ -125,7 +125,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { if (!isPlayerCaster(caster) && target != null && !target.isDead) { double dist = caster.getDistanceToEntity(target); if (dist <= pullRadius) { - getPulledEntities().put(target.getUniqueID(), new PullState(target)); + getPulledEntities().put(target.getEntityId(), new PullState(target)); return; } } @@ -144,7 +144,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } if (!validTargets.isEmpty()) { EntityLivingBase chosen = validTargets.get(caster.worldObj.rand.nextInt(validTargets.size())); - getPulledEntities().put(chosen.getUniqueID(), new PullState(chosen)); + getPulledEntities().put(chosen.getEntityId(), new PullState(chosen)); } } } @@ -168,8 +168,8 @@ public class AbilityVortex extends Ability implements IAbilityVortex { // Safety cap: force-complete if active too long int maxActiveTicks = Math.max(40, (int)(pullRadius / pullStrength * 3)); if (tick >= maxActiveTicks) { - for (Map.Entry entry : new HashMap<>(getPulledEntities()).entrySet()) { - EntityLivingBase entity = findEntity(caster, caster.worldObj, entry.getKey()); + for (Map.Entry entry : new HashMap<>(getPulledEntities()).entrySet()) { + EntityLivingBase entity = findEntity(caster.worldObj, entry.getKey()); if (entity != null && !entity.isDead) { onTargetArrived(caster, entity, caster.worldObj); } @@ -191,13 +191,13 @@ public class AbilityVortex extends Ability implements IAbilityVortex { ticksSincePullDamage = 0; } - for (Map.Entry entry : new HashMap<>(getPulledEntities()).entrySet()) { - UUID uuid = entry.getKey(); + for (Map.Entry entry : new HashMap<>(getPulledEntities()).entrySet()) { + int entityId = entry.getKey(); PullState state = entry.getValue(); - EntityLivingBase entity = findEntity(caster, caster.worldObj, uuid); + EntityLivingBase entity = findEntity(caster.worldObj, entityId); if (entity == null || entity.isDead) { - getPulledEntities().remove(uuid); + getPulledEntities().remove(entityId); continue; } @@ -207,7 +207,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); if (dist <= 1.5) { - getPulledEntities().remove(uuid); + getPulledEntities().remove(entityId); onTargetArrived(caster, entity, caster.worldObj); continue; } @@ -228,13 +228,19 @@ public class AbilityVortex extends Ability implements IAbilityVortex { if (state.stuckTicks >= 5) { // Entity is stuck (wall, block, partial obstruction) - treat as arrived - getPulledEntities().remove(uuid); + getPulledEntities().remove(entityId); onTargetArrived(caster, entity, caster.worldObj); continue; } anyStillPulling = true; + // Record position BEFORE movement so next tick's stuck detection + // can measure the actual progress (pull + AI movement combined) + state.lastX = entity.posX; + state.lastY = entity.posY; + state.lastZ = entity.posZ; + // Clamp speed to never exceed half the remaining distance, preventing overshoot/slingshot double maxSpeed = dist * 0.5; double effectiveSpeed = Math.min(pullStrength, maxSpeed); @@ -257,11 +263,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex { entity.moveEntity(motionX, motionY, motionZ); } - // Update tracked position for next tick's stuck detection - state.lastX = entity.posX; - state.lastY = entity.posY; - state.lastZ = entity.posZ; - if (shouldDealPullDamage) { applyAbilityDamage(caster, entity, pullDamage * 0.5f, 0); } @@ -284,20 +285,12 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } /** - * Find an entity by UUID within the vortex pull area. - * Uses AABB search instead of iterating all loaded entities. - * Search area is expanded beyond pullRadius to account for entity drift. + * Find an entity by entity ID. */ - @SuppressWarnings("unchecked") - private EntityLivingBase findEntity(EntityLivingBase caster, World world, UUID uuid) { - double searchRadius = pullRadius + 5; - AxisAlignedBB searchBox = caster.boundingBox.expand(searchRadius, searchRadius / 2, searchRadius); - List nearbyEntities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); - - for (EntityLivingBase entity : nearbyEntities) { - if (entity.getUniqueID().equals(uuid)) { - return entity; - } + private EntityLivingBase findEntity(World world, int entityId) { + Entity entity = world.getEntityByID(entityId); + if (entity instanceof EntityLivingBase) { + return (EntityLivingBase) entity; } return null; } @@ -356,6 +349,9 @@ public class AbilityVortex extends Ability implements IAbilityVortex { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public float getKnockback() { return knockback; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java index 1a7734a1e..5d12b4c11 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java @@ -10,6 +10,7 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.controllers.data.MagicData; import java.util.List; @@ -69,6 +70,11 @@ public abstract class AbilityBarrier extends AbilityEnergy { return false; // Barriers are defensive } + @Override + public boolean hasMagic() { + return true; // Barriers use magic for defense interactions + } + @Override public boolean allowFreeOnCast() { return true; @@ -86,6 +92,7 @@ public abstract class AbilityBarrier extends AbilityEnergy { if (tick == 1) { barrierEntity = createBarrierEntity(caster, target); if (barrierEntity != null) { + inheritMagicData(barrierEntity, caster); applyBarrierHealthModifiers(caster); barrierEntity.setupCharging(getWindUpTicks()); spawnAbilityEntity(barrierEntity); @@ -102,12 +109,20 @@ public abstract class AbilityBarrier extends AbilityEnergy { // No windup or entity died — create fresh barrierEntity = createBarrierEntity(caster, target); if (barrierEntity != null) { + inheritMagicData(barrierEntity, caster); applyBarrierHealthModifiers(caster); spawnAbilityEntity(barrierEntity); } } } + private void inheritMagicData(EntityEnergyBarrier entity, EntityLivingBase caster) { + MagicData resolved = resolveMagicData(caster); + if (resolved != null) { + entity.setMagicData(resolved.copy()); + } + } + @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { if (barrierEntity == null || barrierEntity.isDead) { @@ -198,6 +213,18 @@ public abstract class AbilityBarrier extends AbilityEnergy { barrierData.useHealth = useHealth; } + @Override + public float getDisplayBarrierHealth() { return barrierData.useHealth ? barrierData.maxHealth : 0; } + + @Override + public boolean isDisplayReflect() { return barrierData.reflect; } + + @Override + public float getDisplayReflectStrength() { return barrierData.reflectStrengthPct; } + + @Override + public boolean isDisplayAbsorbing() { return barrierData.absorbing; } + public int getBarrierDuration() { return barrierData.durationTicks; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index 0e08fab02..7e7986d20 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -25,6 +25,7 @@ import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import noppes.npcs.api.ability.type.IAbilityEnergyProjectile; import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.controllers.data.MagicData; import java.util.List; @@ -214,6 +215,10 @@ public abstract class AbilityEnergyProjectile entity.setEffects(this.effects); entity.setIgnoreIFrames(this.isIgnoreIFrames()); entity.setSourceAbility(this); + MagicData resolvedMagic = resolveMagicData(caster); + if (resolvedMagic != null) { + entity.setMagicData(resolvedMagic.copy()); + } return entity; } @@ -634,6 +639,9 @@ public abstract class AbilityEnergyProjectile combatData.damage = damage; } + @Override + public float getDisplayDamage() { return combatData.damage; } + @Override public float getKnockback() { return combatData.knockback; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java index 918f493ba..e9c4e0c16 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java @@ -49,6 +49,7 @@ public class AbilityLaser extends AbilityEnergyProjectile im this.lockMovement = LockMode.WINDUP_AND_ACTIVE; this.rotationMode = RotationMode.TRACK; this.rotationPhase = LockMode.WINDUP_AND_ACTIVE; + this.trackDelay = 3; this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; this.windUpAnimationName = "Ability_Laser_Windup"; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java index 3fc833ff4..67c2d9c0d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySweeper.java @@ -235,6 +235,9 @@ public class AbilitySweeper extends AbilityEnergy implements IAbilitySweeper { this.damage = damage; } + @Override + public float getDisplayDamage() { return damage; } + public int getDamageInterval() { return damageInterval; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java index 104cae221..bfe8c5b36 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityWall.java @@ -200,6 +200,9 @@ public class AbilityWall extends AbilityBarrier { panelData.setLaunchDamage(damage); } + @Override + public float getDisplayDamage() { return panelData.launching ? panelData.launchDamage : 0; } + public float getLaunchKnockback() { return panelData.launchKnockback; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/energy/IEnergyHandler.java b/src/main/java/kamkeel/npcs/controllers/data/energy/IEnergyHandler.java new file mode 100644 index 000000000..bd2e39a74 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/energy/IEnergyHandler.java @@ -0,0 +1,49 @@ +package kamkeel.npcs.controllers.data.energy; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; + +/** + * Extension point for handling energy entity damage without a sourceAbility. + * Registered with EnergyController. DBC Addon implements this to route + * script-created energy entity damage through the DBC damage pipeline. + */ +public interface IEnergyHandler { + + /** + * Handle damage for an energy entity with custom damage data. + * Chain of Responsibility — first handler returning true claims the damage. + * + * @param energyEntity The energy entity dealing damage + * @param owner The entity that owns/created the energy entity + * @param target The entity being damaged + * @param damage Base damage amount + * @param knockback Horizontal knockback strength + * @param knockbackUp Vertical knockback strength + * @param kbDirX Knockback direction X + * @param kbDirZ Knockback direction Z + * @param damageMultiplier Damage multiplier (e.g. falloff) + * @param damageData Custom damage configuration NBT (e.g. DBC stats) + * @return true if this handler processed the damage, false to pass to next handler + */ + boolean onEnergyDamage(Entity energyEntity, EntityLivingBase owner, + EntityLivingBase target, float damage, + float knockback, float knockbackUp, + double kbDirX, double kbDirZ, + float damageMultiplier, + NBTTagCompound damageData); + + /** + * Modify outgoing damage for an energy entity with custom damage data. + * Cumulative — each handler receives the previous handler's output. + * + * @param energyEntity The energy entity dealing damage + * @param owner The entity that owns/created the energy entity + * @param baseDamage Current damage value (after previous handlers) + * @param damageData Custom damage configuration NBT + * @return Modified damage value + */ + float modifyEnergyDamage(Entity energyEntity, EntityLivingBase owner, + float baseDamage, NBTTagCompound damageData); +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index e84e0df00..753f997a1 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -6,6 +6,7 @@ import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData; +import kamkeel.npcs.util.AnchorPointHelper; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.entity.Entity; @@ -224,11 +225,16 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { dirZ = 0.0; } - // Keep origin centered on look vector while active and clear owner bbox. - Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ); - setLookVectorLaunchPosition(livingOwner, direction, false); - // Lower beam origin slightly so it doesn't obscure the player's crosshair - posY -= 0.15; + // Position: use anchor if launchFromAnchor, otherwise use look vector position + if (anchorData.launchFromAnchor) { + Vec3 anchorPos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData); + setPosition(anchorPos.xCoord, anchorPos.yCoord, anchorPos.zCoord); + } else { + Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ); + setLookVectorLaunchPosition(livingOwner, direction, false); + // Lower beam origin slightly so it doesn't obscure the player's crosshair + posY -= 0.15; + } syncStartPositionToCurrent(); syncPositionStateToCurrent(false); } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java index ba2352b31..23120b80b 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -5,6 +5,8 @@ import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.EnergyController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect; import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; @@ -24,7 +26,6 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.UUID; /** * Shared zone entity for Trap and Hazard abilities. @@ -166,12 +167,21 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD private transient boolean armed = false; private transient int triggerCount = 0; private transient int ticksSinceLastTrigger = 0; - private transient Set triggeredEntities = new HashSet<>(); + private transient Set triggeredEntities = new HashSet<>(); private transient int triggerFlashTick = -1; private transient int ticksSinceDamage = 0; private transient Set damagedThisTick = new HashSet<>(); + private transient Ability sourceAbility = null; + + /** + * Custom damage data for script-created zones that don't have a sourceAbility. + * Used by EnergyController handlers (e.g. DBC Addon) to carry damage configuration + * directly on the entity. Synced via spawn data for client sync. + */ + private NBTTagCompound customDamageData = null; + private boolean previewMode = false; private EntityLivingBase previewOwner = null; @@ -197,6 +207,30 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.setPosition(x, groundY, z); } + /** + * Initialize as a hazard zone with default values. Used by EnergyController factory. + * Scripts can then configure properties before calling spawn(). + */ + public void initAsHazard(Entity owner, double x, double y, double z) { + this.zoneType = ZoneType.HAZARD; + this.ownerEntityId = owner.getEntityId(); + double groundY = Ability.findGroundLevel(worldObj, x, y, z) + GROUND_OFFSET; + this.setPosition(x, groundY, z); + this.ticksSinceDamage = damageInterval; + } + + /** + * Initialize as a trap zone with default values. Used by EnergyController factory. + * Scripts can then configure properties before calling spawn(). + */ + public void initAsTrap(Entity owner, double x, double y, double z) { + this.zoneType = ZoneType.TRAP; + this.ownerEntityId = owner.getEntityId(); + double groundY = Ability.findGroundLevel(worldObj, x, y, z) + GROUND_OFFSET; + this.setPosition(x, groundY, z); + this.ticksSinceLastTrigger = triggerCooldown; + } + public static EntityAbilityZone createTrap(World world, EntityLivingBase owner, double x, double y, double z, ZoneShape shape, @@ -360,7 +394,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD for (EntityLivingBase entity : entities) { if (owner != null && entity == owner) continue; if (entity.isDead) continue; - if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; + if (maxTriggers == 1 && triggeredEntities.contains(entity.getEntityId())) continue; if (owner instanceof EntityLivingBase && !AbilityTargetHelper.shouldAffect((EntityLivingBase) owner, entity, TargetFilter.ENEMIES, false)) continue; double dx = entity.posX - posX; @@ -439,7 +473,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD if (anyDamaged) { triggerCount++; ticksSinceLastTrigger = 0; - triggeredEntities.add(triggerer.getUniqueID()); + triggeredEntities.add(triggerer.getEntityId()); triggerFlashTick = ticksExisted; worldObj.setEntityState(this, TRIGGER_FLASH_STATUS); } @@ -505,15 +539,36 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD int previousHurtResistantTime = Ability.clearHurtResistanceIfNeeded(target, ignoreIFrames); try { - if (owner instanceof EntityNPCInterface) { - return target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); - } else if (owner instanceof EntityPlayer) { - return target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); - } else if (owner instanceof EntityLivingBase) { - return target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); - } else { - return target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); + // Route through ability extender (e.g. DBC Addon damage scaling) + boolean handled = false; + if (sourceAbility != null && owner instanceof EntityLivingBase) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + handled = AbilityController.Instance.fireOnAbilityDamage( + sourceAbility, (EntityLivingBase) owner, target, + dmg, 0.0f, 0.0f, dx, dz, 1.0f); } + // Fallback: route through EnergyController for script-created entities with custom damage data + if (!handled && customDamageData != null && owner instanceof EntityLivingBase) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + handled = EnergyController.Instance.fireOnEnergyDamage( + this, (EntityLivingBase) owner, target, + dmg, 0.0f, 0.0f, dx, dz, 1.0f, customDamageData); + } + + if (!handled) { + if (owner instanceof EntityNPCInterface) { + return target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); + } else if (owner instanceof EntityPlayer) { + return target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); + } else if (owner instanceof EntityLivingBase) { + return target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); + } else { + return target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); + } + } + return true; } finally { Ability.restoreHurtResistanceIfNeeded(target, ignoreIFrames, previousHurtResistantTime); } @@ -541,6 +596,22 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.previewOwner = owner; } + public Ability getSourceAbility() { + return sourceAbility; + } + + public void setSourceAbility(Ability ability) { + this.sourceAbility = ability; + } + + public NBTTagCompound getCustomDamageData() { + return customDamageData; + } + + public void setCustomDamageData(NBTTagCompound data) { + this.customDamageData = data; + } + // ═══════════════════════════════════════════════════════════════════ // RENDERING SUPPORT // ═══════════════════════════════════════════════════════════════════ @@ -714,6 +785,51 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.particleGlow = particleGlow; } + // ═══════════════════════════════════════════════════════════════════ + // PUBLIC SETTERS (for EnergyController / scripting API) + // ═══════════════════════════════════════════════════════════════════ + + public void setShape(ZoneShape shape) { this.shape = shape; } + public void setRadius(float radius) { this.radius = radius; } + public void setZoneHeight(float height) { this.zoneHeight = height; } + public void setDurationTicks(int ticks) { this.durationTicks = ticks; this.maxTicks = ticks; } + public void setInnerColor(int color) { this.innerColor = color; } + public void setOuterColor(int color) { this.outerColor = color; } + public void setOuterColorEnabled(boolean enabled) { this.outerColorEnabled = enabled; } + public void setParticleDensity(float density) { this.particleDensity = density; } + public void setParticleScale(float scale) { this.particleScale = scale; } + public void setAnimSpeed(float speed) { this.animSpeed = speed; } + public void setIgnoreIFrames(boolean ignore) { this.ignoreIFrames = ignore; } + + // Hazard-specific + public float getDamagePerSecond() { return damagePerSecond; } + public void setDamagePerSecond(float dps) { this.damagePerSecond = dps; } + public int getDamageInterval() { return damageInterval; } + public void setDamageInterval(int ticks) { this.damageInterval = ticks; } + public boolean isAffectsCaster() { return affectsCaster; } + public void setAffectsCaster(boolean affects) { this.affectsCaster = affects; } + + // Trap-specific + public float getTriggerRadius() { return triggerRadius; } + public void setTriggerRadius(float radius) { this.triggerRadius = radius; } + public int getArmTime() { return armTime; } + public void setArmTime(int ticks) { this.armTime = ticks; } + public int getMaxTriggers() { return maxTriggers; } + public void setMaxTriggers(int max) { this.maxTriggers = max; } + public int getTriggerCooldown() { return triggerCooldown; } + public void setTriggerCooldown(int ticks) { this.triggerCooldown = ticks; } + public float getDamage() { return damage; } + public void setDamage(float damage) { this.damage = damage; } + public float getDamageRadius() { return damageRadius; } + public void setDamageRadius(float radius) { this.damageRadius = radius; } + public float getKnockback() { return knockback; } + public void setKnockback(float knockback) { this.knockback = knockback; } + public void setVisible(boolean visible) { this.visible = visible; } + + public boolean isIgnoreIFrames() { return ignoreIFrames; } + + public int getOwnerEntityId() { return ownerEntityId; } + // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ @@ -781,6 +897,17 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD } catch (java.io.IOException e) { noppes.npcs.LogWriter.error("Error writing zone effects spawn data", e); } + + // Custom damage data + boolean hasDamageData = customDamageData != null; + buffer.writeBoolean(hasDamageData); + if (hasDamageData) { + try { + ByteBufUtils.writeNBT(buffer, customDamageData); + } catch (java.io.IOException e) { + noppes.npcs.LogWriter.error("Error writing zone custom damage data", e); + } + } } @Override @@ -843,5 +970,14 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD } } } + + // Custom damage data + if (buffer.readableBytes() > 0 && buffer.readBoolean()) { + try { + this.customDamageData = ByteBufUtils.readNBT(buffer); + } catch (java.io.IOException e) { + noppes.npcs.LogWriter.error("Error reading zone custom damage data", e); + } + } } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java index cb7c92534..46c2647e8 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java @@ -12,6 +12,7 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; +import noppes.npcs.controllers.data.MagicData; /** * Universal base class for all energy ability entities (Projectiles, Barriers, Sweeper). @@ -43,6 +44,17 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit * The ability that spawned this entity. Transient, not saved to NBT. */ protected transient Ability sourceAbility = null; + + /** + * Custom damage data for script-created entities that don't have a sourceAbility. + * Used by EnergyController handlers (e.g. DBC Addon) to carry damage configuration + * directly on the entity. Persistent — saved to spawn data for client sync. + */ + protected NBTTagCompound customDamageData = null; + + /** Magic data for this entity — defines magic types for outgoing damage or barrier defense. */ + protected MagicData magicData = new MagicData(); + protected boolean ignoreIFrames = false; protected boolean previewMode = false; protected EntityLivingBase previewOwner = null; @@ -141,6 +153,24 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit this.sourceAbility = ability; } + // ==================== CUSTOM DAMAGE DATA ==================== + + public NBTTagCompound getCustomDamageData() { + return customDamageData; + } + + public void setCustomDamageData(NBTTagCompound data) { + this.customDamageData = data; + } + + public MagicData getMagicData() { + return magicData; + } + + public void setMagicData(MagicData data) { + this.magicData = data != null ? data : new MagicData(); + } + public boolean isIgnoreIFrames() { return ignoreIFrames; } @@ -325,6 +355,10 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit nbt.setBoolean("IgnoreIFrames", ignoreIFrames); displayData.writeNBT(nbt); lightningData.writeNBT(nbt); + if (customDamageData != null) { + nbt.setTag("CustomDamageData", customDamageData); + } + magicData.writeToNBT(nbt); } /** @@ -336,6 +370,10 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit this.ignoreIFrames = nbt.getBoolean("IgnoreIFrames"); displayData.readNBT(nbt); lightningData.readNBT(nbt); + if (nbt.hasKey("CustomDamageData")) { + this.customDamageData = nbt.getCompoundTag("CustomDamageData"); + } + magicData.readToNBT(nbt); } // ==================== SPAWN DATA ==================== diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java index b16448c33..9111e7dd8 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java @@ -2,6 +2,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; +import kamkeel.npcs.util.AttributeAttackUtil; import kamkeel.npcs.util.CNPCDebug; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -12,6 +13,8 @@ import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.CustomNpcs; import noppes.npcs.EventHooks; +import noppes.npcs.controllers.data.MagicData; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import java.lang.ref.WeakReference; @@ -248,6 +251,13 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { float multiplier = barrierData.getMultiplier(typeId); float finalDamage = damage * multiplier; + // Apply magic interactions if projectile is a CNPC+ energy entity + if (projectileEntity instanceof EntityEnergyAbility) { + float magicMultiplier = AttributeAttackUtil.calculateMagicInteractionMultiplier( + ((EntityEnergyAbility) projectileEntity).getMagicData(), this.magicData); + finalDamage *= magicMultiplier; + } + if (!worldObj.isRemote) { float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, null, finalDamage); if (eventDamage < 0) { @@ -329,6 +339,11 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { float multiplier = barrierData.getMultiplier(typeId); float damage = baseDamage * multiplier; + // Apply magic interactions (e.g., Water projectile vs Fire barrier) + float magicMultiplier = AttributeAttackUtil.calculateMagicInteractionMultiplier( + projectile.getMagicData(), this.magicData); + damage *= magicMultiplier; + // Fire hit event (may cancel or modify damage) if (!worldObj.isRemote) { float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, projectile, damage); @@ -419,6 +434,24 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { float damage = amount * barrierData.meleeDamageMultiplier; + // Apply magic interactions from attacker's magic vs barrier's magic + Entity attacker = source.getEntity(); + if (attacker != null) { + MagicData attackerMagic = null; + if (attacker instanceof EntityPlayer) { + PlayerData data = PlayerData.get((EntityPlayer) attacker); + if (data != null) attackerMagic = data.magicData; + } else if (attacker instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) attacker; + if (npc.stats != null) attackerMagic = npc.stats.magicData; + } + if (attackerMagic != null) { + float magicMultiplier = AttributeAttackUtil.calculateMagicInteractionMultiplier( + attackerMagic, this.magicData); + damage *= magicMultiplier; + } + } + // Fire hit event with null projectile (melee hit) float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, null, damage); if (eventDamage < 0) return false; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java index d20975b7d..b23f1f269 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java @@ -2,16 +2,28 @@ package kamkeel.npcs.entity; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.EnergyController; +import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData; +import kamkeel.npcs.util.AttributeAttackUtil; import kamkeel.npcs.util.CNPCDebug; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; import net.minecraft.util.MathHelper; import net.minecraft.world.World; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; /** - * Short-lived visual explosion entity for energy projectile impacts. - * This is render-only and never applies damage itself. + * Short-lived explosion entity for energy projectile impacts and standalone API use. + * Renders an expanding voxel shell. When damage is enabled, applies area damage on the first tick. */ public class EntityEnergyExplosion extends EntityEnergyAbility { @@ -21,12 +33,23 @@ public class EntityEnergyExplosion extends EntityEnergyAbility { private float renderRadius = 0.0f; private long renderSeed = 0L; + // ==================== DAMAGE (for standalone API use) ==================== + private float damage = 0f; + private float knockback = 0f; + private float knockbackUp = 0.1f; + private float damageFalloff = 0.5f; + private boolean damageEnabled = false; + private boolean hasDamaged = false; + public EntityEnergyExplosion(World world) { super(world); this.noClip = true; this.setSize(0.1f, 0.1f); } + /** + * Constructor for projectile-spawned explosions (render-only, no damage). + */ public EntityEnergyExplosion(World world, EntityEnergyProjectile source, float radius) { this(world); @@ -43,6 +66,19 @@ public class EntityEnergyExplosion extends EntityEnergyAbility { this.renderSeed = world != null ? world.rand.nextLong() : 0L; } + /** + * Constructor for standalone API-spawned explosions. + */ + public EntityEnergyExplosion(World world, Entity owner, double x, double y, double z, float radius) { + this(world); + if (owner != null) { + this.ownerEntityId = owner.getEntityId(); + } + this.setPosition(x, y, z); + setExplosionRadius(radius); + this.renderSeed = world != null ? world.rand.nextLong() : 0L; + } + @Override protected void entityInit() { super.entityInit(); @@ -58,6 +94,12 @@ public class EntityEnergyExplosion extends EntityEnergyAbility { float eased = 1.0f - (1.0f - progress) * (1.0f - progress); this.renderRadius = maxRadius * eased; + // Apply damage on first server tick + if (damageEnabled && !hasDamaged && !worldObj.isRemote && ticksExisted >= 1) { + applyExplosionDamage(); + hasDamaged = true; + } + // Debug logging { boolean isClient = worldObj.isRemote; @@ -74,12 +116,110 @@ public class EntityEnergyExplosion extends EntityEnergyAbility { } } + // ==================== DAMAGE LOGIC ==================== + + private void applyExplosionDamage() { + if (damage <= 0 || maxRadius <= 0) return; + + Entity owner = getOwnerEntity(); + double explosionRadSq = maxRadius * maxRadius; + + AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox( + posX - maxRadius, posY - maxRadius, posZ - maxRadius, + posX + maxRadius, posY + maxRadius, posZ + maxRadius + ); + + @SuppressWarnings("unchecked") + List targets = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox); + + for (EntityLivingBase target : targets) { + if (target == owner) continue; + if (target.boundingBox == null) continue; + + double closestX = Math.max(target.boundingBox.minX, Math.min(posX, target.boundingBox.maxX)); + double closestY = Math.max(target.boundingBox.minY, Math.min(posY, target.boundingBox.maxY)); + double closestZ = Math.max(target.boundingBox.minZ, Math.min(posZ, target.boundingBox.maxZ)); + double dx = closestX - posX; + double dy = closestY - posY; + double dz = closestZ - posZ; + double distSq = dx * dx + dy * dy + dz * dz; + if (distSq > explosionRadSq) continue; + + float falloff = 1.0f; + if (damageFalloff != 0.0f && distSq > 0.0D) { + float dist = (float) Math.sqrt(distSq); + falloff = 1.0f - (dist / maxRadius) * damageFalloff; + } + + float finalDmg = damage * falloff; + float finalKb = knockback * falloff; + + // Route through EnergyController for addon integration (e.g. DBC) + boolean handled = false; + if (customDamageData != null && owner instanceof EntityLivingBase) { + double kbDirX = target.posX - posX; + double kbDirZ = target.posZ - posZ; + handled = EnergyController.Instance.fireOnEnergyDamage( + this, (EntityLivingBase) owner, target, + finalDmg, finalKb, knockbackUp, kbDirX, kbDirZ, 1.0f, customDamageData); + } + + int prevHurtResist = Ability.clearHurtResistanceIfNeeded(target, ignoreIFrames); + try { + if (!handled) { + float dmgToApply = finalDmg; + + // Magic pipeline + if (this.magicData != null && !this.magicData.isEmpty() && owner instanceof EntityLivingBase) { + dmgToApply = AttributeAttackUtil.calculateAbilityDamage( + (EntityLivingBase) owner, target, finalDmg, this.magicData); + } + + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmgToApply); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmgToApply); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmgToApply); + } else { + target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmgToApply); + } + } + } finally { + Ability.restoreHurtResistanceIfNeeded(target, ignoreIFrames, prevHurtResist); + } + + // Apply knockback + if (finalKb > 0) { + double kbDx = target.posX - posX; + double kbDz = target.posZ - posZ; + double len = Math.sqrt(kbDx * kbDx + kbDz * kbDz); + if (len > 0) { + target.addVelocity((kbDx / len) * finalKb * 0.5, knockbackUp, (kbDz / len) * finalKb * 0.5); + target.velocityChanged = true; + } + } + } + + worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f); + } + + // ==================== RADIUS / DURATION ==================== + public void setExplosionRadius(float radius) { this.maxRadius = Math.max(0.5f, sanitize(radius, 2.0f, MAX_ENTITY_RADIUS)); this.durationTicks = Math.max(6, Math.min(18, (int) (6 + this.maxRadius * 1.8f))); this.setSize(this.maxRadius * 2.0f, this.maxRadius * 2.0f); } + public float getExplosionRadius() { + return maxRadius; + } + + public int getDurationTicks() { + return durationTicks; + } + public float getInterpolatedRadius(float partialTicks) { return this.prevRenderRadius + (this.renderRadius - this.prevRenderRadius) * partialTicks; } @@ -94,6 +234,47 @@ public class EntityEnergyExplosion extends EntityEnergyAbility { return renderSeed; } + // ==================== DAMAGE GETTERS/SETTERS ==================== + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = Math.max(0, damage); + this.damageEnabled = this.damage > 0; + } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = Math.max(0, knockback); + } + + public float getKnockbackUp() { + return knockbackUp; + } + + public void setKnockbackUp(float knockbackUp) { + this.knockbackUp = knockbackUp; + } + + public float getDamageFalloff() { + return damageFalloff; + } + + public void setDamageFalloff(float falloff) { + this.damageFalloff = MathHelper.clamp_float(falloff, 0.0f, 1.0f); + } + + public boolean isDamageEnabled() { + return damageEnabled; + } + + // ==================== RENDER / COLLISION ==================== + @Override @SideOnly(Side.CLIENT) public boolean isInRangeToRenderDist(double distance) { @@ -106,6 +287,8 @@ public class EntityEnergyExplosion extends EntityEnergyAbility { return false; } + // ==================== NBT ==================== + @Override protected void readEntityFromNBT(NBTTagCompound nbt) { // Intentionally empty - transient visual entity. @@ -125,6 +308,12 @@ public class EntityEnergyExplosion extends EntityEnergyAbility { nbt.setFloat("ExplosionRadius", maxRadius); nbt.setInteger("ExplosionDuration", durationTicks); nbt.setLong("ExplosionSeed", renderSeed); + if (damageEnabled) { + nbt.setFloat("Damage", damage); + nbt.setFloat("Knockback", knockback); + nbt.setFloat("KnockbackUp", knockbackUp); + nbt.setFloat("DamageFalloff", damageFalloff); + } } @Override @@ -146,5 +335,12 @@ public class EntityEnergyExplosion extends EntityEnergyAbility { this.renderSeed = nbt.hasKey("ExplosionSeed") ? nbt.getLong("ExplosionSeed") : 0L; this.renderRadius = 0.0f; this.prevRenderRadius = 0.0f; + if (nbt.hasKey("Damage")) { + this.damage = nbt.getFloat("Damage"); + this.knockback = nbt.getFloat("Knockback"); + this.knockbackUp = nbt.getFloat("KnockbackUp"); + this.damageFalloff = nbt.getFloat("DamageFalloff"); + this.damageEnabled = this.damage > 0; + } } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java index 6a0ac0db5..eab43440f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java @@ -2,6 +2,8 @@ package kamkeel.npcs.entity; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.EnergyController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; @@ -217,12 +219,32 @@ public class EntityEnergyPanel extends EntityEnergyBarrier { if (panelData.launchDamage > 0) { int previousHurtResistantTime = Ability.clearHurtResistanceIfNeeded(target, ignoreIFrames); try { - if (owner instanceof EntityNPCInterface) { - target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), panelData.launchDamage); - } else if (owner instanceof EntityPlayer) { - target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), panelData.launchDamage); - } else { - target.attackEntityFrom(DamageSource.generic, panelData.launchDamage); + // Route through ability extender (e.g. DBC Addon damage scaling) + boolean handled = false; + if (sourceAbility != null && owner instanceof EntityLivingBase) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + handled = AbilityController.Instance.fireOnAbilityDamage( + sourceAbility, (EntityLivingBase) owner, target, + panelData.launchDamage, panelData.launchKnockback, 0.0f, dx, dz, 1.0f); + } + // Fallback: route through EnergyController for script-created entities with custom damage data + if (!handled && customDamageData != null && owner instanceof EntityLivingBase) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + handled = EnergyController.Instance.fireOnEnergyDamage( + this, (EntityLivingBase) owner, target, + panelData.launchDamage, panelData.launchKnockback, 0.0f, dx, dz, 1.0f, customDamageData); + } + + if (!handled) { + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), panelData.launchDamage); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), panelData.launchDamage); + } else { + target.attackEntityFrom(DamageSource.generic, panelData.launchDamage); + } } } finally { Ability.restoreHurtResistanceIfNeeded(target, ignoreIFrames, previousHurtResistantTime); @@ -347,8 +369,25 @@ public class EntityEnergyPanel extends EntityEnergyBarrier { } } - // Test the UPCOMING movement so the barrier intercepts before entity - // collision runs in updateProjectile(). + if (mode == PanelMode.LAUNCHED) { + // For moving panels, use RELATIVE motion in the panel's reference frame. + // This correctly captures the combined closing speed when both objects + // are moving toward each other, preventing phase-through at high speeds. + double relMotionX = projectile.motionX - this.motionX; + double relMotionY = projectile.motionY - this.motionY; + double relMotionZ = projectile.motionZ - this.motionZ; + + double nextX = projectile.posX + relMotionX; + double nextY = projectile.posY + relMotionY; + double nextZ = projectile.posZ + relMotionZ; + + return isIncomingRay( + nextX, nextY, nextZ, + projectile.posX, projectile.posY, projectile.posZ, + projectile.getOwnerEntityId()); + } + + // Non-moving panels: standard test using projectile's absolute motion double nextX = projectile.posX + projectile.motionX; double nextY = projectile.posY + projectile.motionY; double nextZ = projectile.posZ + projectile.motionZ; @@ -366,6 +405,16 @@ public class EntityEnergyPanel extends EntityEnergyBarrier { double prevPosX, double prevPosY, double prevPosZ, int ownerEntityId) { + if (mode == PanelMode.LAUNCHED) { + // Use relative motion for moving panels + double relMotionX = motionX - this.motionX; + double relMotionY = motionY - this.motionY; + double relMotionZ = motionZ - this.motionZ; + double adjX = prevPosX + relMotionX; + double adjY = prevPosY + relMotionY; + double adjZ = prevPosZ + relMotionZ; + return isIncomingRay(adjX, adjY, adjZ, prevPosX, prevPosY, prevPosZ, ownerEntityId); + } return isIncomingRay(posX, posY, posZ, prevPosX, prevPosY, prevPosZ, ownerEntityId); } @@ -642,6 +691,15 @@ public class EntityEnergyPanel extends EntityEnergyBarrier { return panelData; } + public void setPanelYaw(float yaw) { + this.panelYaw = yaw; + this.prevPanelYaw = yaw; + } + + public void setMode(PanelMode mode) { + this.mode = mode; + } + // ==================== NBT ==================== @Override diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index df3d320c0..4b55b82e0 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -3,6 +3,7 @@ package kamkeel.npcs.entity; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.EnergyController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityEnergyProjectile; import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect; @@ -17,6 +18,7 @@ import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData; import kamkeel.npcs.network.packets.data.energy.ProjectileReflectPacket; import kamkeel.npcs.network.packets.data.energyexplosion.EnergyExplosionSpawnPacket; import kamkeel.npcs.util.AnchorPointHelper; +import kamkeel.npcs.util.AttributeAttackUtil; import kamkeel.npcs.util.CNPCDebug; import net.minecraft.block.Block; import net.minecraft.block.material.Material; @@ -38,6 +40,7 @@ import noppes.npcs.EventHooks; import noppes.npcs.NpcDamageSource; import noppes.npcs.api.entity.IEntity; import noppes.npcs.config.ConfigEnergy; +import noppes.npcs.config.ConfigMain; import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -926,19 +929,36 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { sourceAbility, (EntityLivingBase) owner, target, dmg, kb, kbUp, dx, dz, damageMultiplier); } + // Fallback: route through EnergyController for script-created entities with custom damage data + if (!handled && customDamageData != null && owner instanceof EntityLivingBase) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f; + handled = EnergyController.Instance.fireOnEnergyDamage( + this, (EntityLivingBase) owner, target, + dmg, kb, kbUp, dx, dz, damageMultiplier, customDamageData); + } int previousHurtResistantTime = Ability.clearHurtResistanceIfNeeded(target, ignoreIFrames); try { if (!handled) { + float finalDmg = dmg; + + // Apply magic pipeline for player and NPC casters + if (this.magicData != null && !this.magicData.isEmpty() && owner instanceof EntityLivingBase) { + finalDmg = AttributeAttackUtil.calculateAbilityDamage( + (EntityLivingBase) owner, target, dmg, this.magicData); + } + // Default damage path if (owner instanceof EntityNPCInterface) { - defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); + defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), finalDmg); } else if (owner instanceof EntityPlayer) { - defaultDamageApplied = target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); + defaultDamageApplied = target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), finalDmg); } else if (owner instanceof EntityLivingBase) { - defaultDamageApplied = target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); + defaultDamageApplied = target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), finalDmg); } else { - defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); + defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", null), finalDmg); } } } finally { diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityEnergySweeper.java index 1b4992152..d8596cf08 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergySweeper.java @@ -2,6 +2,8 @@ package kamkeel.npcs.entity; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.EnergyController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; import kamkeel.npcs.util.CNPCDebug; @@ -312,14 +314,34 @@ public class EntityEnergySweeper extends EntityEnergyAbility { boolean ignoreIFrames = isIgnoreIFrames(); int previousHurtResistantTime = Ability.clearHurtResistanceIfNeeded(target, ignoreIFrames); try { - if (owner instanceof EntityNPCInterface) { - target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), damage); - } else if (owner instanceof EntityPlayer) { - target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), damage); - } else if (owner instanceof EntityLivingBase) { - target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), damage); - } else { - target.attackEntityFrom(new NpcDamageSource("npc_ability", null), damage); + // Route through ability extender (e.g. DBC Addon damage scaling) + boolean handled = false; + if (sourceAbility != null && owner instanceof EntityLivingBase) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + handled = AbilityController.Instance.fireOnAbilityDamage( + sourceAbility, (EntityLivingBase) owner, target, + damage, 0.0f, 0.2f, dx, dz, 1.0f); + } + // Fallback: route through EnergyController for script-created entities with custom damage data + if (!handled && customDamageData != null && owner instanceof EntityLivingBase) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + handled = EnergyController.Instance.fireOnEnergyDamage( + this, (EntityLivingBase) owner, target, + damage, 0.0f, 0.2f, dx, dz, 1.0f, customDamageData); + } + + if (!handled) { + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), damage); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), damage); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), damage); + } else { + target.attackEntityFrom(new NpcDamageSource("npc_ability", null), damage); + } } } finally { Ability.restoreHurtResistanceIfNeeded(target, ignoreIFrames, previousHurtResistantTime); @@ -381,6 +403,24 @@ public class EntityEnergySweeper extends EntityEnergyAbility { return baseYaw; } + // ==================== PUBLIC SETTERS (for EnergyController / scripting API) ==================== + + public void setBeamLength(float length) { this.beamLength = length; } + public void setBeamWidth(float width) { this.beamWidth = width; } + public void setBeamHeight(float height) { this.beamHeight = height; } + public float getDamage() { return damage; } + public void setDamage(float damage) { this.damage = damage; } + public int getDamageInterval() { return damageInterval; } + public void setDamageInterval(int interval) { this.damageInterval = interval; } + public boolean isPiercing() { return piercing; } + public void setPiercing(boolean piercing) { this.piercing = piercing; } + public float getSweepSpeed() { return sweepSpeed; } + public void setSweepSpeed(float speed) { this.sweepSpeed = speed; } + public int getNumberOfRotations() { return numberOfRotations; } + public void setNumberOfRotations(int rotations) { this.numberOfRotations = rotations; } + public boolean isLockOnTarget() { return lockOnTarget; } + public void setLockOnTarget(boolean lock) { this.lockOnTarget = lock; } + // ==================== NBT ==================== @Override diff --git a/src/main/java/kamkeel/npcs/network/PacketUtil.java b/src/main/java/kamkeel/npcs/network/PacketUtil.java index 9235dc1f7..0db53b7fc 100644 --- a/src/main/java/kamkeel/npcs/network/PacketUtil.java +++ b/src/main/java/kamkeel/npcs/network/PacketUtil.java @@ -21,10 +21,37 @@ import noppes.npcs.items.ItemNpcTool; import java.io.IOException; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; public class PacketUtil { + // ==================== SCRIPT SESSION TOKEN SYSTEM ==================== + // Prevents saving script data before the client has received it from the server. + // Each GET generates a token sent to the client; each SAVE must echo it back. + + private static final Map scriptSessionTokens = new ConcurrentHashMap<>(); + + public static String createScriptSession(EntityPlayerMP player) { + String token = Long.toHexString(ThreadLocalRandom.current().nextLong()); + scriptSessionTokens.put(player.getUniqueID(), token); + return token; + } + + public static boolean verifyScriptSession(EntityPlayer player, String token) { + if (token == null || token.isEmpty()) + return false; + String expected = scriptSessionTokens.get(player.getUniqueID()); + return expected != null && expected.equals(token); + } + + public static void clearScriptSession(EntityPlayer player) { + scriptSessionTokens.remove(player.getUniqueID()); + } + public static boolean verifyItemPacket(String name, EnumItemPacketType type, EntityPlayer player) { if (player == null) return false; @@ -216,11 +243,14 @@ public class PacketUtil { } public static void getScripts(IScriptHandler data, EntityPlayerMP player) { + String token = createScriptSession(player); + NBTTagCompound compound = new NBTTagCompound(); compound.setBoolean("ScriptEnabled", data.getEnabled()); compound.setString("ScriptLanguage", data.getLanguage()); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(data.getConsoleText())); + compound.setString("ScriptSessionToken", token); GuiDataPacket.sendGuiData(player, compound); List containers = data.getScripts(); for (int i = 0; i < containers.size(); i++) { @@ -237,9 +267,18 @@ public class PacketUtil { GuiDataPacket.sendGuiData(player, loadComplete); } - public static void saveScripts(IScriptHandler data, ByteBuf buffer) throws IOException { + public static boolean saveScripts(IScriptHandler data, ByteBuf buffer, EntityPlayer player) throws IOException { int tab = buffer.readInt(); int totalScripts = buffer.readInt(); + NBTTagCompound compound = ByteBufUtils.readNBT(buffer); + + // Verify script session token to prevent saving unloaded defaults + String token = compound.getString("ScriptSessionToken"); + if (!verifyScriptSession(player, token)) { + LogWriter.error(String.format("Rejected script save from %s: session not verified (data not loaded)", player.getCommandSenderName())); + return false; + } + if (totalScripts == 0) { data.getScripts().clear(); } @@ -250,12 +289,10 @@ public class PacketUtil { } else while (data.getScripts().size() < totalScripts) { data.getScripts().add(new ScriptContainer(data)); } - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); // Use factory method to create correct script unit type based on NBT - IScriptUnit script = IScriptUnit.createFromNBT(tabCompound, data); + IScriptUnit script = IScriptUnit.createFromNBT(compound, data); data.getScripts().set(tab, script); } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); data.setLanguage(compound.getString("ScriptLanguage")); if (!ScriptController.Instance.languages.containsKey(data.getLanguage())) { if (!ScriptController.Instance.languages.isEmpty()) { @@ -266,5 +303,6 @@ public class PacketUtil { } data.setEnabled(compound.getBoolean("ScriptEnabled")); } + return true; } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java index ec3f6ff20..eceb04286 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java @@ -87,14 +87,20 @@ public final class AbilityScriptPacket extends AbstractPacket { String id = ByteBufUtils.readUTF8String(in); Ability ability = AbilityController.Instance.resolveAbility(id); - if (ability == null) + if (ability == null) { + // Ability not yet saved — send default empty script data so the GUI is usable + if (requestedAction == Action.GET) { + PacketUtil.getScripts((IScriptHandler) new AbilityScript(id), (EntityPlayerMP) player); + } return; + } AbilityScript data = ability.getOrCreateScriptHandler(); if (requestedAction == Action.GET) { PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); } else { - data.saveScript(in); + if (!data.saveScript(in, player)) + return; // Persist to disk AbilityController.Instance.saveCustomAbility(ability); if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java index ce94c98f9..ac2303628 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java @@ -7,6 +7,7 @@ import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; import kamkeel.npcs.network.enums.EnumRequestPacket; import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import kamkeel.npcs.util.ByteBufUtils; @@ -15,6 +16,7 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.tileentity.TileEntity; import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.LogWriter; import noppes.npcs.blocks.tiles.TileScripted; import noppes.npcs.config.ConfigScript; import noppes.npcs.controllers.ScriptController; @@ -81,8 +83,10 @@ public final class BlockScriptPacket extends AbstractPacket { TileEntity tile = player.worldObj.getTileEntity(in.readInt(), in.readInt(), in.readInt()); if (!(tile instanceof TileScripted)) return; + String token = PacketUtil.createScriptSession((EntityPlayerMP) player); NBTTagCompound compound = ((TileScripted) tile).getNBT(new NBTTagCompound()); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); + compound.setString("ScriptSessionToken", token); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); NBTTagCompound loadComplete = new NBTTagCompound(); @@ -95,8 +99,14 @@ public final class BlockScriptPacket extends AbstractPacket { TileEntity tile = player.worldObj.getTileEntity(in.readInt(), in.readInt(), in.readInt()); if (!(tile instanceof TileScripted)) return; + NBTTagCompound compound = ByteBufUtils.readNBT(in); + String token = compound.getString("ScriptSessionToken"); + if (!PacketUtil.verifyScriptSession(player, token)) { + LogWriter.error(String.format("Rejected block script save from %s: session not verified", player.getCommandSenderName())); + return; + } TileScripted script = (TileScripted) tile; - script.setNBT(ByteBufUtils.readNBT(in)); + script.setNBT(compound); script.lastInited = -1; } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java index 45640bbfb..a8c455207 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java @@ -87,14 +87,20 @@ public final class ChainedAbilityScriptPacket extends AbstractPacket { String id = ByteBufUtils.readUTF8String(in); ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(id); - if (chain == null) + if (chain == null) { + // Chain not yet saved — send default empty script data so the GUI is usable + if (requestedAction == Action.GET) { + PacketUtil.getScripts((IScriptHandler) new ChainedAbilityScript(id), (EntityPlayerMP) player); + } return; + } ChainedAbilityScript data = chain.getOrCreateScriptHandler(); if (requestedAction == Action.GET) { PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); } else { - data.saveScript(in); + if (!data.saveScript(in, player)) + return; // Persist to disk AbilityController.Instance.saveChainedAbility(chain); if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/EffectScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/EffectScriptPacket.java index 8171f085d..705e191be 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/EffectScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/EffectScriptPacket.java @@ -90,7 +90,8 @@ public final class EffectScriptPacket extends AbstractPacket { if (requestedAction == Action.GET) { PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); } else { - data.saveScript(in); + if (!data.saveScript(in, player)) + return; if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { LogWriter.script(String.format("[%s] (Player) %s SAVED EFFECT %s [%s]", "EFFECT SCRIPTS", player.getCommandSenderName(), effect.getName())); } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/EventScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/EventScriptPacket.java index 69f660a2c..2d63c5bbd 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/EventScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/EventScriptPacket.java @@ -89,7 +89,8 @@ public final class EventScriptPacket extends AbstractPacket { if (requestedAction == Action.GET) { PacketUtil.getScripts(data, (EntityPlayerMP) player); } else { - PacketUtil.saveScripts(data, in); + if (!PacketUtil.saveScripts(data, in, player)) + return; npc.updateAI = true; npc.script.hasInited = false; if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ForgeScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ForgeScriptPacket.java index 86584a766..e99abb702 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ForgeScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ForgeScriptPacket.java @@ -84,7 +84,8 @@ public final class ForgeScriptPacket extends AbstractPacket { PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); } else { int tab = in.getInt(in.readerIndex()); - PacketUtil.saveScripts(data, in); + if (!PacketUtil.saveScripts(data, in, player)) + return; ScriptController.Instance.lastForgeUpdate = System.currentTimeMillis(); if (tab == -1) ScriptController.Instance.saveForgeScriptsSync(); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/GlobalNPCScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/GlobalNPCScriptPacket.java index ae9e44a3d..9f1237638 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/GlobalNPCScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/GlobalNPCScriptPacket.java @@ -84,7 +84,8 @@ public final class GlobalNPCScriptPacket extends AbstractPacket { PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); } else { int tab = in.getInt(in.readerIndex()); - PacketUtil.saveScripts(data, in); + if (!PacketUtil.saveScripts(data, in, player)) + return; ScriptController.Instance.lastGlobalNpcUpdate = System.currentTimeMillis(); if (tab == -1) ScriptController.Instance.saveGlobalScriptsSync(); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/NPCScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/NPCScriptPacket.java index 7d5e0d3ed..35633cb07 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/NPCScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/NPCScriptPacket.java @@ -81,11 +81,19 @@ public final class NPCScriptPacket extends AbstractPacket { Action requestedAction = Action.values()[in.readInt()]; if (requestedAction == Action.GET) { + String token = PacketUtil.createScriptSession((EntityPlayerMP) player); NBTTagCompound compound = npc.script.writeToNBT(new NBTTagCompound()); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); + compound.setString("ScriptSessionToken", token); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); } else { - npc.script.readFromNBT(ByteBufUtils.readNBT(in)); + NBTTagCompound compound = ByteBufUtils.readNBT(in); + String token = compound.getString("ScriptSessionToken"); + if (!PacketUtil.verifyScriptSession(player, token)) { + LogWriter.error(String.format("Rejected NPC script save from %s: session not verified", player.getCommandSenderName())); + return; + } + npc.script.readFromNBT(compound); npc.updateAI = true; npc.script.hasInited = false; if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/PlayerScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/PlayerScriptPacket.java index 242e7d092..30d999815 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/PlayerScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/PlayerScriptPacket.java @@ -84,7 +84,8 @@ public final class PlayerScriptPacket extends AbstractPacket { PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); } else { int tab = in.getInt(in.readerIndex()); - PacketUtil.saveScripts(data, in); + if (!PacketUtil.saveScripts(data, in, player)) + return; ScriptController.Instance.lastPlayerUpdate = System.currentTimeMillis(); if (tab == -1) ScriptController.Instance.savePlayerScriptsSync(); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/RecipeScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/RecipeScriptPacket.java index 3627631eb..36ab65201 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/RecipeScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/RecipeScriptPacket.java @@ -102,7 +102,8 @@ public final class RecipeScriptPacket extends AbstractPacket { if (action == Action.GET) { PacketUtil.getScripts(script, player); } else { - script.saveScript(in); + if (!script.saveScript(in, player)) + return; if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { // TODO: Log Recipe Script Saving } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java index 7d21242bd..a1ae83610 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java @@ -7,6 +7,7 @@ import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; import kamkeel.npcs.network.enums.EnumRequestPacket; import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import kamkeel.npcs.util.ByteBufUtils; @@ -14,6 +15,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.LogWriter; import noppes.npcs.config.ConfigScript; import noppes.npcs.controllers.ScriptController; import noppes.npcs.scripted.NpcAPI; @@ -72,8 +74,10 @@ public final class ItemScriptPacket extends AbstractPacket { if (requestedAction == Action.GET) { ScriptCustomItem iw = (ScriptCustomItem) NpcAPI.Instance().getIItemStack(player.getHeldItem()); iw.loadScriptData(); + String token = PacketUtil.createScriptSession((EntityPlayerMP) player); NBTTagCompound compound = iw.getMCNbt(); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); + compound.setString("ScriptSessionToken", token); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); NBTTagCompound loadComplete = new NBTTagCompound(); @@ -85,6 +89,11 @@ public final class ItemScriptPacket extends AbstractPacket { } NBTTagCompound compound = ByteBufUtils.readNBT(in); + String token = compound.getString("ScriptSessionToken"); + if (!PacketUtil.verifyScriptSession(player, token)) { + LogWriter.error(String.format("Rejected item script save from %s: session not verified", player.getCommandSenderName())); + return; + } ScriptCustomItem wrapper = (ScriptCustomItem) NpcAPI.Instance().getIItemStack(player.getHeldItem()); wrapper.setMCNbt(compound); wrapper.saveScriptData(); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java index faa14f4d1..a711aace2 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/LinkedItemScriptPacket.java @@ -90,7 +90,8 @@ public final class LinkedItemScriptPacket extends AbstractPacket { if (requestedAction == Action.GET) { PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); } else { - data.saveScript(in); + if (!data.saveScript(in, player)) + return; if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { LogWriter.script(String.format("[%s] (Player) %s SAVED LINKED ITEM [%s]", "LINKED ITEM SCRIPTS", player.getCommandSenderName(), linkedItem.getName())); } diff --git a/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java b/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java index f2fd8bf44..76f6fbffa 100644 --- a/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java +++ b/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java @@ -7,7 +7,9 @@ import noppes.npcs.config.ConfigMain; import noppes.npcs.controllers.MagicController; import noppes.npcs.controllers.data.Magic; import noppes.npcs.controllers.data.MagicEntry; +import noppes.npcs.controllers.data.MagicData; import noppes.npcs.controllers.data.PlayerData; +import net.minecraft.entity.EntityLivingBase; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashMap; @@ -20,10 +22,17 @@ import static kamkeel.npcs.controllers.AttributeController.getTracker; public class AttributeAttackUtil { private static final Random random = new Random(); + + /** + * Crit decision from the last LivingAttackEvent for vanilla/modded mob targets. + * Set in LivingAttackEvent, consumed in LivingHurtEvent. + * null = no roll occurred, true = critted, false = did not crit. + */ + public static Boolean lastAttackCritted = null; // --- Helper Classes & Methods --- // Container for allocation results. - private static class AllocationResult { + public static class AllocationResult { public Map allocation; public float leftover; @@ -37,7 +46,7 @@ public class AttributeAttackUtil { * Allocates physical damage into magic allocations based on MagicData splits. * Returns both a mapping (magic ID -> allocated damage) and any leftover physical damage. */ - private static AllocationResult allocateMagicDamage(float physicalDamage, Map magicData) { + public static AllocationResult allocateMagicDamage(float physicalDamage, Map magicData) { Map allocation = new HashMap<>(); float totalSplit = 0f; if (magicData != null) { @@ -57,7 +66,7 @@ public class AttributeAttackUtil { /** * Adds extra magic damage from attribute sources if attributes are enabled. */ - private static void addAttributeMagicDamage(Map allocation, + public static void addAttributeMagicDamage(Map allocation, Map attributeDamage, Map magicBoost) { if (!ConfigMain.AttributesEnabled) return; @@ -74,7 +83,7 @@ public class AttributeAttackUtil { * Applies magic interactions. For each magic in the allocation, multiplies its damage by (1 + bonus) * for each opposing magic the defender possesses. */ - private static void applyMagicInteractions(Map allocation, + public static void applyMagicInteractions(Map allocation, Set defenderMagicIDs, MagicController magicController) { for (Map.Entry entry : allocation.entrySet()) { @@ -97,7 +106,7 @@ public class AttributeAttackUtil { * Applies defender magic defenses if attributes are enabled. * Subtracts tracker-based magic defense (scaled by resistance) from each magic allocation. */ - private static float applyDefenderMagicDefense(Map allocation, + public static float applyDefenderMagicDefense(Map allocation, PlayerAttributeTracker defender, MagicController magicController) { if (!ConfigMain.AttributesEnabled) return 0f; @@ -168,10 +177,31 @@ public class AttributeAttackUtil { public static float applyCrit(float damage, PlayerAttributeTracker tracker) { if (ConfigMain.AttributesEnabled) { + if (rollCrit(tracker)) + damage = applyCritDamage(damage, tracker); + } + return damage; + } + + /** + * Rolls for a critical hit based on the tracker's CRITICAL_CHANCE attribute. + */ + public static boolean rollCrit(PlayerAttributeTracker tracker) { + if (ConfigMain.AttributesEnabled && tracker != null) { float critChance = tracker.getAttributeValue(CustomAttributes.CRITICAL_CHANCE); + return random.nextFloat() < (critChance / 100f); + } + return false; + } + + /** + * Applies crit damage unconditionally (no chance roll). + * Use when the crit decision was already made via {@link #rollCrit}. + */ + public static float applyCritDamage(float damage, PlayerAttributeTracker tracker) { + if (ConfigMain.AttributesEnabled && tracker != null) { float critBonus = tracker.getAttributeValue(CustomAttributes.CRITICAL_DAMAGE); - if (random.nextFloat() < (critChance / 100f)) - damage = (damage * (1 + (float) ConfigMain.AttributesCriticalBoost / 100f)) + critBonus; + damage = (damage * (1 + (float) ConfigMain.AttributesCriticalBoost / 100f)) + critBonus; } return damage; } @@ -256,4 +286,141 @@ public class AttributeAttackUtil { return mainDamage + gearDamage; } + + // --- Magic Interaction Utility --- + + /** + * Calculate magic interaction multiplier between attacker's magic types + * and defender's magic types. Used for barrier defense and entity-vs-entity interactions. + * + * @param attackerMagic Attacker's magic data (projectile or weapon) + * @param defenderMagic Defender's magic data (barrier or entity) + * @return Multiplier (1.0 = no interaction, >1.0 = bonus damage, <1.0 = resistance) + */ + public static float calculateMagicInteractionMultiplier(MagicData attackerMagic, MagicData defenderMagic) { + if (attackerMagic == null || defenderMagic == null) return 1.0f; + if (attackerMagic.isEmpty() || defenderMagic.isEmpty()) return 1.0f; + + MagicController mc = MagicController.getInstance(); + float multiplier = 1.0f; + + for (int attackMagicId : attackerMagic.getMagics().keySet()) { + Magic magic = mc.getMagic(attackMagicId); + if (magic == null || magic.interactions == null) continue; + for (Map.Entry interaction : magic.interactions.entrySet()) { + if (defenderMagic.getMagics().containsKey(interaction.getKey())) { + multiplier *= (1 + interaction.getValue()); + } + } + } + return multiplier; + } + + // --- Ability Damage Pipeline --- + + /** + * Apply gear magic boost percentages to the split-allocated magic damage. + * This multiplies each magic type's allocated damage by (1 + boost/100). + * Unlike {@link #addAttributeMagicDamage}, this does NOT add flat gear damage — + * it only applies the percentage multiplier from gear to the existing allocation. + */ + public static void applyMagicBoostToAllocation(Map allocation, Map magicBoost) { + if (!ConfigMain.AttributesEnabled) return; + for (Map.Entry entry : allocation.entrySet()) { + float boost = magicBoost.getOrDefault(entry.getKey(), 0f); + if (boost != 0f) { + entry.setValue(entry.getValue() * (1 + boost / 100f)); + } + } + } + + /** + * Apply gear magic boost percentages to barrier/dome health. + * Each magic type's split determines what portion of health is affected by its boost. + * E.g., Thunder [100%] dome + 50% Thunder gear boost = health * 1.5 + */ + public static float applyMagicBoostToHealth(EntityPlayer caster, MagicData magicData, float health) { + if (!ConfigMain.AttributesEnabled) return health; + if (magicData == null || magicData.isEmpty()) return health; + + PlayerAttributeTracker tracker = getTracker(caster); + if (tracker == null) return health; + + float multiplier = 1.0f; + for (Map.Entry entry : magicData.getMagics().entrySet()) { + float split = entry.getValue().split; + float boost = tracker.magicBoost.getOrDefault(entry.getKey(), 0f); + multiplier += split * (boost / 100f); + } + return health * multiplier; + } + + /** + * Apply the magic pipeline to ability damage. Works for both player and NPC casters. + * Abilities do NOT get main attack or critical boosts — they have their own scaling. + * This handles: + *
    + *
  • Magic allocation (splits from ability/caster magic data)
  • + *
  • Gear magic boost multipliers on split damage (player casters only, no flat gear damage)
  • + *
  • Magic interactions vs defender
  • + *
  • Defender magic defense/resistance (player defenders only)
  • + *
+ * If no magic data is present, damage passes through unchanged. + * + * @param caster Caster (player or NPC) + * @param target Target entity + * @param damage Base ability damage (already scaled by ability's own system) + * @param abilityMagic Resolved magic data (ability's own, or caster's fallback) + * @return Final damage after magic splits, boosts, interactions, and defense + */ + public static float calculateAbilityDamage(EntityLivingBase caster, EntityLivingBase target, + float damage, MagicData abilityMagic) { + if (abilityMagic == null || abilityMagic.isEmpty()) return damage; + + // 1. Allocate base damage into magic types using ability's splits + AllocationResult result = allocateMagicDamage(damage, abilityMagic.getMagics()); + float leftover = result.leftover; + + // 2. Apply gear magic boost multipliers to the split allocation (player casters only) + if (caster instanceof EntityPlayer && ConfigMain.AttributesEnabled) { + PlayerAttributeTracker tracker = getTracker((EntityPlayer) caster); + if (tracker != null) { + applyMagicBoostToAllocation(result.allocation, tracker.magicBoost); + } + } + + // 3. Magic interactions against target + MagicController magicController = MagicController.getInstance(); + Set defenderMagicIDs = null; + + if (target instanceof EntityPlayer) { + PlayerData defenderData = PlayerData.get((EntityPlayer) target); + if (defenderData != null) { + defenderMagicIDs = new HashSet<>(defenderData.magicData.getMagics().keySet()); + } + } else if (target instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) target; + if (npc.stats != null && npc.stats.magicData != null) { + defenderMagicIDs = new HashSet<>(npc.stats.magicData.getMagics().keySet()); + } + } + + if (defenderMagicIDs != null) { + applyMagicInteractions(result.allocation, defenderMagicIDs, magicController); + } + + // 4. Apply defender magic defense (player defenders with attributes only) + if (target instanceof EntityPlayer && ConfigMain.AttributesEnabled) { + PlayerAttributeTracker defenderTracker = getTracker((EntityPlayer) target); + if (defenderTracker != null) { + float adjustedMagic = applyDefenderMagicDefense(result.allocation, defenderTracker, magicController); + return leftover + adjustedMagic; + } + } + + // Sum magic damage + float magicTotal = 0f; + for (float val : result.allocation.values()) magicTotal += val; + return leftover + magicTotal; + } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index e56bf772d..fa6913455 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -4,6 +4,7 @@ import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityAction; import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.enums.RotationMode; import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; import kamkeel.npcs.controllers.data.ability.data.IAbilityAction; import kamkeel.npcs.controllers.data.ability.data.entry.AbilityToggleEntry; @@ -87,6 +88,7 @@ public class DataAbilities extends AbstractDataAbilities { */ private transient boolean hitScanActive = false; private transient EntityLivingBase hitScanTarget = null; + private transient float currentTrackDelay = 0; /** * Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 @@ -227,9 +229,19 @@ public class DataAbilities extends AbstractDataAbilities { protected void onPostPhaseTick(Ability ability, EntityLivingBase target) { // Update hit scan state - actual facing is deferred to applyRotationControl() // which runs AFTER super.onLivingUpdate() to override AI look helper - if (ability != null && ability.isExecuting() - && ability.isHitScanForCurrentPhase() && target != null) { - enableHitScan(target); + if (ability != null && ability.isExecuting() && target != null) { + if (ability.isHitScanForCurrentPhase()) { + enableHitScan(target, ability.getTrackDelay()); + } else { + // Release hit scan if it was previously active + if (hitScanActive) { + releaseRotationControl(); + } + // FREE mode: use AI look helper during ability execution since combat AI may not run + if (ability.getRotationMode() == RotationMode.FREE) { + npc.getLookHelper().setLookPositionWithEntity(target, 30.0F, 30.0F); + } + } } else if (hitScanActive) { releaseRotationControl(); } @@ -867,34 +879,74 @@ public class DataAbilities extends AbstractDataAbilities { /** * Enable hit scan tracking for the given target. */ - private void enableHitScan(EntityLivingBase target) { + private void enableHitScan(EntityLivingBase target, float trackDelay) { if (!hitScanActive) { hitScanActive = true; npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG); } hitScanTarget = target; + currentTrackDelay = trackDelay; } /** - * Snap the NPC to face the target instantly. + * Face the target with optional track delay. + * When trackDelay <= 0, snaps instantly (legacy behavior). + * When trackDelay > 0, lerps toward the target each tick. */ - private void faceTarget(EntityLivingBase target) { + private void faceTarget(EntityLivingBase target, float trackDelay) { double dx = target.posX - npc.posX; double dz = target.posZ - npc.posZ; double dy = (target.posY + target.getEyeHeight() * 0.5) - (npc.posY + npc.getEyeHeight()); double distXZ = Math.sqrt(dx * dx + dz * dz); - float yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); - float pitch = (float) (-(Math.atan2(dy, distXZ)) * 180.0 / Math.PI); + float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); + float targetPitch = (float) (-(Math.atan2(dy, distXZ)) * 180.0 / Math.PI); - npc.rotationYaw = yaw; - npc.rotationYawHead = yaw; - npc.renderYawOffset = yaw; - npc.rotationPitch = pitch; - npc.prevRotationYaw = yaw; - npc.prevRotationYawHead = yaw; - npc.prevRenderYawOffset = yaw; - npc.prevRotationPitch = pitch; + if (trackDelay <= 0) { + // Instant snap (legacy behavior) + npc.rotationYaw = targetYaw; + npc.rotationYawHead = targetYaw; + npc.renderYawOffset = targetYaw; + npc.rotationPitch = targetPitch; + npc.prevRotationYaw = targetYaw; + npc.prevRotationYawHead = targetYaw; + npc.prevRenderYawOffset = targetYaw; + npc.prevRotationPitch = targetPitch; + } else { + float lerpFactor = 1.0f / (1.0f + trackDelay); + + // Wrap yaw difference to [-180, 180] for correct interpolation + float yawDiff = wrapAngle(targetYaw - npc.rotationYawHead); + float pitchDiff = targetPitch - npc.rotationPitch; + + float newYaw = npc.rotationYawHead + yawDiff * lerpFactor; + float newPitch = npc.rotationPitch + pitchDiff * lerpFactor; + + // Set prev to current before updating (for smooth client interpolation) + npc.prevRotationYaw = npc.rotationYaw; + npc.prevRotationYawHead = npc.rotationYawHead; + npc.prevRenderYawOffset = npc.renderYawOffset; + npc.prevRotationPitch = npc.rotationPitch; + + npc.rotationYaw = newYaw; + npc.rotationYawHead = newYaw; + npc.renderYawOffset = newYaw; + npc.rotationPitch = newPitch; + } + } + + /** + * Snap the NPC to face the target instantly (no delay). + */ + private void faceTarget(EntityLivingBase target) { + faceTarget(target, 0); + } + + private static float wrapAngle(float angle) { + angle = angle % 360.0f; + if (angle >= 180.0f) angle -= 360.0f; + if (angle < -180.0f) angle += 360.0f; + return angle; } /** @@ -922,7 +974,7 @@ public class DataAbilities extends AbstractDataAbilities { // Server: apply the appropriate rotation if (hitScanActive && hitScanTarget != null && !hitScanTarget.isDead) { - faceTarget(hitScanTarget); + faceTarget(hitScanTarget, currentTrackDelay); } else if (rotationLocked) { npc.rotationYaw = lockedYaw; npc.rotationYawHead = lockedYawHead; diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 27db9ae4e..3133fb43c 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -8,6 +8,7 @@ import cpw.mods.fml.relauncher.Side; import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.AttributeController; +import kamkeel.npcs.controllers.data.attribute.tracker.PlayerAttributeTracker; import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.type.AbilityCounter; @@ -623,8 +624,21 @@ public class ScriptPlayerEventHandler { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.source.getEntity()); float attackAmount = event.ammount; - if (ConfigMain.AttributesEnabled && !DBCAddon.IsAvailable()) - attackAmount = AttributeAttackUtil.calculateOutgoing((EntityPlayer) event.source.getEntity(), attackAmount); + AttributeAttackUtil.lastAttackCritted = null; + if (ConfigMain.AttributesEnabled && !DBCAddon.IsAvailable()) { + EntityPlayer attackPlayer = (EntityPlayer) event.source.getEntity(); + attackAmount = AttributeAttackUtil.calculateOutgoing(attackPlayer, attackAmount); + // For vanilla/modded mob targets, roll crit once and store for LivingHurtEvent + if (!(event.entityLiving instanceof EntityPlayerMP) && !(event.entityLiving instanceof EntityNPCInterface)) { + PlayerAttributeTracker tracker = AttributeController.getTracker(attackPlayer); + if (AttributeAttackUtil.rollCrit(tracker)) { + AttributeAttackUtil.lastAttackCritted = true; + attackAmount = AttributeAttackUtil.applyCritDamage(attackAmount, tracker); + } else { + AttributeAttackUtil.lastAttackCritted = false; + } + } + } noppes.npcs.scripted.event.player.PlayerEvent.AttackEvent pevent1 = new noppes.npcs.scripted.event.player.PlayerEvent.AttackEvent((IPlayer) NpcAPI.Instance().getIEntity((EntityPlayer) event.source.getEntity()), event.entityLiving, attackAmount, event.source); cancel = cancel || EventHooks.onPlayerAttack(handler, pevent1); @@ -663,6 +677,15 @@ public class ScriptPlayerEventHandler { event.ammount = AttributeAttackUtil.calculateDamagePlayerToPlayer((EntityPlayer) source, (EntityPlayer) event.entityLiving, event.ammount); } else if (!(event.entityLiving instanceof EntityNPCInterface) && source instanceof EntityPlayer) { event.ammount = AttributeAttackUtil.calculateOutgoing((EntityPlayer) source, event.ammount); + if (AttributeAttackUtil.lastAttackCritted != null) { + // Use the crit decision from LivingAttackEvent + if (AttributeAttackUtil.lastAttackCritted) + event.ammount = AttributeAttackUtil.applyCritDamage(event.ammount, AttributeController.getTracker((EntityPlayer) source)); + AttributeAttackUtil.lastAttackCritted = null; + } else { + // Fallback: roll independently (no preceding LivingAttackEvent) + event.ammount = AttributeAttackUtil.applyCrit(event.ammount, AttributeController.getTracker((EntityPlayer) source)); + } } } diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index 64dbb767b..22e0d4f2f 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -188,8 +188,11 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler, ISc } @Override - public void sync() { - sendSavePacket(-1, getScripts().size(), getNBT(new NBTTagCompound())); + public void sync(String sessionToken) { + NBTTagCompound compound = getNBT(new NBTTagCompound()); + if (sessionToken != null && !sessionToken.isEmpty()) + compound.setString("ScriptSessionToken", sessionToken); + sendSavePacket(-1, getScripts().size(), compound); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index d3b1fd237..13c88f366 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -369,6 +369,8 @@ public class GuiScript extends GuiScriptInterface { @Override public void setGuiData(NBTTagCompound compound) { script.readFromNBT(compound); + if (compound.hasKey("ScriptSessionToken")) + scriptSessionToken = compound.getString("ScriptSessionToken"); loadLanguagesData(compound); loaded = true; } @@ -377,7 +379,10 @@ public class GuiScript extends GuiScriptInterface { public void save() { if (loaded) { setScript(); - NPCScriptPacket.Save(script.writeToNBT(new NBTTagCompound())); + NBTTagCompound compound = script.writeToNBT(new NBTTagCompound()); + if (scriptSessionToken != null) + compound.setString("ScriptSessionToken", scriptSessionToken); + NPCScriptPacket.Save(compound); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java new file mode 100644 index 000000000..8543ad208 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java @@ -0,0 +1,250 @@ +package noppes.npcs.client.gui.advanced; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicData; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Sub-GUI for editing magic splits on an Ability. + * Dual-scroll layout: available magics on left, assigned magics on right. + * Only split values (no flat damage) are editable. + */ +public class SubGuiAbilityMagic extends SubGuiInterface implements ICustomScrollListener, ITextfieldListener { + + public MagicData magicData; + + private GuiCustomScroll availableScroll; + private GuiCustomScroll assignedScroll; + + /** Maps display name → magic ID for all available magics. */ + private final HashMap allMagic = new HashMap<>(); + /** Maps formatted display string (right scroll) → base display name. */ + private final HashMap assignedDisplayToName = new HashMap<>(); + private String search = ""; + + private GuiNpcTextField splitField; + + public SubGuiAbilityMagic(MagicData data) { + this.magicData = data; + setBackground("menubg.png"); + xSize = 420; + ySize = 222; + + // Load all available magics from the controller + MagicController controller = MagicController.getInstance(); + for (Map.Entry entry : controller.magics.entrySet()) { + allMagic.put(entry.getValue().getDisplayName(), entry.getKey()); + } + } + + @Override + public void initGui() { + super.initGui(); + + int scrollW = 150; + int scrollH = 140; + int leftX = guiLeft + 10; + int rightX = guiLeft + 250; + int topY = guiTop + 20; + + // Title + addLabel(new GuiNpcLabel(0, "ability.magic.editor", guiLeft + 10, guiTop + 5)); + + // Left scroll: available magics + if (availableScroll == null) { + availableScroll = new GuiCustomScroll(this, 0); + availableScroll.setSize(scrollW, scrollH); + } + availableScroll.guiLeft = leftX; + availableScroll.guiTop = topY; + availableScroll.setList(getSearchList()); + addScroll(availableScroll); + + // Search field below left scroll + addTextField(new GuiNpcTextField(4, this, fontRendererObj, leftX, topY + scrollH + 5, scrollW, 20, search)); + + // Right scroll: assigned magics + if (assignedScroll == null) { + assignedScroll = new GuiCustomScroll(this, 1); + assignedScroll.setSize(scrollW, scrollH); + } + assignedScroll.guiLeft = rightX; + assignedScroll.guiTop = topY; + updateAssignedList(); + addScroll(assignedScroll); + + // Arrow buttons between scrolls + int midX = guiLeft + 175; + addButton(new GuiNpcButton(70, midX, topY + 40, 55, 20, ">")); + addButton(new GuiNpcButton(71, midX, topY + 62, 55, 20, "<")); + + // Standardize button + addButton(new GuiNpcButton(72, midX, topY + 90, 55, 20, "magic.dist")); + getButton(72).setHoverText("magic.distInfo"); + + // Split field below right scroll + int tfY = topY + scrollH + 5; + addLabel(new GuiNpcLabel(5002, "magic.split", rightX - 57, tfY + 5)); + splitField = new GuiNpcTextField(73, this, fontRendererObj, rightX, tfY, 60, 20, ""); + splitField.setFloatsOnly(); + splitField.setMinMaxDefaultFloat(0, 1, 0); + splitField.enabled = false; + addTextField(splitField); + + // Done button + addButton(new GuiNpcButton(80, guiLeft + xSize / 2 - 40, guiTop + ySize - 34, 80, 20, "gui.done")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + // Add magic + if (guibutton.id == 70) { + if (availableScroll.hasSelected()) { + String selected = availableScroll.getSelected(); + Integer id = allMagic.get(selected); + if (id != null && !magicData.hasMagic(id)) { + magicData.addMagic(id, 0, 0); + } + } + updateAssignedList(); + return; + } + // Remove magic + if (guibutton.id == 71) { + if (assignedScroll.hasSelected()) { + Integer id = resolveAssignedId(assignedScroll.getSelected()); + if (id != null && magicData.hasMagic(id)) { + magicData.removeMagic(id); + } + } + updateAssignedList(); + if (splitField != null) { + splitField.setText(""); + splitField.enabled = false; + } + return; + } + // Standardize splits + if (guibutton.id == 72) { + if (magicData.getMagics().size() > 0) { + int count = magicData.getMagics().size(); + float stdSplit = 1.0f / count; + for (Integer key : magicData.getMagics().keySet()) { + magicData.getMagic(key).split = stdSplit; + } + // Update field if something is selected + if (assignedScroll.getSelected() != null && splitField != null) { + Integer id = resolveAssignedId(assignedScroll.getSelected()); + if (id != null && magicData.hasMagic(id)) { + splitField.setText(magicData.getMagic(id).split + ""); + } + } + updateAssignedList(); + } + return; + } + // Done + if (guibutton.id == 80) { + close(); + return; + } + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + if (getTextField(4) != null && getTextField(4).isFocused()) { + if (search.equals(getTextField(4).getText())) + return; + search = getTextField(4).getText().toLowerCase(); + availableScroll.setList(getSearchList()); + availableScroll.resetScroll(); + } + } + + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { + if (guiCustomScroll == assignedScroll) { + if (assignedScroll.getSelected() != null) { + Integer id = resolveAssignedId(assignedScroll.getSelected()); + if (id != null && magicData.hasMagic(id)) { + if (splitField != null) { + splitField.setText(magicData.getMagic(id).split + ""); + splitField.enabled = true; + } + } + } + } + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { + } + + @Override + public void unFocused(GuiNpcTextField textField) { + if (textField.id == 73) { + if (assignedScroll != null && assignedScroll.getSelected() != null) { + Integer id = resolveAssignedId(assignedScroll.getSelected()); + if (id != null && magicData.hasMagic(id)) { + try { + float split = Float.parseFloat(textField.getText()); + magicData.getMagic(id).split = split; + updateAssignedList(); + } catch (NumberFormatException e) { + } + } + } + } + } + + private List getSearchList() { + if (search.isEmpty()) { + return new ArrayList<>(allMagic.keySet()); + } + List list = new ArrayList<>(); + for (String name : allMagic.keySet()) { + if (name.toLowerCase().contains(search)) + list.add(name); + } + return list; + } + + /** + * Resolves the magic ID from a right-scroll selection (which may be formatted as "Name - XX%"). + */ + private Integer resolveAssignedId(String selection) { + if (selection == null) return null; + String baseName = assignedDisplayToName.get(selection); + if (baseName == null) baseName = selection; + return allMagic.get(baseName); + } + + private void updateAssignedList() { + assignedDisplayToName.clear(); + List selected = new ArrayList<>(); + for (Map.Entry entry : allMagic.entrySet()) { + if (magicData.hasMagic(entry.getValue())) { + float split = magicData.getMagic(entry.getValue()).split; + String display = entry.getKey() + " - " + Math.round(split * 100) + "%"; + assignedDisplayToName.put(display, entry.getKey()); + selected.add(display); + } + } + if (assignedScroll != null) { + assignedScroll.setList(selected); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiAbilities.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiAbilities.java index 36def1d43..7e51146a2 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiAbilities.java @@ -25,11 +25,17 @@ import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; import tconstruct.client.tabs.AbstractTab; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicData; +import noppes.npcs.controllers.data.MagicEntry; + import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/standardbg.png"); @@ -92,6 +98,11 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { private AbilityIcon cachedDetailIcon = null; private int cachedDetailIndex = -1; + // ═══ Info panel scroll ═══ + private int infoScrollOffset = 0; + private int infoContentHeight = 0; + private int lastInfoIndex = -1; + // ═══ Hotbar state ═══ private int hoveredSlotIndex = -1; private AbilityIcon[] hotbarIcons = new AbilityIcon[AbilityHotbarData.TOTAL_SLOTS]; @@ -436,11 +447,17 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { int infoIndex = hoveredGridIndex >= 0 ? hoveredGridIndex : selectedIndex; if (infoIndex < 0 || infoIndex >= filteredKeys.size()) return; + // Reset scroll when ability changes + if (infoIndex != lastInfoIndex) { + infoScrollOffset = 0; + lastInfoIndex = infoIndex; + } + FontRenderer fr = mc.fontRenderer; int panelInnerLeft = guiLeft + infoLeft + infoPad; int panelInnerTop = guiTop + infoTop + infoPad; int panelInnerWidth = (infoRight - infoLeft) - infoPad * 2; - int y = panelInnerTop; + int panelHeight = infoBottom - infoTop; // Cached icon for detail preview if (cachedDetailIndex != infoIndex) { @@ -448,14 +465,28 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { cachedDetailIndex = infoIndex; } + Ability ability = filteredAbilities.get(infoIndex); + // Get toggle state for icon rendering and state display int toggleState = 0; PlayerData togglePlayerData = ClientCacheHandler.playerData; - Ability infoAbility = filteredAbilities.get(infoIndex); - if (infoAbility != null && infoAbility.isToggleable() && togglePlayerData != null && togglePlayerData.abilityData != null) { + if (ability != null && ability.isToggleable() && togglePlayerData != null && togglePlayerData.abilityData != null) { toggleState = togglePlayerData.abilityData.getToggleState(filteredKeys.get(infoIndex)); } + // GL Scissor to clip info panel content + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scaleFactor = sr.getScaleFactor(); + int scissorX = (guiLeft + infoLeft) * scaleFactor; + int scissorY = mc.displayHeight - (guiTop + infoBottom) * scaleFactor; + int scissorW = (infoRight - infoLeft) * scaleFactor; + int scissorH = panelHeight * scaleFactor; + + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GL11.glScissor(scissorX, scissorY, scissorW, scissorH); + + int y = panelInnerTop - infoScrollOffset; + // Preview icon (centered, state-aware for multi-state toggles) if (cachedDetailIcon != null) { float drawSize = cachedDetailIcon.getDrawSize(); @@ -490,9 +521,8 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { } y += 4; - // Type info - Ability ability = filteredAbilities.get(infoIndex); if (ability != null) { + // Type info String typeId = ability.getTypeId(); if (typeId != null && !typeId.isEmpty()) { String translated = StatCollector.translateToLocal(typeId); @@ -503,6 +533,7 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { } } + // Toggle info if (ability.isToggleable()) { String toggleText = "[" + StatCollector.translateToLocal("gui.toggle") + "]"; int textW = fr.getStringWidth(toggleText); @@ -510,12 +541,11 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { y += fr.FONT_HEIGHT + 3; } - // Toggle state (uses pre-computed toggleState) + // Toggle state if (ability.isToggleable()) { String stateText; String prefix; if (toggleState > 0) { - // Active — check for state label (multi-state toggles like Ki Weapon) String stateLabel = ability.getToggleStateLabel(toggleState); stateText = stateLabel != null ? stateLabel : StatCollector.translateToLocal("gui.toggle.active"); prefix = "\u00A72"; @@ -528,20 +558,110 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { y += fr.FONT_HEIGHT + 3; } - // Cooldown info + // Cooldown info — yellow if per-ability cooldown if (ability.getCooldownTicks() > 0) { float seconds = ability.getCooldownTicks() / 20.0f; String cdText = StatCollector.translateToLocal("ability.cooldown") + ": " + String.format("%.1f", seconds) + "s"; int textW2 = fr.getStringWidth(cdText); - fr.drawStringWithShadow("\u00A77" + cdText, panelInnerLeft + (panelInnerWidth - textW2) / 2, y, 0xAAAAAA); + boolean perAbility = ability.isPerAbilityCooldown(); + String cdColor = perAbility ? "\u00A7e" : "\u00A77"; + int cdColorInt = perAbility ? 0xFFFF55 : 0xAAAAAA; + fr.drawStringWithShadow(cdColor + cdText, panelInnerLeft + (panelInnerWidth - textW2) / 2, y, cdColorInt); y += fr.FONT_HEIGHT + 3; - if (ability.isPerAbilityCooldown()) { + if (perAbility) { String perText = "[" + StatCollector.translateToLocal("ability.perAbilityCooldown") + "]"; int ptW = fr.getStringWidth(perText); fr.drawStringWithShadow("\u00A7e" + perText, panelInnerLeft + (panelInnerWidth - ptW) / 2, y, 0xFFFF55); + y += fr.FONT_HEIGHT + 3; } } + + // Damage info + float baseDamage = ability.getDisplayDamage(); + if (baseDamage > 0) { + float displayDamage = baseDamage; + if (AbilityController.Instance != null && mc.thePlayer != null) { + displayDamage = AbilityController.Instance.fireModifyProjectileDamage(ability, mc.thePlayer, baseDamage); + } + String label = ability.isDisplayDamageDPS() + ? StatCollector.translateToLocal("gui.dps") + : StatCollector.translateToLocal("ability.preview.damage"); + String dmgText = label + ": " + String.format("%.1f", displayDamage); + int textW = fr.getStringWidth(dmgText); + fr.drawStringWithShadow("\u00A7c" + dmgText, panelInnerLeft + (panelInnerWidth - textW) / 2, y, 0xFF5555); + y += fr.FONT_HEIGHT + 3; + } + + // Barrier health + float baseHealth = ability.getDisplayBarrierHealth(); + if (baseHealth > 0) { + float displayHealth = baseHealth; + if (AbilityController.Instance != null && mc.thePlayer != null) { + displayHealth = AbilityController.Instance.fireModifyBarrierHealth(ability, mc.thePlayer, baseHealth); + } + String healthText = StatCollector.translateToLocal("ability.preview.health") + ": " + String.format("%.0f", displayHealth); + int textW = fr.getStringWidth(healthText); + fr.drawStringWithShadow("\u00A7a" + healthText, panelInnerLeft + (panelInnerWidth - textW) / 2, y, 0x55FF55); + y += fr.FONT_HEIGHT + 3; + } + + // Barrier reflection + if (ability.isDisplayReflect()) { + String reflectText = StatCollector.translateToLocal("ability.reflect") + ": " + String.format("%.0f%%", ability.getDisplayReflectStrength()); + int textW = fr.getStringWidth(reflectText); + fr.drawStringWithShadow("\u00A7d" + reflectText, panelInnerLeft + (panelInnerWidth - textW) / 2, y, 0xFF55FF); + y += fr.FONT_HEIGHT + 3; + } + + // Barrier absorbing + if (ability.isDisplayAbsorbing()) { + String absorbText = "[" + StatCollector.translateToLocal("ability.absorbing") + "]"; + int textW = fr.getStringWidth(absorbText); + fr.drawStringWithShadow("\u00A7b" + absorbText, panelInnerLeft + (panelInnerWidth - textW) / 2, y, 0x55FFFF); + y += fr.FONT_HEIGHT + 3; + } + + // Magic info + if (ability.hasMagic()) { + y += 2; + MagicData magicData = ability.getMagicData(); + if (magicData.isEmpty()) { + String magicText = StatCollector.translateToLocal("ability.preview.magic.casters"); + int textW = fr.getStringWidth(magicText); + fr.drawStringWithShadow("\u00A79" + magicText, panelInnerLeft + (panelInnerWidth - textW) / 2, y, 0x5555FF); + y += fr.FONT_HEIGHT + 3; + } else { + for (Map.Entry entry : magicData.getMagics().entrySet()) { + Magic magic = MagicController.getInstance() != null + ? MagicController.getInstance().getMagic(entry.getKey()) : null; + String magicName = magic != null ? magic.getDisplayName() : "Magic #" + entry.getKey(); + String splitText = magicName + ": " + Math.round(entry.getValue().split * 100) + "%"; + int textW = fr.getStringWidth(splitText); + fr.drawStringWithShadow("\u00A79" + splitText, panelInnerLeft + (panelInnerWidth - textW) / 2, y, 0x5555FF); + y += fr.FONT_HEIGHT + 1; + } + y += 2; + } + } + } + + // Track content height for scroll limits + infoContentHeight = (y + infoScrollOffset) - panelInnerTop + infoPad * 2; + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + // Scroll indicator + if (infoContentHeight > panelHeight) { + int maxScroll = Math.max(1, infoContentHeight - panelHeight); + if (infoScrollOffset > maxScroll) infoScrollOffset = maxScroll; + float scrollPercent = (float) infoScrollOffset / maxScroll; + int barHeight = panelHeight - 4; + int thumbHeight = Math.max(8, barHeight * panelHeight / infoContentHeight); + int thumbY = guiTop + infoTop + 2 + (int) ((barHeight - thumbHeight) * scrollPercent); + int barX = guiLeft + infoRight - 3; + drawRect(barX, guiTop + infoTop + 2, barX + 2, guiTop + infoTop + 2 + barHeight, 0x40FFFFFF); + drawRect(barX, thumbY, barX + 2, thumbY + thumbHeight, 0xA0FFFFFF); } } @@ -903,11 +1023,26 @@ public class GuiAbilities extends GuiCNPCInventory implements ISubGuiListener { int wheel = Mouse.getEventDWheel(); if (wheel != 0) { - int maxScroll = getMaxScrollRow(); - if (wheel > 0) { - scrollRow = Math.max(0, scrollRow - 1); + int mouseX = Mouse.getEventX() * width / mc.displayWidth; + int mouseY = height - Mouse.getEventY() * height / mc.displayHeight - 1; + + if (mouseX >= guiLeft + infoLeft && mouseX < guiLeft + infoRight + && mouseY >= guiTop + infoTop && mouseY < guiTop + infoBottom) { + // Info panel scroll + int panelHeight = infoBottom - infoTop; + int maxInfoScroll = Math.max(0, infoContentHeight - panelHeight); + if (wheel > 0) { + infoScrollOffset = Math.max(0, infoScrollOffset - 10); + } else { + infoScrollOffset = Math.min(maxInfoScroll, infoScrollOffset + 10); + } } else { - scrollRow = Math.min(maxScroll, scrollRow + 1); + int maxScroll = getMaxScrollRow(); + if (wheel > 0) { + scrollRow = Math.max(0, scrollRow - 1); + } else { + scrollRow = Math.min(maxScroll, scrollRow + 1); + } } } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index c105fa32e..cfcc9b74f 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -46,8 +46,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb public Map> languages = new HashMap(); protected int scriptLimit = 1; public List hookList = new ArrayList(); - protected boolean loaded = false; + protected volatile boolean loaded = false; protected boolean serverDataReceived = false; + protected String scriptSessionToken = null; protected Map textAreas = new HashMap<>(); protected GuiScreen parent; @@ -766,6 +767,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return; } if (result.kind == IScriptHandlerPacket.GuiDataKind.METADATA) { + if (compound.hasKey("ScriptSessionToken")) + scriptSessionToken = compound.getString("ScriptSessionToken"); loadLanguagesData(compound); return; } @@ -776,6 +779,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return; } + if (compound.hasKey("ScriptSessionToken")) + scriptSessionToken = compound.getString("ScriptSessionToken"); loadLanguagesData(compound); } @@ -825,9 +830,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setScript(); if (handler instanceof IScriptHandlerPacket) - ((IScriptHandlerPacket) handler).sync(); - - + ((IScriptHandlerPacket) handler).sync(scriptSessionToken); } public void textUpdate(String text) { diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java index 88ff5fb94..0ff4fcb7e 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java @@ -71,16 +71,29 @@ public interface IScriptHandlerPacket extends IScriptHandler { * GuiScriptInterface is responsible for updating the active {@link IScriptUnit} with current editor text first. */ default void sync() { + sync(null); + } + + /** + * Emit save packets with a session token for server-side verification. + * The token prevents saving unloaded default data due to race conditions. + */ + default void sync(String sessionToken) { List containers = getScripts(); for (int i = 0; i < containers.size(); i++) { IScriptUnit container = containers.get(i); - sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); + NBTTagCompound nbt = container.writeToNBT(new NBTTagCompound()); + if (sessionToken != null && !sessionToken.isEmpty()) + nbt.setString("ScriptSessionToken", sessionToken); + sendSavePacket(i, containers.size(), nbt); } NBTTagCompound scriptData = new NBTTagCompound(); scriptData.setString("ScriptLanguage", getLanguage()); scriptData.setBoolean("ScriptEnabled", getEnabled()); scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(getConsoleText())); + if (sessionToken != null && !sessionToken.isEmpty()) + scriptData.setString("ScriptSessionToken", sessionToken); sendSavePacket(-1, containers.size(), scriptData); } } diff --git a/src/main/java/noppes/npcs/controllers/data/MagicData.java b/src/main/java/noppes/npcs/controllers/data/MagicData.java index fa81eb61e..f70e6ec75 100644 --- a/src/main/java/noppes/npcs/controllers/data/MagicData.java +++ b/src/main/java/noppes/npcs/controllers/data/MagicData.java @@ -76,4 +76,15 @@ public class MagicData implements IMagicData { MagicEntry entry = magics.get(id); return entry == null ? 0 : entry.split; } + + public MagicData copy() { + MagicData copy = new MagicData(); + for (java.util.Map.Entry e : magics.entrySet()) { + MagicEntry entryCopy = new MagicEntry(); + entryCopy.damage = e.getValue().damage; + entryCopy.split = e.getValue().split; + copy.magics.put(e.getKey(), entryCopy); + } + return copy; + } } diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java index 854e77941..9844a02a3 100644 --- a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -2,7 +2,9 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.eventhandler.Event; import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.PacketUtil; import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.Constants; import noppes.npcs.CustomNpcs; @@ -117,9 +119,16 @@ public abstract class SingleScriptHandler extends ScriptHandler { * - tab == 0: Script content NBT follows * - tab != 0 (typically -1): Script metadata (language, enabled) follows */ - public void saveScript(ByteBuf buffer) throws IOException { + public boolean saveScript(ByteBuf buffer, EntityPlayer player) throws IOException { int tab = buffer.readInt(); int totalScripts = buffer.readInt(); + NBTTagCompound compound = ByteBufUtils.readNBT(buffer); + + // Verify session token + String token = compound.getString("ScriptSessionToken"); + if (!PacketUtil.verifyScriptSession(player, token)) { + return false; + } if (totalScripts == 0) { this.container = null; @@ -127,15 +136,14 @@ public abstract class SingleScriptHandler extends ScriptHandler { if (tab == 0) { // Script content - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - this.container = IScriptUnit.createFromNBT(tabCompound, this); + this.container = IScriptUnit.createFromNBT(compound, this); } else { // Metadata (language, enabled) - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); this.setLanguage(compound.getString("ScriptLanguage")); normalizeLanguage(); this.setEnabled(compound.getBoolean("ScriptEnabled")); } + return true; } /** diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 0864e16ec..fd301b802 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -5,15 +5,18 @@ import cpw.mods.fml.common.eventhandler.EventBus; import foxz.command.ScriptedCommand; import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.AttributeController; +import kamkeel.npcs.controllers.EnergyController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.TelegraphController; import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilityZone; import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.entity.EntityEnergyDome; import kamkeel.npcs.entity.EntityAbilityLaser; import kamkeel.npcs.entity.EntityAbilityOrb; import kamkeel.npcs.entity.EntityEnergyPanel; import kamkeel.npcs.entity.EntityEnergySweeper; +import kamkeel.npcs.entity.EntityEnergyExplosion; import kamkeel.npcs.entity.EntityEnergyProjectile; import kamkeel.npcs.entity.EntityEnergySlicer; import net.minecraft.block.Block; @@ -60,6 +63,7 @@ import noppes.npcs.api.IBlock; import noppes.npcs.api.ICommand; import noppes.npcs.api.IContainer; import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.IEnergyController; import noppes.npcs.api.INbt; import noppes.npcs.api.IParticle; import noppes.npcs.api.IPos; @@ -99,12 +103,14 @@ import noppes.npcs.scripted.entity.ScriptDBCPlayer; import noppes.npcs.scripted.entity.ScriptEnergyBeam; import noppes.npcs.scripted.entity.ScriptEnergyDisc; import noppes.npcs.scripted.entity.ScriptEnergyDome; +import noppes.npcs.scripted.entity.ScriptEnergyExplosion; import noppes.npcs.scripted.entity.ScriptEnergyLaser; import noppes.npcs.scripted.entity.ScriptEnergyOrb; import noppes.npcs.scripted.entity.ScriptEnergyPanel; import noppes.npcs.scripted.entity.ScriptEnergyProjectile; import noppes.npcs.scripted.entity.ScriptEnergySlicer; import noppes.npcs.scripted.entity.ScriptEnergySweeper; +import noppes.npcs.scripted.entity.ScriptEnergyZone; import noppes.npcs.scripted.entity.ScriptEntity; import noppes.npcs.scripted.entity.ScriptEntityItem; import noppes.npcs.scripted.entity.ScriptFishHook; @@ -462,6 +468,10 @@ public class NpcAPI extends AbstractNpcAPI { data = new ScriptEntityData(new ScriptEnergyPanel<>((EntityEnergyPanel) entity)); else if (entity instanceof EntityEnergySweeper) data = new ScriptEntityData(new ScriptEnergySweeper<>((EntityEnergySweeper) entity)); + else if (entity instanceof EntityEnergyExplosion) + data = new ScriptEntityData(new ScriptEnergyExplosion<>((EntityEnergyExplosion) entity)); + else if (entity instanceof EntityAbilityZone) + data = new ScriptEntityData(new ScriptEnergyZone<>((EntityAbilityZone) entity)); else data = new ScriptEntityData(new ScriptEntity<>(entity)); entity.registerExtendedProperties("ScriptedObject", data); @@ -612,6 +622,11 @@ public class NpcAPI extends AbstractNpcAPI { return instance; } + @Override + public IEnergyController getEnergyController() { + return EnergyController.Instance; + } + public EventBus events() { return EVENT_BUS; } diff --git a/src/main/java/noppes/npcs/scripted/constants/EntityType.java b/src/main/java/noppes/npcs/scripted/constants/EntityType.java index 62cd482fa..e92babb24 100644 --- a/src/main/java/noppes/npcs/scripted/constants/EntityType.java +++ b/src/main/java/noppes/npcs/scripted/constants/EntityType.java @@ -23,4 +23,5 @@ public class EntityType { public static final int ENERGY_BARRIER = 19; public static final int ENERGY_DOME = 20; public static final int ENERGY_PANEL = 21; + public static final int ENERGY_EXPLOSION = 22; } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java index 4983b59b2..54bc7fd42 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java @@ -2,8 +2,12 @@ package noppes.npcs.scripted.entity; import kamkeel.npcs.entity.EntityEnergyAbility; import net.minecraft.entity.Entity; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.INbt; import noppes.npcs.api.entity.IEnergyAbility; import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.handler.data.IMagicData; +import noppes.npcs.controllers.data.MagicData; import noppes.npcs.scripted.NpcAPI; /** @@ -128,4 +132,35 @@ public abstract class ScriptEnergyAbility extends public void setIgnoreIFrames(boolean ignore) { entity.setIgnoreIFrames(ignore); } + + // ==================== CUSTOM DAMAGE DATA ==================== + + public INbt getDamageData() { + NBTTagCompound data = entity.getCustomDamageData(); + if (data == null) { + data = new NBTTagCompound(); + entity.setCustomDamageData(data); + } + return NpcAPI.Instance().getINbt(data); + } + + public void setDamageData(INbt data) { + if (data == null) { + entity.setCustomDamageData(null); + } else { + entity.setCustomDamageData((NBTTagCompound) data.getMCNBT()); + } + } + + // ==================== MAGIC DATA ==================== + + public IMagicData getMagicData() { + return entity.getMagicData(); + } + + public void setMagicData(IMagicData data) { + if (data instanceof MagicData) { + entity.setMagicData((MagicData) data); + } + } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java index 9c5a8b8be..787c27ee3 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java @@ -60,7 +60,7 @@ public class ScriptEnergyBeam extends ScriptEnergyP // ==================== FIRE ==================== @Override - public void fire(IEntity target) { + public void fireAt(IEntity target) { ensureSpawned(); EntityLivingBase mcTarget = null; if (target != null && target.getMCEntity() instanceof EntityLivingBase) { @@ -70,7 +70,7 @@ public class ScriptEnergyBeam extends ScriptEnergyP } @Override - public void fire(double x, double y, double z) { + public void fireAt(double x, double y, double z) { ensureSpawned(); double dx = x - entity.posX; double dy = y - entity.posY; diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java index c02fdad31..34ebb6305 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java @@ -76,7 +76,7 @@ public class ScriptEnergyDisc extends ScriptEnergyP // ==================== FIRE ==================== @Override - public void fire(IEntity target) { + public void fireAt(IEntity target) { ensureSpawned(); EntityLivingBase mcTarget = null; if (target != null && target.getMCEntity() instanceof EntityLivingBase) { @@ -86,7 +86,7 @@ public class ScriptEnergyDisc extends ScriptEnergyP } @Override - public void fire(double x, double y, double z) { + public void fireAt(double x, double y, double z) { ensureSpawned(); double dx = x - entity.posX; double dy = y - entity.posY; diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyExplosion.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyExplosion.java new file mode 100644 index 000000000..8ce2e359f --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyExplosion.java @@ -0,0 +1,109 @@ +package noppes.npcs.scripted.entity; + +import kamkeel.npcs.entity.EntityEnergyExplosion; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.packets.data.energyexplosion.EnergyExplosionSpawnPacket; +import noppes.npcs.api.entity.IEnergyExplosion; +import noppes.npcs.scripted.constants.EntityType; + +import java.util.UUID; + +/** + * Script wrapper for standalone energy explosion entities. + */ +public class ScriptEnergyExplosion extends ScriptEnergyAbility implements IEnergyExplosion { + + public ScriptEnergyExplosion(T entity) { + super(entity); + } + + // ==================== TYPE ==================== + + @Override + public int getType() { + return EntityType.ENERGY_EXPLOSION; + } + + @Override + public boolean typeOf(int type) { + return type == EntityType.ENERGY_EXPLOSION || super.typeOf(type); + } + + // ==================== RADIUS & DURATION ==================== + + @Override + public float getRadius() { + return entity.getExplosionRadius(); + } + + @Override + public void setRadius(float radius) { + entity.setExplosionRadius(radius); + } + + @Override + public int getDuration() { + return entity.getDurationTicks(); + } + + // ==================== DAMAGE ==================== + + @Override + public float getDamage() { + return entity.getDamage(); + } + + @Override + public void setDamage(float damage) { + entity.setDamage(damage); + } + + @Override + public float getKnockback() { + return entity.getKnockback(); + } + + @Override + public void setKnockback(float knockback) { + entity.setKnockback(knockback); + } + + @Override + public float getKnockbackUp() { + return entity.getKnockbackUp(); + } + + @Override + public void setKnockbackUp(float knockbackUp) { + entity.setKnockbackUp(knockbackUp); + } + + @Override + public float getDamageFalloff() { + return entity.getDamageFalloff(); + } + + @Override + public void setDamageFalloff(float falloff) { + entity.setDamageFalloff(falloff); + } + + // ==================== SPAWNING ==================== + + @Override + public void spawn() { + if (entity.addedToChunk || entity.worldObj == null) return; + + if (!entity.worldObj.isRemote) { + // Send visual to clients via packet (explosion is packet-driven, not a tracked entity) + String instanceId = "energy_explosion_" + UUID.randomUUID(); + PacketHandler.Instance.sendToAll(new EnergyExplosionSpawnPacket( + instanceId, entity.exportSpawnNBT())); + + // Spawn server-side for damage logic (if damage enabled) + if (entity.isDamageEnabled()) { + entity.worldObj.spawnEntityInWorld(entity); + } + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java index 6c0b8f077..eb6159bf5 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java @@ -92,7 +92,7 @@ public class ScriptEnergyLaser extends ScriptEnerg // ==================== FIRE ==================== @Override - public void fire(IEntity target) { + public void fireAt(IEntity target) { ensureSpawned(); EntityLivingBase mcTarget = null; if (target != null && target.getMCEntity() instanceof EntityLivingBase) { @@ -102,7 +102,7 @@ public class ScriptEnergyLaser extends ScriptEnerg } @Override - public void fire(double x, double y, double z) { + public void fireAt(double x, double y, double z) { ensureSpawned(); double dx = x - entity.posX; double dy = y - entity.posY; diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java index cb47a092c..3821190cb 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java @@ -28,7 +28,7 @@ public class ScriptEnergyOrb extends ScriptEnergyPro } @Override - public void fire(IEntity target) { + public void fireAt(IEntity target) { ensureSpawned(); EntityLivingBase mcTarget = null; if (target != null && target.getMCEntity() instanceof EntityLivingBase) { @@ -38,7 +38,7 @@ public class ScriptEnergyOrb extends ScriptEnergyPro } @Override - public void fire(double x, double y, double z) { + public void fireAt(double x, double y, double z) { ensureSpawned(); double dx = x - entity.posX; double dy = y - entity.posY; diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyPanel.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyPanel.java index 04c730fad..edcf90795 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyPanel.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyPanel.java @@ -54,4 +54,23 @@ public class ScriptEnergyPanel extends ScriptEnergy public boolean isLaunched() { return entity.getMode() == EntityEnergyPanel.PanelMode.LAUNCHED; } + + public void setPanelYaw(float yaw) { + entity.setPanelYaw(yaw); + } + + public void setPanelMode(int mode) { + EntityEnergyPanel.PanelMode[] values = EntityEnergyPanel.PanelMode.values(); + if (mode >= 0 && mode < values.length) { + entity.setMode(values[mode]); + } + } + + // ==================== SPAWNING ==================== + + public void spawn() { + if (!entity.addedToChunk && entity.worldObj != null) { + entity.worldObj.spawnEntityInWorld(entity); + } + } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java index db810848e..a4ca6697b 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java @@ -259,7 +259,7 @@ public class ScriptEnergyProjectile extends Sc // ==================== FIRE ==================== - public void fire(IEntity target) { + public void fireAt(IEntity target) { if (target != null) { Entity mcTarget = target.getMCEntity(); setMotionToward(mcTarget.posX, mcTarget.posY + mcTarget.getEyeHeight(), mcTarget.posZ); @@ -267,12 +267,12 @@ public class ScriptEnergyProjectile extends Sc ensureSpawned(); } - public void fire(double x, double y, double z) { + public void fireAt(double x, double y, double z) { setMotionToward(x, y, z); ensureSpawned(); } - public void fire(float yaw, float pitch) { + public void fireDirection(float yaw, float pitch) { float yawRad = (float) Math.toRadians(yaw); float pitchRad = (float) Math.toRadians(pitch); entity.motionX = -Math.sin(yawRad) * Math.cos(pitchRad) * entity.getSpeed(); @@ -281,16 +281,16 @@ public class ScriptEnergyProjectile extends Sc ensureSpawned(); } - public void fire(IEntityLivingBase caster) { + public void fireFrom(IEntityLivingBase caster) { if (caster == null) return; initFromCaster(caster); - fire(caster.getRotation(), caster.getPitch()); + fireDirection(caster.getRotation(), caster.getPitch()); } - public void fire(IEntityLivingBase caster, IEntity target) { + public void fireFrom(IEntityLivingBase caster, IEntity target) { if (caster == null) return; initFromCaster(caster); - fire(target); + fireAt(target); } /** diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java index f120350c4..7614939dd 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java @@ -48,7 +48,7 @@ public class ScriptEnergySlicer extends ScriptEner // ==================== FIRE ==================== @Override - public void fire(IEntity target) { + public void fireAt(IEntity target) { ensureSpawned(); EntityLivingBase mcTarget = null; if (target != null && target.getMCEntity() instanceof EntityLivingBase) { @@ -58,7 +58,7 @@ public class ScriptEnergySlicer extends ScriptEner } @Override - public void fire(double x, double y, double z) { + public void fireAt(double x, double y, double z) { ensureSpawned(); double dx = x - entity.posX; double dy = y - entity.posY; diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySweeper.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySweeper.java index 52207238d..021bcafb5 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySweeper.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySweeper.java @@ -1,9 +1,116 @@ package noppes.npcs.scripted.entity; import kamkeel.npcs.entity.EntityEnergySweeper; +import noppes.npcs.api.entity.IEnergySweeper; + +public class ScriptEnergySweeper extends ScriptEnergyAbility implements IEnergySweeper { -public class ScriptEnergySweeper extends ScriptEnergyAbility { public ScriptEnergySweeper(T entity) { super(entity); } + + // ==================== BEAM DIMENSIONS ==================== + + @Override + public float getBeamLength() { + return entity.getBeamLength(); + } + + @Override + public void setBeamLength(float length) { + entity.setBeamLength(length); + } + + @Override + public float getBeamWidth() { + return entity.getBeamWidth(); + } + + @Override + public void setBeamWidth(float width) { + entity.setBeamWidth(width); + } + + @Override + public float getBeamHeight() { + return entity.getBeamHeight(); + } + + @Override + public void setBeamHeight(float height) { + entity.setBeamHeight(height); + } + + // ==================== ROTATION ==================== + + @Override + public float getSweepSpeed() { + return entity.getSweepSpeed(); + } + + @Override + public void setSweepSpeed(float degreesPerTick) { + entity.setSweepSpeed(degreesPerTick); + } + + @Override + public int getNumberOfRotations() { + return entity.getNumberOfRotations(); + } + + @Override + public void setNumberOfRotations(int rotations) { + entity.setNumberOfRotations(rotations); + } + + @Override + public boolean isLockOnTarget() { + return entity.isLockOnTarget(); + } + + @Override + public void setLockOnTarget(boolean lock) { + entity.setLockOnTarget(lock); + } + + // ==================== DAMAGE ==================== + + @Override + public float getDamage() { + return entity.getDamage(); + } + + @Override + public void setDamage(float damage) { + entity.setDamage(damage); + } + + @Override + public int getDamageInterval() { + return entity.getDamageInterval(); + } + + @Override + public void setDamageInterval(int ticks) { + entity.setDamageInterval(ticks); + } + + @Override + public boolean isPiercing() { + return entity.isPiercing(); + } + + @Override + public void setPiercing(boolean piercing) { + entity.setPiercing(piercing); + } + + // ==================== SPAWNING ==================== + + @Override + public void spawn() { + if (!entity.addedToChunk && entity.worldObj != null) { + entity.worldObj.spawnEntityInWorld(entity); + } + } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyZone.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyZone.java new file mode 100644 index 000000000..9368a2475 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyZone.java @@ -0,0 +1,277 @@ +package noppes.npcs.scripted.entity; + +import kamkeel.npcs.entity.EntityAbilityZone; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.INbt; +import noppes.npcs.api.entity.IEnergyZone; +import noppes.npcs.scripted.NpcAPI; + +/** + * Script wrapper for EntityAbilityZone (Hazard and Trap zones). + */ +public class ScriptEnergyZone extends ScriptEntity implements IEnergyZone { + + public ScriptEnergyZone(T entity) { + super(entity); + } + + // ==================== ZONE TYPE ==================== + + @Override + public int getZoneType() { + return entity.getZoneType().ordinal(); + } + + // ==================== COMMON ZONE PROPERTIES ==================== + + @Override + public int getZoneShape() { + return entity.getShape().ordinal(); + } + + @Override + public void setZoneShape(int shape) { + EntityAbilityZone.ZoneShape[] values = EntityAbilityZone.ZoneShape.values(); + if (shape >= 0 && shape < values.length) { + entity.setShape(values[shape]); + } + } + + @Override + public float getRadius() { + return entity.getRadius(); + } + + @Override + public void setRadius(float radius) { + entity.setRadius(radius); + } + + @Override + public float getZoneHeight() { + return entity.getZoneHeight(); + } + + @Override + public void setZoneHeight(float height) { + entity.setZoneHeight(height); + } + + @Override + public int getDuration() { + return entity.getDurationTicks(); + } + + @Override + public void setDuration(int ticks) { + entity.setDurationTicks(ticks); + } + + // ==================== COLORS ==================== + + @Override + public int getInnerColor() { + return entity.getInnerColor(); + } + + @Override + public void setInnerColor(int color) { + entity.setInnerColor(color); + } + + @Override + public int getOuterColor() { + return entity.getOuterColor(); + } + + @Override + public void setOuterColor(int color) { + entity.setOuterColor(color); + } + + @Override + public boolean isOuterColorEnabled() { + return entity.isOuterColorEnabled(); + } + + @Override + public void setOuterColorEnabled(boolean enabled) { + entity.setOuterColorEnabled(enabled); + } + + // ==================== VISUALS ==================== + + @Override + public float getParticleDensity() { + return entity.getParticleDensity(); + } + + @Override + public void setParticleDensity(float density) { + entity.setParticleDensity(density); + } + + @Override + public float getParticleScale() { + return entity.getParticleScale(); + } + + @Override + public void setParticleScale(float scale) { + entity.setParticleScale(scale); + } + + @Override + public float getAnimSpeed() { + return entity.getAnimSpeed(); + } + + @Override + public void setAnimSpeed(float speed) { + entity.setAnimSpeed(speed); + } + + @Override + public boolean isIgnoreIFrames() { + return entity.isIgnoreIFrames(); + } + + @Override + public void setIgnoreIFrames(boolean ignore) { + entity.setIgnoreIFrames(ignore); + } + + // ==================== HAZARD-SPECIFIC ==================== + + @Override + public float getDamagePerSecond() { + return entity.getDamagePerSecond(); + } + + @Override + public void setDamagePerSecond(float dps) { + entity.setDamagePerSecond(dps); + } + + @Override + public int getDamageInterval() { + return entity.getDamageInterval(); + } + + @Override + public void setDamageInterval(int ticks) { + entity.setDamageInterval(ticks); + } + + @Override + public boolean isAffectsCaster() { + return entity.isAffectsCaster(); + } + + @Override + public void setAffectsCaster(boolean affects) { + entity.setAffectsCaster(affects); + } + + // ==================== TRAP-SPECIFIC ==================== + + @Override + public float getTriggerRadius() { + return entity.getTriggerRadius(); + } + + @Override + public void setTriggerRadius(float radius) { + entity.setTriggerRadius(radius); + } + + @Override + public int getArmTime() { + return entity.getArmTime(); + } + + @Override + public void setArmTime(int ticks) { + entity.setArmTime(ticks); + } + + @Override + public int getMaxTriggers() { + return entity.getMaxTriggers(); + } + + @Override + public void setMaxTriggers(int max) { + entity.setMaxTriggers(max); + } + + @Override + public int getTriggerCooldown() { + return entity.getTriggerCooldown(); + } + + @Override + public void setTriggerCooldown(int ticks) { + entity.setTriggerCooldown(ticks); + } + + @Override + public float getDamage() { + return entity.getDamage(); + } + + @Override + public void setDamage(float damage) { + entity.setDamage(damage); + } + + @Override + public float getKnockback() { + return entity.getKnockback(); + } + + @Override + public void setKnockback(float knockback) { + entity.setKnockback(knockback); + } + + @Override + public boolean isVisible() { + return entity.isVisible(); + } + + @Override + public void setVisible(boolean visible) { + entity.setVisible(visible); + } + + // ==================== CUSTOM DAMAGE DATA ==================== + + @Override + public INbt getDamageData() { + NBTTagCompound data = entity.getCustomDamageData(); + if (data == null) { + data = new NBTTagCompound(); + entity.setCustomDamageData(data); + } + return NpcAPI.Instance().getINbt(data); + } + + @Override + public void setDamageData(INbt data) { + if (data == null) { + entity.setCustomDamageData(null); + } else { + entity.setCustomDamageData((NBTTagCompound) data.getMCNBT()); + } + } + + // ==================== SPAWNING ==================== + + @Override + public void spawn() { + if (!entity.addedToChunk && entity.worldObj != null) { + entity.worldObj.spawnEntityInWorld(entity); + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index e3641570e..b89a241dd 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -99,8 +99,11 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } @Override - public void sync() { - sendSavePacket(-1, getScripts().size(), getMCNbt()); + public void sync(String sessionToken) { + NBTTagCompound compound = getMCNbt(); + if (sessionToken != null && !sessionToken.isEmpty()) + compound.setString("ScriptSessionToken", sessionToken); + sendSavePacket(-1, getScripts().size(), compound); } public int getType() { diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 82f58177f..e6dc2779e 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1791,6 +1791,13 @@ ability.section.explosive=Explosive ability.section.colors=Colors ability.section.effects=Effects ability.section.burst=Burst +ability.section.magic=Magic +ability.magic.note=Magic Note +ability.magic.usesCaster=Uses the Caster's Magic Data +ability.magic.editor=Magic Editor +ability.preview.damage=Damage +ability.preview.health=Health +ability.preview.magic.casters=Magic: Caster's ability.section.dodgeAnimations=Dodge Animations ability.section.visual=Visual ability.section.projectile1=Projectile 1 @@ -2051,6 +2058,8 @@ ability.hover.weight=Higher weight = more likely to be chosen when multiple abil ability.hover.lockMovement=Prevents NPC pathfinding during the selected phases (Windup, Active, or Both) ability.hover.rotationMode=Free: normal AI rotation. Locked: freeze facing direction. Track: force face target for precise aiming. ability.hover.rotationPhase=When the rotation mode applies (Windup, Active, or Both) +ability.trackDelay=Track Delay +ability.hover.trackDelay=How sluggish tracking is. 0 = instant (impossible to dodge). Higher = slower rotation toward target. Default 3 for lasers. ability.hover.interruptible=If enabled, ability can be interrupted by taking damage ability.hover.ignoreIFrames=Temporarily clears target hurt resistance before applying ability damage, then restores it. ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second)