From 930b7ed4234292e5f5bf2e1315e62627d995babc Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 24 Feb 2026 19:05:08 -0500 Subject: [PATCH] Better Energy Charging Syncing --- .../type/energy/AbilityEnergyProjectile.java | 15 +++ .../energycharge/EnergyChargeTracker.java | 112 ++++++++++++++++++ .../npcs/entity/EntityEnergyProjectile.java | 13 +- src/main/java/noppes/npcs/CustomNpcs.java | 5 +- .../java/noppes/npcs/ServerEventsHandler.java | 16 ++- 5 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.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 6054f3a41..fdf2d827e 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 @@ -10,6 +10,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.ProjectileData; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -287,6 +288,15 @@ public abstract class AbilityEnergyProjectile chargeVisualIds[index] = id; chargeVisualCaster = caster; EnergyChargeSpawnPacket.sendToTracking(id, previewEntity, caster); + + // Register with server-side tracker for late-join support + EnergyChargeTracker.Instance.add(new EnergyChargeTracker.ChargeEntry( + id, + previewEntity.getClass().getName(), + previewEntity.exportSpawnNBT(), + caster.getEntityId(), + (int) caster.worldObj.getTotalWorldTime() + )); } protected void removeChargeVisual(EntityLivingBase caster, int index) { @@ -296,6 +306,7 @@ public abstract class AbilityEnergyProjectile String id = chargeVisualIds[index]; if (id != null && !id.isEmpty()) { EnergyChargeRemovePacket.sendToTracking(id, caster); + EnergyChargeTracker.Instance.remove(id, caster.getEntityId()); chargeVisualIds[index] = null; } } @@ -305,6 +316,10 @@ public abstract class AbilityEnergyProjectile for (int i = 0; i < chargeVisualIds.length; i++) { removeChargeVisual(caster, i); } + // Safety net: ensure tracker is fully cleaned for this caster + if (!isPreview() && caster.worldObj != null && !caster.worldObj.isRemote) { + EnergyChargeTracker.Instance.removeAllForCaster(caster.getEntityId()); + } } // ==================== LIFECYCLE ==================== diff --git a/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java b/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java new file mode 100644 index 000000000..606efb2fc --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargeTracker.java @@ -0,0 +1,112 @@ +package kamkeel.npcs.controllers.data.energycharge; + +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Server-side tracker for active charging preview visuals. + * Stores enough data to resend {@link EnergyChargeSpawnPacket} to late-joining players + * who enter tracking range after the initial broadcast. + *

+ * Keyed by caster entity ID so {@code PlayerEvent.StartTracking} can look up + * active charges for a newly tracked entity. + */ +public class EnergyChargeTracker { + public static final EnergyChargeTracker Instance = new EnergyChargeTracker(); + + public static class ChargeEntry { + public final String instanceId; + public final String entityClassName; + public final NBTTagCompound spawnNbt; + public final int casterEntityId; + public final int startTick; + + public ChargeEntry(String instanceId, String entityClassName, + NBTTagCompound spawnNbt, int casterEntityId, int startTick) { + this.instanceId = instanceId; + this.entityClassName = entityClassName; + this.spawnNbt = spawnNbt; + this.casterEntityId = casterEntityId; + this.startTick = startTick; + } + } + + private final Map> activeCharges = new HashMap<>(); + + public void add(ChargeEntry entry) { + List list = activeCharges.get(entry.casterEntityId); + if (list == null) { + list = new ArrayList<>(); + activeCharges.put(entry.casterEntityId, list); + } + list.add(entry); + } + + public void remove(String instanceId, int casterEntityId) { + List list = activeCharges.get(casterEntityId); + if (list == null) return; + Iterator it = list.iterator(); + while (it.hasNext()) { + if (it.next().instanceId.equals(instanceId)) { + it.remove(); + break; + } + } + if (list.isEmpty()) { + activeCharges.remove(casterEntityId); + } + } + + public void removeAllForCaster(int casterEntityId) { + activeCharges.remove(casterEntityId); + } + + /** + * Send all active charges for a caster to a specific player. + * Adjusts ChargeTick in the NBT to reflect elapsed time so the + * late-joining client starts at the correct visual progress. + */ + public void sendToPlayer(int casterEntityId, EntityPlayerMP player, int currentWorldTick) { + List entries = activeCharges.get(casterEntityId); + if (entries == null || entries.isEmpty()) return; + + for (ChargeEntry entry : entries) { + int elapsed = currentWorldTick - entry.startTick; + int chargeDuration = entry.spawnNbt.hasKey("ChargeDuration") + ? entry.spawnNbt.getInteger("ChargeDuration") : 0; + + // Charge already complete — server remove packet should follow shortly + if (chargeDuration > 0 && elapsed >= chargeDuration) { + continue; + } + + NBTTagCompound adjustedNbt = (NBTTagCompound) entry.spawnNbt.copy(); + if (elapsed > 0) { + adjustedNbt.setInteger("ChargeTick", elapsed); + } + + PacketHandler.Instance.sendToPlayer( + new EnergyChargeSpawnPacket(entry.instanceId, entry.entityClassName, adjustedNbt), + player + ); + } + } + + public boolean hasCharges(int casterEntityId) { + List list = activeCharges.get(casterEntityId); + return list != null && !list.isEmpty(); + } + + public void clear() { + activeCharges.clear(); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index a5f892f69..ff7a66c5e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -221,6 +221,10 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (ticksExisted == 1 && !worldObj.isRemote) { trackProjectile(this); } + } else { + // Preview entities skip super.onUpdate() but still need ticksExisted + // for renderer pulsing effects (e.g., sin(ticksExisted + partialTicks)). + this.ticksExisted++; } if (!previewMode) { @@ -230,8 +234,13 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // Update rotation updateRotation(); - // Lerp render size toward actual size - this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; + // Update render size: during charging, track directly (size already grows smoothly). + // Post-charging, lerp for smooth visual transitions. + if (isCharging()) { + this.renderCurrentSize = this.size; + } else { + this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; + } // Server-synced visual timer used for brief barrier impact lightning. if (!previewMode && !worldObj.isRemote) { diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 2f6d65a7c..4087880ce 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -26,6 +26,7 @@ import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.developer.Developer; import kamkeel.npcs.entity.EntityAbilityBarrier; @@ -37,6 +38,7 @@ import kamkeel.npcs.entity.EntityAbilityPanel; import kamkeel.npcs.entity.EntityAbilitySweeper; import kamkeel.npcs.entity.EntityAbilityZone; import kamkeel.npcs.entity.EntityAbilityDome; +import kamkeel.npcs.entity.EntityEnergyProjectile; import kamkeel.npcs.entity.EntityEnergySlicer; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.util.BukkitUtil; @@ -430,8 +432,9 @@ public class CustomNpcs { AuctionController.Instance.save(); } - kamkeel.npcs.entity.EntityEnergyProjectile.clearAllProjectiles(); + EntityEnergyProjectile.clearAllProjectiles(); EntityAbilityBarrier.clearAllBarriers(); + EnergyChargeTracker.Instance.clear(); if (FMLCommonHandler.instance().getSide().isClient()) clientJaninoCompiler = null; diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 43d78e6eb..9cbede79a 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -4,6 +4,7 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.relauncher.Side; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker; import kamkeel.npcs.entity.EntityAbilityBarrier; import kamkeel.npcs.entity.EntityAbilityDome; import kamkeel.npcs.network.PacketHandler; @@ -300,6 +301,7 @@ public class ServerEventsHandler { public void invoke(LivingDeathEvent event) { if (event.entityLiving.worldObj.isRemote) return; + EnergyChargeTracker.Instance.removeAllForCaster(event.entityLiving.getEntityId()); if (event.source.getEntity() != null) { if (event.source.getEntity() instanceof EntityPlayer) { doExcalibur((EntityPlayer) event.source.getEntity(), event.entityLiving); @@ -535,7 +537,19 @@ public class ServerEventsHandler { @SubscribeEvent public void playerTracking(PlayerEvent.StartTracking event) { - if (!(event.target instanceof EntityPlayerMP || event.target instanceof EntityNPCInterface) || event.target.worldObj.isRemote) + if (event.target.worldObj.isRemote) return; + + // Energy charge visuals: send active charges for newly tracked entity + if (event.target instanceof EntityLivingBase + && event.entityPlayer instanceof EntityPlayerMP) { + EnergyChargeTracker.Instance.sendToPlayer( + event.target.getEntityId(), + (EntityPlayerMP) event.entityPlayer, + (int) event.target.worldObj.getTotalWorldTime() + ); + } + + if (!(event.target instanceof EntityPlayerMP || event.target instanceof EntityNPCInterface)) return; AnimationData animationData = AnimationData.getData(event.target);