From 50fd3c185690048844530bb0eca40c45135d2801 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 21:08:59 -0500 Subject: [PATCH 01/13] Fix Quest / Dialog Modification Names --- .../packets/request/dialog/DialogSavePacket.java | 10 ---------- .../network/packets/request/quest/QuestSavePacket.java | 7 ------- .../npcs/client/gui/global/GuiNPCManageDialogs.java | 3 +++ .../npcs/client/gui/global/GuiNPCManageQuest.java | 3 +++ 4 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/request/dialog/DialogSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/dialog/DialogSavePacket.java index 87206c3c9..716a53feb 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/dialog/DialogSavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/dialog/DialogSavePacket.java @@ -13,13 +13,10 @@ import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; -import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.NoppesUtilServer; import noppes.npcs.controllers.DialogController; -import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.data.Dialog; -import noppes.npcs.controllers.data.Quest; import java.io.IOException; @@ -83,13 +80,6 @@ public final class DialogSavePacket extends AbstractPacket { } else { NoppesUtilServer.sendDialogData((EntityPlayerMP) player, dialog.category); } - - // Send full dialog data back so the client doesn't need a second round-trip - NBTTagCompound dialogData = dialog.writeToNBT(new NBTTagCompound()); - Quest quest = QuestController.Instance.quests.get(dialog.quest); - if (quest != null) - dialogData.setString("DialogQuestName", quest.title); - GuiDataPacket.sendGuiData((EntityPlayerMP) player, dialogData); } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/quest/QuestSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/quest/QuestSavePacket.java index 1eafb6c93..9fdfbb78e 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/quest/QuestSavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/quest/QuestSavePacket.java @@ -13,7 +13,6 @@ import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; -import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.NoppesUtilServer; import noppes.npcs.controllers.QuestController; @@ -80,12 +79,6 @@ public final class QuestSavePacket extends AbstractPacket { } else { NoppesUtilServer.sendQuestData((EntityPlayerMP) player, quest.category); } - - // Send full quest data back so the client doesn't need a second round-trip - NBTTagCompound questData = new NBTTagCompound(); - if (quest.hasNewQuest()) - questData.setString("NextQuestTitle", quest.getNextQuest().title); - GuiDataPacket.sendGuiData((EntityPlayerMP) player, quest.writeToNBT(questData)); } } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageDialogs.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageDialogs.java index eb0ede5bf..9267366d0 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageDialogs.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageDialogs.java @@ -411,6 +411,9 @@ public class GuiNPCManageDialogs extends GuiNPCInterface2 implements IScrollData } if (subgui instanceof SubGuiNpcDialog) { saveType(true); + if (dialog != null && dialog.id >= 0) { + setPrevDialogName(dialog.title); + } } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageQuest.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageQuest.java index f3d1679f2..e4e65d3c4 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageQuest.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageQuest.java @@ -401,6 +401,9 @@ public class GuiNPCManageQuest extends GuiNPCInterface2 implements IScrollData, } if (subgui instanceof SubGuiNpcQuest) { saveType(true); + if (quest != null && quest.id >= 0) { + setPrevQuestName(quest.title); + } } } From 90735e6866c01a6e1a23de133e3b772ba1d33e67 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 21:23:40 -0500 Subject: [PATCH 02/13] Scripting Ability Handling Improvement and Copy Data --- src/api | 2 +- .../data/ability/data/ProjectileData.java | 8 +++--- .../npcs/entity/EntityAbilityBeam.java | 2 +- .../npcs/entity/EntityAbilityDisc.java | 2 +- .../npcs/entity/EntityAbilityDome.java | 9 ++++--- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 2 +- .../npcs/entity/EntityAbilityPanel.java | 11 ++++---- .../npcs/entity/EntityAbilitySweeper.java | 2 +- .../npcs/entity/EntityEnergyProjectile.java | 25 +++++++++++++------ .../npcs/entity/EntityEnergySlicer.java | 2 +- .../npcs/scripted/event/AbilityEvent.java | 6 ++++- .../event/player/PlayerAbilityEvent.java | 6 ++++- .../scripted/wrapper/ScriptDataAbilities.java | 21 ++++++++++++++-- .../wrapper/ScriptPlayerAbilityData.java | 3 ++- 14 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/api b/src/api index 7f95b90fc..34a7ea020 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 7f95b90fc2ff1238df3e902fd55c804ffbdaacbc +Subproject commit 34a7ea020b380358c5f64a2e6d371f343942c6ef diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java index acb16a2a9..a54f526ee 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java @@ -26,10 +26,12 @@ public class ProjectileData { * If colorOverride is true, returns a copy with overridden inner/outer colors. */ public EnergyDisplayData resolveDisplay(EnergyDisplayData primary) { - if (!colorOverride) return primary; + // Always return a copy so entity mutations never bleed back into the ability template. EnergyDisplayData resolved = primary.copy(); - resolved.innerColor = this.innerColor; - resolved.outerColor = this.outerColor; + if (colorOverride) { + resolved.innerColor = this.innerColor; + resolved.outerColor = this.outerColor; + } return resolved; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index e62a52232..a3f5f874a 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -147,7 +147,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { // Beam-specific properties from homing data - this.homingData = homing; + this.homingData = homing != null ? homing.copy() : new EnergyHomingData(); this.beamWidth = beamWidth; this.headSize = headSize; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 56b3c6b7d..44ba4c69e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -64,7 +64,7 @@ public class EntityAbilityDisc extends EntityEnergyProjectile { // Initialize base properties initProjectile(owner, target, x, y, z, 1.0f, display, combat, lightning, lifespan); - this.homingData = homing; + this.homingData = homing != null ? homing.copy() : new EnergyHomingData(); this.boomerang = boomerang; this.boomerangDelay = boomerangDelay; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java index 53bfe8358..383f07246 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java @@ -50,10 +50,11 @@ public class EntityAbilityDome extends EntityAbilityBarrier { this.stepHeight = 0.0F; this.ownerEntityId = owner.getEntityId(); this.setDomeRadius(domeRadius); - this.displayData = display; - this.lightningData = lightning; - this.barrierData = barrier; - this.currentHealth = barrier.maxHealth; + // Defensive copy: entities must never share data objects with the source ability. + this.displayData = display != null ? display.copy() : new EnergyDisplayData(); + this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData(); + this.barrierData = barrier != null ? barrier.copy() : new EnergyBarrierData(); + this.currentHealth = this.barrierData.maxHealth; this.setPosition(x, y, z); } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index a7f4f2af9..f7b9cd7e7 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -38,7 +38,7 @@ public class EntityAbilityOrb extends EntityEnergyProjectile { // Initialize base properties initProjectile(owner, target, x, y, z, orbSize, display, combat, lightning, lifespan); - this.homingData = homing; + this.homingData = homing != null ? homing.copy() : new EnergyHomingData(); // Calculate initial velocity toward target calculateInitialVelocity(owner, target, x, y, z); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java index b34502e8a..200fe922b 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java @@ -56,11 +56,12 @@ public class EntityAbilityPanel extends EntityAbilityBarrier { this.panelYaw = yaw; this.prevPanelYaw = yaw; this.mode = mode; - this.displayData = display; - this.lightningData = lightning; - this.barrierData = barrier; - this.panelData = panel; - this.currentHealth = barrier.maxHealth; + // Defensive copy: entities must never share data objects with the source ability. + this.displayData = display != null ? display.copy() : new EnergyDisplayData(); + this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData(); + this.barrierData = barrier != null ? barrier.copy() : new EnergyBarrierData(); + this.panelData = panel != null ? panel.copy() : new EnergyPanelData(); + this.currentHealth = this.barrierData.maxHealth; // For launched panels, set initial velocity if (mode == PanelMode.LAUNCHED) { diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index 65cb1ce98..f03324618 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -79,7 +79,7 @@ public class EntityAbilitySweeper extends EntityEnergyAbility { this.beamLength = beamLength; this.beamWidth = beamWidth; this.beamHeight = beamHeight; - this.displayData = displayData; + this.displayData = displayData != null ? displayData.copy() : new EnergyDisplayData(); this.sweepSpeed = sweepSpeed; this.numberOfRotations = numberOfRotations; this.damage = damage; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index ff7a66c5e..bec0da39e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -183,10 +183,13 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // Visual this.size = size; - this.displayData = display; - this.combatData = combat; - this.lifespanData = lifespan; // deathWorldTime will be set on first tick when world is available - this.lightningData = lightning; + // Defensive copy: entities must never share data objects with the source ability. + // Without copies, runtime mutations (e.g. barrier reflection changing colors/damage) + // bleed back into the ability template and corrupt future projectiles. + this.displayData = display != null ? display.copy() : new EnergyDisplayData(); + this.combatData = combat != null ? combat.copy() : new EnergyCombatData(); + this.lifespanData = lifespan != null ? lifespan.copy() : new EnergyLifespanData(); + this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData(); syncProjectileColorWatchers(); // Initialize render size @@ -911,7 +914,15 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { * Set the effects list from the ability's configured effects. */ public void setEffects(List effects) { - this.effects = effects != null ? effects : new ArrayList<>(); + if (effects == null || effects.isEmpty()) { + this.effects = new ArrayList<>(); + } else { + // Deep copy: entity must not share the ability's effects list or its entries. + this.effects = new ArrayList<>(effects.size()); + for (AbilityPotionEffect effect : effects) { + this.effects.add(effect.copy()); + } + } } protected void doExplosion() { @@ -1518,8 +1529,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { int chargeDuration) { this.setPreviewMode(true); this.setPreviewOwner(owner); - this.displayData = display; - this.lightningData = lightning; + this.displayData = display != null ? display.copy() : new EnergyDisplayData(); + this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData(); setupChargingState(anchor, chargeDuration); } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java index 8a6d1cb96..96f53f8ca 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java @@ -43,7 +43,7 @@ public class EntityEnergySlicer extends EntityEnergyProjectile { super(world); initProjectile(owner, target, x, y, z, sliceWidth, display, combat, lightning, lifespan); - this.homingData = homing; + this.homingData = homing != null ? homing.copy() : new EnergyHomingData(); this.sliceWidth = sliceWidth; this.sliceThickness = sliceThickness; diff --git a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java index c7a43cfe7..769add405 100644 --- a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java @@ -18,6 +18,7 @@ import noppes.npcs.scripted.NpcAPI; public class AbilityEvent extends NpcEvent implements IAbilityEvent { protected final Ability ability; protected final IEntityLivingBase target; + private transient IAbility abilityCopy; public AbilityEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) { super(npc); @@ -27,7 +28,10 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { @Override public IAbility getAbility() { - return ability; + if (abilityCopy == null && ability != null) { + abilityCopy = ability.deepCopy(); + } + return abilityCopy; } @Override diff --git a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java index e5d46fd15..95bda90e6 100644 --- a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java @@ -18,6 +18,7 @@ import noppes.npcs.scripted.NpcAPI; public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEvent { protected final Ability ability; protected final IEntityLivingBase target; + private transient IAbility abilityCopy; public PlayerAbilityEvent(IPlayer player, Ability ability, EntityLivingBase target) { super(player); @@ -27,7 +28,10 @@ public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEve @Override public IAbility getAbility() { - return ability; + if (abilityCopy == null && ability != null) { + abilityCopy = ability.deepCopy(); + } + return abilityCopy; } @Override diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java index 8e0bda906..d475c40ef 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -36,7 +36,12 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public IAbility[] getAbilities() { - return data.getAbilities().toArray(new IAbility[0]); + List live = data.getAbilities(); + IAbility[] copies = new IAbility[live.size()]; + for (int i = 0; i < live.size(); i++) { + copies[i] = live.get(i).deepCopy(); + } + return copies; } @Override @@ -73,7 +78,8 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public IAbility getAbility(String abilityId) { - return data.getAbility(abilityId); + Ability a = data.getAbility(abilityId); + return a != null ? a.deepCopy() : null; } @Override @@ -115,6 +121,17 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public IAbility getCurrentAbility() { + Ability a = data.getCurrentAbility(); + return a != null ? a.deepCopy() : null; + } + + @Override + public IAbility getSourceAbility(String abilityId) { + return data.getAbility(abilityId); + } + + @Override + public IAbility getSourceCurrentAbility() { return data.getCurrentAbility(); } diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java index 70d915537..1860aa259 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -85,7 +85,8 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { @Override public IAbility getCurrentAbility() { - return data.getCurrentAbility(); + Ability a = data.getCurrentAbility(); + return a != null ? a.deepCopy() : null; } @Override From 6dce4e5cdc0160a790d1b747d0e8f7bc5af5b36f Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 22:28:57 -0500 Subject: [PATCH 03/13] Revert "Smoother + Most-Cost Effective Flight Pathing" This reverts commit a79ba3a0e479e810f259d70869a54407b6250b7a. --- .../npcs/ai/pathfinder/FlyPathFinder.java | 21 +--- .../ai/pathfinder/PathNavigateFlying.java | 99 ++----------------- .../noppes/npcs/entity/EntityNPCFlying.java | 6 +- 3 files changed, 13 insertions(+), 113 deletions(-) diff --git a/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java b/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java index af80c4a28..cc5758ce6 100644 --- a/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java +++ b/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java @@ -173,8 +173,7 @@ public class FlyPathFinder extends PathFinder { for (int j = 0; j < i; ++j) { NPCPathPoint pathpoint5 = this.pathOptions[j]; - float f1 = pathpoint4.totalPathDistance + this.getTraversalCost(pathpoint4, pathpoint5) - + this.getDirectionalPenalty(pathpoint4, pathpoint5, p_75861_3_); + float f1 = pathpoint4.totalPathDistance + this.getTraversalCost(pathpoint4, pathpoint5); if (!pathpoint5.isAssigned() || f1 < pathpoint5.totalPathDistance) { pathpoint5.previous = pathpoint4; @@ -212,24 +211,6 @@ public class FlyPathFinder extends PathFinder { return DIAGONAL_3D_COST; } - private float getDirectionalPenalty(NPCPathPoint from, NPCPathPoint to, NPCPathPoint goal) { - int stepX = to.xCoord - from.xCoord; - int stepY = to.yCoord - from.yCoord; - int stepZ = to.zCoord - from.zCoord; - int goalX = goal.xCoord - from.xCoord; - int goalY = goal.yCoord - from.yCoord; - int goalZ = goal.zCoord - from.zCoord; - int stepLen = Math.abs(stepX) + Math.abs(stepY) + Math.abs(stepZ); - int goalLen = Math.abs(goalX) + Math.abs(goalY) + Math.abs(goalZ); - - if (stepLen == 0 || goalLen == 0) { - return 0.0F; - } - - float alignment = (float) (stepX * goalX + stepY * goalY + stepZ * goalZ) / (float) (stepLen * goalLen); - return (1.0F - alignment) * 0.04F; - } - private float estimateCostToTarget(NPCPathPoint from, NPCPathPoint to) { float dx = Math.abs(to.xCoord - from.xCoord); float dy = Math.abs(to.yCoord - from.yCoord); diff --git a/src/main/java/noppes/npcs/ai/pathfinder/PathNavigateFlying.java b/src/main/java/noppes/npcs/ai/pathfinder/PathNavigateFlying.java index 90856dc79..df2e4c868 100644 --- a/src/main/java/noppes/npcs/ai/pathfinder/PathNavigateFlying.java +++ b/src/main/java/noppes/npcs/ai/pathfinder/PathNavigateFlying.java @@ -21,10 +21,6 @@ import noppes.npcs.entity.EntityNPCInterface; import javax.annotation.Nullable; public class PathNavigateFlying extends PathNavigate { - private static final long MIN_REPATH_INTERVAL_TICKS = 8L; - private static final long MAX_REPATH_INTERVAL_TICKS = 30L; - private static final int PATH_REUSE_TARGET_DELTA_SQ = 2; - private static final int FINAL_NODE_REUSE_DELTA_SQ = 3; private EntityNPCInterface theEntity; private World worldObj; @@ -87,21 +83,16 @@ public class PathNavigateFlying extends PathNavigate { } public void updatePath() { - if (this.targetPos == null) { - this.tryUpdatePath = false; - return; - } - - long now = this.worldObj.getTotalWorldTime(); - if (now - this.lastTimeUpdated < this.getAdaptiveRepathInterval()) { + if (this.worldObj.getTotalWorldTime() - this.lastTimeUpdated > 20L) { + if (this.targetPos != null) { + this.currentPath = null; + this.currentPath = this.getPathToXYZ(this.targetPos.getX(), this.targetPos.getY(), this.targetPos.getZ()); + this.lastTimeUpdated = this.worldObj.getTotalWorldTime(); + this.tryUpdatePath = false; + } + } else { this.tryUpdatePath = true; - return; } - - this.currentPath = null; - this.currentPath = this.getPathToXYZ(this.targetPos.getX(), this.targetPos.getY(), this.targetPos.getZ()); - this.lastTimeUpdated = now; - this.tryUpdatePath = false; } public void setAvoidsWater(boolean p_75491_1_) { @@ -165,8 +156,7 @@ public class PathNavigateFlying extends PathNavigate { BlockPos pos = new BlockPos(x, y, z); if (!this.canNavigate()) { return null; - } else if (this.canReuseCurrentPath(pos)) { - this.targetPos = pos; + } else if (this.currentPath != null && !this.currentPath.isFinished() && pos.equals(this.targetPos)) { return this.currentPath; } else { this.targetPos = pos; @@ -190,8 +180,7 @@ public class PathNavigateFlying extends PathNavigate { return null; } else { BlockPos blockpos = new BlockPos(entity); - if (this.canReuseCurrentPath(blockpos)) { - this.targetPos = blockpos; + if (this.currentPath != null && !this.currentPath.isFinished() && blockpos.equals(this.targetPos)) { return this.currentPath; } else { this.targetPos = blockpos; @@ -226,8 +215,6 @@ public class PathNavigateFlying extends PathNavigate { this.removeSunnyPath(); } - this.smoothCurrentPath(); - if (this.currentPath.getCurrentPathLength() == 0) { return false; } else { @@ -255,72 +242,6 @@ public class PathNavigateFlying extends PathNavigate { return !this.isInLiquid() || (this.canSwim && this.isInLiquid()); } - private long getAdaptiveRepathInterval() { - if (this.targetPos == null) { - return MAX_REPATH_INTERVAL_TICKS; - } - double distance = this.theEntity.getDistance(this.targetPos.getX() + 0.5D, this.targetPos.getY(), this.targetPos.getZ() + 0.5D); - return (long) MathHelper.clamp_double(distance * 0.6D, MIN_REPATH_INTERVAL_TICKS, MAX_REPATH_INTERVAL_TICKS); - } - - private boolean canReuseCurrentPath(BlockPos newTarget) { - if (this.currentPath == null || this.currentPath.isFinished() || this.targetPos == null) { - return false; - } - - if (this.getBlockDistanceSq(this.targetPos, newTarget) <= PATH_REUSE_TARGET_DELTA_SQ) { - return true; - } - - NPCPathPoint finalPoint = this.currentPath.getFinalPathPoint(); - return finalPoint != null && this.getPointDistanceSq(finalPoint, newTarget) <= FINAL_NODE_REUSE_DELTA_SQ; - } - - private int getBlockDistanceSq(BlockPos a, BlockPos b) { - int dx = a.getX() - b.getX(); - int dy = a.getY() - b.getY(); - int dz = a.getZ() - b.getZ(); - return dx * dx + dy * dy + dz * dz; - } - - private int getPointDistanceSq(NPCPathPoint point, BlockPos blockPos) { - int dx = point.xCoord - blockPos.getX(); - int dy = point.yCoord - blockPos.getY(); - int dz = point.zCoord - blockPos.getZ(); - return dx * dx + dy * dy + dz * dz; - } - - private void smoothCurrentPath() { - if (this.currentPath == null) { - return; - } - int pathLength = this.currentPath.getCurrentPathLength(); - if (pathLength < 3) { - return; - } - - int width = MathHelper.ceiling_double_int(this.theEntity.width); - int height = MathHelper.ceiling_double_int(this.theEntity.height + 1.0F); - NPCPathPoint[] points = this.currentPath.points; - int writeIndex = 1; - int anchorIndex = 0; - - for (int readIndex = 2; readIndex < pathLength; readIndex++) { - Vec3 anchor = this.currentPath.getVectorFromIndex(this.theEntity, anchorIndex); - Vec3 probe = this.currentPath.getVectorFromIndex(this.theEntity, readIndex); - if (!this.isDirectPathBetweenPoints(anchor, probe, width, height)) { - points[writeIndex++] = points[readIndex - 1]; - anchorIndex = readIndex - 1; - } - } - - points[writeIndex++] = points[pathLength - 1]; - this.currentPath.setCurrentPathLength(writeIndex); - if (this.currentPath.getCurrentPathIndex() >= writeIndex) { - this.currentPath.setCurrentPathIndex(Math.max(0, writeIndex - 1)); - } - } - public void onUpdateNavigation() { ++this.totalTicks; diff --git a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java index 6e5923e7d..c41e0e843 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java @@ -91,13 +91,11 @@ public abstract class EntityNPCFlying extends EntityNPCInterface { this.prevLimbSwingAmount = this.limbSwingAmount; double distanceX = this.posX - this.prevPosX; double distanceZ = this.posZ - this.prevPosZ; - float distance = MathHelper.sqrt_double(distanceX * distanceX + distanceZ * distanceZ); - float swingScale = this.getNavigator().noPath() ? 0.35F : 0.9F; - distance *= swingScale; + float distance = MathHelper.sqrt_double(distanceX * distanceX + distanceZ * distanceZ) * 4.0F; if (distance > 1.0F) { distance = 1.0F; } - this.limbSwingAmount += (distance - this.limbSwingAmount) * 0.18F; + this.limbSwingAmount += (distance - this.limbSwingAmount) * 0.4F; this.limbSwing += this.limbSwingAmount; } From 455d3039710335105eeda8da3c9bfcb59f5aa894 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 22:28:59 -0500 Subject: [PATCH 04/13] Revert "Better Diagonal Flight Pathing" This reverts commit 960b22315552fe8fd88a9df6b976f72842156ac4. --- .../npcs/ai/pathfinder/FlyPathFinder.java | 37 ++----------------- .../npcs/ai/pathfinder/FlyingMoveHelper.java | 24 +++++------- 2 files changed, 14 insertions(+), 47 deletions(-) diff --git a/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java b/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java index cc5758ce6..ebe00c28e 100644 --- a/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java +++ b/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java @@ -25,9 +25,6 @@ import java.util.Map; import java.util.Set; public class FlyPathFinder extends PathFinder { - private static final float STRAIGHT_COST = 1.0F; - private static final float DIAGONAL_2D_COST = 1.4142135F; - private static final float DIAGONAL_3D_COST = 1.7320508F; /** * Used to find obstacles */ @@ -144,7 +141,7 @@ public class FlyPathFinder extends PathFinder { private NPCPath addToPath(Entity p_75861_1_, NPCPathPoint p_75861_2_, NPCPathPoint p_75861_3_, NPCPathPoint p_75861_4_, float p_75861_5_) { p_75861_2_.totalPathDistance = 0.0F; - p_75861_2_.distanceToNext = this.estimateCostToTarget(p_75861_2_, p_75861_3_); + p_75861_2_.distanceToNext = p_75861_2_.distanceToSquared(p_75861_3_); p_75861_2_.distanceToTarget = p_75861_2_.distanceToNext; this.path.clearPath(); this.path.addPoint(p_75861_2_); @@ -164,7 +161,7 @@ public class FlyPathFinder extends PathFinder { return this.createEntityPath(p_75861_2_, p_75861_3_); } - if (this.estimateCostToTarget(pathpoint4, p_75861_3_) < this.estimateCostToTarget(pathpoint3, p_75861_3_)) { + if (pathpoint4.distanceToSquared(p_75861_3_) < pathpoint3.distanceToSquared(p_75861_3_)) { pathpoint3 = pathpoint4; } @@ -173,12 +170,12 @@ public class FlyPathFinder extends PathFinder { for (int j = 0; j < i; ++j) { NPCPathPoint pathpoint5 = this.pathOptions[j]; - float f1 = pathpoint4.totalPathDistance + this.getTraversalCost(pathpoint4, pathpoint5); + float f1 = pathpoint4.totalPathDistance + pathpoint4.distanceToSquared(pathpoint5); if (!pathpoint5.isAssigned() || f1 < pathpoint5.totalPathDistance) { pathpoint5.previous = pathpoint4; pathpoint5.totalPathDistance = f1; - pathpoint5.distanceToNext = this.estimateCostToTarget(pathpoint5, p_75861_3_); + pathpoint5.distanceToNext = pathpoint5.distanceToSquared(p_75861_3_); if (pathpoint5.isAssigned()) { this.path.changeDistance(pathpoint5, pathpoint5.totalPathDistance + pathpoint5.distanceToNext); @@ -197,32 +194,6 @@ public class FlyPathFinder extends PathFinder { } } - private float getTraversalCost(NPCPathPoint from, NPCPathPoint to) { - int dx = Math.abs(to.xCoord - from.xCoord); - int dy = Math.abs(to.yCoord - from.yCoord); - int dz = Math.abs(to.zCoord - from.zCoord); - int changedAxes = dx + dy + dz; - if (changedAxes <= 1) { - return STRAIGHT_COST; - } - if (changedAxes == 2) { - return DIAGONAL_2D_COST; - } - return DIAGONAL_3D_COST; - } - - private float estimateCostToTarget(NPCPathPoint from, NPCPathPoint to) { - float dx = Math.abs(to.xCoord - from.xCoord); - float dy = Math.abs(to.yCoord - from.yCoord); - float dz = Math.abs(to.zCoord - from.zCoord); - - float max = Math.max(dx, Math.max(dy, dz)); - float min = Math.min(dx, Math.min(dy, dz)); - float mid = dx + dy + dz - max - min; - - return DIAGONAL_3D_COST * min + DIAGONAL_2D_COST * (mid - min) + STRAIGHT_COST * (max - mid); - } - /** * populates pathOptions with available points and returns the number of options found (args: unused1, currentPoint, * unused2, targetPoint, maxDistance) diff --git a/src/main/java/noppes/npcs/ai/pathfinder/FlyingMoveHelper.java b/src/main/java/noppes/npcs/ai/pathfinder/FlyingMoveHelper.java index 18129de11..dd59a22de 100644 --- a/src/main/java/noppes/npcs/ai/pathfinder/FlyingMoveHelper.java +++ b/src/main/java/noppes/npcs/ai/pathfinder/FlyingMoveHelper.java @@ -7,8 +7,6 @@ import noppes.npcs.entity.EntityNPCFlying; import noppes.npcs.entity.EntityNPCInterface; public class FlyingMoveHelper extends EntityMoveHelper { - private static final double CLOSE_ENOUGH_SQ = 0.01D; - private static final float MAX_TURN_PER_TICK = 35.0F; private final EntityNPCInterface entity; private double posX; @@ -40,24 +38,22 @@ public class FlyingMoveHelper extends EntityMoveHelper { double d5 = MathHelper.sqrt_double(d4); speed = Math.min(d5 / 5.0D, speed); - if (d4 > CLOSE_ENOUGH_SQ && d5 > 1.0E-4D) { - double targetMotionX = speed * (d0 / d5); - double targetMotionZ = speed * (d2 / d5); - double horizontalLerp = Math.min(1.0D, Math.max(0.12D, speed * 0.6D)); - this.entity.motionX += (targetMotionX - this.entity.motionX) * horizontalLerp; - this.entity.motionZ += (targetMotionZ - this.entity.motionZ) * horizontalLerp; + // its here louis! + // This is the hurt time that stuns the NPC!!!!!! + // if (this.entity.hurtTime == 0 && d4 > 0.5D) { + if (d4 >= 0.5D) { + this.entity.motionX += (speed * (d0 / d5) - this.entity.motionX) * speed; + this.entity.motionZ += (speed * (d2 / d5) - this.entity.motionZ) * speed; if (((EntityNPCFlying) this.entity).flyLimitAllow || !this.entity.ais.hasFlyLimit) { - double targetMotionY = verticalSpeed * (d1 / d5); - if (targetMotionY > 0.0D) { - targetMotionY += 0.05D; + this.entity.motionY = verticalSpeed * (d1 / d5); + if (this.entity.motionY > 0) { + this.entity.motionY += 0.1D; } - double verticalLerp = Math.min(1.0D, Math.max(0.15D, verticalSpeed * 0.85D)); - this.entity.motionY += (targetMotionY - this.entity.motionY) * verticalLerp; } this.entity.velocityChanged = true; - this.entity.rotationYaw = this.limitAngle(this.entity.rotationYaw, (float) ((Math.atan2(-d0, -d2) + Math.PI) * -(180F / Math.PI)), MAX_TURN_PER_TICK); + this.entity.rotationYaw = this.limitAngle(this.entity.rotationYaw, (float) ((Math.atan2(-d0, -d2) + Math.PI) * -(180F / Math.PI)), 20.0F); } } } From 92cfbbcc9db32f60d608ac6a6205922fedcd1a3d Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 22:30:19 -0500 Subject: [PATCH 05/13] Fix Panels in Ground Issue --- src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java index 200fe922b..a9bae51a6 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java @@ -176,7 +176,10 @@ public class EntityAbilityPanel extends EntityAbilityBarrier { if (worldObj != null && worldObj.isRemote && mode == PanelMode.HELD && getOwnerEntity() != null) { return; } - super.setPositionAndRotation2(x, y, z, yaw, pitch, posRotationIncrements); + // Bypass vanilla's lerp + push-out-of-blocks logic which causes + // large-BB entities (PLACED/LAUNCHED panels) to shift into the sky. + this.setPosition(x, y, z); + this.setRotation(yaw, pitch); } private void updateLaunched() { From 47ebc2889f57405df1082a66be85512a30f14498 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 23:18:59 -0500 Subject: [PATCH 06/13] Reflection Barrier Fixes --- .../type/energy/AbilityEnergyProjectile.java | 16 ++++ .../npcs/entity/EntityAbilityBeam.java | 44 ++++++++++ .../npcs/entity/EntityAbilityDisc.java | 26 ++++++ .../npcs/entity/EntityAbilityLaser.java | 38 ++++++++- .../npcs/entity/EntityEnergyProjectile.java | 81 +++++++++++++++++++ .../kamkeel/npcs/network/PacketHandler.java | 2 + .../npcs/network/enums/EnumDataPacket.java | 3 + .../data/energy/ProjectileReflectPacket.java | 79 ++++++++++++++++++ 8 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/data/energy/ProjectileReflectPacket.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index fdf2d827e..e0e096b3c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -18,6 +18,7 @@ import kamkeel.npcs.entity.EntityEnergyProjectile; import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeRemovePacket; import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket; import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -341,6 +342,21 @@ public abstract class AbilityEnergyProjectile spawnedCount = 0; } + /** + * Remove a specific entity from this ability's tracking. + * Used when a projectile is reflected — it no longer belongs to this ability, + * so the ability should stop waiting for it and free the caster. + */ + public void detachEntity(Entity entity) { + if (entities == null || entity == null) return; + for (int i = 0; i < entities.length; i++) { + if (entities[i] == entity) { + entities[i] = null; + break; + } + } + } + @Override public boolean isReadyForBurstCompletion(int activeTick) { return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index a3f5f874a..33d6e67bc 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -637,6 +637,50 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { return trailFadeTime; } + // ==================== REFLECTION ==================== + + @Override + protected boolean reflectFromBarrier(EntityAbilityBarrier barrier, float reflectStrengthPct) { + boolean reflected = super.reflectFromBarrier(barrier, reflectStrengthPct); + if (reflected) { + // Switch to FREE_TRAIL: detach from caster (tail-less), trail fades out. + setBeamMode(BeamMode.FREE_TRAIL); + + // Reset origin to the reflection point (current head position). + startX = posX; + startY = posY; + startZ = posZ; + resetHeadOffsets(); + resetTrailStorage(); + + // Disable homing — reflected beams fly straight. + homingData.setHoming(false); + setTargetEntityId(-1); + } + return reflected; + } + + @Override + protected void writeProjectileReflectionData(NBTTagCompound nbt) { + nbt.setByte("BeamMode", (byte) beamMode.ordinal()); + nbt.setDouble("StartX", startX); + nbt.setDouble("StartY", startY); + nbt.setDouble("StartZ", startZ); + } + + @Override + protected void applyProjectileReflectionData(NBTTagCompound nbt) { + int mode = nbt.getByte("BeamMode"); + setBeamMode(mode >= 0 && mode < BeamMode.values().length ? BeamMode.values()[mode] : BeamMode.FREE_TRAIL); + startX = nbt.getDouble("StartX"); + startY = nbt.getDouble("StartY"); + startZ = nbt.getDouble("StartZ"); + resetHeadOffsets(); + resetTrailStorage(); + homingData.setHoming(false); + setTargetEntityId(-1); + } + // ==================== NBT ==================== @Override diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 44ba4c69e..a7ff4ef99 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -395,6 +395,32 @@ public class EntityAbilityDisc extends EntityEnergyProjectile { return lastTravelYaw; } + // ==================== REFLECTION ==================== + + @Override + protected boolean reflectFromBarrier(EntityAbilityBarrier barrier, float reflectStrengthPct) { + boolean reflected = super.reflectFromBarrier(barrier, reflectStrengthPct); + if (reflected) { + // Disable boomerang on reflected discs — the original owner is no longer + // relevant and the return-to-owner logic causes erratic flight paths. + this.boomerang = false; + this.returning = false; + } + return reflected; + } + + @Override + protected void writeProjectileReflectionData(NBTTagCompound nbt) { + nbt.setBoolean("Boomerang", boomerang); + nbt.setBoolean("Returning", returning); + } + + @Override + protected void applyProjectileReflectionData(NBTTagCompound nbt) { + boomerang = nbt.getBoolean("Boomerang"); + returning = nbt.getBoolean("Returning"); + } + // ==================== NBT ==================== @Override diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index c1ec94d48..d573e19fb 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -134,9 +134,10 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { } // After reflection, hold trajectory briefly so the rebound is visible. + // Once reflected, never resume owner-tracking — the laser flies in a fixed direction. if (reflectedLockTicks > 0) { reflectedLockTicks--; - } else { + } else if (!reflected) { // Track owner's anchor position and rotation each tick // so the laser follows the NPC's facing direction updateLaserOriginAndDirection(); @@ -717,6 +718,41 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.endZ = posZ; } + // ==================== REFLECTION SYNC ==================== + + @Override + protected void writeProjectileReflectionData(NBTTagCompound nbt) { + nbt.setDouble("DirX", dirX); + nbt.setDouble("DirY", dirY); + nbt.setDouble("DirZ", dirZ); + nbt.setDouble("StartX", startX); + nbt.setDouble("StartY", startY); + nbt.setDouble("StartZ", startZ); + nbt.setFloat("CurrentLength", currentLength); + nbt.setDouble("EndX", endX); + nbt.setDouble("EndY", endY); + nbt.setDouble("EndZ", endZ); + nbt.setInteger("ReflectedLockTicks", reflectedLockTicks); + nbt.setBoolean("FullyExtended", fullyExtended); + } + + @Override + protected void applyProjectileReflectionData(NBTTagCompound nbt) { + dirX = nbt.getDouble("DirX"); + dirY = nbt.getDouble("DirY"); + dirZ = nbt.getDouble("DirZ"); + startX = nbt.getDouble("StartX"); + startY = nbt.getDouble("StartY"); + startZ = nbt.getDouble("StartZ"); + currentLength = nbt.getFloat("CurrentLength"); + endX = nbt.getDouble("EndX"); + endY = nbt.getDouble("EndY"); + endZ = nbt.getDouble("EndZ"); + reflectedLockTicks = nbt.getInteger("ReflectedLockTicks"); + fullyExtended = nbt.getBoolean("FullyExtended"); + ticksSinceFullExtension = 0; + } + // ==================== NBT ==================== @Override diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index bec0da39e..8e9a4de1b 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -3,6 +3,7 @@ package kamkeel.npcs.entity; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityEnergyProjectile; import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect; import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint; import kamkeel.npcs.controllers.data.ability.enums.HitType; @@ -12,6 +13,7 @@ import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; 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 net.minecraft.block.Block; @@ -120,6 +122,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { protected int targetEntityId = -1; // ==================== STATE ==================== + protected boolean reflected = false; // Set after barrier reflection; prevents owner-tracking on reflected projectiles protected boolean hasHit = false; protected int hitCount = 0; protected final Set hitOnceEntities = new HashSet(); @@ -427,6 +430,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (hitOutcome.shouldReflect()) { if (!reflectFromBarrier(barrier, hitOutcome.reflectStrengthPct)) { beginBarrierImpactPause(true, barrier); + } else { + sendReflectionSync(); } return true; } @@ -498,6 +503,14 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { barrierImpactPauseTicks = 0; barrierImpactDestroyOnResume = false; + // Detach from source ability BEFORE changing ownership. + // This lets the ability see the projectile as gone and free the caster + // (e.g., release movement/rotation locks on the player). + if (sourceAbility instanceof AbilityEnergyProjectile) { + ((AbilityEnergyProjectile) sourceAbility).detachEntity(this); + } + sourceAbility = null; + Entity barrierOwner = barrier.getOwnerEntity(); if (barrierOwner != null) { setOwnerEntityId(barrierOwner.getEntityId()); @@ -517,9 +530,75 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { hitOnceEntities.clear(); lastHitTickByEntity.clear(); hitCount = 0; + reflected = true; return true; } + // ==================== REFLECTION SYNC ==================== + + /** + * Write reflection state for client sync. + * Subclasses should override writeProjectileReflectionData for type-specific fields. + */ + public NBTTagCompound writeReflectionData() { + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setDouble("PosX", posX); + nbt.setDouble("PosY", posY); + nbt.setDouble("PosZ", posZ); + nbt.setDouble("MotionX", motionX); + nbt.setDouble("MotionY", motionY); + nbt.setDouble("MotionZ", motionZ); + nbt.setInteger("InnerColor", getInnerColor()); + nbt.setInteger("OuterColor", getOuterColor()); + nbt.setBoolean("Reflected", reflected); + writeProjectileReflectionData(nbt); + return nbt; + } + + /** + * Apply reflection state from server sync. + * Subclasses should override applyProjectileReflectionData for type-specific fields. + */ + public void applyReflectionData(NBTTagCompound nbt) { + ownerEntityId = nbt.getInteger("OwnerId"); + double px = nbt.getDouble("PosX"); + double py = nbt.getDouble("PosY"); + double pz = nbt.getDouble("PosZ"); + setPosition(px, py, pz); + motionX = nbt.getDouble("MotionX"); + motionY = nbt.getDouble("MotionY"); + motionZ = nbt.getDouble("MotionZ"); + setInnerColor(nbt.getInteger("InnerColor")); + setOuterColor(nbt.getInteger("OuterColor")); + reflected = nbt.getBoolean("Reflected"); + syncPositionStateToCurrent(true); + hitOnceEntities.clear(); + lastHitTickByEntity.clear(); + hitCount = 0; + applyProjectileReflectionData(nbt); + } + + /** + * Subclass hook: write type-specific reflection data. + */ + protected void writeProjectileReflectionData(NBTTagCompound nbt) { + } + + /** + * Subclass hook: apply type-specific reflection data on client. + */ + protected void applyProjectileReflectionData(NBTTagCompound nbt) { + } + + /** + * Send reflection state to all tracking clients. + */ + protected void sendReflectionSync() { + if (worldObj == null || worldObj.isRemote) return; + ProjectileReflectPacket.sendToTracking(this, writeReflectionData()); + } + protected double[] getBarrierImpactNormal(EntityAbilityBarrier barrier, double velocityX, double velocityY, double velocityZ) { if (barrier instanceof EntityAbilityDome) { EntityAbilityDome dome = (EntityAbilityDome) barrier; @@ -2187,6 +2266,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { this.startZ = nbt.getDouble("StartZ"); this.targetEntityId = nbt.getInteger("TargetId"); + this.reflected = nbt.getBoolean("Reflected"); this.motionX = nbt.getDouble("MotionX"); this.motionY = nbt.getDouble("MotionY"); @@ -2243,6 +2323,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { nbt.setDouble("StartZ", startZ); nbt.setInteger("TargetId", targetEntityId); + nbt.setBoolean("Reflected", reflected); nbt.setDouble("MotionX", motionX); nbt.setDouble("MotionY", motionY); diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 787c6e204..a66f56fc4 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -39,6 +39,7 @@ import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeRemovePacket; import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket; import kamkeel.npcs.network.packets.data.energyexplosion.EnergyExplosionSpawnPacket; +import kamkeel.npcs.network.packets.data.energy.ProjectileReflectPacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.player.AuctionActionPacket; @@ -592,6 +593,7 @@ public class PacketHandler { DATA_PACKET.registerPacket(new EnergyChargeSpawnPacket()); DATA_PACKET.registerPacket(new EnergyChargeRemovePacket()); DATA_PACKET.registerPacket(new EnergyExplosionSpawnPacket()); + DATA_PACKET.registerPacket(new ProjectileReflectPacket()); DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket()); DATA_PACKET.registerPacket(new PlayerAbilityStatePacket()); DATA_PACKET.registerPacket(new AbilityHotbarSyncPacket()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index 10ae639e9..8a33b223e 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -76,4 +76,7 @@ public enum EnumDataPacket { // Energy Explosion Preview ENERGY_EXPLOSION_SPAWN, + + // Energy Projectile Reflection Sync + PROJECTILE_REFLECT, } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/energy/ProjectileReflectPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/energy/ProjectileReflectPacket.java new file mode 100644 index 000000000..3950b0b28 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/energy/ProjectileReflectPacket.java @@ -0,0 +1,79 @@ +package kamkeel.npcs.network.packets.data.energy; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.World; + +import java.io.IOException; + +/** + * Syncs projectile reflection state to all tracking clients. + * Sent server→client after a successful barrier reflection so the client + * can update ownerEntityId, position, motion, colors, and type-specific + * reflection data (beam mode, laser direction, disc boomerang, etc.). + */ +public final class ProjectileReflectPacket extends AbstractPacket { + public static final String packetName = "Data|ProjectileReflect"; + + private int entityId; + private NBTTagCompound reflectData; + + public ProjectileReflectPacket() { + } + + public ProjectileReflectPacket(int entityId, NBTTagCompound reflectData) { + this.entityId = entityId; + this.reflectData = reflectData; + } + + @Override + public Enum getType() { + return EnumDataPacket.PROJECTILE_REFLECT; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(entityId); + ByteBufUtils.writeNBT(out, reflectData); + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + int id = in.readInt(); + NBTTagCompound nbt = ByteBufUtils.readNBT(in); + if (nbt == null) return; + + World world = Minecraft.getMinecraft().theWorld; + if (world == null) return; + + Entity entity = world.getEntityByID(id); + if (entity instanceof EntityEnergyProjectile) { + ((EntityEnergyProjectile) entity).applyReflectionData(nbt); + } + } + + public static void sendToTracking(EntityEnergyProjectile projectile, NBTTagCompound reflectData) { + if (projectile == null || reflectData == null) return; + PacketHandler.Instance.sendTracking( + new ProjectileReflectPacket(projectile.getEntityId(), reflectData), + projectile + ); + } +} From 481c7ee5cb6ab0369a9ac46f33a84f947414cd37 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 23:21:57 -0500 Subject: [PATCH 07/13] Fix Slam Jumping into Air on Target Mode --- .../npcs/controllers/data/ability/type/AbilitySlam.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 994da4cba..8f4eeaf8c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -128,7 +128,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { targetX = target.posX; - targetY = target.posY; + targetY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ, 64); targetZ = target.posZ; } } @@ -161,7 +161,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { targetX = target.posX; - targetY = target.posY; + targetY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ, 64); targetZ = target.posZ; } else { targetX = caster.posX; @@ -186,11 +186,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam { if (isAirborne) { double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ, 64); highEnoughForAirSlam = (caster.posY - groundY) >= 3.0; - System.out.println("[Slam] AIR CHECK: posY=" + caster.posY + " groundY=" + groundY + " dist=" + (caster.posY - groundY) + " highEnough=" + highEnoughForAirSlam); } - System.out.println("[Slam] EXECUTE: onGround=" + caster.onGround + " isAirborne=" + isAirborne + " highEnough=" + highEnoughForAirSlam + " -> " + (isAirborne && highEnoughForAirSlam ? "AIR SLAM" : "GROUND SLAM")); - if (isAirborne && highEnoughForAirSlam) { // Air slam: player is already airborne (flying, falling, jumping) and high enough airSlam = true; From 8a998c6f621bb401ec535a8b536d7bea8b4429c6 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 23:52:01 -0500 Subject: [PATCH 08/13] Fix Barrier + DBC Logic --- .../npcs/controllers/AbilityController.java | 5 ++-- .../controllers/data/ability/Ability.java | 2 +- .../ability/extender/IAbilityExtender.java | 6 +++- .../npcs/entity/EntityAbilityLaser.java | 4 +-- .../npcs/entity/EntityEnergyProjectile.java | 30 ++++++++++++++++--- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index 9e9d3de07..8a67683bc 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -1132,10 +1132,11 @@ public class AbilityController implements IAbilityHandler { */ public boolean fireOnAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, float damage, float knockback, float knockbackUp, - double knockbackDirX, double knockbackDirZ) { + double knockbackDirX, double knockbackDirZ, + float damageRatio) { for (IAbilityExtender ext : extenders) { if (ext.onAbilityDamage(ability, caster, target, damage, knockback, knockbackUp, - knockbackDirX, knockbackDirZ)) { + knockbackDirX, knockbackDirZ, damageRatio)) { return true; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 257df22d0..1b3b290fe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -476,7 +476,7 @@ public abstract class Ability implements IAbility, IAbilityAction { // Check for ability extenders (e.g., DBC Addon damage routing) boolean handled = AbilityController.Instance.fireOnAbilityDamage( this, caster, hitEntity, damage, knockback, knockbackUp, - knockbackDirX, knockbackDirZ); + knockbackDirX, knockbackDirZ, 1.0f); if (!handled) { // Default damage path if (caster instanceof EntityNPCInterface) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java b/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java index 294b79ac8..38360eb85 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java @@ -69,11 +69,15 @@ public interface IAbilityExtender { * @param knockbackUp The vertical knockback * @param knockbackDirX The X component of knockback direction * @param knockbackDirZ The Z component of knockback direction + * @param damageRatio Proportional damage factor from barrier interactions (0.0-1.0). + * 1.0 means full damage (no barrier reduction). Extenders that + * recalculate damage from stats should multiply by this ratio. * @return true if damage was handled (skip default), false to pass to next extender or default */ default boolean onAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, float damage, float knockback, float knockbackUp, - double knockbackDirX, double knockbackDirZ) { + double knockbackDirX, double knockbackDirZ, + float damageRatio) { return false; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index d573e19fb..2d1017e38 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -442,7 +442,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { endZ = startZ + dirZ * currentLength; } - if (handleBarrierHitOutcome(dome, hitOutcome)) { + if (handleBarrierHitOutcome(dome, hitOutcome, damage)) { return true; } return false; @@ -475,7 +475,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { endZ = startZ + dirZ * currentLength; } - if (handleBarrierHitOutcome(barrier, hitOutcome)) { + if (handleBarrierHitOutcome(barrier, hitOutcome, damage)) { return true; } return false; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index 8e9a4de1b..6d7c22776 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -395,7 +395,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) { continue; } - if (handleBarrierHitOutcome(barrier, hitOutcome)) { + if (handleBarrierHitOutcome(barrier, hitOutcome, damage)) { return true; } return false; @@ -408,20 +408,29 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { /** * Handle the result of a projectile/barrier interaction. * + * @param fullDamage The DBC-scaled damage that was passed to the barrier (from getModifiedDamage). * @return true if caller should stop processing this tick. */ - protected boolean handleBarrierHitOutcome(EntityAbilityBarrier barrier, EntityAbilityBarrier.ProjectileHitOutcome hitOutcome) { + protected boolean handleBarrierHitOutcome(EntityAbilityBarrier barrier, EntityAbilityBarrier.ProjectileHitOutcome hitOutcome, float fullDamage) { if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) { return false; } if (hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.BROKEN) { if (hitOutcome.remainingProjectileDamage <= 0.0f) { + barrierDamageRatio = 0.0f; setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS)); hasHit = true; return true; } + // Accumulate barrier pass-through ratio for extender damage scaling. + // fullDamage is the DBC-scaled damage the barrier saw; remainingProjectileDamage + // is what survived. Multiply into the running ratio so multiple barriers stack. + if (fullDamage > 0.0f) { + barrierDamageRatio *= (hitOutcome.remainingProjectileDamage / fullDamage); + } + setCombatDamage(hitOutcome.remainingProjectileDamage); setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS)); return false; @@ -524,8 +533,10 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { setOuterColor(barrier.getOuterColor()); float clampedStrength = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct)); - float reducedDamage = getDamage() * (1.0f - clampedStrength / 100.0f); + float reductionFactor = 1.0f - clampedStrength / 100.0f; + float reducedDamage = getDamage() * reductionFactor; setCombatDamage(Math.max(0.0f, reducedDamage)); + barrierDamageRatio *= reductionFactor; hitOnceEntities.clear(); lastHitTickByEntity.clear(); @@ -948,7 +959,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f; handled = AbilityController.Instance.fireOnAbilityDamage( sourceAbility, (EntityLivingBase) owner, target, - dmg, kb, kbUp, dx, dz); + dmg, kb, kbUp, dx, dz, barrierDamageRatio); } if (!handled) { @@ -1916,6 +1927,17 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // ==================== COMBAT GETTERS & SETTERS ==================== + /** + * Proportional damage factor from barrier interactions (0.0-1.0). + * Accumulates multiplicatively when the projectile breaks through barriers or is reflected. + * Used by ability extenders to scale recalculated damage proportionally. + */ + private float barrierDamageRatio = 1.0f; + + public float getBarrierDamageRatio() { + return barrierDamageRatio; + } + public float getDamage() { return combatData.getDamage(); } From 26f5a84f972c745ba2ced37cba9a48123abbc037 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 25 Feb 2026 00:00:55 -0500 Subject: [PATCH 09/13] Better Naming --- .../npcs/controllers/AbilityController.java | 4 ++-- .../ability/extender/IAbilityExtender.java | 8 +++---- .../npcs/entity/EntityEnergyProjectile.java | 21 ++++++++++--------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index 8a67683bc..19a8d079b 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -1133,10 +1133,10 @@ public class AbilityController implements IAbilityHandler { public boolean fireOnAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, float damage, float knockback, float knockbackUp, double knockbackDirX, double knockbackDirZ, - float damageRatio) { + float damageMultiplier) { for (IAbilityExtender ext : extenders) { if (ext.onAbilityDamage(ability, caster, target, damage, knockback, knockbackUp, - knockbackDirX, knockbackDirZ, damageRatio)) { + knockbackDirX, knockbackDirZ, damageMultiplier)) { return true; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java b/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java index 38360eb85..d691a6d18 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/extender/IAbilityExtender.java @@ -69,15 +69,15 @@ public interface IAbilityExtender { * @param knockbackUp The vertical knockback * @param knockbackDirX The X component of knockback direction * @param knockbackDirZ The Z component of knockback direction - * @param damageRatio Proportional damage factor from barrier interactions (0.0-1.0). - * 1.0 means full damage (no barrier reduction). Extenders that - * recalculate damage from stats should multiply by this ratio. + * @param damageMultiplier Accumulated proportional damage multiplier (0.0-1.0). + * 1.0 means full damage (no reduction). Extenders that recalculate + * damage from stats should multiply by this value. * @return true if damage was handled (skip default), false to pass to next extender or default */ default boolean onAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, float damage, float knockback, float knockbackUp, double knockbackDirX, double knockbackDirZ, - float damageRatio) { + float damageMultiplier) { return false; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index 6d7c22776..b33f8d3ec 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -418,7 +418,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.BROKEN) { if (hitOutcome.remainingProjectileDamage <= 0.0f) { - barrierDamageRatio = 0.0f; + damageMultiplier = 0.0f; setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS)); hasHit = true; return true; @@ -428,7 +428,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // fullDamage is the DBC-scaled damage the barrier saw; remainingProjectileDamage // is what survived. Multiply into the running ratio so multiple barriers stack. if (fullDamage > 0.0f) { - barrierDamageRatio *= (hitOutcome.remainingProjectileDamage / fullDamage); + damageMultiplier *= (hitOutcome.remainingProjectileDamage / fullDamage); } setCombatDamage(hitOutcome.remainingProjectileDamage); @@ -536,7 +536,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { float reductionFactor = 1.0f - clampedStrength / 100.0f; float reducedDamage = getDamage() * reductionFactor; setCombatDamage(Math.max(0.0f, reducedDamage)); - barrierDamageRatio *= reductionFactor; + damageMultiplier *= reductionFactor; hitOnceEntities.clear(); lastHitTickByEntity.clear(); @@ -959,7 +959,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f; handled = AbilityController.Instance.fireOnAbilityDamage( sourceAbility, (EntityLivingBase) owner, target, - dmg, kb, kbUp, dx, dz, barrierDamageRatio); + dmg, kb, kbUp, dx, dz, damageMultiplier); } if (!handled) { @@ -1928,14 +1928,15 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // ==================== COMBAT GETTERS & SETTERS ==================== /** - * Proportional damage factor from barrier interactions (0.0-1.0). - * Accumulates multiplicatively when the projectile breaks through barriers or is reflected. - * Used by ability extenders to scale recalculated damage proportionally. + * Accumulated proportional damage multiplier (0.0-1.0). + * Accumulates multiplicatively when the projectile breaks through barriers, is reflected, + * or any other system reduces its effective damage. Used by ability extenders to scale + * recalculated damage proportionally. */ - private float barrierDamageRatio = 1.0f; + private float damageMultiplier = 1.0f; - public float getBarrierDamageRatio() { - return barrierDamageRatio; + public float getDamageMultiplier() { + return damageMultiplier; } public float getDamage() { From f877279df00b26182ab0e9e51ea3bed4f441b87c Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 25 Feb 2026 00:02:09 -0500 Subject: [PATCH 10/13] Update EntityAbilityLaser.java --- src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 2d1017e38..c00f6659e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -1,5 +1,6 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityEnergyProjectile; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; @@ -551,6 +552,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { barrierImpactPauseTicks = 0; barrierImpactDestroyOnResume = false; + // Detach from source ability so the caster is freed from active-phase locks. + if (sourceAbility instanceof AbilityEnergyProjectile) { + ((AbilityEnergyProjectile) sourceAbility).detachEntity(this); + } + sourceAbility = null; + Entity barrierOwner = barrier.getOwnerEntity(); if (barrierOwner != null) { setOwnerEntityId(barrierOwner.getEntityId()); @@ -567,6 +574,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { hitOnceEntities.clear(); lastHitTickByEntity.clear(); + reflected = true; return true; } From 744ffc5ab71785363f17085e879f4167515af052 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 25 Feb 2026 03:52:20 -0500 Subject: [PATCH 11/13] Complete Overhaul to Laser Shot -> Ability Laser --- src/api | 2 +- .../client/renderer/RenderAbilityLaser.java | 23 +- .../npcs/controllers/AbilityController.java | 4 +- ...bilityLaserShot.java => AbilityLaser.java} | 50 ++- .../npcs/entity/EntityAbilityLaser.java | 353 +++++++++--------- .../scripted/entity/ScriptEnergyLaser.java | 20 +- .../assets/customnpcs/lang/en_US.lang | 4 +- 7 files changed, 225 insertions(+), 231 deletions(-) rename src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/{AbilityLaserShot.java => AbilityLaser.java} (82%) diff --git a/src/api b/src/api index 34a7ea020..52cfe77bb 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 34a7ea020b380358c5f64a2e6d371f343942c6ef +Subproject commit 52cfe77bb776f10db17137ddb623503d8fe52ad0 diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java index e38764bfb..c2b2216df 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -41,7 +41,7 @@ public class RenderAbilityLaser extends RenderEnergyAbility { double endY = laser.getEndY(); double endZ = laser.getEndZ(); float width = laser.getLaserWidth(); - float alpha = laser.getLingerAlpha(); + float alpha = 1.0f; // Calculate render offset (entity position is used as reference) double offsetX = x - laser.posX; @@ -95,6 +95,25 @@ public class RenderAbilityLaser extends RenderEnergyAbility { double vertY = dz * horzX - dx * horzZ; double vertZ = dx * horzY - dy * horzX; + // Rotate perpendicular vectors around beam axis (Rodrigues' rotation) + float rotationSpeed = laser.getRotationSpeed(); + if (rotationSpeed != 0.0f) { + double angle = (laser.ticksExisted + partialTicks) * rotationSpeed * Math.PI / 180.0; + double cosA = Math.cos(angle); + double sinA = Math.sin(angle); + + double rHorzX = horzX * cosA + vertX * sinA; + double rHorzY = horzY * cosA + vertY * sinA; + double rHorzZ = horzZ * cosA + vertZ * sinA; + + double rVertX = -horzX * sinA + vertX * cosA; + double rVertY = -horzY * sinA + vertY * cosA; + double rVertZ = -horzZ * sinA + vertZ * cosA; + + horzX = rHorzX; horzY = rHorzY; horzZ = rHorzZ; + vertX = rVertX; vertY = rVertY; vertZ = rVertZ; + } + // Inner scale defines the core width float innerScale = 0.6f; float innerWidth = width * innerScale; @@ -134,7 +153,7 @@ public class RenderAbilityLaser extends RenderEnergyAbility { } private void renderChargingOrb(EntityAbilityLaser laser, double x, double y, double z, float partialTicks) { - float headSize = laser.getLaserWidth() * 1.5f; + float headSize = laser.getLaserWidth() * 0.5f; float chargeProgress = laser.getInterpolatedChargeProgress(partialTicks); float size = headSize * chargeProgress; diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index 19a8d079b..e2d8451f5 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -32,7 +32,7 @@ import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityBeam; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDisc; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDome; -import kamkeel.npcs.controllers.data.ability.type.energy.AbilityLaserShot; +import kamkeel.npcs.controllers.data.ability.type.energy.AbilityLaser; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityOrb; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityShield; import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySweeper; @@ -143,7 +143,7 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:sweeper", AbilitySweeper::new); registerType("cnpc:orb", AbilityOrb::new); registerType("cnpc:disc", AbilityDisc::new); - registerType("cnpc:laser_shot", AbilityLaserShot::new); + registerType("cnpc:laser_shot", AbilityLaser::new); registerType("cnpc:beam", AbilityBeam::new); // registerType("cnpc:slicer", AbilitySlicer::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java similarity index 82% rename from src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java rename to src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java index 94d005d4f..75480b622 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java @@ -16,39 +16,39 @@ import kamkeel.npcs.entity.EntityAbilityLaser; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; -import noppes.npcs.api.ability.type.IAbilityLaserShot; +import noppes.npcs.api.ability.type.IAbilityLaser; import noppes.npcs.client.gui.builder.FieldDef; import java.util.List; /** - * Laser Shot ability: Fast expanding thin line. - * Travels in a straight line from origin. + * Laser ability: Sweeping beam that follows the caster's look vector. + * Expands from origin to maxLength, then stays active until maxLifetime expires. * Single-projectile only (always projectileCount=1). */ -public class AbilityLaserShot extends AbilityEnergyProjectile implements IAbilityLaserShot { +public class AbilityLaser extends AbilityEnergyProjectile implements IAbilityLaser { private float laserWidth = 0.3f; private float expansionSpeed = 3.0f; - private int lingerTicks = 8; + private float maxLength = 32.0f; - public AbilityLaserShot() { + public AbilityLaser() { super( - new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f), - new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f, HitType.PIERCE, 5), + new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 4.0f), + new EnergyCombatData(4.0f, 0.5f, 0.05f, false, 2.0f, 0.5f, HitType.MULTI, 5), new EnergyHomingData(), new EnergyLifespanData(150.0f, 100) ); this.typeId = "ability.cnpc.laser_shot"; - this.name = "Laser Shot"; + this.name = "Laser"; this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 35.0f; + this.maxRange = 30.0f; this.minRange = 3.0f; this.cooldownTicks = 0; this.windUpTicks = 15; this.lockMovement = LockMode.WINDUP_AND_ACTIVE; this.rotationMode = RotationMode.TRACK; - this.rotationPhase = LockMode.WINDUP; + this.rotationPhase = LockMode.WINDUP_AND_ACTIVE; this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; this.windUpAnimationName = "Ability_Laser_Windup"; @@ -69,7 +69,8 @@ public class AbilityLaserShot extends AbilityEnergyProjectile 0 ? Math.min(lifespanData.maxLifetime, 60) : 60; + int lifetime = lifespanData.maxLifetime; + return lifetime > 0 ? Math.min(lifetime, 60) : 60; } /** @@ -101,7 +102,7 @@ public class AbilityLaserShot extends AbilityEnergyProjectile * Design inspired by LouisXIV's energy attack system. */ @@ -31,23 +33,26 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { // Laser-specific properties private float laserWidth = 0.2f; private float expansionSpeed = 2.0f; // Blocks per tick - private int lingerTicks = 10; // How long laser stays visible after reaching max + private float maxLength = 32.0f; // Maximum beam reach in blocks // State - private float currentLength = 0.0f; + private float desiredLength = 0.0f; // Intended beam reach (grows to maxLength) + private float currentLength = 0.0f; // Actual length after block truncation (visual + collision) private boolean fullyExtended = false; - private int ticksSinceFullExtension = 0; // Direction (normalized) private double dirX, dirY, dirZ; - // Lock vertical direction after firing (only update yaw, keep pitch fixed) - private boolean lockVerticalDirection = false; - private int reflectedLockTicks = 0; - // End point for rendering private double endX, endY, endZ; + // Block hit deduplication — prevents event/explosion spam every tick + private int lastBlockHitX = Integer.MIN_VALUE; + private int lastBlockHitY = Integer.MIN_VALUE; + private int lastBlockHitZ = Integer.MIN_VALUE; + private int lastExplosionTick = -100; + private static final int EXPLOSION_COOLDOWN = 10; // ticks between explosive impacts + public EntityAbilityLaser(World world) { super(world); } @@ -60,17 +65,15 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { float laserWidth, EnergyDisplayData display, EnergyCombatData combat, EnergyLightningData lightning, EnergyLifespanData lifespan, - float expansionSpeed, int lingerTicks) { + float expansionSpeed, float maxLength) { super(world); - // Initialize base properties (laser doesn't rotate) initProjectile(owner, target, x, y, z, laserWidth, display, combat, lightning, lifespan); - this.displayData.rotationSpeed = 0.0f; // Laser-specific properties this.laserWidth = laserWidth; this.expansionSpeed = expansionSpeed; - this.lingerTicks = lingerTicks; + this.maxLength = maxLength; // Calculate direction toward target or forward if (target != null) { @@ -118,12 +121,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { @Override protected void updateRotation() { - // Laser doesn't rotate + // Beam rotation is handled in the renderer via perpendicular vector rotation } @Override protected boolean checkMaxDistance() { - // Laser handles distance differently - it expands to max then lingers + // Laser handles distance via maxLength, not max distance return false; } @@ -134,67 +137,57 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { return; } - // After reflection, hold trajectory briefly so the rebound is visible. + // Origin/direction tracking runs on BOTH sides so the client can render the beam. // Once reflected, never resume owner-tracking — the laser flies in a fixed direction. - if (reflectedLockTicks > 0) { - reflectedLockTicks--; - } else if (!reflected) { - // Track owner's anchor position and rotation each tick - // so the laser follows the NPC's facing direction + if (!reflected) { updateLaserOriginAndDirection(); } + // Expand the intended reach — runs on both sides for visual sync if (!fullyExtended) { - // Expand the laser - currentLength += expansionSpeed; - - if (currentLength >= getMaxDistance()) { - currentLength = getMaxDistance(); + desiredLength += expansionSpeed; + if (desiredLength >= maxLength) { + desiredLength = maxLength; fullyExtended = true; } + } - // Update end point - endX = startX + dirX * currentLength; - endY = startY + dirY * currentLength; - endZ = startZ + dirZ * currentLength; + // Start from full desired length, then truncate at block hits + currentLength = desiredLength; - // Check for block collision along the new segment - if (!worldObj.isRemote) { - checkBlockCollision(); - // Re-evaluate barriers against the current beam segment before entity hits. - // This prevents first-tick pass-through where the pre-update barrier check - // still sees a zero-length beam. - if (checkBarrierCollisionForCurrentSegment()) { - return; - } - checkEntityCollisionAlongLine(); + // Block raytrace — truncate beam at first solid block (both sides) + checkBlockCollision(); + + // Update end point + updateEndPoint(); + + // Server-only: barrier collision, entity collision, death + if (!worldObj.isRemote) { + if (checkLaserBarrierCollision()) { + return; } - } else { - // Laser is fully extended, count down linger time - ticksSinceFullExtension++; - // Update end point during linger (origin/direction may still change) - endX = startX + dirX * currentLength; - endY = startY + dirY * currentLength; - endZ = startZ + dirZ * currentLength; - - if (ticksSinceFullExtension >= lingerTicks) { - if (!worldObj.isRemote) { - EventHooks.onEnergyProjectileExpired(this); - } - this.setDead(); - } + checkEntityCollisionAlongLine(); + // Death handled by base class deathWorldTime (from lifespanData.maxLifetime) } } + /** + * Update end point from current origin + direction * length. + */ + private void updateEndPoint() { + endX = startX + dirX * currentLength; + endY = startY + dirY * currentLength; + endZ = startZ + dirZ * currentLength; + } + /** * Update laser origin and direction from the owner's current look rotation. - * Called each tick so the laser origin stays centered on look vector. + * Called each tick so the laser follows the caster's aim (sweeping beam). */ private void updateLaserOriginAndDirection() { Entity owner = getOwnerEntity(); if (owner == null || !(owner instanceof EntityLivingBase)) { - // On client, fall back to entity tracker interpolation when owner not loaded if (worldObj != null && worldObj.isRemote) { handleClientInterpolation(); } @@ -202,7 +195,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { } EntityLivingBase livingOwner = (EntityLivingBase) owner; - // Use owner look vector (head/eye aim) for direction updates so hit logic matches visuals. + // Get owner look vector for direction Vec3 look = livingOwner.getLookVec(); if (look == null) { float yaw = (float) Math.toRadians(owner.rotationYaw); @@ -214,42 +207,28 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { ); } - if (lockVerticalDirection) { - // Keep current vertical component, but align horizontal direction to current look. - double lookHorizontalLen = Math.sqrt(look.xCoord * look.xCoord + look.zCoord * look.zCoord); - if (lookHorizontalLen > 0.0001) { - double currentHorizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ); - if (currentHorizontalLen < 0.0001) { - currentHorizontalLen = Math.sqrt(Math.max(0.0001, 1.0 - dirY * dirY)); - } - dirX = (look.xCoord / lookHorizontalLen) * currentHorizontalLen; - dirZ = (look.zCoord / lookHorizontalLen) * currentHorizontalLen; - } - } else { - dirX = look.xCoord; - dirY = look.yCoord; - dirZ = look.zCoord; - } + // Set direction directly from look vector (no lockVerticalDirection split) + dirX = look.xCoord; + dirY = look.yCoord; + dirZ = look.zCoord; - // Keep direction normalized for consistent expansion and collision math. - Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ); - double dirLen = Math.sqrt(direction.xCoord * direction.xCoord + direction.yCoord * direction.yCoord + direction.zCoord * direction.zCoord); + // Keep direction normalized + double dirLen = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ); if (dirLen > 0.0001) { - direction = Vec3.createVectorHelper( - direction.xCoord / dirLen, - direction.yCoord / dirLen, - direction.zCoord / dirLen - ); + dirX /= dirLen; + dirY /= dirLen; + dirZ /= dirLen; } else { - direction = look != null ? look : Vec3.createVectorHelper(1.0, 0.0, 0.0); + dirX = 1.0; + dirY = 0.0; + dirZ = 0.0; } - dirX = direction.xCoord; - dirY = direction.yCoord; - dirZ = direction.zCoord; - // 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; syncStartPositionToCurrent(); syncPositionStateToCurrent(false); } @@ -257,22 +236,24 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { public void startMoving(EntityLivingBase target) { beginLookVectorLaunch(false); + desiredLength = 0.0f; currentLength = 0.0f; fullyExtended = false; - ticksSinceFullExtension = 0; + lastBlockHitX = Integer.MIN_VALUE; + lastBlockHitY = Integer.MIN_VALUE; + lastBlockHitZ = Integer.MIN_VALUE; + lastExplosionTick = -100; - // Set initial direction: target-based when available, otherwise owner look vector. + // Set initial direction: owner look vector (sweeping beam always follows look) Vec3 look = getOwnerLookVector(); - if (!setDirectionTowardTarget(target, startX, startY, startZ)) { - if (look != null) { - dirX = look.xCoord; - dirY = look.yCoord; - dirZ = look.zCoord; - } else { - dirX = 1.0; - dirY = 0.0; - dirZ = 0.0; - } + if (look != null) { + dirX = look.xCoord; + dirY = look.yCoord; + dirZ = look.zCoord; + } else if (!setDirectionTowardTarget(target, startX, startY, startZ)) { + dirX = 1.0; + dirY = 0.0; + dirZ = 0.0; } // Initialize end point at start (will expand from here) @@ -296,40 +277,69 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { } private void checkBlockCollision() { - MovingObjectPosition blockHit = rayTraceBlocks(startX, startY, startZ, endX, endY, endZ); + if (currentLength <= 0) return; + + double traceEndX = startX + dirX * currentLength; + double traceEndY = startY + dirY * currentLength; + double traceEndZ = startZ + dirZ * currentLength; + + MovingObjectPosition blockHit = rayTraceBlocks(startX, startY, startZ, traceEndX, traceEndY, traceEndZ); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { - if (!worldObj.isRemote) { - EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ); - } - // Laser stops at block - currentLength = (float) Math.sqrt( + // Truncate beam at block hit + float hitDist = (float) Math.sqrt( (blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) + (blockHit.hitVec.yCoord - startY) * (blockHit.hitVec.yCoord - startY) + (blockHit.hitVec.zCoord - startZ) * (blockHit.hitVec.zCoord - startZ) ); - endX = blockHit.hitVec.xCoord; - endY = blockHit.hitVec.yCoord; - endZ = blockHit.hitVec.zCoord; - fullyExtended = true; - - if (isExplosive()) { - // Explode at hit point - double oldPosX = posX; - double oldPosY = posY; - double oldPosZ = posZ; - posX = endX; - posY = endY; - posZ = endZ; - doExplosion(); - posX = oldPosX; - posY = oldPosY; - posZ = oldPosZ; + if (hitDist < currentLength) { + currentLength = hitDist; + endX = blockHit.hitVec.xCoord; + endY = blockHit.hitVec.yCoord; + endZ = blockHit.hitVec.zCoord; } + + // Server-only: fire event and explosion only when block changes or cooldown expires + if (!worldObj.isRemote) { + int bx = blockHit.blockX; + int by = blockHit.blockY; + int bz = blockHit.blockZ; + boolean newBlock = (bx != lastBlockHitX || by != lastBlockHitY || bz != lastBlockHitZ); + + // Fire block impact event only when the beam hits a different block + if (newBlock) { + lastBlockHitX = bx; + lastBlockHitY = by; + lastBlockHitZ = bz; + EventHooks.onEnergyProjectileBlockImpact(this, bx, by, bz); + } + + // Explosive: fire on cooldown so sweeping doesn't explode every tick + if (isExplosive() && (ticksExisted - lastExplosionTick) >= EXPLOSION_COOLDOWN) { + lastExplosionTick = ticksExisted; + double oldPosX = posX; + double oldPosY = posY; + double oldPosZ = posZ; + posX = endX; + posY = endY; + posZ = endZ; + doExplosion(); + posX = oldPosX; + posY = oldPosY; + posZ = oldPosZ; + } + } + } else if (!worldObj.isRemote) { + // Beam is not hitting any block — reset tracking so next block contact triggers fresh + lastBlockHitX = Integer.MIN_VALUE; + lastBlockHitY = Integer.MIN_VALUE; + lastBlockHitZ = Integer.MIN_VALUE; } } private void checkEntityCollisionAlongLine() { + if (currentLength <= 0) return; + // Expand search AABB by entity sizes to ensure we find all potential targets double expand = Math.max(laserWidth, 1.0); double minX = Math.min(startX, endX) - expand; @@ -351,7 +361,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { // Check if entity's bounding box intersects with the laser line if (isEntityOnLine(entity)) { if (processEntityHit(entity, entity.posX, entity.posY + entity.height * 0.5, entity.posZ)) { - // Stop at impact point and terminate + // SINGLE hit or maxHits reached — truncate beam at impact and die double dx = entity.posX - startX; double dy = (entity.posY + entity.height * 0.5) - startY; double dz = entity.posZ - startZ; @@ -362,8 +372,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { endZ = startZ + dirZ * currentLength; return; } - - // PIERCE/MULTI continue through remaining entities. + // MULTI/PIERCE continue through remaining entities } } } @@ -393,24 +402,22 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { // ==================== BARRIER COLLISION ==================== /** - * Override barrier collision for lasers. The base class uses projectile position/velocity - * which doesn't work for lasers (posX/Y/Z stays at origin, motionX/Y/Z is zero). - * Instead, performs line-sphere intersection against domes. + * Disable the base class barrier check — it uses position/velocity which doesn't work + * for lasers (posX/Y/Z stays at origin, motionX/Y/Z is zero). We handle barriers + * exclusively in updateProjectile() via checkLaserBarrierCollision(). */ @Override - @SuppressWarnings("unchecked") protected boolean checkBarrierCollision() { - return checkBarrierCollisionInternal(false); - } - - private boolean checkBarrierCollisionForCurrentSegment() { - return checkBarrierCollisionInternal(true); + return false; } + /** + * Laser-specific barrier collision using line-segment intersection. + * Called from updateProjectile() every tick. + */ @SuppressWarnings("unchecked") - private boolean checkBarrierCollisionInternal(boolean allowWhenFullyExtended) { + private boolean checkLaserBarrierCollision() { if (currentLength <= 0) return false; - if (fullyExtended && !allowWhenFullyExtended) return false; List barriers = EntityAbilityBarrier.getActiveBarriers(worldObj); for (EntityAbilityBarrier barrier : barriers) { @@ -450,8 +457,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { } } else { // For line-based lasers, use a swept segment against generic barrier checks. - // Panel/Dome logic expects previous+current positions and motion; map those - // to [start -> end] each tick. double segMotionX = endX - startX; double segMotionY = endY - startY; double segMotionZ = endZ - startZ; @@ -470,7 +475,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { } if (hitOutcome.result != EntityAbilityBarrier.ProjectileHitResult.BROKEN) { - // Keep current segment length at the exact collision tick position. endX = startX + dirX * currentLength; endY = startY + dirY * currentLength; endZ = startZ + dirZ * currentLength; @@ -541,14 +545,20 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { setPosition(startX, startY, startZ); syncPositionStateToCurrent(true); + desiredLength = 0.0f; currentLength = 0.0f; fullyExtended = false; - ticksSinceFullExtension = 0; endX = startX; endY = startY; endZ = startZ; + lastBlockHitX = Integer.MIN_VALUE; + lastBlockHitY = Integer.MIN_VALUE; + lastBlockHitZ = Integer.MIN_VALUE; + lastExplosionTick = -100; + + // Reset lifetime so reflected laser gets a fresh maxLifetime from the base class + deathWorldTime = -1; - reflectedLockTicks = Math.max(reflectedLockTicks, 10); barrierImpactPauseTicks = 0; barrierImpactDestroyOnResume = false; @@ -597,7 +607,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { if (originDistSq < (double) radius * radius) return -1; // Solve: |start + t*dir - C|^2 = R^2 - // a*t^2 + b*t + c = 0 double a = dirX * dirX + dirY * dirY + dirZ * dirZ; double b = 2.0 * (dirX * ocX + dirY * ocY + dirZ * ocZ); double c = originDistSq - (double) radius * radius; @@ -615,15 +624,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { return -1; } - // ==================== GETTERS FOR RENDERER ==================== - - public void setLockVerticalDirection(boolean lock) { - this.lockVerticalDirection = lock; - } - - public boolean isLockVerticalDirection() { - return lockVerticalDirection; - } + // ==================== GETTERS / SETTERS ==================== public float getLaserWidth() { return laserWidth; @@ -641,12 +642,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.expansionSpeed = speed; } - public int getLingerTicks() { - return lingerTicks; + public float getMaxLength() { + return maxLength; } - public void setLingerTicks(int ticks) { - this.lingerTicks = ticks; + public void setMaxLength(float maxLength) { + this.maxLength = maxLength; } public void setDirection(double x, double y, double z) { @@ -699,15 +700,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { return dirZ; } - /** - * Get the alpha for fade-out effect during linger. - */ - public float getLingerAlpha() { - if (!fullyExtended) return 1.0f; - if (lingerTicks <= 0) return 0.0f; - return Math.max(0.0f, 1.0f - ((float) ticksSinceFullExtension / lingerTicks)); - } - @Override protected float getLaunchClearanceRadius() { return Math.max(0.1f, laserWidth * 0.5f); @@ -715,11 +707,11 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { @Override public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { - // Ensure charging visuals grow from a small orb based on laser width. - this.size = laserWidth; + // Charge orb grows to half the beam width + this.size = laserWidth * 0.5f; + this.desiredLength = 0.0f; this.currentLength = 0.0f; this.fullyExtended = false; - this.ticksSinceFullExtension = 0; super.setupCharging(anchor, chargeDuration); this.endX = posX; this.endY = posY; @@ -736,12 +728,13 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { nbt.setDouble("StartX", startX); nbt.setDouble("StartY", startY); nbt.setDouble("StartZ", startZ); + nbt.setFloat("DesiredLength", desiredLength); nbt.setFloat("CurrentLength", currentLength); nbt.setDouble("EndX", endX); nbt.setDouble("EndY", endY); nbt.setDouble("EndZ", endZ); - nbt.setInteger("ReflectedLockTicks", reflectedLockTicks); nbt.setBoolean("FullyExtended", fullyExtended); + nbt.setFloat("MaxLength", maxLength); } @Override @@ -752,13 +745,13 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { startX = nbt.getDouble("StartX"); startY = nbt.getDouble("StartY"); startZ = nbt.getDouble("StartZ"); + desiredLength = nbt.getFloat("DesiredLength"); currentLength = nbt.getFloat("CurrentLength"); endX = nbt.getDouble("EndX"); endY = nbt.getDouble("EndY"); endZ = nbt.getDouble("EndZ"); - reflectedLockTicks = nbt.getInteger("ReflectedLockTicks"); fullyExtended = nbt.getBoolean("FullyExtended"); - ticksSinceFullExtension = 0; + if (nbt.hasKey("MaxLength")) maxLength = nbt.getFloat("MaxLength"); } // ==================== NBT ==================== @@ -768,13 +761,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.laserWidth = sanitize(nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f, 0.2f, MAX_ENTITY_SIZE); this.expansionSpeed = nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f; if (Float.isNaN(expansionSpeed) || Float.isInfinite(expansionSpeed) || expansionSpeed <= 0) expansionSpeed = 2.0f; - this.lingerTicks = nbt.hasKey("LingerTicks") ? nbt.getInteger("LingerTicks") : 10; - if (lingerTicks <= 0) lingerTicks = 1; - this.lockVerticalDirection = nbt.getBoolean("LockVerticalDir"); - this.ticksSinceFullExtension = nbt.getInteger("TicksSinceExtended"); + this.maxLength = nbt.hasKey("MaxLength") ? nbt.getFloat("MaxLength") : 32.0f; + if (Float.isNaN(maxLength) || Float.isInfinite(maxLength) || maxLength <= 0) maxLength = 32.0f; this.dirX = nbt.getDouble("DirX"); this.dirY = nbt.getDouble("DirY"); this.dirZ = nbt.getDouble("DirZ"); + this.desiredLength = nbt.getFloat("DesiredLength"); this.currentLength = nbt.getFloat("CurrentLength"); this.fullyExtended = nbt.getBoolean("FullyExtended"); this.endX = nbt.getDouble("EndX"); @@ -788,12 +780,11 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { protected void writeProjectileNBT(NBTTagCompound nbt) { nbt.setFloat("LaserWidth", laserWidth); nbt.setFloat("ExpansionSpeed", expansionSpeed); - nbt.setInteger("LingerTicks", lingerTicks); - nbt.setBoolean("LockVerticalDir", lockVerticalDirection); - nbt.setInteger("TicksSinceExtended", ticksSinceFullExtension); + nbt.setFloat("MaxLength", maxLength); nbt.setDouble("DirX", dirX); nbt.setDouble("DirY", dirY); nbt.setDouble("DirZ", dirZ); + nbt.setFloat("DesiredLength", desiredLength); nbt.setFloat("CurrentLength", currentLength); nbt.setBoolean("FullyExtended", fullyExtended); nbt.setDouble("EndX", endX); @@ -809,19 +800,19 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { */ public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, - float expansionSpeed, float maxDistance) { + float expansionSpeed, float maxLength) { setupPreviewState(owner, display, lightning, anchor, chargeDuration); // Laser-specific visual/behavior state. this.laserWidth = laserWidth; this.expansionSpeed = expansionSpeed; - this.lifespanData.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview + this.maxLength = Math.min(maxLength, 5.0f); // Limit for GUI preview + this.desiredLength = 0.0f; this.currentLength = 0.0f; this.fullyExtended = false; - this.ticksSinceFullExtension = 0; - // Charge as an orb from tiny size to laser width while anchored. - this.targetSize = laserWidth; + // Charge as an orb from tiny size to half laser width while anchored. + this.targetSize = laserWidth * 0.5f; setVisualSize(0.01f); setChargeOriginFromAnchor(owner, anchorData); clearMotion(); diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java index 3963794fc..6c0b8f077 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java @@ -45,12 +45,12 @@ public class ScriptEnergyLaser extends ScriptEnerg entity.setExpansionSpeed(speed); } - public int getLingerTicks() { - return entity.getLingerTicks(); + public float getMaxLength() { + return entity.getMaxLength(); } - public void setLingerTicks(int ticks) { - entity.setLingerTicks(ticks); + public void setMaxLength(float maxLength) { + entity.setMaxLength(maxLength); } public float getCurrentLength() { @@ -89,18 +89,6 @@ public class ScriptEnergyLaser extends ScriptEnerg return entity.getEndZ(); } - public float getLingerAlpha() { - return entity.getLingerAlpha(); - } - - public boolean isLockVerticalDirection() { - return entity.isLockVerticalDirection(); - } - - public void setLockVerticalDirection(boolean lock) { - entity.setLockVerticalDirection(lock); - } - // ==================== FIRE ==================== @Override diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index de63d26a3..885947f0d 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1693,7 +1693,7 @@ ability.cnpc.effect=Effect ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap ability.cnpc.disc=Disc -ability.cnpc.laser_shot=Laser Shot +ability.cnpc.laser_shot=Laser ability.cnpc.sweeper=Sweeper # ═══════════════════════════════════════════════════════════════ @@ -1822,6 +1822,7 @@ ability.multiHitDelay=Multi-Hit Delay ability.maxHits=Max Hits ability.distance=Distance ability.maxDistance=Max Distance +ability.maxLength=Max Length ability.lifetime=Lifetime ability.stunTicks=Stun Ticks ability.hitWidth=Hit Width @@ -1829,7 +1830,6 @@ ability.hitLength=Hit Length ability.effects=Effects ability.effect.burn=Burn ability.mode=Mode -ability.duration=Duration ability.anchorPoint=Anchor Point ability.anchor.offsetX=Offset X ability.anchor.offsetY=Offset Y From f73e8d02f993a8a5fc15dd603cca274b70c322bd Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 25 Feb 2026 04:12:20 -0500 Subject: [PATCH 12/13] Update EntityAbilityDome.java --- src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java index 383f07246..3484b1228 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java @@ -297,6 +297,14 @@ public class EntityAbilityDome extends EntityAbilityBarrier { double surfaceX = posX + nx * pushDist; double surfaceY = posY + ny * pushDist - ent.height * 0.5; double surfaceZ = posZ + nz * pushDist; + + // Prevent grounded entities from being teleported below the floor. + // The spherical surface calculation can produce a Y below ground level + // near the dome's equator, and setPosition bypasses collision detection. + if (ent.onGround && surfaceY < ent.posY) { + surfaceY = ent.posY; + } + teleportEntity(ent, surfaceX, surfaceY, surfaceZ); // Cancel radial velocity and add corrective push From d8bc3d4177c71e39aba2e15933485067e50deba3 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 25 Feb 2026 04:31:17 -0500 Subject: [PATCH 13/13] CNPC+ Debug Command for Energy Projectiles + Compile Fixes --- .../client/command/CommandCNPCDebugger.java | 42 +++++ .../kamkeel/npcs/command/CommandKamkeel.java | 1 + .../kamkeel/npcs/command/DebugCommand.java | 41 +++++ .../npcs/entity/EntityAbilityBarrier.java | 28 +++ .../npcs/entity/EntityAbilityBeam.java | 54 +++--- .../npcs/entity/EntityAbilityDisc.java | 11 ++ .../npcs/entity/EntityAbilityDome.java | 10 ++ .../npcs/entity/EntityAbilityLaser.java | 11 ++ .../kamkeel/npcs/entity/EntityAbilityOrb.java | 10 ++ .../npcs/entity/EntityAbilityPanel.java | 10 ++ .../npcs/entity/EntityAbilitySweeper.java | 14 ++ .../npcs/entity/EntityEnergyExplosion.java | 12 ++ .../npcs/entity/EntityEnergyProjectile.java | 50 ++++++ .../npcs/entity/EntityEnergySlicer.java | 8 + .../java/kamkeel/npcs/util/CNPCDebug.java | 163 ++++++++++++++++++ .../java/noppes/npcs/client/ClientProxy.java | 5 + 16 files changed, 438 insertions(+), 32 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/client/command/CommandCNPCDebugger.java create mode 100644 src/main/java/kamkeel/npcs/command/DebugCommand.java create mode 100644 src/main/java/kamkeel/npcs/util/CNPCDebug.java diff --git a/src/main/java/kamkeel/npcs/client/command/CommandCNPCDebugger.java b/src/main/java/kamkeel/npcs/client/command/CommandCNPCDebugger.java new file mode 100644 index 000000000..42e18ed6c --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/command/CommandCNPCDebugger.java @@ -0,0 +1,42 @@ +package kamkeel.npcs.client.command; + +import kamkeel.npcs.util.CNPCDebug; +import net.minecraft.command.CommandBase; +import net.minecraft.command.ICommandSender; +import net.minecraft.util.ChatComponentText; + +/** + * Client-only command to toggle debug logging. + * Registered via ClientCommandHandler so it runs entirely on the client + * without sending a packet to the server. + */ +public class CommandCNPCDebugger extends CommandBase { + + @Override + public String getCommandName() { + return "cnpcdebugger"; + } + + @Override + public String getCommandUsage(ICommandSender sender) { + return "/cnpcdebugger - Toggle client debug logging for a type (e.g. energy)"; + } + + @Override + public int getRequiredPermissionLevel() { + return 0; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) { + if (args.length == 0) { + sender.addChatMessage(new ChatComponentText("\u00A7cUsage: /cnpcdebugger ")); + return; + } + + String type = args[0].toLowerCase(); + boolean newState = CNPCDebug.toggleClient(type); + String stateText = newState ? "\u00A7aENABLED" : "\u00A7cDISABLED"; + sender.addChatMessage(new ChatComponentText("\u00A77[CNPC+] \u00A7eClient debug '" + type + "' " + stateText)); + } +} diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index 0f0e0dbbf..fb3b75ede 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -52,6 +52,7 @@ public class CommandKamkeel extends CommandBase { registerCommand(new AuctionCommand()); if (ConfigMain.AttributesEnabled) registerCommand(new AttributeCommand()); + registerCommand(new DebugCommand()); } public void registerCommand(CommandKamkeelBase command) { diff --git a/src/main/java/kamkeel/npcs/command/DebugCommand.java b/src/main/java/kamkeel/npcs/command/DebugCommand.java new file mode 100644 index 000000000..1cb497a93 --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/DebugCommand.java @@ -0,0 +1,41 @@ +package kamkeel.npcs.command; + +import kamkeel.npcs.util.CNPCDebug; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; + +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class DebugCommand extends CommandKamkeelBase { + + @Override + public String getCommandName() { + return "debug"; + } + + @Override + public String getDescription() { + return "Toggle debug logging types"; + } + + @Override + public String getUsage() { + return ""; + } + + @Override + public boolean runSubCommands() { + return false; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) throws CommandException { + if (args.length == 0) { + throw new CommandException("Usage: /kam debug (e.g. energy)"); + } + + String type = args[0].toLowerCase(); + boolean newState = CNPCDebug.toggleServer(type); + sendResult(sender, "Server debug '" + type + "' " + (newState ? "\u00A7aENABLED" : "\u00A7cDISABLED")); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java index 6c1df56d3..3cb3fe021 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java @@ -2,6 +2,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; +import kamkeel.npcs.util.CNPCDebug; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; @@ -617,4 +618,31 @@ public abstract class EntityAbilityBarrier extends EntityEnergyAbility { this.dataWatcher.updateObject(DW_CHARGING, (byte) (charging ? 1 : 0)); barrierData.readNBT(nbt); } + + // ==================== DEBUG LOGGING ==================== + + /** + * Debug log called every tick for barriers. Subclasses override debugLogBarrierExtra(). + */ + protected void debugLogBarrierTick() { + boolean isClient = worldObj.isRemote; + if (isClient ? !CNPCDebug.isClientEnabled("energy") : !CNPCDebug.isServerEnabled("energy")) + return; + + String className = getClass().getSimpleName(); + String base = String.format("[%s id=%d tick=%d] pos=(%.2f,%.2f,%.2f) charging=%b health=%.1f", + className, getEntityId(), ticksAlive, + posX, posY, posZ, isCharging(), currentHealth); + + String extra = debugLogBarrierExtra(); + String full = extra.isEmpty() ? base : base + " " + extra; + CNPCDebug.log("energy", isClient, full); + } + + /** + * Subclass hook for barrier-specific debug data. + */ + protected String debugLogBarrierExtra() { + return ""; + } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 33d6e67bc..47e2c7d8d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -16,7 +16,6 @@ import net.minecraft.nbt.NBTTagList; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.LogWriter; import java.util.ArrayList; import java.util.Collections; @@ -61,9 +60,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { private int trailFadeTime = 20; // Ticks for trail to fully fade private List trailPointAges = new ArrayList<>(); - // Debug logging - private static final boolean DEBUG_LOGGING = false; - public EntityAbilityBeam(World world) { super(world); } @@ -160,10 +156,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { // Add initial trail point at origin (relative 0,0,0) trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); - if (DEBUG_LOGGING && !world.isRemote) { - LogWriter.info("[Beam] Created at origin " + x + ", " + y + ", " + z + " maxDist=" + lifespan.maxDistance); - } - // Calculate initial velocity toward target calculateInitialVelocity(owner, target, x, y, z); } @@ -222,11 +214,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); if (isFadingMode()) trailPointAges.add(0); - if (DEBUG_LOGGING && !worldObj.isRemote) { - LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ + - ") headOffset=(" + headOffsetX + "," + headOffsetY + "," + headOffsetZ + - ") motion=(" + motionX + "," + motionY + "," + motionZ + ")"); - } } @Override @@ -237,11 +224,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { headOffsetY * headOffsetY + headOffsetZ * headOffsetZ ); - boolean exceeded = distFromOrigin >= getMaxDistance(); - if (exceeded && !worldObj.isRemote && DEBUG_LOGGING) { - LogWriter.info("[Beam] DEAD: Max distance exceeded. dist=" + distFromOrigin + " max=" + getMaxDistance()); - } - return exceeded; + return distFromOrigin >= getMaxDistance(); } @Override @@ -312,17 +295,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { // Add trail point addTrailPoint(); - // Debug log periodically - if (DEBUG_LOGGING && ticksExisted % 20 == 0) { - LogWriter.info("[Beam] Server tick=" + ticksExisted + " headOffset=(" + - String.format("%.2f", headOffsetX) + "," + - String.format("%.2f", headOffsetY) + "," + - String.format("%.2f", headOffsetZ) + ") origin=(" + - String.format("%.2f", startX) + "," + - String.format("%.2f", startY) + "," + - String.format("%.2f", startZ) + ") trail=" + getActiveTrailSize()); - } - // Skip collision checks on first few ticks if (ticksExisted > 2) { checkBlockCollision(headWorldX, headWorldY, headWorldZ); @@ -528,9 +500,27 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { Math.max(prevHeadWorldZ, headZ) + hitSize ); - if (processEntitiesInHitBox(hitBox, headX, headY, headZ) && DEBUG_LOGGING) { - LogWriter.info("[Beam] DEAD: Entity collision at tick " + ticksExisted); - } + processEntitiesInHitBox(hitBox, headX, headY, headZ); + } + + // ==================== DEBUG ==================== + + @Override + protected String debugLogExtra() { + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + return String.format("headOff=(%.2f,%.2f,%.2f) prevHead=(%.2f,%.2f,%.2f) " + + "origin=(%.2f,%.2f,%.2f) headWorld=(%.2f,%.2f,%.2f) " + + "trail=%d mode=%s anchored=%b beamW=%.2f headSz=%.2f " + + "motion=(%.3f,%.3f,%.3f)", + headOffsetX, headOffsetY, headOffsetZ, + prevHeadOffsetX, prevHeadOffsetY, prevHeadOffsetZ, + startX, startY, startZ, + headWorldX, headWorldY, headWorldZ, + getActiveTrailSize(), + beamMode.name(), isAnchoredMode(), beamWidth, headSize, + motionX, motionY, motionZ); } // ==================== GETTERS FOR RENDERER ==================== diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index a7ff4ef99..d7c74c3dc 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -330,6 +330,17 @@ public class EntityAbilityDisc extends EntityEnergyProjectile { updateRotation(); } + // ==================== DEBUG ==================== + + @Override + protected String debugLogExtra() { + return String.format("motion=(%.3f,%.3f,%.3f) radius=%.2f thickness=%.2f " + + "boomerang=%b returning=%b vertical=%b ticksSinceMiss=%d", + motionX, motionY, motionZ, + discRadius, discThickness, + boomerang, returning, vertical, ticksSinceMiss); + } + // ==================== GETTERS ==================== public float getDiscRadius() { diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java index 383f07246..bc00f4332 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java @@ -122,6 +122,8 @@ public class EntityAbilityDome extends EntityAbilityBarrier { if (worldObj.isRemote && barrierData.meleeEnabled && !isCharging()) { adjustMeleeBoundingBox(); } + + debugLogBarrierTick(); } // ==================== INCOMING CHECK ==================== @@ -548,6 +550,14 @@ public class EntityAbilityDome extends EntityAbilityBarrier { return surfaceDist * surfaceDist; } + // ==================== DEBUG ==================== + + @Override + protected String debugLogBarrierExtra() { + return String.format("radius=%.2f targetRadius=%.2f follow=%b", + domeRadius, targetDomeRadius, followCaster); + } + // ==================== GETTERS ==================== public float getDomeRadius() { diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 8bbc8a2fe..3c948d0f0 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -624,6 +624,17 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { return -1; } + // ==================== DEBUG ==================== + + @Override + protected String debugLogExtra() { + return String.format("dir=(%.3f,%.3f,%.3f) desired=%.2f current=%.2f max=%.2f " + + "fullyExtended=%b end=(%.2f,%.2f,%.2f) width=%.2f expSpd=%.2f origin=(%.2f,%.2f,%.2f)", + dirX, dirY, dirZ, desiredLength, currentLength, maxLength, + fullyExtended, endX, endY, endZ, laserWidth, expansionSpeed, + startX, startY, startZ); + } + // ==================== GETTERS / SETTERS ==================== public float getLaserWidth() { diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index f7b9cd7e7..07e032829 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -150,6 +150,16 @@ public class EntityAbilityOrb extends EntityEnergyProjectile { processEntitiesInHitBox(hitBox, nextX, nextY, nextZ); } + // ==================== DEBUG ==================== + + @Override + protected String debugLogExtra() { + return String.format("motion=(%.3f,%.3f,%.3f) homing=%b start=(%.2f,%.2f,%.2f)", + motionX, motionY, motionZ, + homingData != null && homingData.isHoming(), + startX, startY, startZ); + } + // ==================== GETTERS ==================== // Legacy getter for renderer compatibility diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java index a9bae51a6..717250fdd 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java @@ -143,6 +143,8 @@ public class EntityAbilityPanel extends EntityAbilityBarrier { // Stationary, no update needed break; } + + debugLogBarrierTick(); } private void updateHeld() { @@ -548,6 +550,14 @@ public class EntityAbilityPanel extends EntityAbilityBarrier { return df * df + dr * dr + du * du; } + // ==================== DEBUG ==================== + + @Override + protected String debugLogBarrierExtra() { + return String.format("mode=%s yaw=%.1f w=%.2f h=%.2f", + mode.name(), panelYaw, panelData.panelWidth, panelData.panelHeight); + } + // ==================== GETTERS ==================== public PanelMode getMode() { diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index f03324618..69d43da0c 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -3,6 +3,7 @@ package kamkeel.npcs.entity; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; +import kamkeel.npcs.util.CNPCDebug; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; @@ -168,6 +169,19 @@ public class EntityAbilitySweeper extends EntityEnergyAbility { if (!worldObj.isRemote) { handleDamage(owner); } + + // Debug logging + { + boolean isClient = worldObj.isRemote; + if (isClient ? CNPCDebug.isClientEnabled("energy") : CNPCDebug.isServerEnabled("energy")) { + CNPCDebug.log("energy", isClient, String.format( + "[Sweeper id=%d tick=%d] pos=(%.2f,%.2f,%.2f) angle=%.1f prevAngle=%.1f " + + "speed=%.1f rotations=%d/%d length=%.2f width=%.2f", + getEntityId(), ticksExisted, posX, posY, posZ, + currentAngle, prevAngle, sweepSpeed, + completedRotations, numberOfRotations, beamLength, beamWidth)); + } + } } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java index d90295912..d20975b7d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyExplosion.java @@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData; +import kamkeel.npcs.util.CNPCDebug; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.MathHelper; import net.minecraft.world.World; @@ -57,6 +58,17 @@ public class EntityEnergyExplosion extends EntityEnergyAbility { float eased = 1.0f - (1.0f - progress) * (1.0f - progress); this.renderRadius = maxRadius * eased; + // Debug logging + { + boolean isClient = worldObj.isRemote; + if (isClient ? CNPCDebug.isClientEnabled("energy") : CNPCDebug.isServerEnabled("energy")) { + CNPCDebug.log("energy", isClient, String.format( + "[Explosion id=%d tick=%d] pos=(%.2f,%.2f,%.2f) renderRadius=%.2f maxRadius=%.2f progress=%.2f", + getEntityId(), ticksExisted, posX, posY, posZ, + renderRadius, maxRadius, getLifeProgress(0f))); + } + } + if (ticksExisted >= durationTicks) { setDead(); } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index b33f8d3ec..091af5086 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -16,6 +16,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.CNPCDebug; import net.minecraft.block.Block; import net.minecraft.block.material.Material; import net.minecraft.entity.Entity; @@ -334,6 +335,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (!previewMode && !worldObj.isRemote) { EventHooks.onEnergyProjectileTick(this); } + + debugLogTick(); } /** @@ -349,6 +352,53 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (this.rotationValZ > 360.0f) this.rotationValZ -= 360.0f; } + // ==================== DEBUG LOGGING ==================== + + /** + * Debug log called every tick. Logs position/size/charging state. + * Subclasses override debugLogExtra() for type-specific data. + */ + protected void debugLogTick() { + boolean isClient = worldObj.isRemote; + if (isClient ? !CNPCDebug.isClientEnabled("energy") : !CNPCDebug.isServerEnabled("energy")) + return; + + String className = getClass().getSimpleName(); + boolean dwCharging = isCharging(); // DataWatcher value (what client sees) + boolean localCharging = this.charging; // Local field (server truth) + + String base = String.format("[%s id=%d tick=%d] pos=(%.2f,%.2f,%.2f) prev=(%.2f,%.2f,%.2f) " + + "size=%.3f renderSize=%.3f prevRenderSize=%.3f " + + "charging(dw)=%b charging(local)=%b chargeTick=%d/%d chargeProgress=%.3f", + className, getEntityId(), ticksExisted, + posX, posY, posZ, prevPosX, prevPosY, prevPosZ, + size, renderCurrentSize, prevRenderSize, + dwCharging, localCharging, chargeTick, chargeDuration, getChargeProgress()); + + // Flag mismatch between DataWatcher and local field + if (dwCharging != localCharging) { + base += " !!CHARGE_MISMATCH!!"; + } + + // Client interpolation state (only meaningful on client) + if (isClient) { + base += String.format(" interp(steps=%d target=(%.2f,%.2f,%.2f))", + interpSteps, interpTargetX, interpTargetY, interpTargetZ); + } + + String extra = debugLogExtra(); + String full = extra.isEmpty() ? base : base + " " + extra; + CNPCDebug.log("energy", isClient, full); + } + + /** + * Subclass hook for type-specific debug data. + * Return empty string if nothing extra to log. + */ + protected String debugLogExtra() { + return ""; + } + /** * Check if projectile has exceeded max distance. * diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java index 96f53f8ca..772f3f704 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java @@ -192,6 +192,14 @@ public class EntityEnergySlicer extends EntityEnergyProjectile { return lastTravelPitch; } + // ==================== DEBUG ==================== + + @Override + protected String debugLogExtra() { + return String.format("motion=(%.3f,%.3f,%.3f) sliceW=%.2f sliceT=%.2f", + motionX, motionY, motionZ, sliceWidth, sliceThickness); + } + // ==================== GETTERS ==================== public float getSliceWidth() { diff --git a/src/main/java/kamkeel/npcs/util/CNPCDebug.java b/src/main/java/kamkeel/npcs/util/CNPCDebug.java new file mode 100644 index 000000000..66cdf3c77 --- /dev/null +++ b/src/main/java/kamkeel/npcs/util/CNPCDebug.java @@ -0,0 +1,163 @@ +package kamkeel.npcs.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.StreamHandler; + +/** + * Debug logging system for CNPC+ entities. + * Writes to logs/cnpc_debug/[type]_[side].log (file only, no console output). + * Toggle at runtime via /cnpcdebugger [type] (client) or /kam debug [type] (server). + */ +public class CNPCDebug { + + private static final String DIR = "logs/cnpc_debug"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss"); + + // Runtime toggle state per type, separate for client and server + private static final Map serverToggles = new ConcurrentHashMap<>(); + private static final Map clientToggles = new ConcurrentHashMap<>(); + + // Per-type file handlers (lazily initialized) + private static final Map serverHandlers = new ConcurrentHashMap<>(); + private static final Map clientHandlers = new ConcurrentHashMap<>(); + + // ==================== TOGGLE API ==================== + + /** + * Toggle server-side debug for given type. Returns new state. + */ + public static boolean toggleServer(String type) { + type = type.toLowerCase(); + boolean newState = !isServerEnabled(type); + serverToggles.put(type, newState); + if (newState) { + ensureHandler(type, false); + } + return newState; + } + + /** + * Toggle client-side debug for given type. Returns new state. + */ + public static boolean toggleClient(String type) { + type = type.toLowerCase(); + boolean newState = !isClientEnabled(type); + clientToggles.put(type, newState); + if (newState) { + ensureHandler(type, true); + } + return newState; + } + + public static boolean isServerEnabled(String type) { + Boolean val = serverToggles.get(type.toLowerCase()); + return val != null && val; + } + + public static boolean isClientEnabled(String type) { + Boolean val = clientToggles.get(type.toLowerCase()); + return val != null && val; + } + + // ==================== LOGGING API ==================== + + /** + * Log a debug message for the given type on the given side. + * Callers should guard with isServerEnabled/isClientEnabled for performance. + */ + public static void log(String type, boolean client, String msg) { + type = type.toLowerCase(); + Map toggles = client ? clientToggles : serverToggles; + Boolean enabled = toggles.get(type); + if (enabled == null || !enabled) return; + + Handler handler = ensureHandler(type, client); + if (handler == null) return; + + String side = client ? "CLIENT" : "SERVER"; + String timestamp; + synchronized (DATE_FORMAT) { + timestamp = DATE_FORMAT.format(new Date()); + } + String formatted = "[" + timestamp + "][" + side + "][DEBUG-" + type.toUpperCase() + "] " + msg + System.lineSeparator(); + + LogRecord record = new LogRecord(Level.INFO, formatted); + handler.publish(record); + handler.flush(); + } + + /** + * Log on server side. + */ + public static void logServer(String type, String msg) { + log(type, false, msg); + } + + /** + * Log on client side. + */ + public static void logClient(String type, String msg) { + log(type, true, msg); + } + + // ==================== INTERNALS ==================== + + private static Handler ensureHandler(String type, boolean client) { + Map handlers = client ? clientHandlers : serverHandlers; + Handler existing = handlers.get(type); + if (existing != null) return existing; + + synchronized (CNPCDebug.class) { + existing = handlers.get(type); + if (existing != null) return existing; + + try { + File dir = new File(DIR); + if (!dir.exists()) dir.mkdirs(); + + String side = client ? "client" : "server"; + File file = new File(dir, type + "_" + side + ".log"); + + // Rotate old log + if (file.exists()) { + File backup = new File(dir, type + "_" + side + "-prev.log"); + if (backup.exists()) backup.delete(); + file.renameTo(backup); + } + + Handler handler = new StreamHandler(new FileOutputStream(file), new Formatter() { + @Override + public String format(LogRecord record) { + return record.getMessage(); + } + }); + handler.setLevel(Level.ALL); + handlers.put(type, handler); + + // Log header + String timestamp; + synchronized (DATE_FORMAT) { + timestamp = DATE_FORMAT.format(new Date()); + } + LogRecord header = new LogRecord(Level.INFO, + "[" + timestamp + "][" + (client ? "CLIENT" : "SERVER") + "][DEBUG-" + type.toUpperCase() + "] Debug logging started" + System.lineSeparator()); + handler.publish(header); + handler.flush(); + + return handler; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index cd889fa5b..971f3b0fd 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -6,6 +6,7 @@ import cpw.mods.fml.client.registry.RenderingRegistry; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.ObfuscationReflectionHelper; import kamkeel.npcs.addon.client.DBCClient; +import kamkeel.npcs.client.command.CommandCNPCDebugger; import kamkeel.npcs.client.renderer.RenderAbilityBeam; import kamkeel.npcs.client.renderer.RenderAbilityDisc; import kamkeel.npcs.client.renderer.RenderAbilityLaser; @@ -53,6 +54,7 @@ import net.minecraft.util.ChatComponentText; import net.minecraft.util.ReportedException; import net.minecraft.util.ResourceLocation; import net.minecraft.world.World; +import net.minecraftforge.client.ClientCommandHandler; import net.minecraftforge.client.MinecraftForgeClient; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayer; @@ -360,6 +362,9 @@ public class ClientProxy extends CommonProxy { ClientTagMapController.Instance = new ClientTagMapController(); MinecraftForge.EVENT_BUS.register(new ClientEventHandler()); + // Debug commands + ClientCommandHandler.instance.registerCommand(new CommandCNPCDebugger()); + // Telegraph rendering system TelegraphManager.initClient(); MinecraftForge.EVENT_BUS.register(new TelegraphRenderer());