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

981 lines
36 KiB
Java
Raw Normal View History

2026-01-25 20:39:01 -05:00
package kamkeel.npcs.entity;
2026-02-25 00:02:09 -05:00
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityEnergyProjectile;
2026-02-22 03:28:56 -05:00
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData;
2026-03-02 20:09:32 -05:00
import kamkeel.npcs.util.AnchorPointHelper;
2026-02-23 23:19:33 -05:00
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
2026-01-25 20:39:01 -05:00
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
2026-02-18 05:13:51 -05:00
import noppes.npcs.EventHooks;
2026-02-20 03:50:41 -05:00
import noppes.npcs.entity.EntityNPCInterface;
2026-01-25 20:39:01 -05:00
import java.util.List;
/**
* Laser entity - sweeping beam that follows the caster's look vector.
* Expands from origin at expansionSpeed until reaching maxLength,
* then stays active until maxLifetime expires. Always tracks the owner's
* look direction so the beam can be swept mid-shot.
2026-02-18 05:13:51 -05:00
* <p>
2026-01-25 20:39:01 -05:00
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityLaser extends EntityEnergyProjectile {
2026-01-25 20:39:01 -05:00
// Laser-specific properties
private float laserWidth = 0.2f;
private float expansionSpeed = 2.0f; // Blocks per tick
private float maxLength = 32.0f; // Maximum beam reach in blocks
2026-01-25 20:39:01 -05:00
// State
private float desiredLength = 0.0f; // Intended beam reach (grows to maxLength)
private float currentLength = 0.0f; // Actual length after block truncation (visual + collision)
2026-01-25 20:39:01 -05:00
private boolean fullyExtended = false;
2026-03-08 15:39:06 -04:00
private boolean trackOwnerOrigin = true; // When false, laser fires independently (fireAt mode)
2026-01-25 20:39:01 -05:00
// Direction (normalized)
private double dirX, dirY, dirZ;
// End point for rendering
private double endX, endY, endZ;
// Previous tick positions for partial tick interpolation
private double prevStartX, prevStartY, prevStartZ;
private double prevEndX, prevEndY, prevEndZ;
// Block hit deduplication — prevents event/explosion spam every tick
private int lastBlockHitX = Integer.MIN_VALUE;
private int lastBlockHitY = Integer.MIN_VALUE;
private int lastBlockHitZ = Integer.MIN_VALUE;
private int lastExplosionTick = -100;
private static final int EXPLOSION_COOLDOWN = 10; // ticks between explosive impacts
2026-01-25 20:39:01 -05:00
public EntityAbilityLaser(World world) {
super(world);
}
/**
2026-01-29 14:19:57 -05:00
* Full constructor with all parameters using data classes.
2026-01-25 20:39:01 -05:00
*/
2026-01-30 05:01:54 -05:00
public EntityAbilityLaser(World world, EntityLivingBase owner, EntityLivingBase target,
double x, double y, double z,
float laserWidth,
2026-02-03 17:20:36 -03:00
EnergyDisplayData display, EnergyCombatData combat,
EnergyLightningData lightning, EnergyLifespanData lifespan,
float expansionSpeed, float maxLength) {
2026-01-25 20:39:01 -05:00
super(world);
2026-02-24 01:15:37 -05:00
initProjectile(owner, target, x, y, z, laserWidth, display, combat, lightning, lifespan);
2026-01-25 20:39:01 -05:00
// Laser-specific properties
this.laserWidth = laserWidth;
this.expansionSpeed = expansionSpeed;
this.maxLength = maxLength;
2026-01-25 20:39:01 -05:00
// Calculate direction toward target or forward
if (target != null) {
double dx = target.posX - x;
double dy = (target.posY + target.getEyeHeight() - 0.4) - y;
2026-01-25 20:39:01 -05:00
double dz = target.posZ - z;
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;
}
} else {
2026-02-22 22:39:33 -05:00
// Fall back to owner look direction so launch matches visual aim.
Vec3 look = owner == null ? null : owner.getLookVec();
if (look != null) {
this.dirX = look.xCoord;
this.dirY = look.yCoord;
this.dirZ = look.zCoord;
2026-02-24 01:15:37 -05:00
} else if (owner != null) {
2026-02-22 22:39:33 -05:00
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);
2026-02-24 01:15:37 -05:00
} else {
this.dirX = 1.0;
this.dirY = 0.0;
this.dirZ = 0.0;
2026-02-22 22:39:33 -05:00
}
2026-01-25 20:39:01 -05:00
}
// Initialize end point and prev positions
2026-01-25 20:39:01 -05:00
this.endX = x;
this.endY = y;
this.endZ = z;
this.prevStartX = x;
this.prevStartY = y;
this.prevStartZ = z;
this.prevEndX = x;
this.prevEndY = y;
this.prevEndZ = z;
2026-01-25 20:39:01 -05:00
}
2026-02-23 23:19:33 -05:00
@Override
@SideOnly(Side.CLIENT)
public boolean isInRangeToRenderDist(double distance) {
double range = Math.max(128.0D, currentLength * 2.0D + 64.0D);
return distance < range * range;
}
2026-01-25 20:39:01 -05:00
@Override
protected void updateRotation() {
// Beam rotation is handled in the renderer via perpendicular vector rotation
2026-01-25 20:39:01 -05:00
}
@Override
protected boolean checkMaxDistance() {
// Laser handles distance via maxLength, not max distance
2026-01-25 20:39:01 -05:00
return false;
}
@Override
protected void updateProjectile() {
if (isCharging()) {
updateCharging();
return;
}
// Save previous positions for partial tick interpolation
prevStartX = startX;
prevStartY = startY;
prevStartZ = startZ;
prevEndX = endX;
prevEndY = endY;
prevEndZ = endZ;
// Origin/direction tracking runs on BOTH sides so the client can render the beam.
2026-02-24 23:18:59 -05:00
// Once reflected, never resume owner-tracking — the laser flies in a fixed direction.
2026-03-08 15:39:06 -04:00
// When trackOwnerOrigin is false (fireAt mode), skip owner tracking entirely.
if (!reflected && trackOwnerOrigin) {
2026-02-23 00:17:34 -05:00
updateLaserOriginAndDirection();
}
2026-02-06 16:28:38 -05:00
// Expand the intended reach — runs on both sides for visual sync
2026-01-25 20:39:01 -05:00
if (!fullyExtended) {
desiredLength += expansionSpeed;
if (desiredLength >= maxLength) {
desiredLength = maxLength;
2026-01-25 20:39:01 -05:00
fullyExtended = true;
}
}
2026-01-25 20:39:01 -05:00
// Start from full desired length, then truncate at block hits
currentLength = desiredLength;
2026-01-25 20:39:01 -05:00
// Block raytrace — truncate beam at first solid block (both sides)
checkBlockCollision();
2026-02-06 16:28:38 -05:00
// Update end point
updateEndPoint();
2026-02-06 16:28:38 -05:00
// Server-only: barrier collision, entity collision, death
if (!worldObj.isRemote) {
if (checkLaserBarrierCollision()) {
return;
2026-01-25 20:39:01 -05:00
}
checkEntityCollisionAlongLine();
// Death handled by base class deathWorldTime (from lifespanData.maxLifetime)
2026-01-25 20:39:01 -05:00
}
}
/**
* Update end point from current origin + direction * length.
*/
private void updateEndPoint() {
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
}
2026-02-06 16:28:38 -05:00
/**
* Update laser origin and direction from the owner's current look rotation.
* Called each tick so the laser follows the caster's aim (sweeping beam).
2026-02-06 16:28:38 -05:00
*/
private void updateLaserOriginAndDirection() {
Entity owner = getOwnerEntity();
2026-02-23 23:19:33 -05:00
if (owner == null || !(owner instanceof EntityLivingBase)) {
if (worldObj != null && worldObj.isRemote) {
handleClientInterpolation();
}
return;
}
2026-02-06 16:28:38 -05:00
EntityLivingBase livingOwner = (EntityLivingBase) owner;
// Get owner look vector for direction
2026-02-22 22:39:33 -05:00
Vec3 look = livingOwner.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)
);
}
// Set direction directly from look vector (no lockVerticalDirection split)
dirX = look.xCoord;
dirY = look.yCoord;
dirZ = look.zCoord;
2026-02-06 16:28:38 -05:00
// Keep direction normalized
double dirLen = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ);
if (dirLen > 0.0001) {
dirX /= dirLen;
dirY /= dirLen;
dirZ /= dirLen;
2026-02-21 16:49:21 -05:00
} else {
dirX = 1.0;
dirY = 0.0;
dirZ = 0.0;
2026-02-21 16:49:21 -05:00
}
2026-03-06 05:54:10 -05:00
// Position: use anchor if launchFromAnchor, otherwise use look vector position.
// When using an anchor point, recalculate direction from anchor toward the target
// so the beam aims at the target rather than shooting straight from the offset position.
2026-03-02 20:09:32 -05:00
if (anchorData.launchFromAnchor) {
Vec3 anchorPos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData);
setPosition(anchorPos.xCoord, anchorPos.yCoord, anchorPos.zCoord);
2026-03-06 05:54:10 -05:00
Entity targetEntity = getTargetEntity();
if (targetEntity instanceof EntityLivingBase && targetEntity.isEntityAlive()) {
EntityLivingBase target = (EntityLivingBase) targetEntity;
double dx = target.posX - anchorPos.xCoord;
double dy = (target.posY + target.getEyeHeight() - 0.4) - anchorPos.yCoord;
double dz = target.posZ - anchorPos.zCoord;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0.0001) {
dirX = dx / len;
dirY = dy / len;
dirZ = dz / len;
}
}
2026-03-02 20:09:32 -05:00
} else {
Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ);
setLookVectorLaunchPosition(livingOwner, direction, false);
// Lower beam origin slightly so it doesn't obscure the player's crosshair
posY -= 0.15;
}
syncStartPositionToCurrent();
syncPositionStateToCurrent(false);
2026-02-06 16:28:38 -05:00
}
public void startMoving(EntityLivingBase target) {
beginLookVectorLaunch(false);
desiredLength = 0.0f;
currentLength = 0.0f;
fullyExtended = false;
lastBlockHitX = Integer.MIN_VALUE;
lastBlockHitY = Integer.MIN_VALUE;
lastBlockHitZ = Integer.MIN_VALUE;
lastExplosionTick = -100;
2026-03-06 05:54:10 -05:00
// Set initial direction: aim toward target first (like Orb/Beam),
// fall back to owner look vector if no target is available.
if (!setDirectionTowardTarget(target, startX, startY, startZ)) {
Vec3 look = getOwnerLookVector();
if (look != null) {
dirX = look.xCoord;
dirY = look.yCoord;
dirZ = look.zCoord;
} else {
dirX = 1.0;
dirY = 0.0;
dirZ = 0.0;
}
}
2026-02-06 16:28:38 -05:00
// Initialize end point at start (will expand from here)
this.endX = startX;
this.endY = startY;
this.endZ = startZ;
// Initialize prev positions to current so first frame doesn't jump
this.prevStartX = startX;
this.prevStartY = startY;
this.prevStartZ = startZ;
this.prevEndX = startX;
this.prevEndY = startY;
this.prevEndZ = 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;
}
2026-01-25 20:39:01 -05:00
private void checkBlockCollision() {
if (currentLength <= 0) return;
double traceEndX = startX + dirX * currentLength;
double traceEndY = startY + dirY * currentLength;
double traceEndZ = startZ + dirZ * currentLength;
MovingObjectPosition blockHit = rayTraceBlocks(startX, startY, startZ, traceEndX, traceEndY, traceEndZ);
2026-01-25 20:39:01 -05:00
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
// Truncate beam at block hit
float hitDist = (float) Math.sqrt(
2026-01-25 20:39:01 -05:00
(blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) +
2026-02-18 05:13:51 -05:00
(blockHit.hitVec.yCoord - startY) * (blockHit.hitVec.yCoord - startY) +
(blockHit.hitVec.zCoord - startZ) * (blockHit.hitVec.zCoord - startZ)
2026-01-25 20:39:01 -05:00
);
if (hitDist < currentLength) {
currentLength = hitDist;
endX = blockHit.hitVec.xCoord;
endY = blockHit.hitVec.yCoord;
endZ = blockHit.hitVec.zCoord;
2026-01-25 20:39:01 -05:00
}
// Server-only: fire event and explosion only when block changes or cooldown expires
if (!worldObj.isRemote) {
int bx = blockHit.blockX;
int by = blockHit.blockY;
int bz = blockHit.blockZ;
boolean newBlock = (bx != lastBlockHitX || by != lastBlockHitY || bz != lastBlockHitZ);
// Fire block impact event only when the beam hits a different block
if (newBlock) {
lastBlockHitX = bx;
lastBlockHitY = by;
lastBlockHitZ = bz;
EventHooks.onEnergyProjectileBlockImpact(this, bx, by, bz);
}
// Explosive: fire on cooldown so sweeping doesn't explode every tick
if (isExplosive() && (ticksExisted - lastExplosionTick) >= EXPLOSION_COOLDOWN) {
lastExplosionTick = ticksExisted;
double oldPosX = posX;
double oldPosY = posY;
double oldPosZ = posZ;
posX = endX;
posY = endY;
posZ = endZ;
doExplosion();
posX = oldPosX;
posY = oldPosY;
posZ = oldPosZ;
}
}
} else if (!worldObj.isRemote) {
// Beam is not hitting any block — reset tracking so next block contact triggers fresh
lastBlockHitX = Integer.MIN_VALUE;
lastBlockHitY = Integer.MIN_VALUE;
lastBlockHitZ = Integer.MIN_VALUE;
2026-01-25 20:39:01 -05:00
}
}
private void checkEntityCollisionAlongLine() {
if (currentLength <= 0) return;
2026-02-06 16:28:38 -05:00
// Expand search AABB by entity sizes to ensure we find all potential targets
double expand = Math.max(laserWidth, 1.0);
double minX = Math.min(startX, endX) - expand;
double minY = Math.min(startY, endY) - expand;
double minZ = Math.min(startZ, endZ) - expand;
double maxX = Math.max(startX, endX) + expand;
double maxY = Math.max(startY, endY) + expand;
double maxZ = Math.max(startZ, endZ) + expand;
2026-01-25 20:39:01 -05:00
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
for (EntityLivingBase entity : entities) {
if (shouldIgnoreEntity(entity)) continue;
2026-02-21 20:39:16 -05:00
if (!canHitEntityNow(entity)) continue;
2026-01-25 20:39:01 -05:00
2026-02-06 16:28:38 -05:00
// Check if entity's bounding box intersects with the laser line
2026-01-25 20:39:01 -05:00
if (isEntityOnLine(entity)) {
2026-02-21 20:39:16 -05:00
if (processEntityHit(entity, entity.posX, entity.posY + entity.height * 0.5, entity.posZ)) {
// SINGLE hit or maxHits reached — truncate beam at impact and die
2026-02-18 14:10:55 -05:00
double dx = entity.posX - startX;
double dy = (entity.posY + entity.height * 0.5) - startY;
double dz = entity.posZ - startZ;
float impactDist = (float) Math.sqrt(dx * dx + dy * dy + dz * dz);
currentLength = Math.min(impactDist, currentLength);
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
2026-02-21 20:39:16 -05:00
return;
2026-02-18 14:10:55 -05:00
}
// MULTI/PIERCE continue through remaining entities
2026-01-25 20:39:01 -05:00
}
}
}
/**
2026-02-06 16:28:38 -05:00
* Check if an entity's bounding box intersects with the laser line.
2026-02-22 22:39:33 -05:00
* Uses a swept segment vs expanded AABB intercept test for reliable hits.
2026-01-25 20:39:01 -05:00
*/
private boolean isEntityOnLine(EntityLivingBase entity) {
2026-02-22 22:39:33 -05:00
AxisAlignedBB bb = entity.boundingBox;
if (bb == null) return false;
2026-02-06 16:28:38 -05:00
2026-02-22 22:39:33 -05:00
// Minimum effective width keeps thin lasers from visually clipping targets without hitting.
double effectiveWidth = Math.max(laserWidth, 0.65);
double expand = effectiveWidth * 0.5;
AxisAlignedBB expanded = bb.expand(expand, expand, expand);
2026-02-06 16:28:38 -05:00
2026-02-22 22:39:33 -05:00
Vec3 segmentStart = Vec3.createVectorHelper(startX, startY, startZ);
Vec3 segmentEnd = Vec3.createVectorHelper(endX, endY, endZ);
MovingObjectPosition intercept = expanded.calculateIntercept(segmentStart, segmentEnd);
if (intercept != null) return true;
2026-01-25 20:39:01 -05:00
2026-02-22 22:39:33 -05:00
// Handle edge case where the segment starts or ends already inside the target volume.
return expanded.isVecInside(segmentStart) || expanded.isVecInside(segmentEnd);
2026-01-25 20:39:01 -05:00
}
2026-02-20 03:50:41 -05:00
// ==================== BARRIER COLLISION ====================
/**
* Disable the base class barrier check — it uses position/velocity which doesn't work
* for lasers (posX/Y/Z stays at origin, motionX/Y/Z is zero). We handle barriers
* exclusively in updateProjectile() via checkLaserBarrierCollision().
2026-02-20 03:50:41 -05:00
*/
@Override
protected boolean checkBarrierCollision() {
return false;
2026-02-22 22:39:33 -05:00
}
/**
* Laser-specific barrier collision using line-segment intersection.
* Called from updateProjectile() every tick.
*/
2026-02-22 22:39:33 -05:00
@SuppressWarnings("unchecked")
private boolean checkLaserBarrierCollision() {
2026-02-22 22:39:33 -05:00
if (currentLength <= 0) return false;
2026-02-20 03:50:41 -05:00
2026-03-01 03:00:17 -05:00
List<EntityEnergyBarrier> barriers = EntityEnergyBarrier.getActiveBarriers(worldObj);
for (EntityEnergyBarrier barrier : barriers) {
2026-02-20 03:50:41 -05:00
if (barrier.isDead || barrier.isCharging()) continue;
if (barrier.getOwnerEntityId() == this.ownerEntityId) continue;
// Same-faction NPC check
Entity bOwner = barrier.getOwnerEntity();
Entity lOwner = this.getOwnerEntity();
if (bOwner instanceof EntityNPCInterface && lOwner instanceof EntityNPCInterface) {
if (((EntityNPCInterface) bOwner).faction.id == ((EntityNPCInterface) lOwner).faction.id) {
continue;
}
}
2026-03-01 03:00:17 -05:00
if (barrier instanceof EntityEnergyDome) {
EntityEnergyDome dome = (EntityEnergyDome) barrier;
2026-02-20 03:50:41 -05:00
float intersectDist = getLineSphereIntersection(dome);
if (intersectDist >= 0) {
float damage = getModifiedDamage();
2026-03-01 03:00:17 -05:00
EntityEnergyBarrier.ProjectileHitOutcome hitOutcome = dome.onProjectileHitResolved(this, damage);
if (hitOutcome == null || hitOutcome.result == EntityEnergyBarrier.ProjectileHitResult.PASS) {
2026-02-23 00:17:34 -05:00
continue;
}
2026-03-01 03:00:17 -05:00
if (hitOutcome.result != EntityEnergyBarrier.ProjectileHitResult.BROKEN) {
2026-02-23 00:17:34 -05:00
currentLength = Math.max(0.0f, intersectDist);
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
}
2026-02-24 23:52:01 -05:00
if (handleBarrierHitOutcome(dome, hitOutcome, damage)) {
2026-02-23 00:17:34 -05:00
return true;
}
return false;
2026-02-20 03:50:41 -05:00
}
} else {
2026-02-22 22:39:33 -05:00
// For line-based lasers, use a swept segment against generic barrier checks.
double segMotionX = endX - startX;
double segMotionY = endY - startY;
double segMotionZ = endZ - startZ;
boolean incoming = barrier.isIncomingGenericProjectile(
endX, endY, endZ,
segMotionX, segMotionY, segMotionZ,
startX, startY, startZ,
this.ownerEntityId
);
if (incoming) {
2026-02-20 03:50:41 -05:00
float damage = getModifiedDamage();
2026-03-01 03:00:17 -05:00
EntityEnergyBarrier.ProjectileHitOutcome hitOutcome = barrier.onProjectileHitResolved(this, damage);
if (hitOutcome == null || hitOutcome.result == EntityEnergyBarrier.ProjectileHitResult.PASS) {
2026-02-23 00:17:34 -05:00
continue;
}
2026-03-01 03:00:17 -05:00
if (hitOutcome.result != EntityEnergyBarrier.ProjectileHitResult.BROKEN) {
2026-02-23 00:17:34 -05:00
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
}
2026-02-24 23:52:01 -05:00
if (handleBarrierHitOutcome(barrier, hitOutcome, damage)) {
2026-02-20 03:50:41 -05:00
return true;
}
2026-02-23 00:17:34 -05:00
return false;
2026-02-20 03:50:41 -05:00
}
}
}
return false;
}
2026-02-23 00:17:34 -05:00
@Override
2026-03-01 03:00:17 -05:00
protected boolean reflectFromBarrier(EntityEnergyBarrier barrier, float reflectStrengthPct) {
2026-02-23 00:17:34 -05:00
if (barrier == null) {
return false;
}
double vx = dirX;
double vy = dirY;
double vz = dirZ;
double vLenSq = vx * vx + vy * vy + vz * vz;
if (vLenSq < 1.0e-8) {
return false;
}
double[] normal = getBarrierImpactNormal(barrier, vx, vy, vz);
if (normal == null) {
return false;
}
double dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
if (dot > 0.0) {
normal[0] = -normal[0];
normal[1] = -normal[1];
normal[2] = -normal[2];
dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
}
double rx = vx - 2.0 * dot * normal[0];
double ry = vy - 2.0 * dot * normal[1];
double rz = vz - 2.0 * dot * normal[2];
if (ry < -0.06) {
ry = Math.max(0.08, Math.abs(ry) * 0.35);
}
double rLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
if (rLen < 1.0e-8) {
rx = -vx;
ry = -vy;
rz = -vz;
rLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
if (rLen < 1.0e-8) {
return false;
}
}
dirX = rx / rLen;
dirY = ry / rLen;
dirZ = rz / rLen;
// Start a fresh segment at the impact edge to make the rebound visible.
startX = endX;
startY = endY;
startZ = endZ;
setPosition(startX, startY, startZ);
syncPositionStateToCurrent(true);
desiredLength = 0.0f;
2026-02-23 00:17:34 -05:00
currentLength = 0.0f;
fullyExtended = false;
endX = startX;
endY = startY;
endZ = startZ;
lastBlockHitX = Integer.MIN_VALUE;
lastBlockHitY = Integer.MIN_VALUE;
lastBlockHitZ = Integer.MIN_VALUE;
lastExplosionTick = -100;
// Reset lifetime so reflected laser gets a fresh maxLifetime from the base class
deathWorldTime = -1;
2026-02-23 00:17:34 -05:00
barrierImpactPauseTicks = 0;
barrierImpactDestroyOnResume = false;
2026-02-25 00:02:09 -05:00
// Detach from source ability so the caster is freed from active-phase locks.
if (sourceAbility instanceof AbilityEnergyProjectile) {
((AbilityEnergyProjectile<?>) sourceAbility).detachEntity(this);
}
sourceAbility = null;
2026-02-28 19:43:01 -05:00
// Save original owner before transfer for potential target retargeting
int originalOwnerId = ownerEntityId;
2026-02-23 00:17:34 -05:00
Entity barrierOwner = barrier.getOwnerEntity();
if (barrierOwner != null) {
setOwnerEntityId(barrierOwner.getEntityId());
trackProjectile(this);
}
2026-02-28 19:43:01 -05:00
// Target Owner: set the reflected projectile's target to the original caster
if (barrier.getBarrierData().isTargetOwner() && originalOwnerId != -1) {
setTargetEntityId(originalOwnerId);
} else {
setTargetEntityId(-1);
}
2026-02-23 00:17:34 -05:00
setInnerColor(barrier.getInnerColor());
setOuterColor(barrier.getOuterColor());
float clampedStrength = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
float reducedDamage = getDamage() * (1.0f - clampedStrength / 100.0f);
setCombatDamage(Math.max(0.0f, reducedDamage));
hitOnceEntities.clear();
lastHitTickByEntity.clear();
2026-02-25 00:02:09 -05:00
reflected = true;
2026-02-23 00:17:34 -05:00
return true;
}
2026-02-20 03:50:41 -05:00
/**
* Line-sphere intersection between the laser line segment and a dome.
* Returns the distance along the laser direction to the first entry point,
* or -1 if no intersection from outside.
*/
2026-03-01 03:00:17 -05:00
private float getLineSphereIntersection(EntityEnergyDome dome) {
2026-02-20 03:50:41 -05:00
double cx = dome.posX;
double cy = dome.posY;
double cz = dome.posZ;
float radius = dome.getDomeRadius();
// If laser origin is inside the dome, don't block (outgoing)
double ocX = startX - cx;
double ocY = startY - cy;
double ocZ = startZ - cz;
double originDistSq = ocX * ocX + ocY * ocY + ocZ * ocZ;
if (originDistSq < (double) radius * radius) return -1;
// Solve: |start + t*dir - C|^2 = R^2
double a = dirX * dirX + dirY * dirY + dirZ * dirZ;
double b = 2.0 * (dirX * ocX + dirY * ocY + dirZ * ocZ);
double c = originDistSq - (double) radius * radius;
double discriminant = b * b - 4.0 * a * c;
if (discriminant < 0) return -1;
double sqrtD = Math.sqrt(discriminant);
double t1 = (-b - sqrtD) / (2.0 * a); // Entry point
if (t1 >= 0 && t1 <= currentLength) {
return (float) t1;
}
return -1;
}
// ==================== DEBUG ====================
@Override
protected String debugLogExtra() {
return String.format("dir=(%.3f,%.3f,%.3f) desired=%.2f current=%.2f max=%.2f " +
"fullyExtended=%b end=(%.2f,%.2f,%.2f) width=%.2f expSpd=%.2f origin=(%.2f,%.2f,%.2f)",
dirX, dirY, dirZ, desiredLength, currentLength, maxLength,
fullyExtended, endX, endY, endZ, laserWidth, expansionSpeed,
startX, startY, startZ);
}
// ==================== GETTERS / SETTERS ====================
2026-02-06 16:28:38 -05:00
2026-01-25 20:39:01 -05:00
public float getLaserWidth() {
return laserWidth;
}
public void setLaserWidth(float width) {
this.laserWidth = width;
}
public float getExpansionSpeed() {
return expansionSpeed;
}
public void setExpansionSpeed(float speed) {
this.expansionSpeed = speed;
}
public float getMaxLength() {
return maxLength;
}
public void setMaxLength(float maxLength) {
this.maxLength = maxLength;
}
public void setDirection(double x, double y, double z) {
this.dirX = x;
this.dirY = y;
this.dirZ = z;
}
2026-03-08 15:39:06 -04:00
public boolean isTrackOwnerOrigin() {
return trackOwnerOrigin;
}
public void setTrackOwnerOrigin(boolean track) {
this.trackOwnerOrigin = track;
}
2026-01-25 20:39:01 -05:00
public float getCurrentLength() {
return currentLength;
}
public boolean isFullyExtended() {
return fullyExtended;
}
public double getStartX() {
return startX;
}
public double getStartY() {
return startY;
}
public double getStartZ() {
return startZ;
}
public double getEndX() {
return endX;
}
public double getEndY() {
return endY;
}
public double getEndZ() {
return endZ;
}
public double getInterpolatedStartX(float partialTicks) {
return prevStartX + (startX - prevStartX) * partialTicks;
}
public double getInterpolatedStartY(float partialTicks) {
return prevStartY + (startY - prevStartY) * partialTicks;
}
public double getInterpolatedStartZ(float partialTicks) {
return prevStartZ + (startZ - prevStartZ) * partialTicks;
}
public double getInterpolatedEndX(float partialTicks) {
return prevEndX + (endX - prevEndX) * partialTicks;
}
public double getInterpolatedEndY(float partialTicks) {
return prevEndY + (endY - prevEndY) * partialTicks;
}
public double getInterpolatedEndZ(float partialTicks) {
return prevEndZ + (endZ - prevEndZ) * partialTicks;
}
2026-01-25 20:39:01 -05:00
public double getDirX() {
return dirX;
}
public double getDirY() {
return dirY;
}
public double getDirZ() {
return dirZ;
}
@Override
protected float getLaunchClearanceRadius() {
return Math.max(0.1f, laserWidth * 0.5f);
}
@Override
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
// Charge orb grows to half the beam width
this.size = laserWidth * 0.5f;
this.desiredLength = 0.0f;
this.currentLength = 0.0f;
this.fullyExtended = false;
super.setupCharging(anchor, chargeDuration);
this.endX = posX;
this.endY = posY;
this.endZ = posZ;
this.prevStartX = posX;
this.prevStartY = posY;
this.prevStartZ = posZ;
this.prevEndX = posX;
this.prevEndY = posY;
this.prevEndZ = posZ;
}
2026-02-24 23:18:59 -05:00
// ==================== REFLECTION SYNC ====================
@Override
protected void writeProjectileReflectionData(NBTTagCompound nbt) {
nbt.setDouble("DirX", dirX);
nbt.setDouble("DirY", dirY);
nbt.setDouble("DirZ", dirZ);
nbt.setDouble("StartX", startX);
nbt.setDouble("StartY", startY);
nbt.setDouble("StartZ", startZ);
nbt.setFloat("DesiredLength", desiredLength);
2026-02-24 23:18:59 -05:00
nbt.setFloat("CurrentLength", currentLength);
nbt.setDouble("EndX", endX);
nbt.setDouble("EndY", endY);
nbt.setDouble("EndZ", endZ);
nbt.setBoolean("FullyExtended", fullyExtended);
nbt.setFloat("MaxLength", maxLength);
2026-02-24 23:18:59 -05:00
}
@Override
protected void applyProjectileReflectionData(NBTTagCompound nbt) {
dirX = nbt.getDouble("DirX");
dirY = nbt.getDouble("DirY");
dirZ = nbt.getDouble("DirZ");
startX = nbt.getDouble("StartX");
startY = nbt.getDouble("StartY");
startZ = nbt.getDouble("StartZ");
desiredLength = nbt.getFloat("DesiredLength");
2026-02-24 23:18:59 -05:00
currentLength = nbt.getFloat("CurrentLength");
endX = nbt.getDouble("EndX");
endY = nbt.getDouble("EndY");
endZ = nbt.getDouble("EndZ");
fullyExtended = nbt.getBoolean("FullyExtended");
if (nbt.hasKey("MaxLength")) maxLength = nbt.getFloat("MaxLength");
2026-02-24 23:18:59 -05:00
}
// ==================== PROPERTY SYNC ====================
@Override
protected void writeProjectileClientSyncData(NBTTagCompound nbt) {
nbt.setFloat("LaserWidth", laserWidth);
nbt.setFloat("ExpansionSpeed", expansionSpeed);
nbt.setFloat("MaxLength", maxLength);
nbt.setDouble("DirX", dirX);
nbt.setDouble("DirY", dirY);
nbt.setDouble("DirZ", dirZ);
}
@Override
protected void applyProjectileClientSyncData(NBTTagCompound nbt) {
laserWidth = nbt.getFloat("LaserWidth");
expansionSpeed = nbt.getFloat("ExpansionSpeed");
maxLength = nbt.getFloat("MaxLength");
dirX = nbt.getDouble("DirX");
dirY = nbt.getDouble("DirY");
dirZ = nbt.getDouble("DirZ");
}
2026-01-25 20:39:01 -05:00
// ==================== NBT ====================
@Override
protected void readProjectileNBT(NBTTagCompound nbt) {
this.laserWidth = sanitize(nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f, 0.2f, MAX_ENTITY_SIZE);
2026-03-01 16:19:50 -05:00
this.expansionSpeed = sanitize(nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f, 0.1f, MAX_ENTITY_SIZE);
this.maxLength = sanitize(nbt.hasKey("MaxLength") ? nbt.getFloat("MaxLength") : 32.0f, 1.0f, MAX_ENTITY_SIZE);
2026-01-25 20:39:01 -05:00
this.dirX = nbt.getDouble("DirX");
this.dirY = nbt.getDouble("DirY");
this.dirZ = nbt.getDouble("DirZ");
this.desiredLength = nbt.getFloat("DesiredLength");
2026-01-25 20:39:01 -05:00
this.currentLength = nbt.getFloat("CurrentLength");
this.fullyExtended = nbt.getBoolean("FullyExtended");
2026-03-08 15:39:06 -04:00
this.trackOwnerOrigin = !nbt.hasKey("TrackOwnerOrigin") || nbt.getBoolean("TrackOwnerOrigin");
2026-01-25 20:39:01 -05:00
this.endX = nbt.getDouble("EndX");
this.endY = nbt.getDouble("EndY");
this.endZ = nbt.getDouble("EndZ");
readChargingNBT(nbt);
2026-01-25 20:39:01 -05:00
}
@Override
protected void writeProjectileNBT(NBTTagCompound nbt) {
nbt.setFloat("LaserWidth", laserWidth);
nbt.setFloat("ExpansionSpeed", expansionSpeed);
nbt.setFloat("MaxLength", maxLength);
2026-01-25 20:39:01 -05:00
nbt.setDouble("DirX", dirX);
nbt.setDouble("DirY", dirY);
nbt.setDouble("DirZ", dirZ);
nbt.setFloat("DesiredLength", desiredLength);
2026-01-25 20:39:01 -05:00
nbt.setFloat("CurrentLength", currentLength);
nbt.setBoolean("FullyExtended", fullyExtended);
2026-03-08 15:39:06 -04:00
nbt.setBoolean("TrackOwnerOrigin", trackOwnerOrigin);
2026-01-25 20:39:01 -05:00
nbt.setDouble("EndX", endX);
nbt.setDouble("EndY", endY);
nbt.setDouble("EndZ", endZ);
writeChargingNBT(nbt);
2026-01-25 20:39:01 -05:00
}
2026-01-28 20:43:32 -05:00
/**
2026-01-29 14:19:57 -05:00
* Setup this laser in preview mode for GUI display.
* Uses charging-orb visuals during windup before firing.
2026-01-28 20:43:32 -05:00
*/
2026-02-03 17:20:36 -03:00
public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyDisplayData display,
EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration,
float expansionSpeed, float maxLength) {
2026-02-22 15:02:14 -05:00
setupPreviewState(owner, display, lightning, anchor, chargeDuration);
2026-01-28 20:43:32 -05:00
2026-02-22 15:02:14 -05:00
// Laser-specific visual/behavior state.
2026-01-29 14:19:57 -05:00
this.laserWidth = laserWidth;
this.expansionSpeed = expansionSpeed;
this.maxLength = Math.min(maxLength, 5.0f); // Limit for GUI preview
this.desiredLength = 0.0f;
this.currentLength = 0.0f;
this.fullyExtended = false;
// Charge as an orb from tiny size to half laser width while anchored.
this.targetSize = laserWidth * 0.5f;
2026-02-22 15:02:14 -05:00
setVisualSize(0.01f);
setChargeOriginFromAnchor(owner, anchorData);
clearMotion();
this.endX = posX;
this.endY = posY;
this.endZ = posZ;
this.prevStartX = posX;
this.prevStartY = posY;
this.prevStartZ = posZ;
this.prevEndX = posX;
this.prevEndY = posY;
this.prevEndZ = posZ;
// Precompute initial direction from owner look for launch.
2026-02-22 15:02:14 -05:00
Vec3 look = owner == null ? null : owner.getLookVec();
if (look == null) {
2026-02-22 15:02:14 -05:00
if (owner != 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)
);
} else {
look = Vec3.createVectorHelper(1.0, 0.0, 0.0);
}
}
this.dirX = look.xCoord;
this.dirY = look.yCoord;
this.dirZ = look.zCoord;
2026-01-28 20:43:32 -05:00
}
2026-01-25 20:39:01 -05:00
}