2026-01-25 20:39:01 -05:00
|
|
|
package kamkeel.npcs.entity;
|
|
|
|
|
|
2026-01-29 20:34:10 -03:00
|
|
|
import kamkeel.npcs.controllers.data.ability.data.*;
|
2026-01-26 23:55:07 -05:00
|
|
|
import kamkeel.npcs.util.AnchorPointHelper;
|
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;
|
|
|
|
|
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Orb projectile - spherical homing energy ball.
|
|
|
|
|
* Extends EntityAbilityProjectile for shared functionality.
|
|
|
|
|
*
|
|
|
|
|
* Design inspired by LouisXIV's energy attack system.
|
|
|
|
|
*/
|
|
|
|
|
public class EntityAbilityOrb extends EntityAbilityProjectile {
|
|
|
|
|
|
2026-01-26 23:55:07 -05:00
|
|
|
// Charging state (during windup)
|
|
|
|
|
private boolean charging = false;
|
|
|
|
|
private int chargeDuration = 40;
|
|
|
|
|
private int chargeTick = 0;
|
|
|
|
|
private float targetSize = 1.0f; // Full size to grow to during charging
|
|
|
|
|
|
|
|
|
|
// Data watcher index for charging state (synced to clients)
|
|
|
|
|
private static final int DW_CHARGING = 20;
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
public EntityAbilityOrb(World world) {
|
|
|
|
|
super(world);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 23:55:07 -05:00
|
|
|
@Override
|
|
|
|
|
protected void entityInit() {
|
|
|
|
|
super.entityInit();
|
|
|
|
|
// Register data watcher for charging state
|
|
|
|
|
this.dataWatcher.addObject(DW_CHARGING, (byte) 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if orb is in charging state (synced via data watcher).
|
2026-01-28 20:43:32 -05:00
|
|
|
* In preview mode, uses local field since data watcher isn't synced.
|
2026-01-26 23:55:07 -05:00
|
|
|
*/
|
|
|
|
|
public boolean isCharging() {
|
2026-01-28 20:43:32 -05:00
|
|
|
if (previewMode) {
|
|
|
|
|
return this.charging;
|
|
|
|
|
}
|
2026-01-26 23:55:07 -05:00
|
|
|
return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set charging state (server only, synced to clients via data watcher).
|
|
|
|
|
*/
|
|
|
|
|
private void setCharging(boolean value) {
|
|
|
|
|
this.charging = value;
|
|
|
|
|
if (!worldObj.isRemote) {
|
|
|
|
|
this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
/**
|
2026-01-29 14:19:57 -05:00
|
|
|
* Full constructor using data classes for grouped parameters.
|
2026-01-25 20:39:01 -05:00
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
public EntityAbilityOrb(World world, EntityLivingBase owner, EntityLivingBase target,
|
2026-01-29 14:19:57 -05:00
|
|
|
double x, double y, double z, float orbSize,
|
2026-02-03 17:20:36 -03:00
|
|
|
EnergyDisplayData display, EnergyCombatData combat,
|
|
|
|
|
EnergyHomingData homing, EnergyLightningData lightning,
|
2026-01-29 14:19:57 -05:00
|
|
|
EnergyLifespanData lifespan) {
|
2026-01-25 20:39:01 -05:00
|
|
|
super(world);
|
|
|
|
|
|
2026-01-29 14:19:57 -05:00
|
|
|
// Initialize base properties
|
2026-02-03 17:20:36 -03:00
|
|
|
initProjectile(owner, target, x, y, z, orbSize, display, combat, lightning, lifespan);
|
2026-01-25 20:39:01 -05:00
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
this.homingData = homing;
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
// Calculate initial velocity toward target
|
|
|
|
|
if (target != null) {
|
|
|
|
|
double dx = target.posX - x;
|
2026-01-29 14:19:57 -05:00
|
|
|
double dy = (target.posY + target.getEyeHeight()) - 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) {
|
2026-01-29 14:19:57 -05:00
|
|
|
this.motionX = (dx / len) * homing.speed;
|
|
|
|
|
this.motionY = (dy / len) * homing.speed;
|
|
|
|
|
this.motionZ = (dz / len) * homing.speed;
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Fire in NPC's facing direction
|
|
|
|
|
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
|
|
|
|
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
2026-01-29 14:19:57 -05:00
|
|
|
this.motionX = -Math.sin(yaw) * Math.cos(pitch) * homing.speed;
|
|
|
|
|
this.motionY = -Math.sin(pitch) * homing.speed;
|
|
|
|
|
this.motionZ = Math.cos(yaw) * Math.cos(pitch) * homing.speed;
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 23:55:07 -05:00
|
|
|
/**
|
2026-01-29 14:19:57 -05:00
|
|
|
* Setup this orb in charging mode (for windup phase).
|
2026-01-26 23:55:07 -05:00
|
|
|
* The orb will grow from 0 to orbSize over chargeDuration ticks.
|
|
|
|
|
* Position follows the owner based on anchor point.
|
|
|
|
|
*/
|
2026-01-29 20:34:10 -03:00
|
|
|
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
|
2026-01-29 14:19:57 -05:00
|
|
|
setCharging(true);
|
|
|
|
|
this.chargeDuration = chargeDuration;
|
|
|
|
|
this.chargeTick = 0;
|
2026-01-29 20:34:10 -03:00
|
|
|
this.anchorData = anchor;
|
2026-01-29 14:19:57 -05:00
|
|
|
this.targetSize = this.size;
|
|
|
|
|
this.size = 0.01f;
|
|
|
|
|
this.renderCurrentSize = 0.01f;
|
|
|
|
|
this.prevRenderSize = 0.01f;
|
|
|
|
|
this.motionX = 0;
|
|
|
|
|
this.motionY = 0;
|
|
|
|
|
this.motionZ = 0;
|
2026-01-26 23:55:07 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
/**
|
2026-01-29 14:19:57 -05:00
|
|
|
* Setup this orb in preview mode for GUI display.
|
2026-01-28 20:43:32 -05:00
|
|
|
* Follows anchor point and animations like in the real game.
|
|
|
|
|
* Can be fired when transitioning to active phase.
|
|
|
|
|
*/
|
2026-02-03 17:20:36 -03:00
|
|
|
public void setupPreview(EntityLivingBase owner, float orbSize, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) {
|
2026-01-29 14:19:57 -05:00
|
|
|
this.setPreviewMode(true);
|
|
|
|
|
this.setPreviewOwner(owner);
|
2026-01-28 20:43:32 -05:00
|
|
|
|
|
|
|
|
// Set visual properties
|
2026-02-03 17:20:36 -03:00
|
|
|
this.displayData = display;
|
|
|
|
|
this.lightningData = lightning;
|
2026-01-29 14:19:57 -05:00
|
|
|
|
|
|
|
|
// Set charging state
|
|
|
|
|
this.setCharging(true);
|
|
|
|
|
this.chargeDuration = chargeDuration;
|
|
|
|
|
this.chargeTick = 0;
|
2026-01-29 20:34:10 -03:00
|
|
|
this.anchorData = anchor;
|
2026-01-28 20:43:32 -05:00
|
|
|
|
|
|
|
|
// Store target size and start at 0 for grow effect
|
2026-01-29 14:19:57 -05:00
|
|
|
this.targetSize = orbSize;
|
|
|
|
|
this.size = 0.01f;
|
|
|
|
|
this.renderCurrentSize = 0.01f;
|
|
|
|
|
this.prevRenderSize = 0.01f;
|
2026-01-28 20:43:32 -05:00
|
|
|
|
|
|
|
|
// Initial position at anchor point
|
2026-01-29 20:34:10 -03:00
|
|
|
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData);
|
2026-01-29 14:19:57 -05:00
|
|
|
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;
|
2026-01-28 20:43:32 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start preview firing (simulates firing toward a point in front of NPC).
|
|
|
|
|
*/
|
|
|
|
|
public void startPreviewFiring() {
|
|
|
|
|
if (!isCharging()) return;
|
|
|
|
|
|
|
|
|
|
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
|
2026-02-03 17:20:36 -03:00
|
|
|
Entity owner = getOwnerEntity();
|
2026-01-28 20:43:32 -05:00
|
|
|
if (owner != null) {
|
|
|
|
|
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
|
|
|
|
float pitch = (float) Math.toRadians(0); // Fire horizontally
|
2026-02-03 17:20:36 -03:00
|
|
|
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
|
|
|
|
|
motionY = -Math.sin(pitch) * getSpeed();
|
|
|
|
|
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
|
2026-01-28 20:43:32 -05:00
|
|
|
} else {
|
|
|
|
|
// Default: fire forward (positive X in model space)
|
2026-02-03 17:20:36 -03:00
|
|
|
motionX = getSpeed();
|
2026-01-28 20:43:32 -05:00
|
|
|
motionY = 0;
|
|
|
|
|
motionZ = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 23:55:07 -05:00
|
|
|
/**
|
|
|
|
|
* Start the orb moving (exit charging mode).
|
|
|
|
|
* Called by ability when windup ends.
|
|
|
|
|
*/
|
|
|
|
|
public void startMoving(EntityLivingBase target) {
|
|
|
|
|
if (!isCharging()) return;
|
|
|
|
|
|
|
|
|
|
setCharging(false);
|
|
|
|
|
|
|
|
|
|
// Update start position to current position
|
|
|
|
|
startX = posX;
|
|
|
|
|
startY = posY;
|
|
|
|
|
startZ = posZ;
|
|
|
|
|
|
|
|
|
|
// Calculate velocity toward target
|
2026-02-03 17:20:36 -03:00
|
|
|
Entity owner = getOwnerEntity();
|
2026-01-26 23:55:07 -05:00
|
|
|
|
|
|
|
|
if (target != null) {
|
|
|
|
|
double dx = target.posX - posX;
|
2026-01-29 14:19:57 -05:00
|
|
|
double dy = (target.posY + target.getEyeHeight()) - posY;
|
2026-01-26 23:55:07 -05:00
|
|
|
double dz = target.posZ - posZ;
|
|
|
|
|
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
|
if (len > 0) {
|
2026-02-03 17:20:36 -03:00
|
|
|
motionX = (dx / len) * getSpeed();
|
|
|
|
|
motionY = (dy / len) * getSpeed();
|
|
|
|
|
motionZ = (dz / len) * getSpeed();
|
2026-01-26 23:55:07 -05:00
|
|
|
}
|
|
|
|
|
} else if (owner != null) {
|
|
|
|
|
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
|
|
|
|
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
2026-02-03 17:20:36 -03:00
|
|
|
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
|
|
|
|
|
motionY = -Math.sin(pitch) * getSpeed();
|
|
|
|
|
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
|
2026-01-26 23:55:07 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
@Override
|
|
|
|
|
protected void updateProjectile() {
|
2026-01-26 23:55:07 -05:00
|
|
|
// Handle charging state (windup phase)
|
|
|
|
|
if (isCharging()) {
|
|
|
|
|
updateCharging();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
// In preview mode, run movement on client (no server)
|
|
|
|
|
if (previewMode) {
|
|
|
|
|
updatePreviewMovement();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
if (worldObj.isRemote) {
|
|
|
|
|
handleClientInterpolation();
|
|
|
|
|
} else {
|
|
|
|
|
// Server-side logic
|
|
|
|
|
updateHoming();
|
|
|
|
|
checkBlockCollision();
|
|
|
|
|
checkEntityCollision();
|
|
|
|
|
this.moveEntity(motionX, motionY, motionZ);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check wall collision
|
|
|
|
|
if (this.isCollidedHorizontally || this.isCollidedVertically) {
|
|
|
|
|
if (!worldObj.isRemote) {
|
|
|
|
|
hasHit = true;
|
2026-02-03 17:20:36 -03:00
|
|
|
if (isExplosive()) {
|
2026-01-25 20:39:01 -05:00
|
|
|
doExplosion();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.setDead();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-28 20:43:32 -05:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
private void updateHoming() {
|
2026-02-03 17:20:36 -03:00
|
|
|
if (!isHoming()) return;
|
2026-01-25 20:39:01 -05:00
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
Entity target = getTargetEntity();
|
2026-01-25 20:39:01 -05:00
|
|
|
if (target == null || !target.isEntityAlive()) return;
|
|
|
|
|
|
|
|
|
|
double dx = target.posX - posX;
|
2026-01-29 14:19:57 -05:00
|
|
|
double dy = (target.posY + target.getEyeHeight()) - posY;
|
2026-01-25 20:39:01 -05:00
|
|
|
double dz = target.posZ - posZ;
|
|
|
|
|
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
if (dist <= getHomingRange() && dist > 0) {
|
2026-01-25 20:39:01 -05:00
|
|
|
// Calculate effective homing strength - increases when closer to commit to target
|
|
|
|
|
// This prevents orbiting behavior at close range
|
2026-02-03 17:20:36 -03:00
|
|
|
float effectiveStrength = getHomingStrength();
|
|
|
|
|
if (dist < getHomingRange() * 0.3) {
|
2026-01-25 20:39:01 -05:00
|
|
|
// Within 30% of homing range, dramatically increase strength to commit
|
2026-02-03 17:20:36 -03:00
|
|
|
effectiveStrength = Math.min(1.0f, getHomingStrength() * 2.5f);
|
|
|
|
|
} else if (dist < getHomingRange() * 0.6) {
|
2026-01-25 20:39:01 -05:00
|
|
|
// Within 60% of homing range, moderately increase strength
|
2026-02-03 17:20:36 -03:00
|
|
|
effectiveStrength = Math.min(0.8f, getHomingStrength() * 1.5f);
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
double desiredVX = (dx / dist) * getSpeed();
|
|
|
|
|
double desiredVY = (dy / dist) * getSpeed();
|
|
|
|
|
double desiredVZ = (dz / dist) * getSpeed();
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
motionX += (desiredVX - motionX) * effectiveStrength;
|
|
|
|
|
motionY += (desiredVY - motionY) * effectiveStrength;
|
|
|
|
|
motionZ += (desiredVZ - motionZ) * effectiveStrength;
|
|
|
|
|
|
|
|
|
|
// Normalize to maintain speed
|
|
|
|
|
double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ);
|
|
|
|
|
if (vLen > 0) {
|
2026-02-03 17:20:36 -03:00
|
|
|
motionX = (motionX / vLen) * getSpeed();
|
|
|
|
|
motionY = (motionY / vLen) * getSpeed();
|
|
|
|
|
motionZ = (motionZ / vLen) * getSpeed();
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void checkBlockCollision() {
|
|
|
|
|
Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ);
|
|
|
|
|
Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ);
|
2026-01-28 17:16:59 -05:00
|
|
|
// Use full raytrace that doesn't stop at liquids and checks all blocks
|
|
|
|
|
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
|
2026-01-25 20:39:01 -05:00
|
|
|
|
|
|
|
|
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
|
|
|
|
|
hasHit = true;
|
2026-02-03 17:20:36 -03:00
|
|
|
if (isExplosive()) {
|
2026-01-28 17:16:59 -05:00
|
|
|
posX = blockHit.hitVec.xCoord;
|
|
|
|
|
posY = blockHit.hitVec.yCoord;
|
|
|
|
|
posZ = blockHit.hitVec.zCoord;
|
2026-01-25 20:39:01 -05:00
|
|
|
doExplosion();
|
|
|
|
|
}
|
|
|
|
|
this.setDead();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void checkEntityCollision() {
|
|
|
|
|
double hitSize = size * 0.5;
|
|
|
|
|
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
|
|
|
|
|
posX - hitSize, posY - hitSize, posZ - hitSize,
|
|
|
|
|
posX + hitSize, posY + hitSize, posZ + hitSize
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@SuppressWarnings("unchecked")
|
|
|
|
|
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
|
|
|
|
|
|
|
|
|
|
for (EntityLivingBase entity : entities) {
|
|
|
|
|
if (shouldIgnoreEntity(entity)) continue;
|
|
|
|
|
|
|
|
|
|
hasHit = true;
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
if (isExplosive()) {
|
2026-01-25 20:39:01 -05:00
|
|
|
doExplosion();
|
|
|
|
|
} else {
|
|
|
|
|
applyDamage(entity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setDead();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 23:55:07 -05:00
|
|
|
/**
|
|
|
|
|
* Update during charging state - follow owner based on anchor point.
|
|
|
|
|
*/
|
|
|
|
|
private void updateCharging() {
|
|
|
|
|
chargeTick++;
|
|
|
|
|
|
2026-02-03 17:20:36 -03:00
|
|
|
Entity owner = getOwnerEntity();
|
2026-01-26 23:55:07 -05:00
|
|
|
if (owner == null || owner.isDead) {
|
|
|
|
|
setDead();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Grow size based on charge progress
|
|
|
|
|
float progress = getChargeProgress();
|
|
|
|
|
this.size = targetSize * progress;
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// Calculate position based on anchor point, offset downward by half size to center
|
2026-01-26 23:55:07 -05:00
|
|
|
if (owner instanceof EntityLivingBase) {
|
2026-01-29 20:34:10 -03:00
|
|
|
Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData);
|
2026-01-26 23:55:07 -05:00
|
|
|
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the charge progress (0-1).
|
|
|
|
|
*/
|
|
|
|
|
public float getChargeProgress() {
|
|
|
|
|
if (chargeDuration <= 0) return 1.0f;
|
|
|
|
|
return Math.min(1.0f, (float) chargeTick / chargeDuration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get interpolated charge progress for smooth rendering.
|
|
|
|
|
*/
|
|
|
|
|
public float getInterpolatedChargeProgress(float partialTicks) {
|
|
|
|
|
if (chargeDuration <= 0) return 1.0f;
|
|
|
|
|
float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration);
|
|
|
|
|
float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration);
|
|
|
|
|
return prevProgress + (currProgress - prevProgress) * partialTicks;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 20:39:01 -05:00
|
|
|
// ==================== GETTERS ====================
|
|
|
|
|
|
|
|
|
|
// Legacy getter for renderer compatibility
|
|
|
|
|
public float getOrbSize() {
|
|
|
|
|
return size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ==================== NBT ====================
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void readProjectileNBT(NBTTagCompound nbt) {
|
2026-01-26 23:55:07 -05:00
|
|
|
// Charging state
|
|
|
|
|
boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
|
|
|
|
|
this.charging = isCharging;
|
|
|
|
|
this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0));
|
|
|
|
|
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
|
|
|
|
|
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
|
|
|
|
|
this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size;
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void writeProjectileNBT(NBTTagCompound nbt) {
|
2026-01-26 23:55:07 -05:00
|
|
|
// Charging state
|
|
|
|
|
nbt.setBoolean("Charging", isCharging());
|
|
|
|
|
nbt.setInteger("ChargeDuration", chargeDuration);
|
|
|
|
|
nbt.setInteger("ChargeTick", chargeTick);
|
2026-01-29 20:34:10 -03:00
|
|
|
anchorData.writeNBT(nbt);
|
2026-01-26 23:55:07 -05:00
|
|
|
nbt.setFloat("TargetSize", targetSize);
|
2026-01-25 20:39:01 -05:00
|
|
|
}
|
|
|
|
|
}
|