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

508 lines
17 KiB
Java

package kamkeel.npcs.entity;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import noppes.npcs.EventHooks;
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 java.util.List;
/**
* Disc projectile - flat spinning disc with optional boomerang behavior.
* Has a wider, thinner hitbox compared to Orb.
*
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityDisc extends EntityEnergyProjectile {
// Disc-specific movement propertie
// Boomerang properties
private boolean boomerang = false;
private int boomerangDelay = 40; // Ticks before returning
private boolean returning = false;
private int ticksSinceMiss = 0;
// Disc shape properties
private float discRadius = 1.0f; // Width of disc
private float discThickness = 0.2f; // Height of disc
private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward)
// Charging state (disc-specific target sizes)
private float targetDiscRadius = 1.0f; // Full radius to grow to during charging
private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging
public EntityAbilityDisc(World world) {
super(world);
}
/**
* Full constructor with all parameters using data classes.
*/
public EntityAbilityDisc(World world, EntityLivingBase owner, EntityLivingBase target,
double x, double y, double z,
float discRadius, float discThickness,
EnergyDisplayData display, EnergyCombatData combat,
EnergyHomingData homing, EnergyLightningData lightning,
EnergyLifespanData lifespan, EnergyTrajectoryData trajectory,
boolean boomerang, int boomerangDelay) {
super(world);
// Initialize base properties
initProjectile(owner, target, x, y, z, discRadius, display, combat, lightning, lifespan, trajectory);
this.homingData = homing;
this.boomerang = boomerang;
this.boomerangDelay = boomerangDelay;
this.discRadius = discRadius;
this.discThickness = discThickness;
// Calculate initial velocity toward target
calculateInitialVelocity(owner, target, x, y, z);
}
/**
* Setup this disc in charging mode (for windup phase).
* The disc will grow from 0 to discRadius over chargeDuration ticks.
* Position follows the owner based on anchor point.
*/
public void setupCharging(EnergyAnchorData anchor, int chargeDuration, boolean vertical) {
this.targetDiscRadius = this.discRadius;
this.targetDiscThickness = this.discThickness;
super.setupCharging(anchor, chargeDuration);
this.vertical = vertical;
this.discRadius = 0.01f;
this.discThickness = 0.01f;
}
/**
* Setup this disc in preview mode for GUI display.
* Follows anchor point and animations like in the real game.
* Can be fired when transitioning to active phase.
*/
public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, boolean vertical) {
this.setPreviewMode(true);
this.setPreviewOwner(owner);
// Set visual properties
this.displayData = display;
this.lightningData = lightning;
// Set charging state
this.setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.vertical = vertical;
// Store target size and start at 0 for grow effect
this.targetDiscRadius = discRadius;
this.targetDiscThickness = discThickness;
this.discRadius = 0.01f;
this.discThickness = 0.01f;
this.size = 0.01f;
// Initial position at anchor point
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData);
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;
// Clear motion
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
}
/**
* Start preview firing (simulates firing toward a point in front of NPC).
*/
public void startPreviewFiring() {
setCharging(false);
// Update start position to current position
startX = posX;
startY = posY;
startZ = posZ;
// Sync prev position to prevent visual jump on first frame
prevPosX = posX;
prevPosY = posY;
prevPosZ = posZ;
// Fire forward based on owner facing direction
Entity owner = getOwnerEntity();
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = 0; // Fire horizontally
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
} else {
motionX = getSpeed();
motionY = 0;
motionZ = 0;
}
}
/**
* Start the disc moving (exit charging mode).
* Called by ability when windup ends.
*/
public void startMoving(EntityLivingBase target) {
setCharging(false);
// For player casters, snap to look vector for crosshair-aligned launch
snapToPlayerLookVector();
// Update start position to current position
startX = posX;
startY = posY;
startZ = posZ;
// Calculate velocity toward target
Entity owner = getOwnerEntity();
if (target != null) {
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
double dz = target.posZ - posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
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);
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
}
@Override
protected void updateRotation() {
// Disc spins ONLY on Y axis (flat spin like a saw blade)
this.rotationValY += getRotationSpeed();
// No wobble - keep X and Z at 0 for stable flight
if (this.rotationValY > 360.0f) this.rotationValY -= 360.0f;
}
@Override
protected void updateProjectile() {
// Handle charging state (windup phase)
if (isCharging()) {
updateCharging();
return;
}
// In preview mode, run movement on client (no server)
if (previewMode) {
updatePreviewMovement();
return;
}
if (worldObj.isRemote) {
handleClientInterpolation();
} else {
// Server-side logic
if (returning) {
updateReturnToOwner();
} else {
updateHoming();
}
checkBlockCollision();
checkEntityCollision();
this.moveEntity(motionX, motionY, motionZ);
// Track ticks for boomerang delay (checkMaxDistance handles the actual return trigger)
if (boomerang && !returning && !hasHit) {
ticksSinceMiss++;
}
}
// Check wall collision
if (this.isCollidedHorizontally || this.isCollidedVertically) {
if (!worldObj.isRemote) {
hasHit = true;
if (isExplosive()) {
doExplosion();
}
}
this.setDead();
}
}
@Override
protected boolean checkMaxDistance() {
double distTraveled = Math.sqrt(
(posX - startX) * (posX - startX) +
(posY - startY) * (posY - startY) +
(posZ - startZ) * (posZ - startZ)
);
if (boomerang) {
// If not returning yet, check if we should start returning
if (!returning && !hasHit) {
if (distTraveled >= getMaxDistance() * 0.8 || ticksSinceMiss >= boomerangDelay) {
returning = true;
}
}
// If returning, only die when close to owner
if (returning) {
Entity owner = getOwnerEntity();
if (owner != null) {
double distToOwner = Math.sqrt(
(posX - owner.posX) * (posX - owner.posX) +
(posY - owner.posY) * (posY - owner.posY) +
(posZ - owner.posZ) * (posZ - owner.posZ)
);
return distToOwner < 1.5;
}
// Owner gone but returning - keep going toward last known position
return false;
}
// Not returning yet - don't die from distance (we'll trigger return above)
return false;
}
// Non-boomerang: standard max distance check
return distTraveled >= getMaxDistance();
}
/**
* Update movement in preview mode (client-side only, no damage).
*/
private void updatePreviewMovement() {
// Simple movement - just apply motion
this.posX += motionX;
this.posY += motionY;
this.posZ += motionZ;
}
private void updateReturnToOwner() {
Entity owner = getOwnerEntity();
// If owner is gone (unloaded/dead), head back toward start position
double targetX, targetY, targetZ;
if (owner != null) {
targetX = owner.posX;
targetY = owner.posY + owner.height * 0.5;
targetZ = owner.posZ;
} else {
// Return to start position if owner not available
targetX = startX;
targetY = startY;
targetZ = startZ;
}
double dx = targetX - posX;
double dy = targetY - posY;
double dz = targetZ - posZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist > 0) {
// Stronger homing when returning
double returnStrength = Math.min(1.0, getHomingStrength() * 2.5);
double desiredVX = (dx / dist) * getSpeed();
double desiredVY = (dy / dist) * getSpeed();
double desiredVZ = (dz / dist) * getSpeed();
motionX += (desiredVX - motionX) * returnStrength;
motionY += (desiredVY - motionY) * returnStrength;
motionZ += (desiredVZ - motionZ) * returnStrength;
double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ);
if (vLen > 0) {
motionX = (motionX / vLen) * getSpeed();
motionY = (motionY / vLen) * getSpeed();
motionZ = (motionZ / vLen) * getSpeed();
}
}
}
private void checkBlockCollision() {
Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ);
Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ);
// Use full raytrace that doesn't stop at liquids and checks all blocks
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
}
hasHit = true;
if (isExplosive()) {
posX = blockHit.hitVec.xCoord;
posY = blockHit.hitVec.yCoord;
posZ = blockHit.hitVec.zCoord;
doExplosion();
}
this.setDead();
}
}
private void checkEntityCollision() {
// Disc hitbox: wider but thinner
double halfWidth = discRadius * 0.5;
double halfHeight = discThickness * 0.5;
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
posX - halfWidth, posY - halfHeight, posZ - halfWidth,
posX + halfWidth, posY + halfHeight, posZ + halfWidth
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (EntityLivingBase entity : entities) {
if (shouldIgnoreEntity(entity)) continue;
hasHit = true;
if (isExplosive()) {
doExplosion();
} else {
applyDamage(entity);
}
this.setDead();
return;
}
}
/**
* Update during charging state - follow owner based on anchor point.
* Overrides base to grow disc-specific radius/thickness instead of just size.
*/
@Override
protected void updateCharging() {
chargeTick++;
// Grow size based on charge progress
float progress = getChargeProgress();
this.discRadius = targetDiscRadius * progress;
this.discThickness = targetDiscThickness * progress;
this.size = this.discRadius; // Base size for interpolation
Entity owner = getOwnerEntity();
if (owner instanceof EntityLivingBase) {
Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData);
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
}
// Update rotation during charging (disc still spins)
updateRotation();
}
// ==================== GETTERS ====================
public float getDiscRadius() {
return discRadius;
}
public void setDiscRadius(float radius) {
this.discRadius = radius;
}
public float getDiscThickness() {
return discThickness;
}
public void setDiscThickness(float thickness) {
this.discThickness = thickness;
}
public boolean isBoomerang() {
return boomerang;
}
public void setBoomerang(boolean boomerang) {
this.boomerang = boomerang;
}
public int getBoomerangDelay() {
return boomerangDelay;
}
public void setBoomerangDelay(int delay) {
this.boomerangDelay = delay;
}
public boolean isReturning() {
return returning;
}
public boolean isVertical() {
return vertical;
}
public void setVertical(boolean vertical) {
this.vertical = vertical;
}
/**
* Get the yaw angle (degrees) from motion vector for vertical disc orientation.
* During charging, uses owner's facing direction.
*/
public float getTravelYaw() {
if (isCharging()) {
Entity owner = previewMode ? previewOwner : getOwnerEntity();
if (owner != null) {
return owner.rotationYaw;
}
}
double speed = Math.sqrt(motionX * motionX + motionZ * motionZ);
if (speed > 0.001) {
return (float) (Math.atan2(-motionX, motionZ) * 180.0 / Math.PI);
}
return 0.0f;
}
// ==================== NBT ====================
@Override
protected void readProjectileNBT(NBTTagCompound nbt) {
this.boomerang = nbt.hasKey("Boomerang") && nbt.getBoolean("Boomerang");
this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40;
this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f;
this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f;
this.vertical = nbt.hasKey("Vertical") ? nbt.getBoolean("Vertical") : false;
this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning");
// Charging state (common fields handled by base)
readChargingNBT(nbt);
this.targetDiscRadius = nbt.hasKey("TargetDiscRadius") ? nbt.getFloat("TargetDiscRadius") : this.discRadius;
this.targetDiscThickness = nbt.hasKey("TargetDiscThickness") ? nbt.getFloat("TargetDiscThickness") : this.discThickness;
}
@Override
protected void writeProjectileNBT(NBTTagCompound nbt) {
nbt.setBoolean("Boomerang", boomerang);
nbt.setInteger("BoomerangDelay", boomerangDelay);
nbt.setFloat("DiscRadius", discRadius);
nbt.setFloat("DiscThickness", discThickness);
nbt.setBoolean("Vertical", vertical);
nbt.setBoolean("Returning", returning);
// Charging state (common fields handled by base)
writeChargingNBT(nbt);
nbt.setFloat("TargetDiscRadius", targetDiscRadius);
nbt.setFloat("TargetDiscThickness", targetDiscThickness);
}
}