package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityEnergyProjectile; 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; import kamkeel.npcs.util.AnchorPointHelper; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; 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; import noppes.npcs.EventHooks; import noppes.npcs.entity.EntityNPCInterface; 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. *
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityLaser extends EntityEnergyProjectile {
// Laser-specific properties (target value)
private float laserWidth = 0.2f;
// Lerp-smoothed render values
private float renderLaserWidth = 0.2f;
private float prevRenderLaserWidth = 0.2f;
private float expansionSpeed = 2.0f; // Blocks per tick
private float maxLength = 32.0f; // Maximum beam reach in blocks
// State
private float desiredLength = 0.0f; // Intended beam reach (grows to maxLength)
private float currentLength = 0.0f; // Actual length after block truncation (visual + collision)
private boolean fullyExtended = false;
private boolean trackOwnerOrigin = true; // When false, laser fires independently (fireAt mode)
// 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
public EntityAbilityLaser(World world) {
super(world);
}
/**
* Full constructor with all parameters using data classes.
*/
public EntityAbilityLaser(World world, EntityLivingBase owner, EntityLivingBase target,
double x, double y, double z,
float laserWidth,
EnergyDisplayData display, EnergyCombatData combat,
EnergyLightningData lightning, EnergyLifespanData lifespan,
float expansionSpeed, float maxLength) {
super(world);
initProjectile(owner, target, x, y, z, laserWidth, display, combat, lightning, lifespan);
// Laser-specific properties
this.laserWidth = laserWidth;
this.expansionSpeed = expansionSpeed;
this.maxLength = maxLength;
// Calculate direction toward target or forward
if (target != null) {
double dx = target.posX - x;
double dy = (target.posY + target.getEyeHeight() - 0.4) - y;
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 {
// 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;
} else if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
this.dirX = -Math.sin(yaw) * Math.cos(pitch);
this.dirY = -Math.sin(pitch);
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
} else {
this.dirX = 1.0;
this.dirY = 0.0;
this.dirZ = 0.0;
}
}
// Initialize end point and prev positions
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;
}
@Override
@SideOnly(Side.CLIENT)
public boolean isInRangeToRenderDist(double distance) {
double range = Math.max(128.0D, currentLength * 2.0D + 64.0D);
return distance < range * range;
}
@Override
protected void updateRotation() {
// Beam rotation is handled in the renderer via perpendicular vector rotation
}
@Override
protected boolean checkMaxDistance() {
// Laser handles distance via maxLength, not max distance
return false;
}
@Override
protected void updateProjectile() {
// Save previous render value for sub-tick interpolation
prevRenderLaserWidth = renderLaserWidth;
if (isCharging()) {
// During charging, snap render value to target
renderLaserWidth = laserWidth;
updateCharging();
return;
}
// Lerp render value toward target (smooth out sync packet jumps)
renderLaserWidth += (laserWidth - renderLaserWidth) * 0.15f;
// 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.
// Once reflected, never resume owner-tracking — the laser flies in a fixed direction.
// When trackOwnerOrigin is false (fireAt mode), skip owner tracking entirely.
if (!reflected && trackOwnerOrigin) {
updateLaserOriginAndDirection();
}
// Expand the intended reach — runs on both sides for visual sync
if (!fullyExtended) {
desiredLength += expansionSpeed;
if (desiredLength >= maxLength) {
desiredLength = maxLength;
fullyExtended = true;
}
}
// Start from full desired length, then truncate at block hits
currentLength = desiredLength;
// Block raytrace — truncate beam at first solid block (both sides)
checkBlockCollision();
// Update end point
updateEndPoint();
// Server-only: barrier collision, entity collision, death
if (!worldObj.isRemote) {
if (checkLaserBarrierCollision()) {
return;
}
checkEntityCollisionAlongLine();
// Death handled by base class deathWorldTime (from lifespanData.maxLifetime)
}
}
/**
* Update end point from current origin + direction * length.
*/
private void updateEndPoint() {
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
}
/**
* 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).
*/
private void updateLaserOriginAndDirection() {
Entity owner = getOwnerEntity();
if (owner == null || !(owner instanceof EntityLivingBase)) {
if (worldObj != null && worldObj.isRemote) {
handleClientInterpolation();
}
return;
}
EntityLivingBase livingOwner = (EntityLivingBase) owner;
// Get owner look vector for direction
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;
// Keep direction normalized
double dirLen = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ);
if (dirLen > 0.0001) {
dirX /= dirLen;
dirY /= dirLen;
dirZ /= dirLen;
} else {
dirX = 1.0;
dirY = 0.0;
dirZ = 0.0;
}
// 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.
if (anchorData.launchFromAnchor) {
Vec3 anchorPos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData);
setPosition(anchorPos.xCoord, anchorPos.yCoord, anchorPos.zCoord);
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;
}
}
} 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);
}
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;
// 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;
}
}
// 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;
}
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);
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
// Truncate beam at block hit
float hitDist = (float) Math.sqrt(
(blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) +
(blockHit.hitVec.yCoord - startY) * (blockHit.hitVec.yCoord - startY) +
(blockHit.hitVec.zCoord - startZ) * (blockHit.hitVec.zCoord - startZ)
);
if (hitDist < currentLength) {
currentLength = hitDist;
endX = blockHit.hitVec.xCoord;
endY = blockHit.hitVec.yCoord;
endZ = blockHit.hitVec.zCoord;
}
// 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;
}
}
private void checkEntityCollisionAlongLine() {
if (currentLength <= 0) return;
// 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;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ);
@SuppressWarnings("unchecked")
List