Files
CustomNPC-Plus/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java
T

766 lines
27 KiB
Java
Raw Normal View History

2026-01-25 20:39:01 -05:00
package kamkeel.npcs.entity;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.util.AnchorPointHelper;
2026-01-25 20:39:01 -05:00
import net.minecraft.entity.Entity;
import noppes.npcs.EventHooks;
2026-01-25 20:39:01 -05:00
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<Vec3> 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<Integer> trailPointAges = new ArrayList<>();
2026-01-25 20:39:01 -05:00
// Debug logging
2026-01-28 17:16:59 -05:00
private static final boolean DEBUG_LOGGING = false;
2026-01-25 20:39:01 -05:00
public EntityAbilityBeam(World world) {
super(world);
}
/**
2026-01-29 14:19:57 -05:00
* 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).
*/
2026-01-30 05:01:54 -05:00
public EntityAbilityBeam(World world, EntityLivingBase owner, EntityLivingBase target,
double x, double y, double z,
float beamWidth, float headSize,
2026-02-03 17:20:36 -03:00
EnergyDisplayData display, EnergyCombatData combat,
EnergyHomingData homing, EnergyLightningData lightning,
2026-02-03 21:26:27 -03:00
EnergyLifespanData lifespan, EnergyTrajectoryData trajectory,
boolean anchoredMode) {
2026-01-25 20:39:01 -05:00
super(world);
2026-01-29 14:19:57 -05:00
// Initialize base properties via parent
2026-02-03 21:26:27 -03:00
initProjectile(owner, target, x, y, z, headSize, display, combat, lightning, lifespan, trajectory);
2026-01-29 14:19:57 -05:00
// Beam-specific properties from homing data
2026-02-03 17:20:36 -03:00
this.homingData = homing;
2026-01-25 20:39:01 -05:00
this.beamWidth = beamWidth;
this.headSize = headSize;
// Anchored mode controls whether origin follows owner and tail orb is rendered
this.attachedToOwner = anchoredMode;
this.renderTailOrb = anchoredMode;
2026-01-25 20:39:01 -05:00
// 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) {
2026-01-29 14:19:57 -05:00
LogWriter.info("[Beam] Created at origin " + x + ", " + y + ", " + z + " maxDist=" + lifespan.maxDistance);
2026-01-25 20:39:01 -05:00
}
// Calculate initial velocity toward target
calculateInitialVelocity(owner, target, x, y, z);
2026-01-25 20:39:01 -05:00
}
public void setupCharging(EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) {
2026-01-29 14:19:57 -05:00
setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.chargeOffsetDistance = chargeOffsetDistance;
this.anchorData = anchor;
2026-01-29 14:19:57 -05:00
this.fadeTrail = !attachedToOwner;
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
}
2026-02-03 17:20:36 -03:00
public void setupPreview(EntityLivingBase owner, float beamWidth, float headSize, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) {
2026-01-29 14:19:57 -05:00
this.setPreviewMode(true);
this.setPreviewOwner(owner);
2026-01-28 20:43:32 -05:00
// Set visual properties
2026-01-29 14:19:57 -05:00
this.beamWidth = beamWidth;
this.headSize = headSize;
this.size = headSize;
2026-02-03 17:20:36 -03:00
this.displayData = display;
this.lightningData = lightning;
2026-01-28 20:43:32 -05:00
// Set charging state
2026-01-29 14:19:57 -05:00
this.setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.chargeOffsetDistance = chargeOffsetDistance;
this.anchorData = anchor;
2026-01-28 20:43:32 -05:00
// Initial position at anchor point
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData, chargeOffsetDistance);
2026-01-29 14:19:57 -05:00
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;
2026-01-28 20:43:32 -05:00
// Initialize head offsets
2026-01-29 14:19:57 -05:00
this.headOffsetX = 0;
this.headOffsetY = 0;
this.headOffsetZ = 0;
this.prevHeadOffsetX = 0;
this.prevHeadOffsetY = 0;
this.prevHeadOffsetZ = 0;
2026-01-28 20:43:32 -05:00
// Attach to owner for anchor following
2026-01-29 14:19:57 -05:00
this.attachedToOwner = true;
this.renderTailOrb = true;
2026-01-28 20:43:32 -05:00
2026-01-29 14:19:57 -05:00
// Clear motion
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
2026-01-28 20:43:32 -05:00
}
/**
* Start preview firing (simulates firing toward a point in front of NPC).
*/
public void startPreviewFiring() {
setCharging(false);
// Origin (tail) stays at the charged position
2026-01-28 20:43:32 -05:00
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;
2026-01-28 20:43:32 -05:00
// 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));
2026-01-28 20:43:32 -05:00
// Fire forward based on owner facing direction
2026-02-03 17:20:36 -03:00
Entity owner = getOwnerEntity();
2026-01-28 20:43:32 -05:00
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
motionX = -Math.sin(yaw) * getSpeed();
motionY = 0;
motionZ = Math.cos(yaw) * getSpeed();
2026-01-28 20:43:32 -05:00
} else {
2026-02-03 17:20:36 -03:00
motionX = getSpeed();
2026-01-28 20:43:32 -05:00
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)
2026-02-03 17:20:36 -03:00
Entity owner = getOwnerEntity();
if (target != null) {
double dx = target.posX - startX;
2026-01-29 14:19:57 -05:00
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) {
2026-02-03 17:20:36 -03:00
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);
2026-02-03 17:20:36 -03:00
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 + ")");
}
}
2026-01-25 20:39:01 -05:00
@Override
protected boolean checkMaxDistance() {
// Check distance using head offset (distance from origin)
double distFromOrigin = Math.sqrt(
headOffsetX * headOffsetX +
headOffsetY * headOffsetY +
headOffsetZ * headOffsetZ
);
2026-02-03 17:20:36 -03:00
boolean exceeded = distFromOrigin >= getMaxDistance();
2026-01-25 20:39:01 -05:00
if (exceeded && !worldObj.isRemote && DEBUG_LOGGING) {
2026-02-03 17:20:36 -03:00
LogWriter.info("[Beam] DEAD: Max distance exceeded. dist=" + distFromOrigin + " max=" + getMaxDistance());
2026-01-25 20:39:01 -05:00
}
return exceeded;
}
@Override
protected void updateProjectile() {
// Handle charging state (windup phase) - use isCharging() for synced value
if (isCharging()) {
updateCharging();
return;
}
2026-01-25 20:39:01 -05:00
// Store previous head offset for interpolation
prevHeadOffsetX = headOffsetX;
prevHeadOffsetY = headOffsetY;
prevHeadOffsetZ = headOffsetZ;
// Age trail points for fading effect
if (fadeTrail) {
ageTrailPoints();
}
2026-01-28 20:43:32 -05:00
// In preview mode, run movement on client (no server)
if (previewMode) {
updatePreviewMovement();
return;
}
2026-01-25 20:39:01 -05:00
if (worldObj.isRemote) {
// Client: interpolate entity position
2026-01-25 20:39:01 -05:00
handleClientInterpolation();
// Client: derive head offset from entity position and origin
// Entity position = head world position, so headOffset = pos - origin
2026-01-26 19:03:48 -05:00
// 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;
2026-01-25 20:39:01 -05:00
// 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) {
2026-02-03 17:20:36 -03:00
Entity owner = getOwnerEntity();
2026-01-25 20:39:01 -05:00
if (owner != null) {
startX = owner.posX;
startY = owner.posY + owner.getEyeHeight() * 0.7;
2026-01-25 20:39:01 -05:00
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++;
2026-02-03 17:20:36 -03:00
Entity owner = getOwnerEntity();
if (owner != null) {
Vec3 pos;
if (owner instanceof EntityLivingBase) {
pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance);
} else {
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);
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);
}
}
}
2026-01-25 20:39:01 -05:00
private void addTrailPoint() {
// Trail points are RELATIVE to origin
if (trailPoints.isEmpty()) {
trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ));
if (fadeTrail) {
trailPointAges.add(0);
}
2026-01-25 20:39:01 -05:00
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);
}
2026-01-25 20:39:01 -05:00
// Limit trail length (only if not using fading - fading handles its own cleanup)
if (!fadeTrail) {
while (trailPoints.size() > MAX_TRAIL_POINTS) {
trailPoints.remove(0);
}
2026-01-25 20:39:01 -05:00
}
}
}
2026-01-28 20:43:32 -05:00
/**
* 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) {
2026-02-03 17:20:36 -03:00
Entity owner = getOwnerEntity();
2026-01-28 20:43:32 -05:00
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() {
2026-02-03 17:20:36 -03:00
if (!isHoming()) return;
2026-01-25 20:39:01 -05:00
2026-02-03 17:20:36 -03:00
Entity target = getTargetEntity();
2026-01-25 20:39:01 -05:00
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;
2026-01-29 14:19:57 -05:00
double dy = (target.posY + target.getEyeHeight()) - headWorldY;
2026-01-25 20:39:01 -05:00
double dz = target.posZ - headWorldZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
2026-02-03 17:20:36 -03:00
if (dist <= getHomingRange() && dist > 0) {
2026-01-25 20:39:01 -05:00
// Calculate effective homing strength - increases when closer to commit to target
2026-02-03 17:20:36 -03:00
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);
2026-01-25 20:39:01 -05:00
}
2026-02-03 17:20:36 -03:00
double desiredVX = (dx / dist) * getSpeed();
double desiredVY = (dy / dist) * getSpeed();
double desiredVZ = (dz / dist) * getSpeed();
2026-01-25 20:39:01 -05:00
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) {
2026-02-03 17:20:36 -03:00
motionX = (motionX / vLen) * getSpeed();
motionY = (motionY / vLen) * getSpeed();
motionZ = (motionZ / vLen) * getSpeed();
2026-01-25 20:39:01 -05:00
}
}
}
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);
2026-01-28 17:16:59 -05:00
// Use full raytrace that doesn't stop at liquids and checks all blocks
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
2026-01-25 20:39:01 -05:00
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
}
2026-01-25 20:39:01 -05:00
hasHit = true;
2026-02-03 17:20:36 -03:00
if (isExplosive()) {
2026-01-25 20:39:01 -05:00
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<EntityLivingBase> 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;
2026-02-03 17:20:36 -03:00
if (isExplosive()) {
2026-01-25 20:39:01 -05:00
posX = headX;
posY = headY;
posZ = headZ;
doExplosion();
} else {
applyDamage(entity);
}
this.setDead();
return;
}
}
// ==================== GETTERS FOR RENDERER ====================
public float getBeamWidth() {
return beamWidth;
}
public void setBeamWidth(float beamWidth) {
this.beamWidth = beamWidth;
}
2026-01-25 20:39:01 -05:00
public float getHeadSize() {
return headSize;
}
public void setHeadSize(float headSize) {
this.headSize = headSize;
}
public void setAttachedToOwner(boolean attached) {
this.attachedToOwner = attached;
}
2026-01-25 20:39:01 -05:00
/**
* 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<Vec3> 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<Integer> getTrailPointAges() {
return trailPointAges;
}
/**
* Get trail fade time in ticks.
*/
public int getTrailFadeTime() {
return trailFadeTime;
}
2026-01-25 20:39:01 -05:00
// ==================== 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;
2026-01-25 20:39:01 -05:00
2026-01-25 20:39:01 -05:00
// Read trail points (relative to origin)
trailPoints.clear();
trailPointAges.clear();
2026-01-25 20:39:01 -05:00
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);
}
2026-01-25 20:39:01 -05:00
}
@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);
2026-01-25 20:39:01 -05:00
// 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);
2026-01-25 20:39:01 -05:00
}
}