Merge branch 'dev' into syntax-highlighter-unified

This commit is contained in:
Kam committed 2026-03-03 20:30:02 -05:00
commit 513956062d
68 files changed
+2634 -246

No files matched your search

+1 -1
Submodule src/api updated: 9fbd1eceff...4e96425314.
@@ -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);
}
/**
@@ -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) {
@@ -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<IEnergyHandler> handlers = new ArrayList<>();
// ═══════════════════════════════════════════════════════════════════
// HANDLER REGISTRATION
// ═══════════════════════════════════════════════════════════════════
public void registerHandler(IEnergyHandler handler) {
handlers.add(handler);
}
public List<IEnergyHandler> 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);
}
}
@@ -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<Integer, MagicEntry> 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<Integer, MagicEntry> 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<AbilityCondition> getConditions() {
return conditions;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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<UUID, PullState> pulledEntities;
// Runtime state — keyed by entity ID (not UUID, since cloned NPCs share UUIDs)
private transient Map<Integer, PullState> pulledEntities;
private transient boolean pullComplete = false;
private transient int ticksSincePullDamage = 0;
@@ -56,7 +56,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
}
}
private Map<UUID, PullState> getPulledEntities() {
private Map<Integer, PullState> 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<UUID, PullState> entry : new HashMap<>(getPulledEntities()).entrySet()) {
EntityLivingBase entity = findEntity(caster, caster.worldObj, entry.getKey());
for (Map.Entry<Integer, PullState> 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<UUID, PullState> entry : new HashMap<>(getPulledEntities()).entrySet()) {
UUID uuid = entry.getKey();
for (Map.Entry<Integer, PullState> 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<EntityLivingBase> 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;
}
@@ -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;
}
@@ -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<E extends EntityEnergyProjectile>
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<E extends EntityEnergyProjectile>
combatData.damage = damage;
}
@Override
public float getDisplayDamage() { return combatData.damage; }
@Override
public float getKnockback() {
return combatData.knockback;
@@ -49,6 +49,7 @@ public class AbilityLaser extends AbilityEnergyProjectile<EntityAbilityLaser> 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";
@@ -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;
}
@@ -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;
}
@@ -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);
}
@@ -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);
}
@@ -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<UUID> triggeredEntities = new HashSet<>();
private transient Set<Integer> triggeredEntities = new HashSet<>();
private transient int triggerFlashTick = -1;
private transient int ticksSinceDamage = 0;
private transient Set<Integer> 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);
}
}
}
}
@@ -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 ====================
@@ -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;
@@ -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<EntityLivingBase> 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;
}
}
}
@@ -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
@@ -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 {
@@ -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
@@ -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<UUID, String> 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<IScriptUnit> 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;
}
}
@@ -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) {
@@ -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;
}
}
@@ -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) {
@@ -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()));
}
@@ -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) {
@@ -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();
@@ -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();
@@ -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) {
@@ -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();
@@ -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
}
@@ -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();
@@ -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()));
}
@@ -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<Integer, Float> 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<Integer, MagicEntry> magicData) {
public static AllocationResult allocateMagicDamage(float physicalDamage, Map<Integer, MagicEntry> magicData) {
Map<Integer, Float> 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<Integer, Float> allocation,
public static void addAttributeMagicDamage(Map<Integer, Float> allocation,
Map<Integer, Float> attributeDamage,
Map<Integer, Float> 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<Integer, Float> allocation,
public static void applyMagicInteractions(Map<Integer, Float> allocation,
Set<Integer> defenderMagicIDs,
MagicController magicController) {
for (Map.Entry<Integer, Float> 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<Integer, Float> allocation,
public static float applyDefenderMagicDefense(Map<Integer, Float> 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<Integer, Float> 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<Integer, Float> allocation, Map<Integer, Float> magicBoost) {
if (!ConfigMain.AttributesEnabled) return;
for (Map.Entry<Integer, Float> 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<Integer, MagicEntry> 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:
* <ul>
* <li>Magic allocation (splits from ability/caster magic data)</li>
* <li>Gear magic boost multipliers on split damage (player casters only, no flat gear damage)</li>
* <li>Magic interactions vs defender</li>
* <li>Defender magic defense/resistance (player defenders only)</li>
* </ul>
* 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<Integer> 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;
}
}
+69 -17
View File
@@ -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;
@@ -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));
}
}
}
@@ -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
@@ -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);
}
}
@@ -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<String, Integer> allMagic = new HashMap<>();
/** Maps formatted display string (right scroll) → base display name. */
private final HashMap<String, String> 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<Integer, Magic> 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<String> getSearchList() {
if (search.isEmpty()) {
return new ArrayList<>(allMagic.keySet());
}
List<String> 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<String> selected = new ArrayList<>();
for (Map.Entry<String, Integer> 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);
}
}
}
@@ -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<Integer, MagicEntry> 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);
}
}
}
}
@@ -46,8 +46,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
public Map<String, List<String>> languages = new HashMap();
protected int scriptLimit = 1;
public List<String> hookList = new ArrayList<String>();
protected boolean loaded = false;
protected volatile boolean loaded = false;
protected boolean serverDataReceived = false;
protected String scriptSessionToken = null;
protected Map<Integer, GuiScriptTextArea> 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) {
@@ -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<IScriptUnit> 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);
}
}
@@ -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<Integer, MagicEntry> 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;
}
}
@@ -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;
}
/**
@@ -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;
}
@@ -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;
}
@@ -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<T extends EntityEnergyAbility> 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);
}
}
}
@@ -60,7 +60,7 @@ public class ScriptEnergyBeam<T extends EntityAbilityBeam> 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<T extends EntityAbilityBeam> 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;
@@ -76,7 +76,7 @@ public class ScriptEnergyDisc<T extends EntityAbilityDisc> 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<T extends EntityAbilityDisc> 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;
@@ -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<T extends EntityEnergyExplosion> extends ScriptEnergyAbility<T> 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);
}
}
}
}
@@ -92,7 +92,7 @@ public class ScriptEnergyLaser<T extends EntityAbilityLaser> 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<T extends EntityAbilityLaser> 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;
@@ -28,7 +28,7 @@ public class ScriptEnergyOrb<T extends EntityAbilityOrb> 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<T extends EntityAbilityOrb> 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;
@@ -54,4 +54,23 @@ public class ScriptEnergyPanel<T extends EntityEnergyPanel> 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);
}
}
}
@@ -259,7 +259,7 @@ public class ScriptEnergyProjectile<T extends EntityEnergyProjectile> 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<T extends EntityEnergyProjectile> 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<T extends EntityEnergyProjectile> 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);
}
/**
@@ -48,7 +48,7 @@ public class ScriptEnergySlicer<T extends EntityEnergySlicer> 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<T extends EntityEnergySlicer> 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;
@@ -1,9 +1,116 @@
package noppes.npcs.scripted.entity;
import kamkeel.npcs.entity.EntityEnergySweeper;
import noppes.npcs.api.entity.IEnergySweeper;
public class ScriptEnergySweeper<T extends EntityEnergySweeper> extends ScriptEnergyAbility<T> implements IEnergySweeper {
public class ScriptEnergySweeper<T extends EntityEnergySweeper> extends ScriptEnergyAbility<T> {
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);
}
}
}
@@ -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<T extends EntityAbilityZone> extends ScriptEntity<T> 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);
}
}
}
@@ -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() {
@@ -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)