package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.LogWriter; import java.util.ArrayList; import java.util.List; /** * Beam projectile - head with trailing path that curves with homing. * Stays attached to origin point, trail shows path of head. * * IMPORTANT: Trail points are stored RELATIVE to startX/Y/Z (origin). * This ensures rendering is stable regardless of entity position interpolation. * * Design inspired by LouisXIV's energy attack system. */ public class EntityAbilityBeam extends EntityAbilityProjectile { // Beam shape properties private float beamWidth = 0.3f; private float headSize = 0.5f; // Trail - list of points showing beam path (RELATIVE to origin!) private List trailPoints = new ArrayList<>(); private static final int MAX_TRAIL_POINTS = 200; private static final double MIN_POINT_DISTANCE = 0.2; // 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; // 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<>(); // Debug logging private static final boolean DEBUG_LOGGING = false; public EntityAbilityBeam(World world) { super(world); } /** * Full constructor with all parameters using data classes. * @param anchoredMode If true, origin follows owner and tail orb is rendered. * If false, beam is free-moving with trailing length (no tail orb). */ public EntityAbilityBeam(World world, EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float beamWidth, float headSize, EnergyDisplayData display, EnergyCombatData combat, EnergyHomingData homing, EnergyLightningData lightning, EnergyLifespanData lifespan, EnergyTrajectoryData trajectory, boolean anchoredMode) { super(world); // Initialize base properties via parent initProjectile(owner, target, x, y, z, headSize, display, combat, lightning, lifespan, trajectory); // Beam-specific properties from homing data this.homingData = homing; this.beamWidth = beamWidth; this.headSize = headSize; // Anchored mode controls whether origin follows owner and tail orb is rendered this.attachedToOwner = anchoredMode; this.renderTailOrb = anchoredMode; // Initialize head offset at origin (0,0,0 relative) this.headOffsetX = 0; this.headOffsetY = 0; this.headOffsetZ = 0; this.prevHeadOffsetX = 0; this.prevHeadOffsetY = 0; this.prevHeadOffsetZ = 0; // 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); } public void setupCharging(EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { setCharging(true); this.chargeDuration = chargeDuration; this.chargeTick = 0; this.chargeOffsetDistance = chargeOffsetDistance; this.anchorData = anchor; this.fadeTrail = !attachedToOwner; this.motionX = 0; this.motionY = 0; this.motionZ = 0; } public void setupPreview(EntityLivingBase owner, float beamWidth, float headSize, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { this.setPreviewMode(true); this.setPreviewOwner(owner); // Set visual properties this.beamWidth = beamWidth; this.headSize = headSize; this.size = headSize; this.displayData = display; this.lightningData = lightning; // Set charging state this.setCharging(true); this.chargeDuration = chargeDuration; this.chargeTick = 0; this.chargeOffsetDistance = chargeOffsetDistance; this.anchorData = anchor; // Initial position at anchor point Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData, chargeOffsetDistance); 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; // Initialize head offsets this.headOffsetX = 0; this.headOffsetY = 0; this.headOffsetZ = 0; this.prevHeadOffsetX = 0; 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; this.motionZ = 0; } /** * 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; // Reset head offset to origin headOffsetX = 0; headOffsetY = 0; headOffsetZ = 0; prevHeadOffsetX = 0; 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(); trailPoints.add(Vec3.createVectorHelper(0, 0, 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; } } /** * 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; // Head starts at the origin (tail position) headOffsetX = 0; headOffsetY = 0; headOffsetZ = 0; prevHeadOffsetX = 0; 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(); trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); if (fadeTrail) 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(); } if (DEBUG_LOGGING && !worldObj.isRemote) { LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ + ") headOffset=(" + headOffsetX + "," + headOffsetY + "," + headOffsetZ + ") motion=(" + motionX + "," + motionY + "," + motionZ + ")"); } } @Override protected boolean checkMaxDistance() { // Check distance using head offset (distance from origin) double distFromOrigin = Math.sqrt( headOffsetX * headOffsetX + 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; } @Override protected void updateProjectile() { // Handle charging state (windup phase) - use isCharging() for synced value if (isCharging()) { updateCharging(); return; } // Store previous head offset for interpolation prevHeadOffsetX = headOffsetX; prevHeadOffsetY = headOffsetY; prevHeadOffsetZ = headOffsetZ; // Age trail points for fading effect if (fadeTrail) { ageTrailPoints(); } // In preview mode, run movement on client (no server) if (previewMode) { updatePreviewMovement(); return; } if (worldObj.isRemote) { // Client: interpolate entity position handleClientInterpolation(); // Client: derive head offset from entity position and origin // Entity position = head world position, so headOffset = pos - origin // Note: During charging, updateCharging() handles origin positioning // After firing, origin is fixed at the charged position headOffsetX = posX - startX; headOffsetY = posY - startY; headOffsetZ = posZ - startZ; // Add trail point on client addTrailPoint(); } else { // Server-side logic updateHoming(); // Move head offset headOffsetX += motionX; headOffsetY += motionY; headOffsetZ += motionZ; // Update origin if attached to owner if (attachedToOwner) { Entity owner = getOwnerEntity(); if (owner != null) { startX = owner.posX; startY = owner.posY + owner.getEyeHeight() * 0.7; startZ = owner.posZ; } } // Calculate world position of head for collision detection double headWorldX = startX + headOffsetX; double headWorldY = startY + headOffsetY; double headWorldZ = startZ + headOffsetZ; // Update entity position to head (for network sync) this.setPosition(headWorldX, headWorldY, headWorldZ); // 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=" + trailPoints.size()); } // Skip collision checks on first few ticks if (ticksExisted > 2) { checkBlockCollision(headWorldX, headWorldY, headWorldZ); checkEntityCollision(headWorldX, headWorldY, headWorldZ); } } } /** * Update during charging state - follow owner based on anchor point. * Beam-specific: positions based on chargeOffsetDistance and updates startX/Y/Z. */ @Override protected void updateCharging() { chargeTick++; Entity owner = getOwnerEntity(); if (owner == null || owner.isDead) { setDead(); return; } // Calculate position based on anchor point Vec3 pos; if (owner instanceof EntityLivingBase) { pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance); } else { // Fallback for non-living entities (shouldn't happen normally) float yaw = (float) Math.toRadians(owner.rotationYaw); double offsetX = -Math.sin(yaw) * chargeOffsetDistance; double offsetZ = Math.cos(yaw) * chargeOffsetDistance; pos = Vec3.createVectorHelper( owner.posX + offsetX, owner.posY + owner.getEyeHeight() * 0.7, owner.posZ + offsetZ ); } setPosition(pos.xCoord, pos.yCoord, pos.zCoord); // Also update origin for when firing starts startX = pos.xCoord; startY = pos.yCoord; startZ = pos.zCoord; } /** * 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++) { 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); } } } private void addTrailPoint() { // Trail points are RELATIVE to origin if (trailPoints.isEmpty()) { trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); if (fadeTrail) { trailPointAges.add(0); } return; } Vec3 lastPoint = trailPoints.get(trailPoints.size() - 1); double dist = Math.sqrt( (headOffsetX - lastPoint.xCoord) * (headOffsetX - lastPoint.xCoord) + (headOffsetY - lastPoint.yCoord) * (headOffsetY - lastPoint.yCoord) + (headOffsetZ - lastPoint.zCoord) * (headOffsetZ - lastPoint.zCoord) ); if (dist >= MIN_POINT_DISTANCE) { trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); if (fadeTrail) { trailPointAges.add(0); } // Limit trail length (only if not using fading - fading handles its own cleanup) if (!fadeTrail) { while (trailPoints.size() > MAX_TRAIL_POINTS) { trailPoints.remove(0); } } } } /** * Update movement in preview mode (client-side only, no damage). */ private void updatePreviewMovement() { // Move head offset headOffsetX += motionX; headOffsetY += motionY; headOffsetZ += motionZ; // Update origin if attached to owner if (attachedToOwner) { Entity owner = getOwnerEntity(); if (owner != null) { startX = owner.posX; startY = owner.posY + owner.getEyeHeight() * 0.7; startZ = owner.posZ; } } // Calculate world position of head double headWorldX = startX + headOffsetX; double headWorldY = startY + headOffsetY; double headWorldZ = startZ + headOffsetZ; // Update entity position to head this.setPosition(headWorldX, headWorldY, headWorldZ); // Add trail point addTrailPoint(); } @Override protected void updateHoming() { if (!isHoming()) return; Entity target = getTargetEntity(); if (target == null || !target.isEntityAlive()) return; // Calculate world position of head double headWorldX = startX + headOffsetX; double headWorldY = startY + headOffsetY; double headWorldZ = startZ + headOffsetZ; double dx = target.posX - headWorldX; double dy = (target.posY + target.getEyeHeight()) - headWorldY; double dz = target.posZ - headWorldZ; double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); if (dist <= getHomingRange() && dist > 0) { // Calculate effective homing strength - increases when closer to commit to target float effectiveStrength = getHomingStrength(); if (dist < getHomingRange() * 0.3) { effectiveStrength = Math.min(1.0f, getHomingStrength() * 2.5f); } else if (dist < getHomingRange() * 0.6) { effectiveStrength = Math.min(0.8f, getHomingStrength() * 1.5f); } double desiredVX = (dx / dist) * getSpeed(); double desiredVY = (dy / dist) * getSpeed(); double desiredVZ = (dz / dist) * getSpeed(); motionX += (desiredVX - motionX) * effectiveStrength; motionY += (desiredVY - motionY) * effectiveStrength; motionZ += (desiredVZ - motionZ) * effectiveStrength; double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); if (vLen > 0) { motionX = (motionX / vLen) * getSpeed(); motionY = (motionY / vLen) * getSpeed(); motionZ = (motionZ / vLen) * getSpeed(); } } } private void checkBlockCollision(double headX, double headY, double headZ) { // Previous head world position double prevHeadWorldX = startX + prevHeadOffsetX; double prevHeadWorldY = startY + prevHeadOffsetY; double prevHeadWorldZ = startZ + prevHeadOffsetZ; Vec3 currentPos = Vec3.createVectorHelper(prevHeadWorldX, prevHeadWorldY, prevHeadWorldZ); Vec3 nextPos = Vec3.createVectorHelper(headX, headY, headZ); // Use full raytrace that doesn't stop at liquids and checks all blocks MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { hasHit = true; if (isExplosive()) { posX = blockHit.hitVec.xCoord; posY = blockHit.hitVec.yCoord; posZ = blockHit.hitVec.zCoord; doExplosion(); } this.setDead(); } } private void checkEntityCollision(double headX, double headY, double headZ) { double hitSize = headSize * 0.5; AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( headX - hitSize, headY - hitSize, headZ - hitSize, headX + hitSize, headY + hitSize, headZ + hitSize ); @SuppressWarnings("unchecked") List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); for (EntityLivingBase entity : entities) { if (shouldIgnoreEntity(entity)) continue; if (DEBUG_LOGGING) { LogWriter.info("[Beam] DEAD: Entity collision with " + entity.getClass().getSimpleName() + " at tick " + ticksExisted); } hasHit = true; if (isExplosive()) { posX = headX; posY = headY; posZ = headZ; doExplosion(); } else { applyDamage(entity); } this.setDead(); return; } } // ==================== GETTERS FOR RENDERER ==================== public float getBeamWidth() { return beamWidth; } public float getHeadSize() { return headSize; } /** * Get interpolated head offset for smooth rendering. * Returns offset RELATIVE to origin. */ public double getInterpolatedHeadOffsetX(float partialTicks) { return prevHeadOffsetX + (headOffsetX - prevHeadOffsetX) * partialTicks; } public double getInterpolatedHeadOffsetY(float partialTicks) { return prevHeadOffsetY + (headOffsetY - prevHeadOffsetY) * partialTicks; } public double getInterpolatedHeadOffsetZ(float partialTicks) { return prevHeadOffsetZ + (headOffsetZ - prevHeadOffsetZ) * partialTicks; } /** * Get trail points. These are RELATIVE to origin (startX/Y/Z). */ public List getTrailPoints() { return trailPoints; } public double getOriginX() { return startX; } public double getOriginY() { return startY; } public double getOriginZ() { return startZ; } /** * Whether the tail orb should be rendered (only true in anchored mode). */ public boolean shouldRenderTailOrb() { return renderTailOrb; } /** * Whether the beam is attached to its owner (anchored mode). */ public boolean isAttachedToOwner() { return attachedToOwner; } /** * Whether trail should fade (comet effect for non-anchored beams). */ public boolean hasFadingTrail() { return fadeTrail; } /** * Get trail point ages for fading calculation. */ public List getTrailPointAges() { return trailPointAges; } /** * Get trail fade time in ticks. */ public int getTrailFadeTime() { return trailFadeTime; } // ==================== NBT ==================== @Override protected void readProjectileNBT(NBTTagCompound nbt) { this.beamWidth = nbt.hasKey("BeamWidth") ? nbt.getFloat("BeamWidth") : 0.3f; this.headSize = nbt.hasKey("HeadSize") ? nbt.getFloat("HeadSize") : 0.5f; this.headOffsetX = nbt.hasKey("HeadOffsetX") ? nbt.getDouble("HeadOffsetX") : 0; this.headOffsetY = nbt.hasKey("HeadOffsetY") ? nbt.getDouble("HeadOffsetY") : 0; this.headOffsetZ = nbt.hasKey("HeadOffsetZ") ? nbt.getDouble("HeadOffsetZ") : 0; 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; // Read trail points (relative to origin) trailPoints.clear(); trailPointAges.clear(); if (nbt.hasKey("Trail")) { NBTTagList trailList = nbt.getTagList("Trail", 10); for (int i = 0; i < trailList.tagCount(); i++) { NBTTagCompound point = trailList.getCompoundTagAt(i); trailPoints.add(Vec3.createVectorHelper( point.getDouble("X"), point.getDouble("Y"), point.getDouble("Z") )); } } // Read trail point ages (must match trailPoints size for fading trail) if (nbt.hasKey("TrailAges")) { int[] ages = nbt.getIntArray("TrailAges"); for (int age : ages) { 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); } } @Override protected void writeProjectileNBT(NBTTagCompound nbt) { nbt.setFloat("BeamWidth", beamWidth); nbt.setFloat("HeadSize", headSize); nbt.setDouble("HeadOffsetX", headOffsetX); nbt.setDouble("HeadOffsetY", headOffsetY); nbt.setDouble("HeadOffsetZ", headOffsetZ); nbt.setBoolean("AttachedToOwner", attachedToOwner); nbt.setBoolean("RenderTailOrb", renderTailOrb); writeChargingNBT(nbt); nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); nbt.setBoolean("FadeTrail", fadeTrail); nbt.setInteger("TrailFadeTime", trailFadeTime); // Write trail points NBTTagList trailList = new NBTTagList(); for (Vec3 point : trailPoints) { NBTTagCompound pointNbt = new NBTTagCompound(); pointNbt.setDouble("X", point.xCoord); pointNbt.setDouble("Y", point.yCoord); pointNbt.setDouble("Z", point.zCoord); trailList.appendTag(pointNbt); } 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); } nbt.setIntArray("TrailAges", ages); } }