From 18968bf7c787c978cb52e4d29d7f01dd36de0fe8 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 22 Feb 2026 03:07:37 -0500 Subject: [PATCH] Fix Firing Energy Projectiles and Make Charging State purely visual --- .../renderer/EnergyChargePreviewRenderer.java | 38 +++ .../type/energy/AbilityEnergyProjectile.java | 228 ++++++++++++---- .../ability/type/energy/AbilityLaserShot.java | 2 +- .../EnergyChargePreviewManager.java | 68 +++++ .../npcs/entity/EntityAbilityBeam.java | 256 ++++++++++-------- .../npcs/entity/EntityAbilityDisc.java | 58 +--- .../npcs/entity/EntityAbilityLaser.java | 199 ++++++++------ .../kamkeel/npcs/entity/EntityAbilityOrb.java | 156 +++++++---- .../npcs/entity/EntityEnergyAbility.java | 22 ++ .../npcs/entity/EntityEnergyProjectile.java | 253 ++++++++++++++++- .../npcs/entity/EntityEnergySlicer.java | 47 +--- .../kamkeel/npcs/network/PacketHandler.java | 4 + .../npcs/network/enums/EnumDataPacket.java | 4 + .../EnergyChargeRemovePacket.java | 60 ++++ .../energycharge/EnergyChargeSpawnPacket.java | 108 ++++++++ .../java/noppes/npcs/client/ClientProxy.java | 6 + .../noppes/npcs/client/ClientTickHandler.java | 9 + .../npcs/entity/EntityNPCInterface.java | 7 +- 18 files changed, 1086 insertions(+), 439 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/client/renderer/EnergyChargePreviewRenderer.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/energycharge/EnergyChargePreviewManager.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/data/energycharge/EnergyChargeRemovePacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/data/energycharge/EnergyChargeSpawnPacket.java diff --git a/src/main/java/kamkeel/npcs/client/renderer/EnergyChargePreviewRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/EnergyChargePreviewRenderer.java new file mode 100644 index 000000000..66bf4869b --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/EnergyChargePreviewRenderer.java @@ -0,0 +1,38 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager; +import kamkeel.npcs.entity.EntityEnergyProjectile; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraftforge.client.event.RenderWorldLastEvent; + +/** + * Renders packet-driven charging previews through RenderManager. + */ +public class EnergyChargePreviewRenderer { + + @SubscribeEvent + public void onRenderWorldLast(RenderWorldLastEvent event) { + if (EnergyChargePreviewManager.ClientInstance == null || !EnergyChargePreviewManager.ClientInstance.hasPreviews()) { + return; + } + + Minecraft mc = Minecraft.getMinecraft(); + EntityPlayer player = mc.thePlayer; + if (player == null) return; + + float partialTicks = event.partialTicks; + + for (EntityEnergyProjectile entity : EnergyChargePreviewManager.ClientInstance.getPreviews()) { + if (entity == null || entity.isDead) continue; + + double x = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks - RenderManager.renderPosX; + double y = entity.prevPosY + (entity.posY - entity.prevPosY) * partialTicks - RenderManager.renderPosY; + double z = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks - RenderManager.renderPosZ; + + RenderManager.instance.renderEntityWithPosYaw(entity, x, y, z, entity.rotationYaw, partialTicks); + } + } +} 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 40ea82080..3cb81720d 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 @@ -15,6 +15,8 @@ import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.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.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -55,6 +57,10 @@ public abstract class AbilityEnergyProjectile // Transient entity state protected transient E[] entities; + protected transient boolean[] projectileSpawned; + protected transient int spawnedCount; + protected transient String[] chargeVisualIds; + protected transient EntityLivingBase chargeVisualCaster; // ==================== CONSTRUCTOR ==================== @@ -77,7 +83,7 @@ public abstract class AbilityEnergyProjectile /** * Create a single entity for the given projectile index. - * Called during windup (tick 1) and burst refire (onExecute when entities are null). + * Called when a projectile actually fires. */ protected abstract E createEntity(EntityLivingBase caster, EntityLivingBase target, Vec3 spawnPos, EnergyDisplayData resolved, int index); @@ -209,32 +215,99 @@ public abstract class AbilityEnergyProjectile // ==================== ENTITY CREATION ==================== - /** - * Create all entities for this ability. - * Sets effects, source ability, and sibling links. - */ - protected E[] createAllEntities(EntityLivingBase caster, EntityLivingBase target) { - E[] newEntities = createEntityArray(projectileCount); - for (int i = 0; i < projectileCount; i++) { - Vec3 spawnPos = getSpawnPosition(caster, i); - EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); - newEntities[i] = createEntity(caster, target, spawnPos, resolved, i); - newEntities[i].setEffects(this.effects); - newEntities[i].setSourceAbility(this); + protected void initRuntimeState(EntityLivingBase caster) { + entities = createEntityArray(projectileCount); + projectileSpawned = new boolean[projectileCount]; + spawnedCount = 0; + chargeVisualIds = new String[projectileCount]; + chargeVisualCaster = caster; + } + + protected E createProjectileEntity(EntityLivingBase caster, EntityLivingBase target, int index) { + Vec3 spawnPos = getSpawnPosition(caster, index); + EnergyDisplayData resolved = projectiles[index].resolveDisplay(displayData); + E entity = createEntity(caster, target, spawnPos, resolved, index); + entity.setEffects(this.effects); + entity.setSourceAbility(this); + return entity; + } + + protected void spawnProjectileEntity(E entity, int index) { + if (entity == null) return; + spawnAbilityEntity(entity); + entities[index] = entity; + if (!projectileSpawned[index]) { + projectileSpawned[index] = true; + spawnedCount++; } - return newEntities; + } + + protected void spawnAndFireProjectile(EntityLivingBase caster, EntityLivingBase target, int index) { + if (entities != null && entities[index] != null && !entities[index].isDead) return; + E entity = createProjectileEntity(caster, target, index); + + // Initialize launch position/motion BEFORE spawn so the + // initial spawn packet already has centered look-vector data on all clients. + fireEntitySafe(entity, caster, target); + spawnProjectileEntity(entity, index); + } + + /** + * Hook for launch target selection. + * Default returns null so projectiles launch along owner look vector. + */ + protected EntityLivingBase getLaunchTarget(EntityLivingBase caster, EntityLivingBase target) { + return null; } /** * Fire an entity if it's valid. * Clears charging state first so the entity can move and check collisions. */ - protected void fireEntitySafe(E entity, EntityLivingBase target) { + protected void fireEntitySafe(E entity, EntityLivingBase caster, EntityLivingBase target) { if (entity != null && !entity.isDead) { if (entity.isCharging()) { entity.setCharging(false); } - fireEntity(entity, target); + fireEntity(entity, getLaunchTarget(caster, target)); + } + } + + protected String getChargeVisualId(EntityLivingBase caster, int index) { + return "energy_charge:" + caster.getEntityId() + ":" + executionStartTime + ":" + burstIndex + ":" + index; + } + + protected void spawnChargeVisual(EntityLivingBase caster, EntityLivingBase target, int index, int chargeDuration) { + if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return; + if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return; + + E previewEntity = createProjectileEntity(caster, target, index); + previewEntity.setPreviewMode(true); + previewEntity.setPreviewOwner(caster); + setupEntityCharging(previewEntity, projectiles[index], index); + previewEntity.setChargeDuration(Math.max(1, chargeDuration)); + + String id = getChargeVisualId(caster, index); + chargeVisualIds[index] = id; + chargeVisualCaster = caster; + EnergyChargeSpawnPacket.sendToTracking(id, previewEntity, caster); + } + + protected void removeChargeVisual(EntityLivingBase caster, int index) { + if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return; + if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return; + + String id = chargeVisualIds[index]; + if (id != null && !id.isEmpty()) { + EnergyChargeRemovePacket.sendToTracking(id, caster); + chargeVisualIds[index] = null; + } + } + + protected void removeAllChargeVisuals(EntityLivingBase caster) { + if (chargeVisualIds == null || caster == null) return; + for (int i = 0; i < chargeVisualIds.length; i++) { + removeChargeVisual(caster, i); } } @@ -253,6 +326,8 @@ public abstract class AbilityEnergyProjectile @Override public void detach() { entities = null; + projectileSpawned = null; + spawnedCount = 0; } @Override @@ -275,21 +350,31 @@ public abstract class AbilityEnergyProjectile if (caster.worldObj.isRemote && !isPreview()) return; if (tick == 1) { - entities = createAllEntities(caster, target); - for (int i = 0; i < projectileCount; i++) { - EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); - if (isPreview()) { - setupEntityPreview(entities[i], caster, resolved, projectiles[i], i); - } else { - setupEntityCharging(entities[i], projectiles[i], i); + initRuntimeState(caster); + + if (isPreview()) { + for (int i = 0; i < projectileCount; i++) { + E previewEntity = createProjectileEntity(caster, target, i); + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i); + spawnAbilityEntity(previewEntity); + entities[i] = previewEntity; + projectileSpawned[i] = true; + spawnedCount++; } - spawnAbilityEntity(entities[i]); + return; + } + + for (int i = 0; i < projectileCount; i++) { + spawnChargeVisual(caster, target, i, windUpTicks); } } } @Override public void resetForBurst() { + removeAllChargeVisuals(chargeVisualCaster); + // Non-overlap: kill old entities before next burst creates new ones if (!burstOverlap) { cleanup(); @@ -297,54 +382,71 @@ public abstract class AbilityEnergyProjectile // Clear reference so the next iteration creates fresh entities. // For overlap mode, old entities stay alive (tracked in burstEntities). entities = null; + projectileSpawned = null; + spawnedCount = 0; } @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target) { - // Create entities if they don't exist yet. - // They'll already exist if onWindUpTick ran (windup replay), otherwise we create here. - if (entities == null) { - entities = createAllEntities(caster, target); - - // If there's a fire delay, put unfired entities in charging state - // to prevent collision checks before they're actually fired. - // Entity[0] will be fired immediately below, so skip it. - if (fireDelay > 0 && projectileCount > 1) { - for (int i = 1; i < projectileCount; i++) { - if (entities[i] != null) { - entities[i].setCharging(true); - } + // GUI preview keeps the original non-world preview entity flow. + if (isPreview()) { + if (entities == null) { + initRuntimeState(caster); + for (int i = 0; i < projectileCount; i++) { + E previewEntity = createProjectileEntity(caster, target, i); + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i); + spawnAbilityEntity(previewEntity); + entities[i] = previewEntity; + projectileSpawned[i] = true; + spawnedCount++; } } - for (E entity : entities) { - spawnAbilityEntity(entity); + fireEntitySafe(entities[0], caster, target); + if (fireDelay <= 0) { + for (int i = 1; i < projectileCount; i++) { + fireEntitySafe(entities[i], caster, target); + } } + return; } - // Fire first projectile immediately - fireEntitySafe(entities[0], target); + if (entities == null || projectileSpawned == null || entities.length != projectileCount) { + initRuntimeState(caster); + } - // Fire remaining projectiles if no delay + removeAllChargeVisuals(caster); + + // Fire first projectile immediately. + spawnAndFireProjectile(caster, target, 0); + + // Fire remaining projectiles immediately if there is no stagger delay. if (fireDelay <= 0) { for (int i = 1; i < projectileCount; i++) { - fireEntitySafe(entities[i], target); + spawnAndFireProjectile(caster, target, i); + } + } else { + // During active stagger, keep client-only charging visuals for unfired projectiles. + for (int i = 1; i < projectileCount; i++) { + spawnChargeVisual(caster, target, i, fireDelay * i); } } } @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { - if (entities == null) { + if (entities == null || projectileSpawned == null) { signalCompletion(); return; } - // Fire staggered projectiles + // Fire staggered projectiles: spawn real world entity only when it is time to fire. if (fireDelay > 0) { for (int i = 1; i < projectileCount; i++) { if (tick == fireDelay * i) { - fireEntitySafe(entities[i], target); + removeChargeVisual(caster, i); + spawnAndFireProjectile(caster, target, i); } } } @@ -358,21 +460,21 @@ public abstract class AbilityEnergyProjectile } } - // Check if all entities are dead or unreachable - boolean allDead = true; - for (E entity : entities) { - if (entity == null || entity.isDead) continue; + // Before all projectiles have fired, the ability is still active by definition. + boolean allDead = spawnedCount >= projectileCount; + if (allDead) { + for (E entity : entities) { + if (entity == null || entity.isDead) continue; - // Check if entity was removed from world (chunk unload, etc.) - // Skip this check for preview entities - they're not registered in the world - if (!isPreview() && tick > 5 && entity.worldObj != null - && entity.worldObj.getEntityByID(entity.getEntityId()) != entity) { - entity.setDead(); - continue; + if (!isPreview() && tick > 5 && entity.worldObj != null + && entity.worldObj.getEntityByID(entity.getEntityId()) != entity) { + entity.setDead(); + continue; + } + + allDead = false; + break; } - - allDead = false; - break; } // Failsafe timeout: force completion if active phase exceeded max possible flight time @@ -392,15 +494,19 @@ public abstract class AbilityEnergyProjectile @Override public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + removeAllChargeVisuals(caster); } @Override public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + removeAllChargeVisuals(caster); cleanup(); } @Override public void cleanup() { + removeAllChargeVisuals(chargeVisualCaster); + if (entities != null) { for (int i = 0; i < entities.length; i++) { if (entities[i] != null && !entities[i].isDead) { @@ -410,6 +516,10 @@ public abstract class AbilityEnergyProjectile } } entities = null; + projectileSpawned = null; + spawnedCount = 0; + chargeVisualIds = null; + chargeVisualCaster = null; } // ==================== TELEGRAPH ==================== 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/AbilityLaserShot.java index 30e99fedb..dafbc2200 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/AbilityLaserShot.java @@ -139,7 +139,7 @@ public class AbilityLaserShot extends AbilityEnergyProjectile previews = new ConcurrentHashMap(); + + public static void initClient() { + ClientInstance = new EnergyChargePreviewManager(); + } + + public void addPreview(String instanceId, EntityEnergyProjectile entity) { + if (instanceId == null || entity == null) return; + previews.put(instanceId, entity); + } + + public void removePreview(String instanceId) { + if (instanceId == null) return; + previews.remove(instanceId); + } + + public Collection getPreviews() { + return previews.values(); + } + + public boolean hasPreviews() { + return !previews.isEmpty(); + } + + public void clear() { + previews.clear(); + } + + /** + * Tick all preview entities and remove invalid/expired entries. + */ + public void tick(World world) { + if (world == null) { + clear(); + return; + } + + Iterator iterator = previews.values().iterator(); + while (iterator.hasNext()) { + EntityEnergyProjectile entity = iterator.next(); + if (entity == null || entity.isDead || entity.worldObj != world) { + iterator.remove(); + continue; + } + entity.onUpdate(); + if (entity.isDead) { + iterator.remove(); + } + } + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 44589a842..d040d87e4 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -18,6 +18,7 @@ import net.minecraft.world.World; import noppes.npcs.LogWriter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -31,6 +32,11 @@ import java.util.List; */ public class EntityAbilityBeam extends EntityEnergyProjectile { + private enum BeamMode { + ANCHORED, + FREE_TRAIL + } + // Beam shape properties private float beamWidth = 0.3f; private float headSize = 0.5f; @@ -39,22 +45,18 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { private List trailPoints = new ArrayList<>(); private static final int MAX_TRAIL_POINTS = 200; private static final double MIN_POINT_DISTANCE = 0.2; + private static final int TRAIL_COMPACT_THRESHOLD = 128; + private int trailStartIndex = 0; // Head position relative to origin private double headOffsetX, headOffsetY, headOffsetZ; private double prevHeadOffsetX, prevHeadOffsetY, prevHeadOffsetZ; - // Origin stays fixed (or follows owner) - private boolean attachedToOwner = true; - - // Whether to render tail orb (only when anchored) - private boolean renderTailOrb = true; + // Beam behavior mode (single source of truth). + private BeamMode beamMode = BeamMode.ANCHORED; // Charging state (beam-specific) private float chargeOffsetDistance = 1.0f; - - // Trail fading for non-anchored beams (comet effect) - private boolean fadeTrail = false; private int trailFadeTime = 20; // Ticks for trail to fully fade private List trailPointAges = new ArrayList<>(); @@ -65,6 +67,46 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { super(world); } + private static BeamMode modeFromAnchored(boolean anchored) { + return anchored ? BeamMode.ANCHORED : BeamMode.FREE_TRAIL; + } + + private boolean isAnchoredMode() { + return beamMode == BeamMode.ANCHORED; + } + + private boolean isFadingMode() { + return beamMode == BeamMode.FREE_TRAIL; + } + + private int getActiveTrailSize() { + return Math.max(0, trailPoints.size() - trailStartIndex); + } + + private void resetTrailStorage() { + trailPoints.clear(); + trailPointAges.clear(); + trailStartIndex = 0; + } + + private void setBeamMode(BeamMode mode) { + beamMode = mode != null ? mode : BeamMode.ANCHORED; + if (isFadingMode()) { + while (trailPointAges.size() < trailPoints.size()) { + trailPointAges.add(0); + } + while (trailPointAges.size() > trailPoints.size()) { + trailPointAges.remove(trailPointAges.size() - 1); + } + } else { + if (trailStartIndex > 0 && trailStartIndex < trailPoints.size()) { + trailPoints = new ArrayList(trailPoints.subList(trailStartIndex, trailPoints.size())); + } + trailStartIndex = 0; + trailPointAges.clear(); + } + } + /** * Full constructor with all parameters using data classes. * @@ -89,9 +131,8 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { this.beamWidth = beamWidth; this.headSize = headSize; - // Anchored mode controls whether origin follows owner and tail orb is rendered - this.attachedToOwner = anchoredMode; - this.renderTailOrb = anchoredMode; + // Beam mode controls tail attachment/orb/fading behavior. + setBeamMode(modeFromAnchored(anchoredMode)); // Initialize head offset at origin (0,0,0 relative) this.headOffsetX = 0; @@ -118,7 +159,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { this.chargeTick = 0; this.chargeOffsetDistance = chargeOffsetDistance; this.anchorData = anchor; - this.fadeTrail = !attachedToOwner; this.motionX = 0; this.motionY = 0; this.motionZ = 0; @@ -160,10 +200,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { this.prevHeadOffsetY = 0; this.prevHeadOffsetZ = 0; - // Attach to owner for anchor following - this.attachedToOwner = true; - this.renderTailOrb = true; - // Clear motion this.motionX = 0; this.motionY = 0; @@ -174,12 +210,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { * Start preview firing (simulates firing toward a point in front of NPC). */ public void startPreviewFiring() { - setCharging(false); - - // Origin (tail) stays at the charged position - startX = posX; - startY = posY; - startZ = posZ; + beginLookVectorLaunch(true); // Reset head offset to origin headOffsetX = 0; @@ -189,51 +220,21 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { prevHeadOffsetY = 0; prevHeadOffsetZ = 0; - // Origin no longer follows owner after firing - attachedToOwner = false; - - // Sync prev position to prevent visual jump on first frame - prevPosX = posX; - prevPosY = posY; - prevPosZ = posZ; - // Reset trail - trailPoints.clear(); - trailPointAges.clear(); + resetTrailStorage(); trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + if (isFadingMode()) trailPointAges.add(0); - // Fire forward based on owner facing direction - Entity owner = getOwnerEntity(); - if (owner != null) { - float yaw = (float) Math.toRadians(owner.rotationYaw); - motionX = -Math.sin(yaw) * getSpeed(); - motionY = 0; - motionZ = Math.cos(yaw) * getSpeed(); - } else { - motionX = getSpeed(); - motionY = 0; - motionZ = 0; - } + // Fire along look vector for parity with real launch behavior. + setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0); } /** * Start the beam firing (exit charging mode). * Called by ability when windup ends. - *

- * For anchored beams: origin follows owner, head starts at charged position - * For non-anchored beams: origin fixed at charged position, head starts there */ public void startFiring(EntityLivingBase target) { - setCharging(false); - - // For player casters, snap to look vector for crosshair-aligned launch - snapToPlayerLookVector(); - - // Origin (tail) stays at the charged position - where the orb was - // This is the same for both anchored and non-anchored modes - startX = posX; - startY = posY; - startZ = posZ; + beginLookVectorLaunch(false); // Head starts at the origin (tail position) headOffsetX = 0; @@ -243,36 +244,13 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { prevHeadOffsetY = 0; prevHeadOffsetZ = 0; - // Origin no longer follows owner after firing starts - // (for anchored mode, tail is fixed in space; for non-anchored, there's no tail) - attachedToOwner = false; - // Initialize trail with just the origin point - trailPoints.clear(); - trailPointAges.clear(); + resetTrailStorage(); trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); - if (fadeTrail) trailPointAges.add(0); + if (isFadingMode()) trailPointAges.add(0); - // Calculate velocity toward target (head starts at origin = startX/Y/Z) - Entity owner = getOwnerEntity(); - - if (target != null) { - double dx = target.posX - startX; - double dy = (target.posY + target.getEyeHeight()) - startY; - double dz = target.posZ - startZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - motionX = (dx / len) * getSpeed(); - motionY = (dy / len) * getSpeed(); - motionZ = (dz / len) * getSpeed(); - } - } else if (owner != null) { - float yaw = (float) Math.toRadians(owner.rotationYaw); - float pitch = (float) Math.toRadians(owner.rotationPitch); - motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed(); - motionY = -Math.sin(pitch) * getSpeed(); - motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed(); - } + // Calculate velocity toward target when provided; otherwise along look vector. + setMotionTowardTargetOrLookVector(target, startX, startY, startZ, getSpeed(), getSpeed(), 0, 0); if (DEBUG_LOGGING && !worldObj.isRemote) { LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ + @@ -310,7 +288,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { prevHeadOffsetZ = headOffsetZ; // Age trail points for fading effect - if (fadeTrail) { + if (isFadingMode()) { ageTrailPoints(); } @@ -344,7 +322,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { headOffsetZ += motionZ; // Update origin if attached to owner - if (attachedToOwner) { + if (isAnchoredMode()) { Entity owner = getOwnerEntity(); if (owner != null) { startX = owner.posX; @@ -372,7 +350,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { String.format("%.2f", headOffsetZ) + ") origin=(" + String.format("%.2f", startX) + "," + String.format("%.2f", startY) + "," + - String.format("%.2f", startZ) + ") trail=" + trailPoints.size()); + String.format("%.2f", startZ) + ") trail=" + getActiveTrailSize()); } // Skip collision checks on first few ticks @@ -418,25 +396,43 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { * Age trail points and remove old ones (for comet effect). */ private void ageTrailPoints() { - // Age all trail points - for (int i = 0; i < trailPointAges.size(); i++) { + if (trailStartIndex >= trailPoints.size()) { + resetTrailStorage(); + return; + } + + // Age all active trail points. + for (int i = trailStartIndex; i < trailPointAges.size(); i++) { trailPointAges.set(i, trailPointAges.get(i) + 1); } - // Remove trail points that have fully faded - while (!trailPointAges.isEmpty() && trailPointAges.get(0) >= trailFadeTime) { - trailPointAges.remove(0); - if (!trailPoints.isEmpty()) { - trailPoints.remove(0); - } + // Move logical start index forward for fully faded points. + while (trailStartIndex < trailPointAges.size() && trailPointAges.get(trailStartIndex) >= trailFadeTime) { + trailStartIndex++; + } + + if (trailStartIndex >= trailPoints.size()) { + resetTrailStorage(); + return; + } + + // Compact stale prefix to avoid unbounded growth of dead entries. + if (trailStartIndex >= TRAIL_COMPACT_THRESHOLD && trailStartIndex * 2 >= trailPoints.size()) { + trailPoints = new ArrayList(trailPoints.subList(trailStartIndex, trailPoints.size())); + trailPointAges = new ArrayList(trailPointAges.subList(trailStartIndex, trailPointAges.size())); + trailStartIndex = 0; } } private void addTrailPoint() { + if (trailStartIndex >= trailPoints.size()) { + resetTrailStorage(); + } + // Trail points are RELATIVE to origin if (trailPoints.isEmpty()) { trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); - if (fadeTrail) { + if (isFadingMode()) { trailPointAges.add(0); } return; @@ -451,12 +447,12 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { if (dist >= MIN_POINT_DISTANCE) { trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); - if (fadeTrail) { + if (isFadingMode()) { trailPointAges.add(0); } // Limit trail length (only if not using fading - fading handles its own cleanup) - if (!fadeTrail) { + if (!isFadingMode()) { while (trailPoints.size() > MAX_TRAIL_POINTS) { trailPoints.remove(0); } @@ -474,7 +470,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { headOffsetZ += motionZ; // Update origin if attached to owner - if (attachedToOwner) { + if (isAnchoredMode()) { Entity owner = getOwnerEntity(); if (owner != null) { startX = owner.posX; @@ -586,7 +582,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { } public void setAttachedToOwner(boolean attached) { - this.attachedToOwner = attached; + setBeamMode(modeFromAnchored(attached)); } /** @@ -609,7 +605,13 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { * Get trail points. These are RELATIVE to origin (startX/Y/Z). */ public List getTrailPoints() { - return trailPoints; + if (trailStartIndex <= 0) { + return trailPoints; + } + if (trailStartIndex >= trailPoints.size()) { + return Collections.emptyList(); + } + return trailPoints.subList(trailStartIndex, trailPoints.size()); } public double getOriginX() { @@ -628,28 +630,34 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { * Whether the tail orb should be rendered (only true in anchored mode). */ public boolean shouldRenderTailOrb() { - return renderTailOrb; + return isAnchoredMode(); } /** * Whether the beam is attached to its owner (anchored mode). */ public boolean isAttachedToOwner() { - return attachedToOwner; + return isAnchoredMode(); } /** * Whether trail should fade (comet effect for non-anchored beams). */ public boolean hasFadingTrail() { - return fadeTrail; + return isFadingMode(); } /** * Get trail point ages for fading calculation. */ public List getTrailPointAges() { - return trailPointAges; + if (trailStartIndex <= 0) { + return trailPointAges; + } + if (trailStartIndex >= trailPointAges.size()) { + return Collections.emptyList(); + } + return trailPointAges.subList(trailStartIndex, trailPointAges.size()); } /** @@ -671,17 +679,26 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { this.prevHeadOffsetX = this.headOffsetX; this.prevHeadOffsetY = this.headOffsetY; this.prevHeadOffsetZ = this.headOffsetZ; - this.attachedToOwner = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner"); - this.renderTailOrb = !nbt.hasKey("RenderTailOrb") || nbt.getBoolean("RenderTailOrb"); readChargingNBT(nbt); this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; - this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail"); this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20; + boolean attachedLegacy = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner"); + BeamMode loadedMode = modeFromAnchored(attachedLegacy); + if (nbt.hasKey("BeamMode")) { + try { + loadedMode = BeamMode.valueOf(nbt.getString("BeamMode")); + } catch (IllegalArgumentException ignored) { + loadedMode = modeFromAnchored(attachedLegacy); + } + } else if (nbt.hasKey("FadeTrail")) { + loadedMode = nbt.getBoolean("FadeTrail") ? BeamMode.FREE_TRAIL : modeFromAnchored(attachedLegacy); + } // Read trail points (relative to origin) trailPoints.clear(); trailPointAges.clear(); + trailStartIndex = 0; if (nbt.hasKey("Trail")) { NBTTagList trailList = nbt.getTagList("Trail", 10); for (int i = 0; i < trailList.tagCount(); i++) { @@ -701,10 +718,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { trailPointAges.add(age); } } - // If trail points exist but ages don't (legacy data), initialize ages to 0 - while (trailPointAges.size() < trailPoints.size()) { - trailPointAges.add(0); - } + setBeamMode(loadedMode); } @Override @@ -714,16 +728,18 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { nbt.setDouble("HeadOffsetX", headOffsetX); nbt.setDouble("HeadOffsetY", headOffsetY); nbt.setDouble("HeadOffsetZ", headOffsetZ); - nbt.setBoolean("AttachedToOwner", attachedToOwner); - nbt.setBoolean("RenderTailOrb", renderTailOrb); + nbt.setBoolean("AttachedToOwner", isAnchoredMode()); + nbt.setBoolean("RenderTailOrb", isAnchoredMode()); + nbt.setString("BeamMode", beamMode.name()); writeChargingNBT(nbt); nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); - nbt.setBoolean("FadeTrail", fadeTrail); + nbt.setBoolean("FadeTrail", isFadingMode()); nbt.setInteger("TrailFadeTime", trailFadeTime); // Write trail points NBTTagList trailList = new NBTTagList(); - for (Vec3 point : trailPoints) { + for (int i = trailStartIndex; i < trailPoints.size(); i++) { + Vec3 point = trailPoints.get(i); NBTTagCompound pointNbt = new NBTTagCompound(); pointNbt.setDouble("X", point.xCoord); pointNbt.setDouble("Y", point.yCoord); @@ -733,9 +749,11 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { nbt.setTag("Trail", trailList); // Write trail point ages for fading trail sync - int[] ages = new int[trailPointAges.size()]; - for (int i = 0; i < trailPointAges.size(); i++) { - ages[i] = trailPointAges.get(i); + int activeSize = isFadingMode() ? Math.max(0, trailPoints.size() - trailStartIndex) : 0; + int[] ages = new int[activeSize]; + for (int i = 0; i < activeSize; i++) { + int idx = trailStartIndex + i; + ages[i] = idx < trailPointAges.size() ? trailPointAges.get(idx) : 0; } nbt.setIntArray("TrailAges", ages); } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index b5daa8701..16a1423f0 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -137,31 +137,8 @@ public class EntityAbilityDisc extends EntityEnergyProjectile { * Start preview firing (simulates firing toward a point in front of NPC). */ public void startPreviewFiring() { - setCharging(false); - - // Update start position to current position - startX = posX; - startY = posY; - startZ = posZ; - - // Sync prev position to prevent visual jump on first frame - prevPosX = posX; - prevPosY = posY; - prevPosZ = posZ; - - // Fire forward based on owner facing direction - Entity owner = getOwnerEntity(); - if (owner != null) { - float yaw = (float) Math.toRadians(owner.rotationYaw); - float pitch = 0; // Fire horizontally - motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed(); - motionY = -Math.sin(pitch) * getSpeed(); - motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed(); - } else { - motionX = getSpeed(); - motionY = 0; - motionZ = 0; - } + beginLookVectorLaunch(true); + setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0); } /** @@ -169,35 +146,8 @@ public class EntityAbilityDisc extends EntityEnergyProjectile { * Called by ability when windup ends. */ public void startMoving(EntityLivingBase target) { - setCharging(false); - - // For player casters, snap to look vector for crosshair-aligned launch - snapToPlayerLookVector(); - - // Update start position to current position - startX = posX; - startY = posY; - startZ = posZ; - - // Calculate velocity toward target - Entity owner = getOwnerEntity(); - if (target != null) { - double dx = target.posX - posX; - double dy = (target.posY + target.getEyeHeight()) - posY; - double dz = target.posZ - posZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - motionX = (dx / len) * getSpeed(); - motionY = (dy / len) * getSpeed(); - motionZ = (dz / len) * getSpeed(); - } - } else if (owner != null) { - float yaw = (float) Math.toRadians(owner.rotationYaw); - float pitch = (float) Math.toRadians(owner.rotationPitch); - motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed(); - motionY = -Math.sin(pitch) * getSpeed(); - motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed(); - } + beginLookVectorLaunch(false); + setMotionTowardTargetOrLookVector(target, posX, posY, posZ, getSpeed(), getSpeed(), 0, 0); } @Override diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index ec851f802..67440bf07 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.data.EnergyAnchorData; import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData; import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; @@ -8,7 +9,6 @@ import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; -import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; @@ -156,13 +156,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { } /** - * Update laser origin and direction from the owner's current anchor position and rotation. - * Called each tick so the laser follows the NPC when tracking a target. + * Update laser origin and direction from the owner's current look rotation. + * Called each tick so the laser origin stays centered on look vector. */ private void updateLaserOriginAndDirection() { Entity owner = getOwnerEntity(); if (owner == null || !(owner instanceof EntityLivingBase)) return; - EntityLivingBase livingOwner = (EntityLivingBase) owner; // Update direction from owner's current rotation @@ -180,71 +179,51 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { dirZ = Math.cos(yaw) * Math.cos(pitch); } - // Update origin: for player casters, snap to look vector so laser aligns with crosshair. - // For NPCs, use the configured anchor point. - if (owner instanceof EntityPlayer) { - double eyeY = owner.posY + owner.getEyeHeight(); - float frontDist = Math.max(0.5f, laserWidth * 0.5f); - startX = owner.posX + dirX * frontDist; - startY = eyeY + dirY * frontDist; - startZ = owner.posZ + dirZ * frontDist; - } else if (anchorData != null) { - Vec3 pos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData); - startX = pos.xCoord; - startY = pos.yCoord; - startZ = pos.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); + if (dirLen > 0.0001) { + direction = Vec3.createVectorHelper( + direction.xCoord / dirLen, + direction.yCoord / dirLen, + direction.zCoord / dirLen + ); } else { - startX = owner.posX; - startY = owner.posY + owner.height * 0.7; - startZ = owner.posZ; - } - - // Keep entity positioned at origin - prevPosX = startX; - prevPosY = startY; - prevPosZ = startZ; - setPosition(startX, startY, startZ); - } - - public void startMoving(EntityLivingBase target) { - setCharging(false); - - Entity owner = getOwnerEntity(); - // Set initial direction from owner's rotation - if (owner != null) { - float yaw = (float) Math.toRadians(owner.rotationYaw); - float pitch = (float) Math.toRadians(owner.rotationPitch); - this.dirX = -Math.sin(yaw) * Math.cos(pitch); - this.dirY = -Math.sin(pitch); - this.dirZ = Math.cos(yaw) * Math.cos(pitch); - } else if (target != null) { - double dx = target.posX - posX; - double dy = (target.posY + target.getEyeHeight() - 0.4) - posY; - double dz = target.posZ - posZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - this.dirX = dx / len; - this.dirY = dy / len; - this.dirZ = dz / len; + Vec3 look = livingOwner.getLookVec(); + if (look != null) { + direction = look; } } - // For player casters, snap origin to look vector so laser aligns with crosshair. - // For NPCs, use current anchor position. - if (owner instanceof EntityPlayer) { - double eyeY = owner.posY + owner.getEyeHeight(); - float frontDist = Math.max(0.5f, laserWidth * 0.5f); - startX = owner.posX + dirX * frontDist; - startY = eyeY + dirY * frontDist; - startZ = owner.posZ + dirZ * frontDist; - setPosition(startX, startY, startZ); - prevPosX = startX; - prevPosY = startY; - prevPosZ = startZ; - } else { - startX = posX; - startY = posY; - startZ = posZ; + dirX = direction.xCoord; + dirY = direction.yCoord; + dirZ = direction.zCoord; + + // Keep origin centered on look vector while active and clear owner bbox. + setLookVectorLaunchPosition(livingOwner, direction, false); + syncStartPositionToCurrent(); + syncPositionStateToCurrent(false); + } + + public void startMoving(EntityLivingBase target) { + beginLookVectorLaunch(false); + + currentLength = 0.0f; + fullyExtended = false; + ticksSinceFullExtension = 0; + + // Set initial direction: target-based when available, otherwise owner look vector. + 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; + } } // Initialize end point at start (will expand from here) @@ -253,6 +232,20 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.endZ = startZ; } + private boolean setDirectionTowardTarget(EntityLivingBase target, double sourceX, double sourceY, double sourceZ) { + if (target == null) return false; + double dx = target.posX - sourceX; + double dy = (target.posY + target.getEyeHeight() - 0.4) - sourceY; + double dz = target.posZ - sourceZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len <= 0.0001) return false; + + dirX = dx / len; + dirY = dy / len; + dirZ = dz / len; + return true; + } + private void checkBlockCollision() { MovingObjectPosition blockHit = rayTraceBlocks(startX, startY, startZ, endX, endY, endZ); @@ -342,8 +335,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { double dirLenXZ = Math.sqrt(dirX * dirX + dirZ * dirZ); double dot; if (dirLenXZ > 0.001) { - dot = (vx * dirX + vz * dirZ) / (dirLenXZ * dirLenXZ) * dirLenXZ; - // Remap to full 3D parameter dot = (vx * dirX + vz * dirZ + (entity.posY + entity.height * 0.5 - startY) * dirY); } else { // Laser fires nearly straight up/down - use full 3D projection @@ -561,6 +552,24 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { return Math.max(0.0f, 1.0f - ((float) ticksSinceFullExtension / lingerTicks)); } + @Override + protected float getLaunchClearanceRadius() { + return Math.max(0.1f, laserWidth * 0.5f); + } + + @Override + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + // Ensure charging visuals grow from a small orb based on laser width. + this.size = laserWidth; + this.currentLength = 0.0f; + this.fullyExtended = false; + this.ticksSinceFullExtension = 0; + super.setupCharging(anchor, chargeDuration); + this.endX = posX; + this.endY = posY; + this.endZ = posZ; + } + // ==================== NBT ==================== @Override @@ -605,10 +614,11 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { /** * Setup this laser in preview mode for GUI display. - * Laser doesn't have charging state - spawns at active phase and fires immediately. + * Uses charging-orb visuals during windup before firing. */ public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyDisplayData display, - EnergyLightningData lightning, float expansionSpeed, float maxDistance) { + EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, + float expansionSpeed, float maxDistance) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -619,28 +629,37 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.expansionSpeed = expansionSpeed; this.lifespanData.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview this.lightningData = lightning; + this.currentLength = 0.0f; + this.fullyExtended = false; + this.ticksSinceFullExtension = 0; - // Position at chest height - double x = owner.posX; - double y = owner.posY + owner.height * 0.7; - double z = owner.posZ; - this.setPosition(x, y, z); - this.prevPosX = x; - this.prevPosY = y; - this.prevPosZ = z; - this.startX = x; - this.startY = y; - this.startZ = z; + // Charge as an orb at the configured anchor, matching other energy projectiles. + setupCharging(anchor, chargeDuration); + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData); + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; + this.endX = pos.xCoord; + this.endY = pos.yCoord; + this.endZ = pos.zCoord; - // Fire in owner's facing direction - float yaw = (float) Math.toRadians(owner.rotationYaw); - this.dirX = -Math.sin(yaw); - this.dirY = 0; - this.dirZ = Math.cos(yaw); - - // Initialize end point (same as start, will expand) - this.endX = x; - this.endY = y; - this.endZ = z; + // Precompute initial direction from owner look for launch. + Vec3 look = owner.getLookVec(); + if (look == null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + look = Vec3.createVectorHelper( + -Math.sin(yaw) * Math.cos(pitch), + -Math.sin(pitch), + Math.cos(yaw) * Math.cos(pitch) + ); + } + this.dirX = look.xCoord; + this.dirY = look.yCoord; + this.dirZ = look.zCoord; } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index efb1db6a9..6a12bf9ab 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -14,6 +14,9 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import noppes.npcs.LogWriter; + +import java.util.Locale; /** * Orb projectile - spherical homing energy ball. @@ -22,6 +25,10 @@ import net.minecraft.world.World; * Design inspired by LouisXIV's energy attack system. */ public class EntityAbilityOrb extends EntityEnergyProjectile { + private static final int HOMING_STARTUP_TICKS = 4; + private static final int HOMING_RAMP_TICKS = 8; + private static final boolean DEBUG_ORB_FIRE_TRACE = false; + private static final int DEBUG_TRACE_TICKS = 14; public EntityAbilityOrb(World world) { super(world); @@ -71,7 +78,7 @@ public class EntityAbilityOrb extends EntityEnergyProjectile { this.renderCurrentSize = 0.01f; this.prevRenderSize = 0.01f; - // Initial position at anchor point + // Initial position follows anchor helper during charging. Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData); this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); this.prevPosX = pos.xCoord; @@ -91,32 +98,8 @@ public class EntityAbilityOrb extends EntityEnergyProjectile { * Start preview firing (simulates firing toward a point in front of NPC). */ public void startPreviewFiring() { - setCharging(false); - - // Update start position to current position - startX = posX; - startY = posY; - startZ = posZ; - - // Sync prev position to prevent visual jump on first frame - prevPosX = posX; - prevPosY = posY; - prevPosZ = posZ; - - // Fire forward based on owner facing direction - Entity owner = getOwnerEntity(); - if (owner != null) { - float yaw = (float) Math.toRadians(owner.rotationYaw); - float pitch = (float) Math.toRadians(0); // Fire horizontally - motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed(); - motionY = -Math.sin(pitch) * getSpeed(); - motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed(); - } else { - // Default: fire forward (positive X in model space) - motionX = getSpeed(); - motionY = 0; - motionZ = 0; - } + beginLookVectorLaunch(true); + setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0); } /** @@ -124,35 +107,28 @@ public class EntityAbilityOrb extends EntityEnergyProjectile { * Called by ability when windup ends. */ public void startMoving(EntityLivingBase target) { - setCharging(false); - - // For player casters, snap to look vector for crosshair-aligned launch - snapToPlayerLookVector(); - - // Update start position to current position - startX = posX; - startY = posY; - startZ = posZ; - - // Calculate velocity toward target + // Launch strictly along owner's look vector using shared launch origin. Entity owner = getOwnerEntity(); - if (target != null) { - double dx = target.posX - posX; - double dy = (target.posY + target.getEyeHeight()) - posY; - double dz = target.posZ - posZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - motionX = (dx / len) * getSpeed(); - motionY = (dy / len) * getSpeed(); - motionZ = (dz / len) * getSpeed(); + if (owner instanceof EntityLivingBase) { + Vec3 look = getOwnerLookVector(); + if (look != null) { + debugTrace("startMoving:before", look, -1.0f); + if (beginLookVectorLaunch((EntityLivingBase) owner, look, false)) { + debugTrace("setLaunchPosition", look, -1.0f); + } + motionX = look.xCoord * getSpeed(); + motionY = look.yCoord * getSpeed(); + motionZ = look.zCoord * getSpeed(); + debugTrace("startMoving:after", look, -1.0f); + return; } - } else if (owner != null) { - float yaw = (float) Math.toRadians(owner.rotationYaw); - float pitch = (float) Math.toRadians(owner.rotationPitch); - motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed(); - motionY = -Math.sin(pitch) * getSpeed(); - motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed(); } + + // Fallback if owner is unavailable. + setCharging(false); + syncStartPositionToCurrent(); + setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0); + debugTrace("startMoving:fallback", null, -1.0f); } @Override @@ -170,13 +146,17 @@ public class EntityAbilityOrb extends EntityEnergyProjectile { } if (worldObj.isRemote) { + if (shouldDebugTick()) debugTrace("client:beforeInterpolation", null, -1.0f); handleClientInterpolation(); + if (shouldDebugTick()) debugTrace("client:afterInterpolation", null, -1.0f); } else { // Server-side logic + if (shouldDebugTick()) debugTrace("server:beforeMovement", null, -1.0f); updateMovement(); checkBlockCollision(); checkEntityCollision(); this.moveEntity(motionX, motionY, motionZ); + if (shouldDebugTick()) debugTrace("server:afterMove", null, -1.0f); } // Check wall collision @@ -197,10 +177,28 @@ public class EntityAbilityOrb extends EntityEnergyProjectile { if (!isTrajectoryConcluded()) { updateTrajectory(); } else { - updateHoming(); + if (ticksExisted > HOMING_STARTUP_TICKS) { + int homingTicks = ticksExisted - HOMING_STARTUP_TICKS; + if (homingTicks >= HOMING_RAMP_TICKS) { + if (shouldDebugTick()) debugTrace("updateMovement:homingFull", null, 1.0f); + updateHoming(); + } else { + float ramp = homingTicks / (float) HOMING_RAMP_TICKS; + if (shouldDebugTick()) debugTrace("updateMovement:homingRamp", null, ramp); + updateHomingWithRamp(ramp); + } + } } } + private void updateHomingWithRamp(float ramp) { + if (ramp <= 0.0f) return; + float originalStrength = homingData.homingStrength; + homingData.homingStrength = originalStrength * ramp; + updateHoming(); + homingData.homingStrength = originalStrength; + } + private void updateTrajectory() { if (isTrajectoryConcluded()) return; // i really fucking hate math @@ -224,12 +222,54 @@ public class EntityAbilityOrb extends EntityEnergyProjectile { } private void checkEntityCollision() { - double hitSize = size * 0.5; + // Swept collision to avoid fast orbs skipping targets between ticks. + double nextX = posX + motionX; + double nextY = posY + motionY; + double nextZ = posZ + motionZ; + // Keep collision radius proportional to rendered orb size. + double hitSize = Math.max(0.05, size * 0.5); AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( - posX - hitSize, posY - hitSize, posZ - hitSize, - posX + hitSize, posY + hitSize, posZ + hitSize + Math.min(posX, nextX) - hitSize, Math.min(posY, nextY) - hitSize, Math.min(posZ, nextZ) - hitSize, + Math.max(posX, nextX) + hitSize, Math.max(posY, nextY) + hitSize, Math.max(posZ, nextZ) + hitSize ); - processEntitiesInHitBox(hitBox, posX, posY, posZ); + processEntitiesInHitBox(hitBox, nextX, nextY, nextZ); + } + + private boolean shouldDebugTick() { + return DEBUG_ORB_FIRE_TRACE && !isCharging() && !previewMode && ticksExisted <= DEBUG_TRACE_TICKS; + } + + private void debugTrace(String stage, Vec3 look, float ramp) { + if (!DEBUG_ORB_FIRE_TRACE || worldObj == null) return; + + String side = worldObj.isRemote ? "CLIENT" : "SERVER"; + Entity owner = getOwnerEntity(); + String ownerInfo = owner == null + ? "null" + : owner.getEntityId() + "@(" + fmt(owner.posX) + "," + fmt(owner.posY) + "," + fmt(owner.posZ) + ")"; + String lookInfo = look == null + ? "null" + : "(" + fmt(look.xCoord) + "," + fmt(look.yCoord) + "," + fmt(look.zCoord) + ")"; + + LogWriter.info( + "[OrbFireTrace][" + side + "][" + stage + "] " + + "id=" + getEntityId() + + " tick=" + ticksExisted + + " charging=" + isCharging() + + " start=(" + fmt(startX) + "," + fmt(startY) + "," + fmt(startZ) + ")" + + " pos=(" + fmt(posX) + "," + fmt(posY) + "," + fmt(posZ) + ")" + + " prev=(" + fmt(prevPosX) + "," + fmt(prevPosY) + "," + fmt(prevPosZ) + ")" + + " motion=(" + fmt(motionX) + "," + fmt(motionY) + "," + fmt(motionZ) + ")" + + " size=" + fmt(size) + + " speed=" + fmt(getSpeed()) + + " ramp=" + (ramp < 0 ? "n/a" : fmt(ramp)) + + " owner=" + ownerInfo + + " look=" + lookInfo + ); + } + + private static String fmt(double v) { + return String.format(Locale.ROOT, "%.4f", v); } // ==================== GETTERS ==================== diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java index 585eadc7d..3b1bc0cc1 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java @@ -83,6 +83,10 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit } } + public void setChargeDuration(int duration) { + this.chargeDuration = duration; + } + public float getChargeProgress() { if (chargeDuration <= 0) return 1.0f; return Math.min(1.0f, (float) chargeTick / chargeDuration); @@ -333,6 +337,24 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit readEnergyBaseNBT(nbt); } + /** + * Export spawn NBT for non-world preview rendering sync. + */ + public final NBTTagCompound exportSpawnNBT() { + NBTTagCompound nbt = new NBTTagCompound(); + writeSpawnNBT(nbt); + return nbt; + } + + /** + * Import spawn NBT for non-world preview rendering sync. + */ + public final void importSpawnNBT(NBTTagCompound nbt) { + if (nbt != null) { + readSpawnNBT(nbt); + } + } + @Override public void writeSpawnData(ByteBuf buffer) { try { diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index 9dfa105d6..fae432710 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -516,27 +516,237 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // ==================== LAUNCH HELPERS ==================== /** - * For player casters, snap the projectile to a position along the player's look vector. - * This ensures projectiles launch aligned with the crosshair rather than from the hand anchor. - * Only affects player casters; NPC projectiles launch from their anchor position as configured. + * Snaps the projectile position to the owner's look vector on launch. + * This ensures projectiles launch aligned with the owner's facing direction + * rather than from the last anchor point position. * Should be called in startMoving/startFiring BEFORE setting startX/Y/Z. */ - protected void snapToPlayerLookVector() { + protected void snapToLookVector() { + setLookVectorLaunchPosition(true); + } + + /** + * Shared launch transition: exit charging, snap to look-vector launch origin, + * sync start position, and optionally reset interpolation state. + * + * @param resetInterpolationState true to reset prev/lastTick/interp state to launch origin. + * @return true when look-vector launch origin was resolved from owner/look data. + */ + protected boolean beginLookVectorLaunch(boolean resetInterpolationState) { + setCharging(false); + boolean positioned = setLookVectorLaunchPosition(true); + syncStartPositionToCurrent(); + if (resetInterpolationState) { + syncPositionStateToCurrent(true); + } + return positioned; + } + + /** + * Shared launch transition with explicit owner/look vector data. + */ + protected boolean beginLookVectorLaunch(EntityLivingBase owner, Vec3 look, boolean resetInterpolationState) { + setCharging(false); + boolean positioned = setLookVectorLaunchPosition(owner, look, true); + syncStartPositionToCurrent(); + if (resetInterpolationState) { + syncPositionStateToCurrent(true); + } + return positioned; + } + + /** + * Resolve the owner's look vector. + * Falls back to yaw/pitch math if look vector is unavailable. + */ + protected Vec3 getOwnerLookVector() { Entity owner = getOwnerEntity(); - if (!(owner instanceof EntityPlayer)) return; + if (!(owner instanceof EntityLivingBase)) return null; Vec3 look = owner.getLookVec(); - double eyeY = owner.posY + owner.getEyeHeight(); - float frontDist = Math.max(0.5f, size * 0.5f); + if (look != null) return look; - double newX = owner.posX + look.xCoord * frontDist; - double newY = eyeY + look.yCoord * frontDist; - double newZ = owner.posZ + look.zCoord * frontDist; + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + return Vec3.createVectorHelper( + -Math.sin(yaw) * Math.cos(pitch), + -Math.sin(pitch), + Math.cos(yaw) * Math.cos(pitch) + ); + } + + /** + * Shared launch origin: owner position + chest height + look-vector offset. + * + * @param syncPositionState When true, sync prev/lastTick/interp targets to this position. + */ + protected boolean setLookVectorLaunchPosition(boolean syncPositionState) { + Entity owner = getOwnerEntity(); + if (!(owner instanceof EntityLivingBase)) return false; + Vec3 look = getOwnerLookVector(); + if (look == null) return false; + return setLookVectorLaunchPosition((EntityLivingBase) owner, look, syncPositionState); + } + + /** + * Shared launch origin using provided owner/look data. + */ + protected boolean setLookVectorLaunchPosition(EntityLivingBase owner, Vec3 look, boolean syncPositionState) { + if (owner == null || look == null) return false; + + double originX = owner.posX; + double originY = owner.posY + owner.height * 0.7; + double originZ = owner.posZ; + + float frontDist = computeLaunchFrontDistance(owner, look, originX, originY, originZ); + double newX = originX + look.xCoord * frontDist; + double newY = originY + look.yCoord * frontDist; + double newZ = originZ + look.zCoord * frontDist; setPosition(newX, newY, newZ); - prevPosX = newX; - prevPosY = newY; - prevPosZ = newZ; + if (syncPositionState) { + syncPositionState(newX, newY, newZ, true); + } + return true; + } + + /** + * Radius used to clear the caster hitbox on launch. + * Subclasses may override if their effective launch footprint differs from {@code size}. + */ + protected float getLaunchClearanceRadius() { + return Math.max(0.1f, size * 0.5f); + } + + private float computeLaunchFrontDistance(EntityLivingBase owner, Vec3 look, double originX, double originY, double originZ) { + // Legacy baseline offset retained as lower bound. + float baseline = size * 0.4f; + float clearance = getLaunchClearanceRadius() + 0.05f; + + AxisAlignedBB bb = owner.boundingBox; + if (bb == null) { + return Math.max(baseline, owner.width * 0.5f + clearance); + } + + double dx = look.xCoord; + double dy = look.yCoord; + double dz = look.zCoord; + + double tx = computeRayExitT(originX, dx, bb.minX, bb.maxX); + double ty = computeRayExitT(originY, dy, bb.minY, bb.maxY); + double tz = computeRayExitT(originZ, dz, bb.minZ, bb.maxZ); + + double tExit = Math.min(tx, Math.min(ty, tz)); + if (Double.isInfinite(tExit) || tExit < 0) { + return Math.max(baseline, owner.width * 0.5f + clearance); + } + + return (float) Math.max(baseline, tExit + clearance); + } + + private double computeRayExitT(double origin, double dir, double min, double max) { + final double EPS = 1.0e-6; + if (dir > EPS) { + return (max - origin) / dir; + } else if (dir < -EPS) { + return (min - origin) / dir; + } + return Double.POSITIVE_INFINITY; + } + + /** + * Set projectile motion along owner look vector. + * + * @return true if owner/look were available. + */ + protected boolean setMotionAlongLookVector(float speed) { + Vec3 look = getOwnerLookVector(); + if (look == null) return false; + + motionX = look.xCoord * speed; + motionY = look.yCoord * speed; + motionZ = look.zCoord * speed; + return true; + } + + /** + * Set motion along owner look vector or apply explicit fallback values. + */ + protected void setMotionAlongLookVectorOrFallback(float speed, double fallbackX, double fallbackY, double fallbackZ) { + if (!setMotionAlongLookVector(speed)) { + motionX = fallbackX; + motionY = fallbackY; + motionZ = fallbackZ; + } + } + + /** + * Set projectile motion toward a target from a given source point. + * + * @return true if target was valid and motion was updated. + */ + protected boolean setMotionTowardTarget(EntityLivingBase target, double sourceX, double sourceY, double sourceZ, float speed) { + if (target == null) return false; + + double dx = target.posX - sourceX; + double dy = (target.posY + target.getEyeHeight()) - sourceY; + double dz = target.posZ - sourceZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len <= 0.0001) return false; + + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + return true; + } + + /** + * Set motion toward target from source point; when target is unavailable, + * fall back to owner look vector, then explicit fallback values. + */ + protected void setMotionTowardTargetOrLookVector(EntityLivingBase target, double sourceX, double sourceY, double sourceZ, float speed, + double fallbackX, double fallbackY, double fallbackZ) { + if (!setMotionTowardTarget(target, sourceX, sourceY, sourceZ, speed)) { + setMotionAlongLookVectorOrFallback(speed, fallbackX, fallbackY, fallbackZ); + } + } + + /** + * Sync start position (distance origin) to the current entity position. + */ + protected void syncStartPositionToCurrent() { + startX = posX; + startY = posY; + startZ = posZ; + } + + /** + * Sync prev/lastTick and optional interpolation state to an explicit position. + */ + protected void syncPositionState(double x, double y, double z, boolean resetInterpolation) { + prevPosX = x; + prevPosY = y; + prevPosZ = z; + lastTickPosX = x; + lastTickPosY = y; + lastTickPosZ = z; + + if (resetInterpolation) { + interpTargetX = x; + interpTargetY = y; + interpTargetZ = z; + interpTargetMotionX = motionX; + interpTargetMotionY = motionY; + interpTargetMotionZ = motionZ; + interpSteps = 0; + } + } + + /** + * Sync prev/lastTick and optional interpolation state to current entity position. + */ + protected void syncPositionStateToCurrent(boolean resetInterpolation) { + syncPositionState(posX, posY, posZ, resetInterpolation); } // ==================== CHARGING METHODS (projectile-specific) ==================== @@ -1118,6 +1328,23 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { this.motionY = nbt.getDouble("MotionY"); this.motionZ = nbt.getDouble("MotionZ"); + // Authoritative launch origin comes from spawn NBT (set during fire/startMoving). + // Sync full client position state to this origin to avoid first-tick spawn/interp pops. + this.setPosition(this.startX, this.startY, this.startZ); + this.prevPosX = this.startX; + this.prevPosY = this.startY; + this.prevPosZ = this.startZ; + this.lastTickPosX = this.startX; + this.lastTickPosY = this.startY; + this.lastTickPosZ = this.startZ; + this.interpTargetX = this.startX; + this.interpTargetY = this.startY; + this.interpTargetZ = this.startZ; + this.interpTargetMotionX = this.motionX; + this.interpTargetMotionY = this.motionY; + this.interpTargetMotionZ = this.motionZ; + this.interpSteps = 0; + // Always initialize render size from NBT size so entities // loading in from outside render distance render correctly this.renderCurrentSize = this.size; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java index 1c2a868a8..4a8c3fbc2 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java @@ -102,55 +102,16 @@ public class EntityEnergySlicer extends EntityEnergyProjectile { * Start preview firing. */ public void startPreviewFiring() { - setCharging(false); - startX = posX; - startY = posY; - startZ = posZ; - prevPosX = posX; - prevPosY = posY; - prevPosZ = posZ; - - Entity owner = getOwnerEntity(); - if (owner != null) { - float yaw = (float) Math.toRadians(owner.rotationYaw); - motionX = -Math.sin(yaw) * getSpeed(); - motionY = 0; - motionZ = Math.cos(yaw) * getSpeed(); - } else { - motionX = getSpeed(); - motionY = 0; - motionZ = 0; - } + beginLookVectorLaunch(true); + setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0); } /** * Start the slicer moving (exit charging mode). */ public void startMoving(EntityLivingBase target) { - setCharging(false); - snapToPlayerLookVector(); - startX = posX; - startY = posY; - startZ = posZ; - - Entity owner = getOwnerEntity(); - if (target != null) { - double dx = target.posX - posX; - double dy = (target.posY + target.getEyeHeight()) - posY; - double dz = target.posZ - posZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - motionX = (dx / len) * getSpeed(); - motionY = (dy / len) * getSpeed(); - motionZ = (dz / len) * getSpeed(); - } - } else if (owner != null) { - float yaw = (float) Math.toRadians(owner.rotationYaw); - float pitch = (float) Math.toRadians(owner.rotationPitch); - motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed(); - motionY = -Math.sin(pitch) * getSpeed(); - motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed(); - } + beginLookVectorLaunch(false); + setMotionTowardTargetOrLookVector(target, posX, posY, posZ, getSpeed(), getSpeed(), 0, 0); } @Override diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 8e784b5b8..8b57a070c 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -36,6 +36,8 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; 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.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.player.AuctionActionPacket; @@ -585,6 +587,8 @@ public class PacketHandler { // Data | Ability Packets DATA_PACKET.registerPacket(new TelegraphSpawnPacket()); DATA_PACKET.registerPacket(new TelegraphRemovePacket()); + DATA_PACKET.registerPacket(new EnergyChargeSpawnPacket()); + DATA_PACKET.registerPacket(new EnergyChargeRemovePacket()); 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 ec351a2c9..938453b31 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -70,4 +70,8 @@ public enum EnumDataPacket { // Auction System AUCTION_DATA, + + // Energy Charge Preview + ENERGY_CHARGE_SPAWN, + ENERGY_CHARGE_REMOVE, } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/energycharge/EnergyChargeRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/data/energycharge/EnergyChargeRemovePacket.java new file mode 100644 index 000000000..e9a3a4db6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/energycharge/EnergyChargeRemovePacket.java @@ -0,0 +1,60 @@ +package kamkeel.npcs.network.packets.data.energycharge; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager; +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.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; + +import java.io.IOException; + +/** + * Removes a client-only charging preview entity. + */ +public final class EnergyChargeRemovePacket extends AbstractPacket { + public static final String packetName = "Data|EnergyChargeRemove"; + + private String instanceId; + + public EnergyChargeRemovePacket() { + } + + public EnergyChargeRemovePacket(String instanceId) { + this.instanceId = instanceId; + } + + @Override + public Enum getType() { + return EnumDataPacket.ENERGY_CHARGE_REMOVE; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, instanceId); + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + String id = ByteBufUtils.readString(in); + if (id != null && EnergyChargePreviewManager.ClientInstance != null) { + EnergyChargePreviewManager.ClientInstance.removePreview(id); + } + } + + public static void sendToTracking(String instanceId, Entity trackingEntity) { + if (instanceId == null || trackingEntity == null) return; + PacketHandler.Instance.sendTracking(new EnergyChargeRemovePacket(instanceId), trackingEntity); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/data/energycharge/EnergyChargeSpawnPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/energycharge/EnergyChargeSpawnPacket.java new file mode 100644 index 000000000..16957aaaa --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/energycharge/EnergyChargeSpawnPacket.java @@ -0,0 +1,108 @@ +package kamkeel.npcs.network.packets.data.energycharge; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager; +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; +import java.lang.reflect.Constructor; + +/** + * Spawns a client-only charging preview entity from serialized spawn data. + */ +public final class EnergyChargeSpawnPacket extends AbstractPacket { + public static final String packetName = "Data|EnergyChargeSpawn"; + + private String instanceId; + private String entityClassName; + private NBTTagCompound spawnNbt; + + public EnergyChargeSpawnPacket() { + } + + public EnergyChargeSpawnPacket(String instanceId, String entityClassName, NBTTagCompound spawnNbt) { + this.instanceId = instanceId; + this.entityClassName = entityClassName; + this.spawnNbt = spawnNbt; + } + + @Override + public Enum getType() { + return EnumDataPacket.ENERGY_CHARGE_SPAWN; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, instanceId); + ByteBufUtils.writeString(out, entityClassName); + ByteBufUtils.writeNBT(out, spawnNbt); + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + String id = ByteBufUtils.readString(in); + String className = ByteBufUtils.readString(in); + NBTTagCompound nbt = ByteBufUtils.readNBT(in); + + if (id == null || className == null || nbt == null) return; + if (EnergyChargePreviewManager.ClientInstance == null) return; + + World world = Minecraft.getMinecraft().theWorld; + if (world == null) return; + + EntityEnergyProjectile preview = createPreviewEntity(className, world); + if (preview == null) return; + + preview.setPreviewMode(true); + preview.importSpawnNBT(nbt); + + // Ensure the preview follows current owner immediately on first render frame. + Entity owner = preview.getOwnerEntity(); + if (owner instanceof net.minecraft.entity.EntityLivingBase) { + preview.setPreviewOwner((net.minecraft.entity.EntityLivingBase) owner); + } + + EnergyChargePreviewManager.ClientInstance.addPreview(id, preview); + } + + @SideOnly(Side.CLIENT) + private static EntityEnergyProjectile createPreviewEntity(String className, World world) { + try { + Class clazz = Class.forName(className); + if (!EntityEnergyProjectile.class.isAssignableFrom(clazz)) { + return null; + } + + Constructor constructor = clazz.getConstructor(World.class); + Object instance = constructor.newInstance(world); + return (EntityEnergyProjectile) instance; + } catch (Exception ignored) { + return null; + } + } + + public static void sendToTracking(String instanceId, EntityEnergyProjectile previewEntity, Entity trackingEntity) { + if (previewEntity == null || trackingEntity == null) return; + String className = previewEntity.getClass().getName(); + NBTTagCompound nbt = previewEntity.exportSpawnNBT(); + PacketHandler.Instance.sendTracking(new EnergyChargeSpawnPacket(instanceId, className, nbt), trackingEntity); + } +} diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index a221e35ae..180a93194 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -14,9 +14,11 @@ import kamkeel.npcs.client.renderer.RenderAbilitySweeper; import kamkeel.npcs.client.renderer.RenderAbilityZone; import kamkeel.npcs.client.renderer.RenderAbilityDome; import kamkeel.npcs.client.renderer.RenderAbilityPanel; +import kamkeel.npcs.client.renderer.EnergyChargePreviewRenderer; import kamkeel.npcs.client.renderer.RenderEnergySlicer; import kamkeel.npcs.client.renderer.TelegraphRenderer; import kamkeel.npcs.client.renderer.lightning.LightningHandler; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager; import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.entity.EntityAbilityBeam; import kamkeel.npcs.entity.EntityAbilityDisc; @@ -356,6 +358,10 @@ public class ClientProxy extends CommonProxy { TelegraphManager.initClient(); MinecraftForge.EVENT_BUS.register(new TelegraphRenderer()); + // Energy charge preview rendering system + EnergyChargePreviewManager.initClient(); + MinecraftForge.EVENT_BUS.register(new EnergyChargePreviewRenderer()); + // Lightning effect rendering system MinecraftForge.EVENT_BUS.register(new LightningHandler()); diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 58d6f5a3f..3a9416563 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -8,6 +8,7 @@ import cpw.mods.fml.common.gameevent.TickEvent.Phase; import cpw.mods.fml.relauncher.Side; import kamkeel.npcs.client.renderer.lightning.LightningBolt; import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager; import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.data.RequestProperSpawnData; @@ -62,6 +63,9 @@ public class ClientTickHandler { ClientCacheHandler.clearCache(); ClientAbilityState.reset(); RequestProperSpawnData.clear(); + if (EnergyChargePreviewManager.ClientInstance != null) { + EnergyChargePreviewManager.ClientInstance.clear(); + } } this.prevWorld = mc.theWorld; } @@ -164,6 +168,11 @@ public class ClientTickHandler { TelegraphManager.ClientInstance.tick(mc.theWorld); } + // Tick packet-driven energy charge previews + if (EnergyChargePreviewManager.ClientInstance != null) { + EnergyChargePreviewManager.ClientInstance.tick(mc.theWorld); + } + // Update lightning bolts for ability effects LightningBolt.updateAll(); diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 4834504b3..5c759d59c 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -1834,12 +1834,15 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti try { readSpawnData(ByteBufUtils.readNBT(buf)); } catch (IOException e) { - RequestProperSpawnData.reportMissingData(this); + if(this.worldObj != null && this.worldObj.isRemote){ + RequestProperSpawnData.reportMissingData(this); + } } } public void readSpawnData(NBTTagCompound compound) { - if ((compound.hasNoTags() || !compound.hasKey("MaxHealth", Constants.NBT.TAG_DOUBLE))) { + if (this.worldObj != null && this.worldObj.isRemote && + (compound.hasNoTags() || !compound.hasKey("MaxHealth", Constants.NBT.TAG_DOUBLE))) { RequestProperSpawnData.reportMissingData(this); return; }