Files
CustomNPC-Plus/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java
T
2026-01-26 04:42:23 -05:00

459 lines
15 KiB
Java

package kamkeel.npcs.entity;
import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.world.World;
import noppes.npcs.LogWriter;
import noppes.npcs.NpcDamageSource;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Sweeper ability entity - a rotating beam attached to the NPC.
* Unlike other projectiles, this handles its own damage logic since it's
* attached to and rotates around the caster.
*
* Design: Similar to Beam trail visuals but rotating around origin.
*/
public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpawnData {
// Beam properties
private float beamLength = 10.0f;
private float beamWidth = 0.3f; // Thin like beam trail
private float beamHeight = 0.8f;
private int innerColor = 0xFF6600;
private int outerColor = 0xFF0000;
private boolean outerColorEnabled = true;
private float outerColorWidth = 1.8f;
// Combat properties
private float damage = 5.0f;
private int damageInterval = 5;
private boolean piercing = true;
// Rotation
private float currentAngle = 0;
private float prevAngle = 0;
private float sweepSpeed = 3.0f;
private int numberOfRotations = 2;
private int completedRotations = 0;
private float baseYaw = 0; // Starting yaw direction
private boolean lockOnTarget = false;
// Owner tracking
private int ownerEntityId = -1;
private int targetEntityId = -1;
// Lifetime
private int maxTicks = 400;
private long deathWorldTime = -1;
// Damage state
private transient int ticksSinceDamage = 0;
private transient Set<Integer> hitThisTick = new HashSet<>();
public EntityAbilitySweeper(World world) {
super(world);
this.setSize(0.1f, 0.1f);
this.noClip = true;
this.isImmuneToFire = true;
this.ignoreFrustumCheck = true;
}
public EntityAbilitySweeper(World world, EntityNPCInterface owner, EntityLivingBase target,
float beamLength, float beamWidth, float beamHeight,
int innerColor, int outerColor,
boolean outerColorEnabled, float outerColorWidth,
float sweepSpeed, int numberOfRotations,
float damage, int damageInterval, boolean piercing,
boolean lockOnTarget) {
this(world);
this.ownerEntityId = owner.getEntityId();
this.targetEntityId = target != null ? target.getEntityId() : -1;
this.beamLength = beamLength;
this.beamWidth = beamWidth;
this.beamHeight = beamHeight;
this.innerColor = innerColor;
this.outerColor = outerColor;
this.outerColorEnabled = outerColorEnabled;
this.outerColorWidth = outerColorWidth;
this.sweepSpeed = sweepSpeed;
this.numberOfRotations = numberOfRotations;
this.damage = damage;
this.damageInterval = damageInterval;
this.piercing = piercing;
this.lockOnTarget = lockOnTarget;
// Calculate max ticks based on rotations
this.maxTicks = (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10;
// Calculate base yaw
if (lockOnTarget && target != null) {
double dx = target.posX - owner.posX;
double dz = target.posZ - owner.posZ;
this.baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
} else {
this.baseYaw = owner.rotationYaw;
}
// Position at owner
this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ);
// Ready to deal damage immediately
this.ticksSinceDamage = damageInterval;
}
@Override
protected void entityInit() {
// No DataWatcher needed
}
@Override
public void onUpdate() {
this.prevPosX = this.posX;
this.prevPosY = this.posY;
this.prevPosZ = this.posZ;
this.prevAngle = this.currentAngle;
super.onUpdate();
// Set death time on first tick
if (deathWorldTime < 0 && worldObj != null) {
deathWorldTime = worldObj.getTotalWorldTime() + maxTicks;
}
// Check lifetime
if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) {
this.setDead();
return;
}
// Follow owner
Entity owner = getOwner();
if (owner == null) {
this.setDead();
return;
}
// Update position to follow owner
this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ);
// Check if sweep is done
if (completedRotations >= numberOfRotations) {
this.setDead();
return;
}
// Update rotation
currentAngle += sweepSpeed;
if (currentAngle >= 360) {
currentAngle -= 360;
completedRotations++;
if (!worldObj.isRemote) {
LogWriter.info("[Sweeper] Completed rotation " + completedRotations + "/" + numberOfRotations);
}
}
// Server handles damage
if (!worldObj.isRemote) {
handleDamage(owner);
}
}
/**
* Handle damage detection and application.
* This runs on server only.
*/
private void handleDamage(Entity owner) {
hitThisTick.clear();
ticksSinceDamage++;
if (ticksSinceDamage < damageInterval) {
return;
}
ticksSinceDamage = 0;
// Calculate beam direction using the ACTUAL render angle
float beamYaw = baseYaw + currentAngle;
float yawRad = (float) Math.toRadians(beamYaw);
double startX = posX;
double startY = posY;
double startZ = posZ;
double dirX = -Math.sin(yawRad);
double dirZ = Math.cos(yawRad);
// Sample points along the beam for collision
int samples = (int) (beamLength / 0.5);
for (int i = 1; i <= samples; i++) {
double progress = (double) i / samples;
double checkX = startX + dirX * beamLength * progress;
double checkY = startY;
double checkZ = startZ + dirZ * beamLength * progress;
// Narrow hitbox for thin beam
AxisAlignedBB checkBox = AxisAlignedBB.getBoundingBox(
checkX - beamWidth * 2, checkY - 0.5, checkZ - beamWidth * 2,
checkX + beamWidth * 2, checkY + 1.0, checkZ + beamWidth * 2
);
@SuppressWarnings("unchecked")
List<Entity> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, checkBox);
for (Entity entity : entities) {
if (!(entity instanceof EntityLivingBase)) continue;
if (entity == owner) continue;
if (hitThisTick.contains(entity.getEntityId())) continue;
EntityLivingBase livingEntity = (EntityLivingBase) entity;
// Check if actually in beam path
if (isInBeamPath(livingEntity, startX, startY, startZ, dirX, dirZ)) {
hitThisTick.add(entity.getEntityId());
applyDamage(livingEntity, owner);
if (!piercing) {
return;
}
}
}
}
}
private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ,
double dirX, double dirZ) {
double entityX = entity.posX - startX;
double entityZ = entity.posZ - startZ;
// Dot product to find projection distance
double projectionDist = entityX * dirX + entityZ * dirZ;
// Check if within beam length
if (projectionDist < 0 || projectionDist > beamLength) {
return false;
}
// Calculate perpendicular distance
double projX = dirX * projectionDist;
double projZ = dirZ * projectionDist;
double perpX = entityX - projX;
double perpZ = entityZ - projZ;
double perpDist = Math.sqrt(perpX * perpX + perpZ * perpZ);
// Check if within beam width (use wider hitbox than visual)
if (perpDist > beamWidth * 3 + entity.width / 2) {
return false;
}
// Height check - entity's feet must be below beam top (allows jumping over)
double entityFeetY = entity.posY;
double beamTopY = startY + 0.5;
return entityFeetY < beamTopY;
}
private void applyDamage(EntityLivingBase target, Entity owner) {
EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null;
if (npc != null) {
target.attackEntityFrom(new NpcDamageSource("npc_ability", npc), damage);
} else {
target.attackEntityFrom(new NpcDamageSource("npc_ability", null), damage);
}
// Small upward knockback to help escape
target.addVelocity(0, 0.2, 0);
target.velocityChanged = true;
}
private Entity getOwner() {
if (ownerEntityId == -1) return null;
return worldObj.getEntityByID(ownerEntityId);
}
// ==================== RENDERING ====================
@Override
@SideOnly(Side.CLIENT)
public boolean isInRangeToRenderDist(double distance) {
return distance < 16384.0D; // 128 blocks squared
}
@Override
public boolean shouldRenderInPass(int pass) {
return pass == 1; // Translucent pass
}
@Override
public float getBrightness(float partialTicks) {
return 1.0f;
}
@Override
@SideOnly(Side.CLIENT)
public int getBrightnessForRender(float partialTicks) {
return 0xF000F0;
}
// ==================== GETTERS FOR RENDERER ====================
public float getBeamLength() {
return beamLength;
}
public float getBeamWidth() {
return beamWidth;
}
public float getBeamHeight() {
return beamHeight;
}
public int getInnerColor() {
return innerColor;
}
public int getOuterColor() {
return outerColor;
}
public boolean isOuterColorEnabled() {
return outerColorEnabled;
}
public float getOuterColorWidth() {
return outerColorWidth;
}
/**
* Get the full interpolated angle INCLUDING base yaw for rendering.
*/
public float getInterpolatedAngle(float partialTicks) {
// Handle wrap-around for the rotating part
float diff = currentAngle - prevAngle;
if (diff > 180) diff -= 360;
if (diff < -180) diff += 360;
float interpAngle = prevAngle + diff * partialTicks;
// Add base yaw for correct world orientation
return baseYaw + interpAngle;
}
public float getCurrentAngle() {
return currentAngle;
}
public float getBaseYaw() {
return baseYaw;
}
// ==================== NBT ====================
@Override
protected void readEntityFromNBT(NBTTagCompound nbt) {
this.beamLength = nbt.getFloat("BeamLength");
this.beamWidth = nbt.getFloat("BeamWidth");
this.beamHeight = nbt.getFloat("BeamHeight");
this.innerColor = nbt.getInteger("InnerColor");
this.outerColor = nbt.getInteger("OuterColor");
this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled");
this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f;
this.sweepSpeed = nbt.getFloat("SweepSpeed");
this.numberOfRotations = nbt.getInteger("NumRotations");
this.completedRotations = nbt.getInteger("CompletedRotations");
this.maxTicks = nbt.getInteger("MaxTicks");
this.ownerEntityId = nbt.getInteger("OwnerId");
this.targetEntityId = nbt.getInteger("TargetId");
this.currentAngle = nbt.getFloat("CurrentAngle");
this.baseYaw = nbt.getFloat("BaseYaw");
this.deathWorldTime = nbt.getLong("DeathWorldTime");
this.damage = nbt.getFloat("Damage");
this.damageInterval = nbt.getInteger("DamageInterval");
this.piercing = nbt.getBoolean("Piercing");
this.lockOnTarget = nbt.getBoolean("LockOnTarget");
}
@Override
protected void writeEntityToNBT(NBTTagCompound nbt) {
nbt.setFloat("BeamLength", beamLength);
nbt.setFloat("BeamWidth", beamWidth);
nbt.setFloat("BeamHeight", beamHeight);
nbt.setInteger("InnerColor", innerColor);
nbt.setInteger("OuterColor", outerColor);
nbt.setBoolean("OuterColorEnabled", outerColorEnabled);
nbt.setFloat("OuterColorWidth", outerColorWidth);
nbt.setFloat("SweepSpeed", sweepSpeed);
nbt.setInteger("NumRotations", numberOfRotations);
nbt.setInteger("CompletedRotations", completedRotations);
nbt.setInteger("MaxTicks", maxTicks);
nbt.setInteger("OwnerId", ownerEntityId);
nbt.setInteger("TargetId", targetEntityId);
nbt.setFloat("CurrentAngle", currentAngle);
nbt.setFloat("BaseYaw", baseYaw);
nbt.setLong("DeathWorldTime", deathWorldTime);
nbt.setFloat("Damage", damage);
nbt.setInteger("DamageInterval", damageInterval);
nbt.setBoolean("Piercing", piercing);
nbt.setBoolean("LockOnTarget", lockOnTarget);
}
// ==================== SPAWN DATA ====================
@Override
public void writeSpawnData(ByteBuf buffer) {
buffer.writeFloat(beamLength);
buffer.writeFloat(beamWidth);
buffer.writeFloat(beamHeight);
buffer.writeInt(innerColor);
buffer.writeInt(outerColor);
buffer.writeBoolean(outerColorEnabled);
buffer.writeFloat(outerColorWidth);
buffer.writeFloat(sweepSpeed);
buffer.writeInt(numberOfRotations);
buffer.writeInt(completedRotations);
buffer.writeInt(maxTicks);
buffer.writeInt(ownerEntityId);
buffer.writeInt(targetEntityId);
buffer.writeFloat(currentAngle);
buffer.writeFloat(baseYaw);
buffer.writeFloat(damage);
buffer.writeInt(damageInterval);
buffer.writeBoolean(piercing);
buffer.writeBoolean(lockOnTarget);
}
@Override
public void readSpawnData(ByteBuf buffer) {
this.beamLength = buffer.readFloat();
this.beamWidth = buffer.readFloat();
this.beamHeight = buffer.readFloat();
this.innerColor = buffer.readInt();
this.outerColor = buffer.readInt();
this.outerColorEnabled = buffer.readBoolean();
this.outerColorWidth = buffer.readFloat();
this.sweepSpeed = buffer.readFloat();
this.numberOfRotations = buffer.readInt();
this.completedRotations = buffer.readInt();
this.maxTicks = buffer.readInt();
this.ownerEntityId = buffer.readInt();
this.targetEntityId = buffer.readInt();
this.currentAngle = buffer.readFloat();
this.baseYaw = buffer.readFloat();
this.damage = buffer.readFloat();
this.damageInterval = buffer.readInt();
this.piercing = buffer.readBoolean();
this.lockOnTarget = buffer.readBoolean();
this.prevAngle = this.currentAngle;
}
}