package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.AnchorPoint; 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 noppes.npcs.entity.EntityNPCInterface; 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-specific movement properties private float speed = 0.5f; private boolean homing = true; private float homingStrength = 0.35f; // Increased from 0.15 for better tracking private float homingRange = 20.0f; // 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 (during windup) private boolean charging = false; private int chargeDuration = 40; private int chargeTick = 0; private float chargeOffsetDistance = 1.0f; private AnchorPoint anchorPoint = AnchorPoint.FRONT; // 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<>(); // Data watcher index for charging state (synced to clients) private static final int DW_CHARGING = 20; // Debug logging private static final boolean DEBUG_LOGGING = true; public EntityAbilityBeam(World world) { super(world); } @Override protected void entityInit() { super.entityInit(); // Register data watcher for charging state this.dataWatcher.addObject(DW_CHARGING, (byte) 0); } /** * Check if beam is in charging state (synced via data watcher). */ public boolean isCharging() { return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; } /** * Set charging state (server only, synced to clients via data watcher). */ private void setCharging(boolean value) { this.charging = value; if (!worldObj.isRemote) { this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); } } /** * Full constructor with all parameters (no lightning). */ public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float beamWidth, float headSize, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { this(world, owner, target, x, y, z, beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, false, 0.15f, 0.5f); } /** * Full constructor with all parameters including lightning. */ public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float beamWidth, float headSize, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime, boolean lightningEffect, float lightningDensity, float lightningRadius) { this(world, owner, target, x, y, z, beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, lightningEffect, lightningDensity, lightningRadius, true); // Default to anchored mode } /** * Full constructor with all parameters including lightning and anchored mode. * @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, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float beamWidth, float headSize, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime, boolean lightningEffect, float lightningDensity, float lightningRadius, boolean anchoredMode) { super(world); // Initialize base properties with lightning initProjectile(owner, target, x, y, z, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, lightningEffect, lightningDensity, lightningRadius, 6); // Beam-specific properties this.speed = speed; this.homing = homing; this.homingStrength = homingStrength; this.homingRange = homingRange; 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=" + maxDistance); } // Calculate initial velocity toward target if (target != null) { double dx = target.posX - x; double dy = (target.posY + target.height * 0.5) - y; double dz = target.posZ - z; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { this.motionX = (dx / len) * speed; this.motionY = (dy / len) * speed; this.motionZ = (dz / len) * speed; } } else { float yaw = (float) Math.toRadians(owner.rotationYaw); float pitch = (float) Math.toRadians(owner.rotationPitch); this.motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; this.motionY = -Math.sin(pitch) * speed; this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; } } /** * Create a beam in charging mode (for windup phase). * The beam will grow from 0 to headSize over chargeDuration ticks. * Position follows the owner based on anchor point. */ public static EntityAbilityBeam createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, float beamWidth, float headSize, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime, boolean lightningEffect, float lightningDensity, float lightningRadius, boolean anchoredMode, int chargeDuration, float chargeOffsetDistance, AnchorPoint anchorPoint) { // Calculate initial position based on anchor point Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint, chargeOffsetDistance); double spawnX = spawnPos.xCoord; double spawnY = spawnPos.yCoord; double spawnZ = spawnPos.zCoord; EntityAbilityBeam beam = new EntityAbilityBeam( world, owner, target, spawnX, spawnY, spawnZ, beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, lightningEffect, lightningDensity, lightningRadius, anchoredMode); // Set charging state (uses data watcher for client sync) beam.setCharging(true); beam.chargeDuration = chargeDuration; beam.chargeTick = 0; beam.chargeOffsetDistance = chargeOffsetDistance; beam.anchorPoint = anchorPoint; // Non-anchored beams have fading trail (comet effect) beam.fadeTrail = !anchoredMode; // Clear motion while charging beam.motionX = 0; beam.motionY = 0; beam.motionZ = 0; return beam; } /** * 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) { if (!isCharging()) return; setCharging(false); // 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 = getOwner(); if (target != null) { double dx = target.posX - startX; double dy = (target.posY + target.height * 0.5) - startY; double dz = target.posZ - startZ; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { motionX = (dx / len) * speed; motionY = (dy / len) * speed; motionZ = (dz / len) * speed; } } 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) * speed; motionY = -Math.sin(pitch) * speed; motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; } 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 >= maxDistance; if (exceeded && !worldObj.isRemote && DEBUG_LOGGING) { LogWriter.info("[Beam] DEAD: Max distance exceeded. dist=" + distFromOrigin + " max=" + maxDistance); } 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(); } 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 = getOwner(); 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. */ private void updateCharging() { chargeTick++; Entity owner = getOwner(); if (owner == null || owner.isDead) { setDead(); return; } // Calculate position based on anchor point Vec3 pos; if (owner instanceof EntityLivingBase) { pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint, 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); } } } } private void updateHoming() { if (!homing) return; Entity target = getTarget(); 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.height * 0.5) - headWorldY; double dz = target.posZ - headWorldZ; double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); if (dist <= homingRange && dist > 0) { // Calculate effective homing strength - increases when closer to commit to target float effectiveStrength = homingStrength; if (dist < homingRange * 0.3) { effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); } else if (dist < homingRange * 0.6) { effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); } double desiredVX = (dx / dist) * speed; double desiredVY = (dy / dist) * speed; double desiredVZ = (dz / dist) * speed; 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) * speed; motionY = (motionY / vLen) * speed; motionZ = (motionZ / vLen) * speed; } } } 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); MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { if (DEBUG_LOGGING) { LogWriter.info("[Beam] DEAD: Block collision at tick " + ticksExisted); } hasHit = true; if (explosive) { 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 (explosive) { 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; } /** * Get the charge progress (0-1). */ public float getChargeProgress() { if (chargeDuration <= 0) return 1.0f; return Math.min(1.0f, (float) chargeTick / chargeDuration); } /** * Get interpolated charge progress for smooth rendering. */ public float getInterpolatedChargeProgress(float partialTicks) { if (chargeDuration <= 0) return 1.0f; float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); return prevProgress + (currProgress - prevProgress) * partialTicks; } /** * 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.speed = nbt.hasKey("Speed") ? nbt.getFloat("Speed") : 0.5f; this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; 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"); // Read charging state and sync to data watcher boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); this.charging = isCharging; this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; 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(); 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") )); } } } @Override protected void writeProjectileNBT(NBTTagCompound nbt) { nbt.setFloat("Speed", speed); nbt.setBoolean("Homing", homing); nbt.setFloat("HomingStrength", homingStrength); nbt.setFloat("HomingRange", homingRange); 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); nbt.setBoolean("Charging", isCharging()); nbt.setInteger("ChargeDuration", chargeDuration); nbt.setInteger("ChargeTick", chargeTick); nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); nbt.setInteger("AnchorPoint", anchorPoint.getId()); 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); } }