Fix Preview Handling for Abilities, Fix Disc Positioning in Real World

This commit is contained in:
Kam committed 2026-02-06 05:04:19 -05:00
1 parent 70583743ce
commit 2e5c4c3bf4
29 files changed
+714 -801

No files matched your search

@@ -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;
}
// ═══════════════════════════════════════════════════════════════════
@@ -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);
}
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
}
@@ -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<EntityLivingBase> 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<EntityLivingBase> 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());
}
}
}
}
@@ -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) {
@@ -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<Entity> 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<Entity> 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);
}
}
}
@@ -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<FieldDef> defs) {
@@ -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;
}
@@ -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;
}
@@ -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;
}
}
@@ -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<Entity> 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<Entity> 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();
}
@@ -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<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
// Get all entities in radius
AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> 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);
}
}
}
}
@@ -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;
}
@@ -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<FieldDef> defs) {
@@ -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<FieldDef> defs) {
@@ -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<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
@SuppressWarnings("unchecked")
List<EntityLivingBase> 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;
}
}
}
}
@@ -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<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
if (!isPreview()) {
if (aoe) {
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> 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;
@@ -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;
@@ -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;
@@ -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);
}
/**
@@ -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<Integer> 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);
}
@@ -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);
}
@@ -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<Entity> 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 "";