mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 18:57:32 +00:00
508 lines
17 KiB
Java
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);
|
|
}
|
|
}
|