From 2e5c4c3bf41eab22c1bd9bb2e3ec59bef74c886a Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 05:04:19 -0500 Subject: [PATCH] Fix Preview Handling for Abilities, Fix Disc Positioning in Real World --- .../controllers/data/ability/Ability.java | 112 ++++------ .../data/ability/PreviewEntityHandler.java | 13 ++ .../data/ability/type/AbilityBeam.java | 35 ++- .../data/ability/type/AbilityBeamDual.java | 45 ++-- .../data/ability/type/AbilityCharge.java | 83 ++----- .../data/ability/type/AbilityCutter.java | 20 +- .../data/ability/type/AbilityDash.java | 85 ++------ .../data/ability/type/AbilityDisc.java | 37 ++-- .../data/ability/type/AbilityDiscDual.java | 51 +++-- .../data/ability/type/AbilityHazard.java | 92 ++++---- .../data/ability/type/AbilityHeal.java | 38 ++-- .../data/ability/type/AbilityHeavyHit.java | 68 +++--- .../data/ability/type/AbilityLaserShot.java | 31 +-- .../data/ability/type/AbilityOrb.java | 32 ++- .../data/ability/type/AbilityOrbBarrage.java | 26 +-- .../data/ability/type/AbilityOrbDual.java | 42 ++-- .../data/ability/type/AbilityProjectile.java | 89 ++++---- .../data/ability/type/AbilityShockwave.java | 70 +++--- .../data/ability/type/AbilitySlam.java | 140 +++--------- .../data/ability/type/AbilitySweeper.java | 14 +- .../data/ability/type/AbilityTeleport.java | 63 ++++-- .../data/ability/type/AbilityTrap.java | 34 +-- .../data/ability/type/AbilityVortex.java | 48 +++-- .../npcs/entity/EntityAbilityBeam.java | 25 ++- .../npcs/entity/EntityAbilityDisc.java | 1 - .../npcs/entity/EntityAbilityLaser.java | 4 - .../npcs/entity/EntityAbilitySweeper.java | 13 ++ .../java/noppes/npcs/client/ClientProxy.java | 2 + .../gui/util/AbilityPreviewExecutor.java | 202 +++++++++++------- 29 files changed, 714 insertions(+), 801 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 03faf42d5..e6404a0cb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -110,6 +110,13 @@ public abstract class Ability implements IAbility { protected transient long executionStartTime; protected transient TelegraphInstance telegraphInstance; + // ═══════════════════════════════════════════════════════════════════ + // PREVIEW STATE (client-side only, not saved) + // ═══════════════════════════════════════════════════════════════════ + + protected transient boolean previewMode = false; + protected transient PreviewEntityHandler previewEntityHandler; + // ═══════════════════════════════════════════════════════════════════ // ABSTRACT METHODS // ═══════════════════════════════════════════════════════════════════ @@ -837,6 +844,8 @@ public abstract class Ability implements IAbility { currentTick = 0; currentTarget = null; telegraphInstance = null; + previewMode = false; + previewEntityHandler = null; } /** @@ -849,90 +858,57 @@ public abstract class Ability implements IAbility { } // ═══════════════════════════════════════════════════════════════════ - // CLIENT-SIDE PREVIEW METHODS + // PREVIEW MODE SUPPORT // ═══════════════════════════════════════════════════════════════════ /** - * Get total active phase duration for preview (in ticks). - * Override in subclasses to provide accurate preview duration. - * - * @return Duration of active phase in ticks (default: 40 = 2 seconds) + * Whether this ability is running in preview mode (GUI preview). + * When true, abilities should skip damage, effects, sounds, and particles + * but still run their core logic (entity spawning, movement, timing). */ - public int getPreviewActiveDuration() { - return 40; + public boolean isPreview() { + return previewMode; + } + + public void setPreviewMode(boolean preview) { + this.previewMode = preview; + } + + public void setPreviewEntityHandler(PreviewEntityHandler handler) { + this.previewEntityHandler = handler; } /** - * Called during preview windup tick (client-side only). - * Override for visual updates during windup preview. - * Do NOT apply damage or world effects here. - * - * @param npc The preview NPC - * @param tick Current tick within windup phase + * Spawn an entity during ability execution. + * In preview mode, routes to PreviewEntityHandler instead of world.spawnEntityInWorld(). */ - @SideOnly(Side.CLIENT) - public void onPreviewWindUpTick(EntityNPCInterface npc, int tick) { - // Default: no-op. Override in subclasses for visual effects. + protected void spawnAbilityEntity(World world, Entity entity) { + if (previewMode && previewEntityHandler != null) { + previewEntityHandler.onEntitySpawned(entity); + } else { + world.spawnEntityInWorld(entity); + } } /** - * Called during preview active tick (client-side only). - * Override for visual updates during active preview. - * Do NOT apply damage or world effects here. - * - * @param npc The preview NPC - * @param tick Current tick within active phase + * Kill an entity spawned by this ability. + * In preview mode, notifies PreviewEntityHandler of removal. */ - @SideOnly(Side.CLIENT) - public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { - // Default: no-op. Override in subclasses for visual effects. + protected void killAbilityEntity(Entity entity) { + if (entity == null) return; + entity.setDead(); + if (previewMode && previewEntityHandler != null) { + previewEntityHandler.onEntityRemoved(entity); + } } /** - * Create a preview entity for GUI display (client-side only). - * The entity should be in preview mode and not apply damage. - * Override in abilities that spawn entities (Beam, Orb, Disc, etc.) - * - * @param npc The preview NPC - * @return Preview entity, or null if ability doesn't spawn entities + * Maximum duration (in ticks) for preview before auto-stopping. + * Override in movement abilities that have variable duration. + * Default: 200 ticks (10 seconds). */ - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - return null; // Override in entity-spawning abilities - } - - /** - * Whether to spawn preview entity during WINDUP phase (true) or ACTIVE phase (false). - * Most abilities (Orb, Beam, Disc) spawn during windup for charging effect. - * Laser spawns at active phase since it has no charging state. - */ - public boolean spawnPreviewDuringWindup() { - return true; // Default: spawn during windup for charging - } - - // ── Preview target (set by executor for abilities that need targeting) ── - - protected transient EntityLivingBase previewTarget; - - /** - * Set the fake target entity for preview mode. - * Called by AbilityPreviewExecutor before starting preview. - */ - public void setPreviewTarget(EntityLivingBase target) { - this.previewTarget = target; - } - - public EntityLivingBase getPreviewTarget() { - return previewTarget; - } - - /** - * Called once when transitioning from WINDUP to ACTIVE in preview mode. - * Override in movement-based abilities to initiate movement. - */ - @SideOnly(Side.CLIENT) - public void onPreviewExecute(EntityNPCInterface npc) { - // Default: no-op. Override in movement abilities. + public int getMaxPreviewDuration() { + return 200; } // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java b/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java new file mode 100644 index 000000000..77fae9774 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java @@ -0,0 +1,13 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.entity.Entity; + +/** + * Interface for routing entity spawn/kill calls during preview mode. + * In preview mode, entities are not added to the world but instead + * tracked by the GUI for rendering in the 3D viewport. + */ +public interface PreviewEntityHandler { + void onEntitySpawned(Entity entity); + void onEntityRemoved(Entity entity); +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 5157eea01..8e3bfc575 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -76,29 +76,29 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start firing the beam that was spawned during windup if (beamEntity != null && !beamEntity.isDead) { - beamEntity.startFiring(target); + if (isPreview()) { + beamEntity.startPreviewFiring(); + } else { + beamEntity.startFiring(target); + } } - - // Ability stays active until entity dies (prevents firing another while projectile is alive) - // Movement locking is handled separately by the base class } @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn beam in charging mode on first tick of windup if (tick == 1) { float offsetDist = 1.0f; - // Create beam in charging mode - follows caster based on anchor point during windup Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist); beamEntity = new EntityAbilityBeam( world, caster, target, @@ -106,11 +106,16 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { beamWidth, headSize, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData, lockMovement.locksActive()); - beamEntity.setupCharging(anchorData, windUpTicks, offsetDist); + + if (isPreview()) { + beamEntity.setupPreview(caster, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, offsetDist); + } else { + beamEntity.setupCharging(anchorData, windUpTicks, offsetDist); + } beamEntity.setEffects(this.effects); beamEntity.setSourceAbility(this); - world.spawnEntityInWorld(beamEntity); + spawnAbilityEntity(world, beamEntity); } } @@ -267,17 +272,7 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); - beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, 1.0f); - return beam; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java index 762145fc4..89c5b126f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java @@ -81,37 +81,41 @@ public class AbilityBeamDual extends Ability { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start firing the beam that was spawned during windup if (beamEntity != null && !beamEntity.isDead) { - beamEntity.startFiring(target); + if (isPreview()) { + beamEntity.startPreviewFiring(); + } else { + beamEntity.startFiring(target); + } } if (dualFire) { if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) { - beamEntity2.startFiring(target); + if (isPreview()) { + beamEntity2.startPreviewFiring(); + } else { + beamEntity2.startFiring(target); + } } } else if (beamEntity2 != null) { beamEntity2.setDead(); } - - // Ability stays active until entity dies (prevents firing another while projectile is alive) - // Movement locking is handled separately by the base class } @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn beam in charging mode on first tick of windup if (tick == 1) { float offsetDist = 1.0f; - // Create beam in charging mode - follows caster based on anchor point during windup Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist); beamEntity = new EntityAbilityBeam( world, caster, target, @@ -131,16 +135,21 @@ public class AbilityBeamDual extends Ability { beamEntity.setSiblingEntityId(beamEntity2.getEntityId()); beamEntity2.setSiblingEntityId(beamEntity.getEntityId()); - beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist); - beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist); + if (isPreview()) { + beamEntity.setupPreview(caster, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, offsetDist); + beamEntity2.setupPreview(caster, beamWidth, headSize, displayData[1], lightningData[1], anchorData[1], windUpTicks, offsetDist); + } else { + beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist); + beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist); + } beamEntity.setEffects(this.effects); if (dualFire) beamEntity2.setEffects(this.effects); beamEntity.setSourceAbility(this); beamEntity2.setSourceAbility(this); - world.spawnEntityInWorld(beamEntity); - world.spawnEntityInWorld(beamEntity2); + spawnAbilityEntity(world, beamEntity); + spawnAbilityEntity(world, beamEntity2); } } @@ -501,17 +510,7 @@ public class AbilityBeamDual extends Ability { } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); - beam.setupPreview(npc, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, 1.0f); - return beam; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index e09ff7e11..8e179dc73 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -138,7 +138,9 @@ public class AbilityCharge extends Ability implements IAbilityCharge { if (chargeDirection == null) return; // Enforce rotation every tick - enforceLockedRotation(caster); + if (!isPreview()) { + enforceLockedRotation(caster); + } // Calculate distance traveled double distanceTraveled = Math.sqrt( @@ -150,12 +152,15 @@ public class AbilityCharge extends Ability implements IAbilityCharge { if (distanceTraveled >= maxRange) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } signalCompletion(); return; } - if (isChargeBlocked(caster)) { + // Block detection (skip in preview - no real world collision) + if (!isPreview() && isChargeBlocked(caster)) { stopMomentum(caster); signalCompletion(); return; @@ -165,10 +170,12 @@ public class AbilityCharge extends Ability implements IAbilityCharge { caster.motionX = chargeDirection.xCoord * chargeSpeed; caster.motionY = 0; caster.motionZ = chargeDirection.zCoord * chargeSpeed; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } - // Server-side collision damage - if (!world.isRemote) { + // Server-side collision damage (skip in preview) + if (!world.isRemote && !isPreview()) { AxisAlignedBB hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); @SuppressWarnings("unchecked") @@ -211,7 +218,9 @@ public class AbilityCharge extends Ability implements IAbilityCharge { private void stopMomentum(EntityLivingBase caster) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } } private boolean isChargeBlocked(EntityLivingBase caster) { @@ -277,66 +286,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge { return instance; } - // ==================== PREVIEW MODE ==================== - - private transient double previewDirX, previewDirZ; - private transient double previewStartX, previewStartZ; - private transient boolean previewCharging = false; - @Override - @SideOnly(Side.CLIENT) - public void onPreviewExecute(EntityNPCInterface npc) { - previewStartX = npc.posX; - previewStartZ = npc.posZ; - previewCharging = false; - - // Calculate direction toward fake target - if (previewTarget != null) { - double dx = previewTarget.posX - npc.posX; - double dz = previewTarget.posZ - npc.posZ; - double len = Math.sqrt(dx * dx + dz * dz); - if (len > 0) { - previewDirX = dx / len; - previewDirZ = dz / len; - } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); - previewDirX = -Math.sin(yaw); - previewDirZ = Math.cos(yaw); - } - } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); - previewDirX = -Math.sin(yaw); - previewDirZ = Math.cos(yaw); - } - previewCharging = true; - } - - @Override - @SideOnly(Side.CLIENT) - public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { - if (!previewCharging) return; - - // Check distance traveled - double distTraveled = Math.sqrt( - Math.pow(npc.posX - previewStartX, 2) + - Math.pow(npc.posZ - previewStartZ, 2) - ); - - if (distTraveled >= maxRange) { - previewCharging = false; - return; - } - - npc.prevPosX = npc.posX; - npc.prevPosY = npc.posY; - npc.prevPosZ = npc.posZ; - - npc.posX += previewDirX * chargeSpeed; - npc.posZ += previewDirZ * chargeSpeed; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return (int) Math.ceil(maxRange / chargeSpeed) + 5; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 405dac612..22831f4ff 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -109,31 +109,35 @@ public class AbilityCutter extends Ability implements IAbilityCutter { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; switch (sweepMode) { case SWIPE: float prevRotation = currentRotation; currentRotation += sweepSpeed; if (currentRotation > arcAngle / 2.0f) { - // Final sweep — check from prevRotation to end of arc - performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f); + if (!isPreview()) { + performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f); + } signalCompletion(); return; } - // Check entities between previous and current sweep position - performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation); + if (!isPreview()) { + performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation); + } break; case SPIN: if (tick >= spinDurationTicks) { - signalCompletion(); // Spin duration complete + signalCompletion(); return; } float prevSpin = currentRotation; currentRotation = (currentRotation + sweepSpeed) % 360.0f; - hitEntities.clear(); - performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation); + if (!isPreview()) { + hitEntities.clear(); + performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation); + } break; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 1a1e76aa3..f80f336aa 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -172,7 +172,7 @@ public class AbilityDash extends Ability implements IAbilityDash { if (dashDirection == null) return; // Stall detection: if entity hasn't moved since last tick, it's stuck against a wall - if (tick > 1) { + if (!isPreview() && tick > 1) { double movedThisTick = Math.sqrt( Math.pow(caster.posX - prevTickX, 2) + Math.pow(caster.posZ - prevTickZ, 2)); @@ -197,12 +197,15 @@ public class AbilityDash extends Ability implements IAbilityDash { if (distanceTraveled >= dashDistance) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } signalCompletion(); return; } - if (isDashBlocked(caster)) { + // Block detection (skip in preview - no real world collision) + if (!isPreview() && isDashBlocked(caster)) { caster.motionX = 0; caster.motionZ = 0; caster.velocityChanged = true; @@ -214,24 +217,30 @@ public class AbilityDash extends Ability implements IAbilityDash { caster.motionX = dashDirection.xCoord * dashSpeed; caster.motionY = 0; caster.motionZ = dashDirection.zCoord * dashSpeed; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; - // Trail particles - world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); + // Trail particles + world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); + } } @Override public void onComplete(EntityLivingBase caster, EntityLivingBase target) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } } @Override public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } } @Override @@ -247,66 +256,8 @@ public class AbilityDash extends Ability implements IAbilityDash { return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); } - // ==================== PREVIEW MODE ==================== - - private transient double previewDirX, previewDirZ; - private transient double previewStartX, previewStartZ; - private transient boolean previewDashing = false; - @Override - @SideOnly(Side.CLIENT) - public void onPreviewExecute(EntityNPCInterface npc) { - previewStartX = npc.posX; - previewStartZ = npc.posZ; - previewDashing = false; - - // Pick a random direction based on mode - DashDirection[] directions = dashMode == DashMode.AGGRESSIVE - ? AGGRESSIVE_DIRECTIONS - : DEFENSIVE_DIRECTIONS; - DashDirection dir = directions[RANDOM.nextInt(directions.length)]; - - float baseYaw; - if (previewTarget != null) { - double dx = previewTarget.posX - npc.posX; - double dz = previewTarget.posZ - npc.posZ; - baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - } else { - baseYaw = npc.rotationYaw; - } - - float dashYaw = baseYaw + dir.getAngleOffset(); - float yawRad = (float) Math.toRadians(dashYaw); - previewDirX = -Math.sin(yawRad); - previewDirZ = Math.cos(yawRad); - previewDashing = true; - } - - @Override - @SideOnly(Side.CLIENT) - public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { - if (!previewDashing) return; - - double distTraveled = Math.sqrt( - Math.pow(npc.posX - previewStartX, 2) + - Math.pow(npc.posZ - previewStartZ, 2) - ); - - if (distTraveled >= dashDistance) { - previewDashing = false; - return; - } - - npc.prevPosX = npc.posX; - npc.prevPosY = npc.posY; - npc.prevPosZ = npc.posZ; - - npc.posX += previewDirX * dashSpeed; - npc.posZ += previewDirZ * dashSpeed; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return (int) Math.ceil(dashDistance / dashSpeed) + 5; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 0f91869fa..77d137eb9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; @@ -23,7 +22,6 @@ import noppes.npcs.api.ability.type.IAbilityDisc; import java.util.Arrays; import java.util.List; -import noppes.npcs.entity.EntityNPCInterface; /** * Disc ability: Spawns a flat spinning disc projectile. @@ -79,27 +77,27 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start moving the disc that was spawned during windup if (discEntity != null && !discEntity.isDead) { - discEntity.startMoving(target); + if (isPreview()) { + discEntity.startPreviewFiring(); + } else { + discEntity.startMoving(target); + } } - - // Ability stays active until entity dies (prevents firing another while projectile is alive) - // Movement locking is handled separately by the base class } @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn disc in charging mode on first tick of windup if (tick == 1) { - // Create disc in charging mode - follows caster based on anchor point during windup Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); discEntity = new EntityAbilityDisc( world, caster, target, @@ -107,11 +105,16 @@ public class AbilityDisc extends Ability implements IAbilityDisc { discRadius, discThickness, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData, boomerang, boomerangDelay); - discEntity.setupCharging(anchorData, windUpTicks); + + if (isPreview()) { + discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks); + } else { + discEntity.setupCharging(anchorData, windUpTicks); + } discEntity.setEffects(this.effects); discEntity.setSourceAbility(this); - world.spawnEntityInWorld(discEntity); + spawnAbilityEntity(world, discEntity); } } @@ -264,17 +267,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); - disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks); - return disc; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java index 421861fcd..2fbd6cbda 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java @@ -14,12 +14,10 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.util.ValueUtil; import java.util.Arrays; @@ -89,19 +87,27 @@ public class AbilityDiscDual extends Ability { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } - // Start moving the orb that was spawned during windup + // Start moving the disc that was spawned during windup if (discEntity != null && !discEntity.isDead) { - discEntity.startMoving(target); + if (isPreview()) { + discEntity.startPreviewFiring(); + } else { + discEntity.startMoving(target); + } } if (dualFire) { if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) { - discEntity2.startMoving(target); + if (isPreview()) { + discEntity2.startPreviewFiring(); + } else { + discEntity2.startMoving(target); + } } } else if (discEntity2 != null) { discEntity2.setDead(); @@ -113,7 +119,7 @@ public class AbilityDiscDual extends Ability { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn disc in charging mode on first tick of windup if (tick == 1) { @@ -137,8 +143,13 @@ public class AbilityDiscDual extends Ability { discEntity.setSiblingEntityId(discEntity2.getEntityId()); discEntity2.setSiblingEntityId(discEntity.getEntityId()); - discEntity.setupCharging(anchorData[0], windUpTicks); - discEntity2.setupCharging(anchorData[1], windUpTicks); + if (isPreview()) { + discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks); + discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks); + } else { + discEntity.setupCharging(anchorData[0], windUpTicks); + discEntity2.setupCharging(anchorData[1], windUpTicks); + } discEntity.setEffects(this.effects); @@ -148,8 +159,8 @@ public class AbilityDiscDual extends Ability { discEntity.setSourceAbility(this); discEntity2.setSourceAbility(this); - world.spawnEntityInWorld(discEntity); - world.spawnEntityInWorld(discEntity2); + spawnAbilityEntity(world, discEntity); + spawnAbilityEntity(world, discEntity2); } } @@ -164,7 +175,11 @@ public class AbilityDiscDual extends Ability { } if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) { - discEntity2.startMoving(target); + if (isPreview()) { + discEntity2.startPreviewFiring(); + } else { + discEntity2.startMoving(target); + } } } else { if (discEntity == null || discEntity.isDead) { @@ -511,17 +526,7 @@ public class AbilityDiscDual extends Ability { } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); - disc.setupPreview(npc, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks); - return disc; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 99ea47172..112b2a637 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -243,7 +243,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Check if hazard duration has ended if (tick >= durationTicks) { @@ -251,53 +251,55 @@ public class AbilityHazard extends Ability implements IAbilityHazard { return; } - damagedThisTick.clear(); - ticksSinceDamage++; + if (!isPreview()) { + damagedThisTick.clear(); + ticksSinceDamage++; - switch (placement) { - case FOLLOW_CASTER: - zoneX = caster.posX; - zoneY = caster.posY; - zoneZ = caster.posZ; - break; - case FOLLOW_TARGET: - if (target != null) { - zoneX = target.posX; - zoneY = target.posY; - zoneZ = target.posZ; - } - break; - default: - break; - } - - if (ticksSinceDamage >= damageInterval) { - ticksSinceDamage = 0; - - AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( - zoneX - radius, zoneY - heightBelow, zoneZ - radius, - zoneX + radius, zoneY + heightAbove, zoneZ + radius - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); - - for (EntityLivingBase entity : entities) { - if (entity == caster && !affectsCaster) continue; - if (damagedThisTick.contains(entity.getEntityId())) continue; - if (!isInZone(entity, caster)) continue; - - if (damagePerSecond > 0) { - if (ignoreInvulnFrames) { - entity.hurtResistantTime = 0; + switch (placement) { + case FOLLOW_CASTER: + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; + break; + case FOLLOW_TARGET: + if (target != null) { + zoneX = target.posX; + zoneY = target.posY; + zoneZ = target.posZ; } - // Apply damage with scripted event support (no knockback for hazard) - boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0); - if (!wasHit) continue; // Skip debuffs if hit was cancelled - } + break; + default: + break; + } - applyDebuffs(entity); - damagedThisTick.add(entity.getEntityId()); + if (ticksSinceDamage >= damageInterval) { + ticksSinceDamage = 0; + + AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( + zoneX - radius, zoneY - heightBelow, zoneZ - radius, + zoneX + radius, zoneY + heightAbove, zoneZ + radius + ); + + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + + for (EntityLivingBase entity : entities) { + if (entity == caster && !affectsCaster) continue; + if (damagedThisTick.contains(entity.getEntityId())) continue; + if (!isInZone(entity, caster)) continue; + + if (damagePerSecond > 0) { + if (ignoreInvulnFrames) { + entity.hurtResistantTime = 0; + } + // Apply damage with scripted event support (no knockback for hazard) + boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0); + if (!wasHit) continue; // Skip debuffs if hit was cancelled + } + + applyDebuffs(entity); + damagedThisTick.add(entity.getEntityId()); + } } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index c1faf000b..b60c46d9d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -75,27 +75,31 @@ public class AbilityHeal extends Ability implements IAbilityHeal { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; - getHealedAllies().clear(); + if (!isPreview()) { + getHealedAllies().clear(); - // Always find allies if we're healing them (needed for both instant and HoT) - if (healAllies && healRadius > 0) { - findAlliesInRadius(caster, world); + // Always find allies if we're healing them (needed for both instant and HoT) + if (healAllies && healRadius > 0) { + findAlliesInRadius(caster, world); + } + + if (instantHeal) { + // Instant heal - apply all healing now + if (healSelf) { + healEntity(caster); + spawnHealParticles(world, caster); + } + + for (EntityLivingBase ally : getHealedAllies()) { + healEntity(ally); + spawnHealParticles(world, ally); + } + } } if (instantHeal) { - // Instant heal - apply all healing now - if (healSelf) { - healEntity(caster); - spawnHealParticles(world, caster); - } - - for (EntityLivingBase ally : getHealedAllies()) { - healEntity(ally); - spawnHealParticles(world, ally); - } - // Instant heal completes immediately signalCompletion(); } @@ -104,7 +108,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote || instantHeal) return; + if ((world.isRemote && !isPreview()) || instantHeal) return; // Heal over time - distribute heal across duration ticks if (tick % 10 == 0) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 0e236de32..dd5d5fc09 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -68,51 +68,53 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } - // Calculate forward and right vectors from caster yaw - float yawRad = (float) Math.toRadians(caster.rotationYaw); - double forwardX = -Math.sin(yawRad); - double forwardZ = Math.cos(yawRad); - double rightX = forwardZ; // perpendicular right - double rightZ = -forwardX; + if (!isPreview()) { + // Calculate forward and right vectors from caster yaw + float yawRad = (float) Math.toRadians(caster.rotationYaw); + double forwardX = -Math.sin(yawRad); + double forwardZ = Math.cos(yawRad); + double rightX = forwardZ; // perpendicular right + double rightZ = -forwardX; - // Search area: AABB that encompasses the rectangle - float searchDist = Math.max(hitLength, hitWidth) + 1.0f; - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABBExcludingEntity( - caster, caster.boundingBox.expand(searchDist, 2, searchDist)); + // Search area: AABB that encompasses the rectangle + float searchDist = Math.max(hitLength, hitWidth) + 1.0f; + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABBExcludingEntity( + caster, caster.boundingBox.expand(searchDist, 2, searchDist)); - boolean anyHit = false; - for (Entity entity : entities) { - if (!(entity instanceof EntityLivingBase) || entity == caster) continue; - EntityLivingBase livingTarget = (EntityLivingBase) entity; + boolean anyHit = false; + for (Entity entity : entities) { + if (!(entity instanceof EntityLivingBase) || entity == caster) continue; + EntityLivingBase livingTarget = (EntityLivingBase) entity; - double dx = livingTarget.posX - caster.posX; - double dz = livingTarget.posZ - caster.posZ; + double dx = livingTarget.posX - caster.posX; + double dz = livingTarget.posZ - caster.posZ; - // Project onto forward direction (must be in front, within hitLength) - double forwardDist = dx * forwardX + dz * forwardZ; - if (forwardDist < 0 || forwardDist > hitLength) continue; + // Project onto forward direction (must be in front, within hitLength) + double forwardDist = dx * forwardX + dz * forwardZ; + if (forwardDist < 0 || forwardDist > hitLength) continue; - // Project onto right direction (must be within hitWidth to each side) - double sideDist = dx * rightX + dz * rightZ; - if (Math.abs(sideDist) > hitWidth) continue; + // Project onto right direction (must be within hitWidth to each side) + double sideDist = dx * rightX + dz * rightZ; + if (Math.abs(sideDist) > hitWidth) continue; - // Entity is within the rectangle - apply damage - boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback); - if (wasHit) { - applyEffects(livingTarget); - anyHit = true; + // Entity is within the rectangle - apply damage + boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback); + if (wasHit) { + applyEffects(livingTarget); + anyHit = true; + } } - } - // Play hit sound even if nothing was hit (the attack still happens) - if (!anyHit) { - world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f); + // Play hit sound even if nothing was hit (the attack still happens) + if (!anyHit) { + world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f); + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 4be2315d5..d211de6d2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityLaser; import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -22,7 +21,6 @@ import noppes.npcs.api.ability.type.IAbilityLaserShot; import java.util.Arrays; import java.util.List; -import noppes.npcs.entity.EntityNPCInterface; /** * Laser Shot ability: Fast expanding thin line that pierces through targets. @@ -84,7 +82,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; if (tick == 1) { Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); @@ -96,17 +94,21 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { expansionSpeed, lingerTicks ); - laserEntity.setupCharging(anchorData, windUpTicks); + if (isPreview()) { + laserEntity.setupPreview(caster, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance); + } else { + laserEntity.setupCharging(anchorData, windUpTicks); + } laserEntity.setEffects(this.effects); laserEntity.setSourceAbility(this); - world.spawnEntityInWorld(laserEntity); + spawnAbilityEntity(world, laserEntity); } } @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } @@ -235,25 +237,10 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityLaser laser = new EntityAbilityLaser(npc.worldObj); - laser.setupPreview(npc, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance); - return laser; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60; } - @Override - public boolean spawnPreviewDuringWindup() { - return false; // Laser has no charging, spawns at active phase - } - @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 96881cf4e..6a87b38b4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -75,14 +75,18 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start moving the orb that was spawned during windup if (orbEntity != null && !orbEntity.isDead) { - orbEntity.startMoving(target); + if (isPreview()) { + orbEntity.startPreviewFiring(); + } else { + orbEntity.startMoving(target); + } } // Ability stays active until entity dies (prevents firing another while projectile is alive) @@ -91,21 +95,25 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn orb in charging mode on first tick of windup if (tick == 1) { - // Create orb in charging mode - follows caster based on anchor point during windup Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); orbEntity = new EntityAbilityOrb( world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, displayData, combatData, homingData, lightningData, lifespanData, trajectoryData); - orbEntity.setupCharging(anchorData, windUpTicks); + + if (isPreview()) { + orbEntity.setupPreview(caster, orbSize, displayData, lightningData, anchorData, windUpTicks); + } else { + orbEntity.setupCharging(anchorData, windUpTicks); + } orbEntity.setEffects(this.effects); orbEntity.setSourceAbility(this); - world.spawnEntityInWorld(orbEntity); + spawnAbilityEntity(world, orbEntity); } } @@ -405,17 +413,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); - orb.setupPreview(npc, orbSize, displayData, lightningData, anchorData, windUpTicks); - return orb; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java index 71be07a34..dda01f68a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java @@ -89,7 +89,7 @@ public class AbilityOrbBarrage extends Ability { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } @@ -107,12 +107,10 @@ public class AbilityOrbBarrage extends Ability { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn orb in charging mode on first tick of windup if (tick == 1) { - // Create orb in charging mode - follows caster based on anchor point during windup - if (dualCharging) { chargingEntities = new EntityAbilityOrb[2]; } else { @@ -128,11 +126,15 @@ public class AbilityOrbBarrage extends Ability { colorData, combatData, homingData, lightningData, lifespanData, trajectoryData ); - orbEntity.setupCharging(anchorData[i], windUpTicks); + if (isPreview()) { + orbEntity.setupPreview(caster, orbSize, colorData, lightningData, anchorData[i], windUpTicks); + } else { + orbEntity.setupCharging(anchorData[i], windUpTicks); + } orbEntity.setSourceAbility(this); chargingEntities[i] = orbEntity; - world.spawnEntityInWorld(orbEntity); + spawnAbilityEntity(world, orbEntity); } } } @@ -540,17 +542,7 @@ public class AbilityOrbBarrage extends Ability { public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); - orb.setupPreview(npc, orbSize, colorData, lightningData, anchorData[0], windUpTicks); - return orb; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java index bd2870438..7b8362464 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java @@ -80,19 +80,27 @@ public class AbilityOrbDual extends Ability { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start moving the orb that was spawned during windup if (orbEntity != null && !orbEntity.isDead) { - orbEntity.startMoving(target); + if (isPreview()) { + orbEntity.startPreviewFiring(); + } else { + orbEntity.startMoving(target); + } } if (dualFire) { if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) { - orbEntity2.startMoving(target); + if (isPreview()) { + orbEntity2.startPreviewFiring(); + } else { + orbEntity2.startMoving(target); + } } } else if (orbEntity2 != null) { orbEntity2.setDead(); @@ -104,11 +112,10 @@ public class AbilityOrbDual extends Ability { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn orb in charging mode on first tick of windup if (tick == 1) { - // Create orb in charging mode - follows caster based on anchor point during windup Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); orbEntity = new EntityAbilityOrb( world, caster, target, @@ -124,8 +131,13 @@ public class AbilityOrbDual extends Ability { orbEntity.setSiblingEntityId(orbEntity2.getEntityId()); orbEntity2.setSiblingEntityId(orbEntity.getEntityId()); - orbEntity.setupCharging(anchorData[0], windUpTicks); - orbEntity2.setupCharging(anchorData[1], windUpTicks); + if (isPreview()) { + orbEntity.setupPreview(caster, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks); + orbEntity2.setupPreview(caster, orbSize, displayData[1], lightningData[1], anchorData[1], windUpTicks); + } else { + orbEntity.setupCharging(anchorData[0], windUpTicks); + orbEntity2.setupCharging(anchorData[1], windUpTicks); + } orbEntity.setEffects(this.effects); @@ -135,8 +147,8 @@ public class AbilityOrbDual extends Ability { orbEntity.setSourceAbility(this); orbEntity2.setSourceAbility(this); - world.spawnEntityInWorld(orbEntity); - world.spawnEntityInWorld(orbEntity2); + spawnAbilityEntity(world, orbEntity); + spawnAbilityEntity(world, orbEntity2); } } @@ -570,17 +582,7 @@ public class AbilityOrbDual extends Ability { } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); - orb.setupPreview(npc, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks); - return orb; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index e5f6f5df6..75c27a507 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -62,59 +62,64 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote || target == null) return; - - // Calculate direction to target - double dx = target.posX - caster.posX; - double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); - double dz = target.posZ - caster.posZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - - if (len > 0) { - dx /= len; - dy /= len; - dz /= len; + if ((world.isRemote && !isPreview()) || target == null) { + signalCompletion(); + return; } - // Deal instant damage with scripted event support - // TODO: Use custom EntityAbilityProjectile for actual tracking - applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); + if (!isPreview()) { + // Calculate direction to target + double dx = target.posX - caster.posX; + double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); + double dz = target.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - // Play sound - world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); + if (len > 0) { + dx /= len; + dy /= len; + dz /= len; + } - // Handle splash damage for explosive projectiles - if (explosive && explosionRadius > 0) { - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABBExcludingEntity(target, - target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); + // Deal instant damage with scripted event support + // TODO: Use custom EntityAbilityProjectile for actual tracking + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); - for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != caster) { - EntityLivingBase living = (EntityLivingBase) entity; - float dist = target.getDistanceToEntity(living); - if (dist < explosionRadius) { - float falloff = 1.0f - (dist / explosionRadius); - // Apply splash damage with scripted event support (no knockback) - applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); + // Play sound + world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); + + // Handle splash damage for explosive projectiles + if (explosive && explosionRadius > 0) { + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABBExcludingEntity(target, + target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); + + for (Entity entity : entities) { + if (entity instanceof EntityLivingBase && entity != caster) { + EntityLivingBase living = (EntityLivingBase) entity; + float dist = target.getDistanceToEntity(living); + if (dist < explosionRadius) { + float falloff = 1.0f - (dist / explosionRadius); + // Apply splash damage with scripted event support (no knockback) + applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); + } } } + + // Explosion particles + world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f); + for (int i = 0; i < 10; i++) { + world.spawnParticle("explode", + target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius, + target.posY + world.rand.nextDouble() * target.height, + target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius, + 0, 0.1, 0); + } } - // Explosion particles - world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f); - for (int i = 0; i < 10; i++) { - world.spawnParticle("explode", - target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius, - target.posY + world.rand.nextDouble() * target.height, - target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius, - 0, 0.1, 0); - } + // Spawn projectile particles (visual trail) + spawnProjectileParticles(world, caster, target); } - // Spawn projectile particles (visual trail) - spawnProjectileParticles(world, caster, target); - // Projectile is instant signalCompletion(); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 775c15515..6788a3ea3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -63,48 +63,50 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - // Shockwave is instant - apply effect immediately after windup + if (!isPreview()) { + // Shockwave is instant - apply effect immediately after windup - // Get all entities in radius - AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); + // Get all entities in radius + AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); - int count = 0; - for (EntityLivingBase entity : entities) { - if (entity == caster) continue; - if (entity.isDead) continue; + int count = 0; + for (EntityLivingBase entity : entities) { + if (entity == caster) continue; + if (entity.isDead) continue; - double dist = caster.getDistanceToEntity(entity); - if (dist > pushRadius) continue; + double dist = caster.getDistanceToEntity(entity); + if (dist > pushRadius) continue; - count++; - if (count > maxTargets) break; + count++; + if (count > maxTargets) break; - // Calculate push direction (away from caster) - double dx = entity.posX - caster.posX; - double dz = entity.posZ - caster.posZ; - double len = Math.sqrt(dx * dx + dz * dz); + // Calculate push direction (away from caster) + double dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dz * dz); - if (len > 0) { - dx /= len; - dz /= len; - } else { - // Entity is directly on top of caster, push in random direction - double angle = Math.random() * Math.PI * 2; - dx = Math.cos(angle); - dz = Math.sin(angle); - } + if (len > 0) { + dx /= len; + dz /= len; + } else { + // Entity is directly on top of caster, push in random direction + double angle = Math.random() * Math.PI * 2; + dx = Math.cos(angle); + dz = Math.sin(angle); + } - // Scale knockback by distance (closer = stronger) - float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; - float finalPush = pushStrength * distFactor; - // Apply damage with custom knockback direction - boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); + // Scale knockback by distance (closer = stronger) + float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; + float finalPush = pushStrength * distFactor; + // Apply damage with custom knockback direction + boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); - // Apply effects if hit connected - if (wasHit) { - applyEffects(entity); + // Apply effects if hit connected + if (wasHit) { + applyEffects(entity); + } } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 1be741d35..15b4060b8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -187,11 +187,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { caster.motionZ = 0; caster.motionY = vy; hasLaunched = true; - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(true); } - caster.velocityChanged = true; - caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); + if (!isPreview()) { + caster.velocityChanged = true; + caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); + } return; } @@ -233,10 +235,12 @@ public class AbilitySlam extends Ability implements IAbilitySlam { caster.motionY = vy; hasLaunched = true; - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(true); } - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } // Face the target float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); @@ -252,7 +256,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam { airTicks++; // Continuously reset fall distance during slam to prevent fall damage - caster.fallDistance = 0; + if (!isPreview()) { + caster.fallDistance = 0; + } // Check for landing if (caster.onGround && airTicks > 3) { @@ -268,11 +274,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } // While in air, face toward target - double dx = targetX - caster.posX; - double dz = targetZ - caster.posZ; - float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - caster.rotationYaw = targetYaw; - caster.rotationYawHead = targetYaw; + if (!isPreview()) { + double dx = targetX - caster.posX; + double dz = targetZ - caster.posZ; + float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; + } } /** @@ -280,22 +288,25 @@ public class AbilitySlam extends Ability implements IAbilitySlam { */ private void onLanding(EntityLivingBase caster, World world) { hasLanded = true; - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(false); } // Signal that the ability has completed its active phase signalCompletion(); - // Reset fall distance to prevent fall damage on landing - caster.fallDistance = 0; - // Stop horizontal momentum caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; - if (world.isRemote) return; + if (!isPreview()) { + // Reset fall distance to prevent fall damage on landing + caster.fallDistance = 0; + caster.velocityChanged = true; + } + + if (world.isRemote && !isPreview()) return; + if (isPreview()) return; // Play slam impact sound on landing world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f); @@ -361,7 +372,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { @Override public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(false); } hasLaunched = false; @@ -371,7 +382,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { @Override public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(false); } hasLaunched = false; @@ -387,97 +398,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam { airTicks = 0; } - // ==================== PREVIEW MODE ==================== - - private transient double previewVelX, previewVelY, previewVelZ; - private transient boolean previewLaunched = false; - private transient double previewGroundY; - @Override - @SideOnly(Side.CLIENT) - public void onPreviewExecute(EntityNPCInterface npc) { - previewLaunched = false; - - double tx, ty, tz; - if (previewTarget != null && targetingMode == TargetingMode.AOE_TARGET) { - tx = previewTarget.posX; - ty = previewTarget.posY; - tz = previewTarget.posZ; - } else { - tx = npc.posX; - ty = npc.posY; - tz = npc.posZ; - } - - previewGroundY = npc.posY; - - double dx = tx - npc.posX; - double dz = tz - npc.posZ; - double horizontalDist = Math.sqrt(dx * dx + dz * dz); - - // Calculate vertical velocity for exact leapHeight - double arcHeight = Math.max(1.0, leapHeight); - double vy = calculateLaunchVelocity(arcHeight); - - if (horizontalDist < 0.5) { - previewVelX = 0; - previewVelZ = 0; - previewVelY = vy; - } else { - if (horizontalDist > maxRange) { - double scale = maxRange / horizontalDist; - dx *= scale; - dz *= scale; - horizontalDist = maxRange; - } - - double speedFactor = Math.max(0.1, leapSpeed); - int flightTicks = (int) Math.max(10, Math.min(horizontalDist * 1.5 / speedFactor, 40)); - double drag = 0.91; - double dragPowN = Math.pow(drag, flightTicks); - double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN); - - double dirX = dx / horizontalDist; - double dirZ = dz / horizontalDist; - previewVelX = dirX * vHorizontal; - previewVelZ = dirZ * vHorizontal; - previewVelY = vy; - } - previewLaunched = true; - } - - @Override - @SideOnly(Side.CLIENT) - public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { - if (!previewLaunched) return; - - // Apply velocity - npc.prevPosX = npc.posX; - npc.prevPosY = npc.posY; - npc.prevPosZ = npc.posZ; - - npc.posX += previewVelX; - npc.posY += previewVelY; - npc.posZ += previewVelZ; - - // Gravity and drag - previewVelY -= 0.08; - previewVelY *= 0.98; - previewVelX *= 0.91; - previewVelZ *= 0.91; - - // Ground clamp - stop falling below starting Y - if (npc.posY < previewGroundY && previewVelY < 0) { - npc.posY = previewGroundY; - previewVelY = 0; - previewVelX = 0; - previewVelZ = 0; - previewLaunched = false; - } - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return 60; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 2fcc62956..3155310ef 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -73,7 +73,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } @@ -85,7 +85,12 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { sweepSpeed, numberOfRotations, damage, damageInterval, piercing, lockOnTarget); - world.spawnEntityInWorld(activeEntity); + + if (isPreview()) { + activeEntity.setupPreview(caster); + } + + spawnAbilityEntity(world, activeEntity); // Ability stays active until entity dies (prevents firing another while projectile is alive) // Movement locking is handled separately by the base class @@ -200,6 +205,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } + @Override + public int getMaxPreviewDuration() { + return (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10; + } + @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index cb06b7b6b..fe847bf3a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -128,7 +128,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } private void performBlink(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; double oldX = caster.posX; double oldY = caster.posY; @@ -137,36 +137,47 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { Vec3 destination = calculateDestination(caster, target, world); if (destination == null) return; - boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight; - if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ, - destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) { - destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ, + if (!isPreview()) { + boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight; + if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ, + destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) { + destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ, + destination.xCoord, destination.yCoord, destination.zCoord); + if (destination == null) return; + } + + destination = findSafeDestination(world, oldX, oldY, oldZ, destination.xCoord, destination.yCoord, destination.zCoord); if (destination == null) return; + + // Damage at origin + if (damageAtStart) { + dealDamageAt(caster, world, oldX, oldY, oldZ); + } + + // Spawn particles at origin + spawnTeleportParticles(world, oldX, oldY, oldZ); } - destination = findSafeDestination(world, oldX, oldY, oldZ, - destination.xCoord, destination.yCoord, destination.zCoord); - if (destination == null) return; - - // Damage at origin - if (damageAtStart) { - dealDamageAt(caster, world, oldX, oldY, oldZ); - } - - // Spawn particles at origin - spawnTeleportParticles(world, oldX, oldY, oldZ); - // Teleport - caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); + if (isPreview()) { + caster.setPosition(destination.xCoord, destination.yCoord, destination.zCoord); + caster.prevPosX = destination.xCoord; + caster.prevPosY = destination.yCoord; + caster.prevPosZ = destination.zCoord; + } else { + caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); + } caster.fallDistance = 0; - // Spawn particles at destination - spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord); + if (!isPreview()) { + // Spawn particles at destination + spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord); - // Damage at destination - if (damageAtEnd) { - dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord); + // Damage at destination + if (damageAtEnd) { + dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord); + } } // Face target after teleport @@ -561,6 +572,12 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { this.damageRadius = damageRadius; } + @Override + public int getMaxPreviewDuration() { + int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; + return blinkLimit * blinkDelayTicks + 10; + } + @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 102c4395f..1623bacd8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -207,26 +207,28 @@ public class AbilityTrap extends Ability implements IAbilityTrap { return; } - AxisAlignedBB box = AxisAlignedBB.getBoundingBox( - trapX - triggerRadius, trapY - 1, trapZ - triggerRadius, - trapX + triggerRadius, trapY + 2, trapZ + triggerRadius - ); + if (!isPreview()) { + AxisAlignedBB box = AxisAlignedBB.getBoundingBox( + trapX - triggerRadius, trapY - 1, trapZ - triggerRadius, + trapX + triggerRadius, trapY + 2, trapZ + triggerRadius + ); - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); - for (EntityLivingBase entity : entities) { - if (entity == caster) continue; - if (entity.isDead) continue; - if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; + for (EntityLivingBase entity : entities) { + if (entity == caster) continue; + if (entity.isDead) continue; + if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; - double dx = entity.posX - trapX; - double dz = entity.posZ - trapZ; - double dist = Math.sqrt(dx * dx + dz * dz); + double dx = entity.posX - trapX; + double dz = entity.posZ - trapZ; + double dist = Math.sqrt(dx * dx + dz * dz); - if (dist <= triggerRadius) { - triggerTrap(caster, entity, world); - return; + if (dist <= triggerRadius) { + triggerTrap(caster, entity, world); + return; + } } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index e31a1e6ce..73f9816f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -84,29 +84,31 @@ public class AbilityVortex extends Ability implements IAbilityVortex { pullComplete = false; ticksSincePullDamage = 0; - if (aoe) { - AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); + if (!isPreview()) { + if (aoe) { + AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); - int count = 0; - for (EntityLivingBase entity : entities) { - if (entity == caster) continue; - if (entity.isDead) continue; + int count = 0; + for (EntityLivingBase entity : entities) { + if (entity == caster) continue; + if (entity.isDead) continue; - double dist = caster.getDistanceToEntity(entity); - if (dist <= pullRadius) { - getPulledEntities().add(entity.getUniqueID()); - count++; - if (count >= maxTargets) break; + double dist = caster.getDistanceToEntity(entity); + if (dist <= pullRadius) { + getPulledEntities().add(entity.getUniqueID()); + count++; + if (count >= maxTargets) break; + } } - } - } else { - // Single target mode - still check pullRadius - if (target != null && !target.isDead) { - double dist = caster.getDistanceToEntity(target); - if (dist <= pullRadius) { - getPulledEntities().add(target.getUniqueID()); + } else { + // Single target mode - still check pullRadius + if (target != null && !target.isDead) { + double dist = caster.getDistanceToEntity(target); + if (dist <= pullRadius) { + getPulledEntities().add(target.getUniqueID()); + } } } } @@ -114,6 +116,12 @@ public class AbilityVortex extends Ability implements IAbilityVortex { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (isPreview()) { + // No entities to pull in preview, just run animation for a duration + if (tick >= 60) signalCompletion(); + return; + } + if (pullComplete || getPulledEntities().isEmpty()) { signalCompletion(); return; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 60175169a..1427b298f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -222,24 +222,39 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { setCharging(false); - // Update start position to current position + // Origin (tail) stays at the charged position startX = posX; startY = posY; startZ = posZ; + // Reset head offset to origin + headOffsetX = 0; + headOffsetY = 0; + headOffsetZ = 0; + prevHeadOffsetX = 0; + prevHeadOffsetY = 0; + prevHeadOffsetZ = 0; + + // Origin no longer follows owner after firing + attachedToOwner = false; + // Sync prev position to prevent visual jump on first frame prevPosX = posX; prevPosY = posY; prevPosZ = posZ; + // Reset trail + trailPoints.clear(); + trailPointAges.clear(); + trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + // 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(); + motionX = -Math.sin(yaw) * getSpeed(); + motionY = 0; + motionZ = Math.cos(yaw) * getSpeed(); } else { motionX = getSpeed(); motionY = 0; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index bbd77abd0..3578ac9bf 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -38,7 +38,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { private int chargeTick = 0; private float targetDiscRadius = 1.0f; // Full radius to grow to during charging private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging - private EnergyAnchorData anchorData = new EnergyAnchorData(); // Data watcher index for charging state (synced to clients) private static final int DW_CHARGING = 20; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 1a036573e..9c09bd467 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -1,6 +1,5 @@ package kamkeel.npcs.entity; -import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; @@ -46,7 +45,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { private boolean charging = false; private int chargeDuration = 40; private int chargeTick = 0; - private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); private float targetSize = 1.0f; // Full size to grow to during charging // Data watcher index for charging state (synced to clients) @@ -454,7 +452,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { 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; - anchorData.readNBT(nbt); } @Override @@ -476,7 +473,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { nbt.setInteger("ChargeDuration", chargeDuration); nbt.setInteger("ChargeTick", chargeTick); nbt.setFloat("TargetSize", targetSize); - anchorData.writeNBT(nbt); } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index 8eceb11de..4d072090e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -60,6 +60,10 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa private int maxTicks = 400; private long deathWorldTime = -1; + // Preview mode + private boolean previewMode = false; + private EntityLivingBase previewOwner = null; + // Damage state private transient int ticksSinceDamage = 0; private transient Set hitThisTick = new HashSet<>(); @@ -281,7 +285,16 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa target.velocityChanged = true; } + /** + * Set up preview mode. Owner is stored directly since world entity lookup won't work. + */ + public void setupPreview(EntityLivingBase owner) { + this.previewMode = true; + this.previewOwner = owner; + } + private Entity getOwner() { + if (previewMode && previewOwner != null) return previewOwner; if (ownerEntityId == -1) return null; return worldObj.getEntityByID(ownerEntityId); } diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 3255452c9..4331ab0ea 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -509,6 +509,8 @@ public class ClientProxy extends CommonProxy { } else { abilityNpc = new EntityCustomNpc(Minecraft.getMinecraft().theWorld); abilityNpc.display.texture = "customnpcs:textures/entity/humanmale/AnimationBody.png"; + abilityNpc.height = 1.8f; + abilityNpc.width = 0.6f; } return new GuiNpcManageAbilities(abilityNpc, npc != null); } diff --git a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java index 90204fdc4..2da4a1a7b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java +++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java @@ -4,15 +4,10 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.PreviewEntityHandler; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; -import kamkeel.npcs.entity.EntityAbilityBeam; -import kamkeel.npcs.entity.EntityAbilityDisc; -import kamkeel.npcs.entity.EntityAbilityLaser; -import kamkeel.npcs.entity.EntityAbilityOrb; -import kamkeel.npcs.entity.EntityAbilityProjectile; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; -import net.minecraft.world.World; import noppes.npcs.controllers.data.Animation; import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.entity.EntityCustomNpc; @@ -25,38 +20,39 @@ import java.util.List; /** * Client-side executor for previewing abilities in the GUI. * - * Lifecycle: - * 1. WINDUP phase - plays windup animation, entities spawn and charge - * 2. ACTIVE phase - plays active animation, entities fire and move - * 3. EXTENDED phase - continues for +100 ticks after animations to see results - * 4. IDLE - preview complete + * Uses unified execution: calls the REAL ability methods (onWindUpTick, onExecute, onActiveTick) + * with previewMode=true. Abilities skip damage/effects/sounds when isPreview() returns true. * - * Features: - * - Invisible fake target NPC placed in front of the preview NPC for targeting - * - NPC movement tracking for abilities that move the NPC (e.g. slam) + * Lifecycle: + * 1. WINDUP phase - real onWindUpTick() called, telegraph shown, entities may spawn + * 2. ACTIVE phase - real onExecute() then onActiveTick() called, physics simulated + * 3. EXTENDED phase - 100 extra ticks to observe results (entities continue updating) + * 4. IDLE - preview complete */ @SideOnly(Side.CLIENT) -public class AbilityPreviewExecutor { +public class AbilityPreviewExecutor implements PreviewEntityHandler { // ==================== PREVIEW STATE ==================== private Ability previewAbility; private EntityNPCInterface previewNpc; - private AbilityPhase phase = AbilityPhase.IDLE; + + /** Executor's own phase tracking (separate from ability.getPhase()) */ + private enum ExecutorPhase { IDLE, WINDUP, ACTIVE, EXTENDED } + private ExecutorPhase executorPhase = ExecutorPhase.IDLE; + private int currentTick = 0; private boolean playing = false; private boolean paused = false; // ==================== TIMING ==================== private static final int EXTENDED_DURATION = 100; - private int activeDuration = 40; + private int maxPreviewDuration = 200; // ==================== FAKE TARGET ==================== - /** Invisible fake target NPC for ability targeting (placed 5 blocks in front of preview NPC) */ private EntityNPCInterface fakeTarget; private static final double TARGET_DISTANCE = 5.0; // ==================== NPC START POSITION ==================== - /** Saved NPC position for restoring after movement abilities */ private double npcStartX, npcStartY, npcStartZ; // ==================== PREVIEW CONTENT ==================== @@ -74,7 +70,8 @@ public class AbilityPreviewExecutor { } /** - * Start previewing an ability. + * Start previewing an ability using unified execution. + * Sets previewMode=true on the ability, then calls its real methods. */ public void startPreview(Ability ability, EntityNPCInterface npc) { stop(); @@ -84,31 +81,35 @@ public class AbilityPreviewExecutor { this.currentTick = 0; this.playing = true; this.paused = false; - this.activeDuration = ability.getPreviewActiveDuration(); + this.maxPreviewDuration = ability.getMaxPreviewDuration(); // Save NPC start position for restoring later this.npcStartX = npc.posX; this.npcStartY = npc.posY; this.npcStartZ = npc.posZ; + // Enable preview mode on the ability + ability.setPreviewMode(true); + ability.setPreviewEntityHandler(this); + // Create invisible fake target NPC for targeting createFakeTarget(npc); - // Set preview target on the ability - ability.setPreviewTarget(fakeTarget); - // Start tracking NPC movement if (parentGui != null) { parentGui.startTrackingMovement(); } + // Use the ability's real start() method with the fake target + ability.start(fakeTarget); + if (ability.getWindUpTicks() <= 0) { // No windup — go straight to active - this.phase = AbilityPhase.ACTIVE; + this.executorPhase = ExecutorPhase.ACTIVE; transitionToActive(); } else { // Normal windup flow - this.phase = AbilityPhase.WINDUP; + this.executorPhase = ExecutorPhase.WINDUP; // Create telegraph for preview previewTelegraph = ability.createTelegraph(npc, fakeTarget); @@ -121,24 +122,17 @@ public class AbilityPreviewExecutor { data.setAnimation(windUpAnim); data.animation.paused = false; } - - // Spawn preview entity on first tick if ability spawns during windup - if (ability.spawnPreviewDuringWindup()) { - spawnPreviewEntity(); - } } } /** * Create an invisible fake target NPC placed in front of the preview NPC. - * This entity is NOT rendered - it only exists for ability targeting logic. */ private void createFakeTarget(EntityNPCInterface npc) { if (npc.worldObj == null) return; try { fakeTarget = new EntityCustomNpc(npc.worldObj); - // Position target in front of NPC based on its facing direction float facingYaw = GuiAbilityInterface.NPC_FACING_YAW; double yawRad = Math.toRadians(facingYaw); double targetX = npc.posX - Math.sin(yawRad) * TARGET_DISTANCE; @@ -153,15 +147,12 @@ public class AbilityPreviewExecutor { } } - /** - * Get the fake target entity for abilities that need a target. - */ public EntityLivingBase getFakeTarget() { return fakeTarget; } public void play() { - if (previewAbility == null || phase == AbilityPhase.IDLE) { + if (previewAbility == null || executorPhase == ExecutorPhase.IDLE) { return; } playing = true; @@ -187,9 +178,17 @@ public class AbilityPreviewExecutor { } public void stop() { + // Clean up the ability + if (previewAbility != null) { + previewAbility.cleanup(); + previewAbility.setPreviewMode(false); + previewAbility.setPreviewEntityHandler(null); + previewAbility.reset(); + } + playing = false; paused = false; - phase = AbilityPhase.IDLE; + executorPhase = ExecutorPhase.IDLE; currentTick = 0; previewTelegraph = null; @@ -211,9 +210,11 @@ public class AbilityPreviewExecutor { previewNpc.prevPosX = npcStartX; previewNpc.prevPosY = npcStartY; previewNpc.prevPosZ = npcStartZ; + previewNpc.motionX = 0; + previewNpc.motionY = 0; + previewNpc.motionZ = 0; } - // Remove fake target fakeTarget = null; if (previewNpc != null) { @@ -224,7 +225,7 @@ public class AbilityPreviewExecutor { } /** - * Tick the preview forward. + * Tick the preview forward. Calls real ability methods. */ public void tick() { if (!playing || paused || previewAbility == null || previewNpc == null) { @@ -239,14 +240,14 @@ public class AbilityPreviewExecutor { tickEntities(); - switch (phase) { + switch (executorPhase) { case WINDUP: tickWindup(); break; case ACTIVE: tickActive(); break; - case IDLE: + case EXTENDED: tickExtended(); break; default: @@ -255,7 +256,8 @@ public class AbilityPreviewExecutor { } private void tickWindup() { - previewAbility.onPreviewWindUpTick(previewNpc, currentTick); + // Call the REAL windup tick method + previewAbility.onWindUpTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick); if (currentTick >= previewAbility.getWindUpTicks()) { transitionToActive(); @@ -263,35 +265,41 @@ public class AbilityPreviewExecutor { } private void tickActive() { - previewAbility.onPreviewActiveTick(previewNpc, currentTick); + // Call the REAL active tick method + previewAbility.onActiveTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick); - if (currentTick == 1 && !previewAbility.spawnPreviewDuringWindup()) { - spawnPreviewEntity(); - fireEntities(); + // Simulate physics for movement abilities + simulatePhysics(); + + // Check if the ability signaled completion (phase went to IDLE) + if (previewAbility.getPhase() == AbilityPhase.IDLE) { + transitionToExtended(); + return; } - if (currentTick >= activeDuration) { + // Safety timeout + if (currentTick >= maxPreviewDuration) { transitionToExtended(); } } private void tickExtended() { + // Continue ticking entities so they can finish their animations if (currentTick >= EXTENDED_DURATION) { playing = false; } } private void transitionToActive() { - phase = AbilityPhase.ACTIVE; + executorPhase = ExecutorPhase.ACTIVE; currentTick = 0; previewTelegraph = null; - // Notify ability of execution (for movement abilities like Slam/Charge/Dash) - previewAbility.onPreviewExecute(previewNpc); - - fireEntities(); + // Call the REAL onExecute method (fires entities, initiates movement, etc.) + previewAbility.onExecute(previewNpc, fakeTarget, previewNpc.worldObj); + // Start active animation Animation activeAnim = previewAbility.getActiveAnimation(); if (activeAnim != null && previewNpc != null) { AnimationData data = previewNpc.display.animationData; @@ -301,7 +309,7 @@ public class AbilityPreviewExecutor { } private void transitionToExtended() { - phase = AbilityPhase.IDLE; + executorPhase = ExecutorPhase.EXTENDED; currentTick = 0; if (previewNpc != null) { @@ -310,26 +318,40 @@ public class AbilityPreviewExecutor { } } - private void spawnPreviewEntity() { - Entity entity = previewAbility.createPreviewEntity(previewNpc); - if (entity != null) { - previewEntities.add(entity); - if (parentGui != null) { - parentGui.addPreviewEntity(entity); - } - } - } + /** + * Simulate basic physics for movement abilities (Slam, Charge, Dash). + * Applies gravity, drag, and ground clamping to NPC position. + */ + private void simulatePhysics() { + if (!previewAbility.hasAbilityMovement()) return; + if (previewNpc == null) return; - private void fireEntities() { - for (Entity entity : previewEntities) { - if (entity instanceof EntityAbilityOrb) { - ((EntityAbilityOrb) entity).startPreviewFiring(); - } else if (entity instanceof EntityAbilityBeam) { - ((EntityAbilityBeam) entity).startPreviewFiring(); - } else if (entity instanceof EntityAbilityDisc) { - ((EntityAbilityDisc) entity).startPreviewFiring(); - } + // Save previous position for interpolation + previewNpc.prevPosX = previewNpc.posX; + previewNpc.prevPosY = previewNpc.posY; + previewNpc.prevPosZ = previewNpc.posZ; + + // Apply velocity + previewNpc.posX += previewNpc.motionX; + previewNpc.posY += previewNpc.motionY; + previewNpc.posZ += previewNpc.motionZ; + + // Apply gravity + previewNpc.motionY -= 0.08; + previewNpc.motionY *= 0.98; + + // Apply horizontal drag + previewNpc.motionX *= 0.91; + previewNpc.motionZ *= 0.91; + + // Ground clamp: if below start position and falling, stop + if (previewNpc.posY < npcStartY && previewNpc.motionY < 0) { + previewNpc.posY = npcStartY; + previewNpc.motionY = 0; + previewNpc.onGround = true; } + + previewNpc.fallDistance = 0; } private void tickEntities() { @@ -347,12 +369,38 @@ public class AbilityPreviewExecutor { } } + // ==================== PreviewEntityHandler ==================== + + @Override + public void onEntitySpawned(Entity entity) { + if (entity == null) return; + previewEntities.add(entity); + if (parentGui != null) { + parentGui.addPreviewEntity(entity); + } + } + + @Override + public void onEntityRemoved(Entity entity) { + if (entity == null) return; + previewEntities.remove(entity); + if (parentGui != null) { + parentGui.removePreviewEntity(entity); + } + } + // ==================== GETTERS ==================== public boolean isPlaying() { return playing; } public boolean isPaused() { return paused; } public boolean isActive() { return playing || paused; } - public AbilityPhase getPhase() { return phase; } + public AbilityPhase getPhase() { + switch (executorPhase) { + case WINDUP: return AbilityPhase.WINDUP; + case ACTIVE: return AbilityPhase.ACTIVE; + default: return AbilityPhase.IDLE; + } + } public int getCurrentTick() { return currentTick; } public TelegraphInstance getTelegraph() { return previewTelegraph; } public List getPreviewEntities() { return previewEntities; } @@ -364,11 +412,11 @@ public class AbilityPreviewExecutor { return "Stopped"; } else if (paused) { return "Paused"; - } else if (phase == AbilityPhase.WINDUP) { + } else if (executorPhase == ExecutorPhase.WINDUP) { return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0); - } else if (phase == AbilityPhase.ACTIVE) { - return "Active: " + currentTick + "/" + activeDuration; - } else if (phase == AbilityPhase.IDLE && playing) { + } else if (executorPhase == ExecutorPhase.ACTIVE) { + return "Active: " + currentTick + "/" + maxPreviewDuration; + } else if (executorPhase == ExecutorPhase.EXTENDED) { return "Extended: " + currentTick + "/" + EXTENDED_DURATION; } return "";