diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index 4a783f551..cf5e1ca58 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -119,8 +119,9 @@ public class AbilityCommand extends CommandKamkeelBase { sendResult(sender, " Type: \u00A7d" + ability.getTypeId()); sendResult(sender, " Cooldown: \u00A7e" + ability.getCooldownTicks() + " ticks"); sendResult(sender, " Wind Up: \u00A7e" + ability.getWindUpTicks() + " ticks"); - sendResult(sender, " Active: \u00A7e" + ability.getActiveTicks() + " ticks"); - sendResult(sender, " Recovery: \u00A7e" + ability.getRecoveryTicks() + " ticks"); + if (ability.isInterruptible()) { + sendResult(sender, " Dazed: \u00A7e" + ability.getDazedTicks() + " ticks"); + } sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange()); sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No")); } 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 02d2227ad..e5dc7e493 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -47,18 +47,13 @@ public abstract class Ability implements IAbility { protected float maxRange = 20; // Timing (ticks) - protected int cooldownTicks = 100; + protected int cooldownTicks = 0; // Added to global cooldown after ability completes protected int windUpTicks = 20; - protected int activeTicks = 10; - protected int recoveryTicks = 20; + protected int dazedTicks = 80; // Only used when interrupted during WINDUP (if interruptible) // Interruption protected boolean interruptible = true; - // Wait for completion - if true, ability controls when active phase ends (ignores activeTicks) - // Used for abilities like Slam that need to wait for landing, or Beam that runs until entity dies - protected boolean waitForCompletion = false; - // Feedback protected boolean lockMovement = true; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up @@ -507,11 +502,8 @@ public abstract class Ability implements IAbility { /** * Tick the ability forward. Called every game tick while executing. *
- * Normal flow: WINDUP -> ACTIVE -> IDLE (no recovery in normal completion) - * Interrupted flow: WINDUP -> RECOVERY -> IDLE (recovery only on interrupt) - *
- * If waitForCompletion is true, the ability controls when active ends - * by calling signalCompletion() instead of using activeTicks. + * Normal flow: WINDUP -> ACTIVE -> IDLE + * Interrupted flow: WINDUP -> DAZED -> IDLE (dazed only on interrupt during WINDUP) * * @return true if phase changed this tick */ @@ -529,18 +521,13 @@ public abstract class Ability implements IAbility { } break; case ACTIVE: - // If waitForCompletion is true, ability must call signalCompletion() - // Otherwise, auto-complete when activeTicks is reached - if (!waitForCompletion && currentTick >= activeTicks) { - phase = AbilityPhase.IDLE; - currentTick = 0; - return true; - } + // Abilities control their own completion by calling signalCompletion() + // No auto-complete - each ability determines when it's done break; - case RECOVERY: - // Recovery phase is only entered on interrupt - // Count down recoveryTicks then go to IDLE - if (currentTick >= recoveryTicks) { + case DAZED: + // Dazed phase is only entered on interrupt during WINDUP + // NPC cannot attack during this time + if (currentTick >= dazedTicks) { phase = AbilityPhase.IDLE; currentTick = 0; return true; @@ -552,13 +539,14 @@ public abstract class Ability implements IAbility { /** * Signal that the ability has completed its active phase. - * Called by abilities with waitForCompletion=true when they are done. + * Called by abilities when they are done. * For example, AbilitySlam calls this when the NPC lands. * * @return true if phase changed to IDLE */ public boolean signalCompletion() { if (phase == AbilityPhase.ACTIVE) { + cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; return true; @@ -567,13 +555,14 @@ public abstract class Ability implements IAbility { } /** - * Interrupt this ability. Transitions to RECOVERY phase if interruptible. + * Interrupt this ability. Transitions to DAZED phase if interruptible during WINDUP. * Called when NPC takes direct damage during WINDUP. */ public void interrupt() { + cleanup(); if (interruptible && phase == AbilityPhase.WINDUP) { - // Interrupted during windup - go to recovery (dazed state) - phase = AbilityPhase.RECOVERY; + // Interrupted during windup - go to dazed state + phase = AbilityPhase.DAZED; currentTick = 0; } else { // Force stop - go directly to IDLE @@ -624,15 +613,25 @@ public abstract class Ability implements IAbility { } /** - * Reset all execution state (called on combat end) + * Reset all execution state (called on combat end, NPC death, target lost, etc.) */ public void reset() { + cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; currentTarget = null; telegraphInstance = null; } + /** + * Cleanup any resources created by this ability. + * Override this to kill spawned entities, cancel telegraphs, etc. + * Called when ability ends for any reason (completion, interrupt, reset, NPC death). + */ + public void cleanup() { + // Override in subclasses to clean up spawned entities, etc. + } + // ═══════════════════════════════════════════════════════════════════ // CONDITION CHECKING // ═══════════════════════════════════════════════════════════════════ @@ -661,10 +660,8 @@ public abstract class Ability implements IAbility { nbt.setFloat("maxRange", maxRange); nbt.setInteger("cooldown", cooldownTicks); nbt.setInteger("windUp", windUpTicks); - nbt.setInteger("active", activeTicks); - nbt.setInteger("recovery", recoveryTicks); + nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); - nbt.setBoolean("waitForCompletion", waitForCompletion); nbt.setBoolean("lockMovement", lockMovement); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); @@ -708,10 +705,8 @@ public abstract class Ability implements IAbility { maxRange = nbt.getFloat("maxRange"); cooldownTicks = nbt.getInteger("cooldown"); windUpTicks = nbt.getInteger("windUp"); - activeTicks = nbt.getInteger("active"); - recoveryTicks = nbt.getInteger("recovery"); + dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); - waitForCompletion = nbt.getBoolean("waitForCompletion"); lockMovement = nbt.getBoolean("lockMovement"); // Support old telegraphColor key for backwards compatibility if (nbt.hasKey("windUpColor")) { @@ -857,20 +852,12 @@ public abstract class Ability implements IAbility { this.windUpTicks = Math.max(0, windUpTicks); } - public int getActiveTicks() { - return activeTicks; + public int getDazedTicks() { + return dazedTicks; } - public void setActiveTicks(int activeTicks) { - this.activeTicks = Math.max(1, activeTicks); - } - - public int getRecoveryTicks() { - return recoveryTicks; - } - - public void setRecoveryTicks(int recoveryTicks) { - this.recoveryTicks = Math.max(0, recoveryTicks); + public void setDazedTicks(int dazedTicks) { + this.dazedTicks = Math.max(0, dazedTicks); } public boolean isInterruptible() { @@ -881,14 +868,6 @@ public abstract class Ability implements IAbility { this.interruptible = interruptible; } - public boolean isWaitForCompletion() { - return waitForCompletion; - } - - public void setWaitForCompletion(boolean waitForCompletion) { - this.waitForCompletion = waitForCompletion; - } - public boolean isLockMovement() { return lockMovement; } @@ -978,7 +957,7 @@ public abstract class Ability implements IAbility { } /** - * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=RECOVERY). + * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=DAZED). * Required for IAbility interface. */ @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java index 4b7f2ba3e..3aea051f3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java @@ -20,7 +20,7 @@ public enum AbilityAnimation {; animationNames.put(IDLE, idle); animationNames.put(WINDUP, windUp); animationNames.put(ACTIVE, active); - animationNames.put(RECOVERY, recovery); + animationNames.put(DAZED, recovery); } AbilityAnimation(String idle, String windUp, String active) { @@ -80,8 +80,8 @@ public enum AbilityAnimation {; return getAnimation(ACTIVE); } - public IAnimation recovery() { - return getAnimation(RECOVERY); + public IAnimation dazed() { + return getAnimation(DAZED); } private File getFileFromName(String fileName) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java index 6338279ba..0611a0274 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java @@ -4,5 +4,5 @@ public enum AbilityPhase { IDLE, // Not executing WINDUP, // Charging up (telegraph visible) ACTIVE, // Executing (damage happens) - RECOVERY // Finishing animation + DAZED // Stunned after interrupt during WINDUP (cannot attack) } 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 696367ce8..7a1d9babf 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 @@ -46,10 +46,8 @@ public class AbilityCharge extends Ability { this.maxRange = 20.0f; this.minRange = 4.0f; this.lockMovement = false; // Movement IS the ability - this.cooldownTicks = 80; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 40; - this.recoveryTicks = 10; // LINE telegraph showing charge path this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; @@ -161,11 +159,13 @@ public class AbilityCharge extends Ability { npc.motionX = 0; npc.motionZ = 0; npc.velocityChanged = true; + signalCompletion(); return; } if (isChargeBlocked(npc)) { stopMomentum(npc); + signalCompletion(); return; } 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 8274b4d79..b3052eeac 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 @@ -38,6 +38,7 @@ public class AbilityCutter extends Ability { private SweepMode sweepMode = SweepMode.SWIPE; private float sweepSpeed = 6.0f; + private int spinDurationTicks = 60; // Duration for SPIN mode private boolean piercing = true; private float innerRadius = 0.0f; @@ -52,10 +53,8 @@ public class AbilityCutter extends Ability { this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 8.0f; this.lockMovement = true; - this.cooldownTicks = 80; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 15; - this.recoveryTicks = 15; this.telegraphType = TelegraphType.CONE; this.windUpSound = "random.bow"; this.activeSound = "random.break"; @@ -110,6 +109,7 @@ public class AbilityCutter extends Ability { switch (sweepMode) { case SWIPE: if (currentRotation > arcAngle / 2.0f) { + signalCompletion(); // Swipe arc complete return; } performSweepDamage(npc, world, innerRadius, range, currentRotation); @@ -117,6 +117,10 @@ public class AbilityCutter extends Ability { break; case SPIN: + if (tick >= spinDurationTicks) { + signalCompletion(); // Spin duration complete + return; + } currentRotation = (currentRotation + sweepSpeed) % 360.0f; hitEntities.clear(); performSweepDamage(npc, world, innerRadius, range, currentRotation); @@ -195,6 +199,7 @@ public class AbilityCutter extends Ability { nbt.setFloat("knockback", knockback); nbt.setString("sweepMode", sweepMode.name()); nbt.setFloat("sweepSpeed", sweepSpeed); + nbt.setInteger("spinDurationTicks", spinDurationTicks); nbt.setBoolean("piercing", piercing); nbt.setFloat("innerRadius", innerRadius); } @@ -211,6 +216,7 @@ public class AbilityCutter extends Ability { this.sweepMode = SweepMode.SWIPE; } this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 6.0f; + this.spinDurationTicks = nbt.hasKey("spinDurationTicks") ? nbt.getInteger("spinDurationTicks") : 60; this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; } @@ -264,6 +270,14 @@ public class AbilityCutter extends Ability { this.sweepSpeed = sweepSpeed; } + public int getSpinDurationTicks() { + return spinDurationTicks; + } + + public void setSpinDurationTicks(int spinDurationTicks) { + this.spinDurationTicks = Math.max(1, spinDurationTicks); + } + public boolean isPiercing() { return piercing; } 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 4edc18f82..586e402a8 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 @@ -87,10 +87,8 @@ public class AbilityDash extends Ability { this.maxRange = 20.0f; this.minRange = 0.0f; this.lockMovement = false; - this.cooldownTicks = 40; + this.cooldownTicks = 0; this.windUpTicks = 5; - this.activeTicks = 8; - this.recoveryTicks = 10; // No telegraph for dash - it's a quick evasive move this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -174,6 +172,7 @@ public class AbilityDash extends Ability { npc.motionX = 0; npc.motionZ = 0; npc.velocityChanged = true; + signalCompletion(); return; } @@ -181,6 +180,7 @@ public class AbilityDash extends Ability { npc.motionX = 0; npc.motionZ = 0; npc.velocityChanged = true; + signalCompletion(); return; } 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 95dcd4afb..67fa23cdf 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 @@ -72,7 +72,7 @@ public class AbilityDisc extends Ability { private AnchorPoint anchorPoint = AnchorPoint.FRONT; // Transient state for disc entity (used during windup charging) - private transient int discEntityId = -1; + private transient EntityAbilityDisc discEntity = null; public AbilityDisc() { this.typeId = "ability.cnpc.disc"; @@ -80,10 +80,8 @@ public class AbilityDisc extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 30.0f; this.minRange = 5.0f; - this.cooldownTicks = 80; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 1; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; } @@ -111,17 +109,19 @@ public class AbilityDisc extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote) return; - - // Find and start moving the disc that was spawned during windup - if (discEntityId != -1) { - Entity entity = world.getEntityByID(discEntityId); - if (entity instanceof EntityAbilityDisc) { - EntityAbilityDisc disc = (EntityAbilityDisc) entity; - disc.startMoving(target); - } - discEntityId = -1; + if (world.isRemote) { + signalCompletion(); + return; } + + // Start moving the disc that was spawned during windup + if (discEntity != null && !discEntity.isDead) { + discEntity.startMoving(target); + } + discEntity = null; + + // Disc entity manages itself - ability is done + signalCompletion(); } @Override @@ -131,7 +131,7 @@ public class AbilityDisc extends Ability { // Spawn disc in charging mode on first tick of windup if (tick == 1) { // Create disc in charging mode - follows NPC based on anchor point during windup - EntityAbilityDisc disc = EntityAbilityDisc.createCharging( + discEntity = EntityAbilityDisc.createCharging( world, npc, target, discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, @@ -145,8 +145,7 @@ public class AbilityDisc extends Ability { windUpTicks // Charge duration = windup duration ); - world.spawnEntityInWorld(disc); - discEntityId = disc.getEntityId(); + world.spawnEntityInWorld(discEntity); } } @@ -159,15 +158,12 @@ public class AbilityDisc extends Ability { } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - // Despawn charging disc if interrupted during windup - if (discEntityId != -1) { - Entity entity = npc.worldObj.getEntityByID(discEntityId); - if (entity != null) { - entity.setDead(); - } - discEntityId = -1; + public void cleanup() { + // Despawn disc entity if still alive + if (discEntity != null && !discEntity.isDead) { + discEntity.setDead(); } + discEntity = null; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 18969b43c..3bcc5072c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -71,7 +71,7 @@ public class AbilityEnergyBeam extends Ability { private AnchorPoint anchorPoint = AnchorPoint.FRONT; // Transient state for beam entity (used during windup charging) - private transient int beamEntityId = -1; + private transient EntityAbilityBeam beamEntity = null; public AbilityEnergyBeam() { this.typeId = "ability.cnpc.beam"; @@ -79,10 +79,8 @@ public class AbilityEnergyBeam extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 5.0f; - this.cooldownTicks = 150; + this.cooldownTicks = 0; this.windUpTicks = 40; - this.activeTicks = 200; // Match maxLifetime to keep NPC locked while beam is active - this.recoveryTicks = 5; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; } @@ -110,17 +108,19 @@ public class AbilityEnergyBeam extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote) return; - - // Find and start firing the beam that was spawned during windup - if (beamEntityId != -1) { - Entity entity = world.getEntityByID(beamEntityId); - if (entity instanceof EntityAbilityBeam) { - EntityAbilityBeam beam = (EntityAbilityBeam) entity; - beam.startFiring(target); - } - beamEntityId = -1; + if (world.isRemote) { + signalCompletion(); + return; } + + // Start firing the beam that was spawned during windup + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.startFiring(target); + } + beamEntity = null; + + // Beam entity manages itself - ability is done + signalCompletion(); } @Override @@ -132,7 +132,7 @@ public class AbilityEnergyBeam extends Ability { float offsetDist = 1.0f; // Create beam in charging mode - follows NPC based on anchor point during windup - EntityAbilityBeam beam = EntityAbilityBeam.createCharging( + beamEntity = EntityAbilityBeam.createCharging( world, npc, target, beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, @@ -147,8 +147,7 @@ public class AbilityEnergyBeam extends Ability { anchorPoint // Anchor point for charging position ); - world.spawnEntityInWorld(beam); - beamEntityId = beam.getEntityId(); + world.spawnEntityInWorld(beamEntity); } } @@ -161,15 +160,12 @@ public class AbilityEnergyBeam extends Ability { } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - // Despawn charging beam if interrupted during windup - if (beamEntityId != -1) { - Entity entity = npc.worldObj.getEntityByID(beamEntityId); - if (entity != null) { - entity.setDead(); - } - beamEntityId = -1; + public void cleanup() { + // Despawn beam entity if still alive + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.setDead(); } + beamEntity = null; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 404589c6e..831ef97fd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -25,6 +25,7 @@ public class AbilityGuard extends Ability { private static final Random RANDOM = new Random(); // Type-specific parameters + private int durationTicks = 60; private float damageReduction = 0.5f; private boolean canCounter = false; private CounterType counterType = CounterType.FLAT; @@ -48,10 +49,8 @@ public class AbilityGuard extends Ability { this.name = "Guard"; this.targetingMode = TargetingMode.SELF; this.lockMovement = true; - this.cooldownTicks = 120; + this.cooldownTicks = 0; this.windUpTicks = 10; - this.activeTicks = 60; - this.recoveryTicks = 10; this.interruptible = false; // Hard to interrupt while guarding // No telegraph for guard - it's a defensive stance this.telegraphType = TelegraphType.NONE; @@ -95,6 +94,11 @@ public class AbilityGuard extends Ability { if (canCounter && counterTriggered && lastAttacker != null && !world.isRemote) { performCounter(npc, world); } + + // Check if guard duration has ended + if (tick >= durationTicks) { + signalCompletion(); + } } /** @@ -175,6 +179,7 @@ public class AbilityGuard extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("damageReduction", damageReduction); nbt.setBoolean("canCounter", canCounter); nbt.setString("counterType", counterType.name()); @@ -186,6 +191,7 @@ public class AbilityGuard extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f; this.canCounter = nbt.hasKey("canCounter") && nbt.getBoolean("canCounter"); try { @@ -206,6 +212,14 @@ public class AbilityGuard extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public float getDamageReduction() { return damageReduction; } 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 3161c5dbe..3e9998f6f 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 @@ -42,6 +42,7 @@ public class AbilityHazard extends Ability { FOLLOW_TARGET } + private int durationTicks = 100; private float radius = 4.0f; private float innerRadius = 0.0f; private float coneAngle = 45.0f; @@ -81,10 +82,8 @@ public class AbilityHazard extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; this.lockMovement = true; - this.cooldownTicks = 200; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 100; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; } @@ -232,6 +231,12 @@ public class AbilityHazard extends Ability { public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; + // Check if hazard duration has ended + if (tick >= durationTicks) { + signalCompletion(); + return; + } + damagedThisTick.clear(); ticksSinceDamage++; @@ -342,6 +347,7 @@ public class AbilityHazard extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("radius", radius); nbt.setFloat("innerRadius", innerRadius); nbt.setFloat("coneAngle", coneAngle); @@ -366,6 +372,7 @@ public class AbilityHazard extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 100; this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f; this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; this.coneAngle = nbt.hasKey("coneAngle") ? nbt.getFloat("coneAngle") : 45.0f; @@ -404,6 +411,14 @@ public class AbilityHazard extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public float getRadius() { return radius; } 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 3cf04db2c..59ef4751a 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 @@ -25,6 +25,7 @@ import java.util.List; public class AbilityHeal extends Ability { // Type-specific parameters + private int durationTicks = 60; private float healAmount = 10.0f; private float healPercent = 0.0f; private boolean healSelf = true; @@ -40,10 +41,8 @@ public class AbilityHeal extends Ability { this.name = "Heal"; this.targetingMode = TargetingMode.SELF; this.lockMovement = true; - this.cooldownTicks = 200; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 10; - this.recoveryTicks = 10; // No telegraph for heal - it's a self/ally buff this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -92,6 +91,9 @@ public class AbilityHeal extends Ability { healEntity(ally); spawnHealParticles(world, ally); } + + // Instant heal completes immediately + signalCompletion(); } // If not instant, HoT healing is applied in onActiveTick } @@ -100,16 +102,16 @@ public class AbilityHeal extends Ability { public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { if (world.isRemote || instantHeal) return; - // Heal over time - distribute heal across active ticks + // Heal over time - distribute heal across duration ticks if (tick % 10 == 0) { // Calculate tick-based heal for fixed amount - float tickHeal = (healAmount / (float) activeTicks) * 10; + float tickHeal = (healAmount / (float) durationTicks) * 10; if (healSelf) { float selfTickHeal = tickHeal; // Add percentage-based heal portion if (healPercent > 0) { - selfTickHeal += (npc.getMaxHealth() * healPercent / (float) activeTicks) * 10; + selfTickHeal += (npc.getMaxHealth() * healPercent / (float) durationTicks) * 10; } npc.heal(selfTickHeal); if (tick % 20 == 0) { @@ -123,7 +125,7 @@ public class AbilityHeal extends Ability { float allyTickHeal = tickHeal; // Add percentage-based heal portion for each ally if (healPercent > 0) { - allyTickHeal += (ally.getMaxHealth() * healPercent / (float) activeTicks) * 10; + allyTickHeal += (ally.getMaxHealth() * healPercent / (float) durationTicks) * 10; } ally.heal(allyTickHeal); if (tick % 20 == 0) { @@ -133,6 +135,11 @@ public class AbilityHeal extends Ability { } } } + + // Check if heal duration has ended + if (tick >= durationTicks) { + signalCompletion(); + } } /** @@ -202,6 +209,7 @@ public class AbilityHeal extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("healAmount", healAmount); nbt.setFloat("healPercent", healPercent); nbt.setBoolean("healSelf", healSelf); @@ -212,6 +220,7 @@ public class AbilityHeal extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; this.healAmount = nbt.hasKey("healAmount") ? nbt.getFloat("healAmount") : 10.0f; this.healPercent = nbt.hasKey("healPercent") ? nbt.getFloat("healPercent") : 0.0f; this.healSelf = !nbt.hasKey("healSelf") || nbt.getBoolean("healSelf"); @@ -221,6 +230,14 @@ public class AbilityHeal extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public float getHealAmount() { return healAmount; } 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 fc68200b7..32335daeb 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 @@ -29,10 +29,8 @@ public class AbilityHeavyHit extends Ability { this.maxRange = 3.0f; this.minRange = 0.0f; this.lockMovement = true; - this.cooldownTicks = 80; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 5; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.POINT; this.showTelegraph = false; this.windUpSound = "random.anvil_use"; @@ -63,7 +61,10 @@ public class AbilityHeavyHit extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote || target == null) return; + if (world.isRemote || target == null) { + signalCompletion(); + return; + } // Apply damage with scripted event support boolean wasHit = applyAbilityDamage(npc, target, damage, knockback); @@ -72,6 +73,9 @@ public class AbilityHeavyHit extends Ability { if (wasHit) { applyEffects(target); } + + // Heavy Hit is instant + signalCompletion(); } @Override 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 77df6ff35..8084aaad4 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 @@ -60,10 +60,8 @@ public class AbilityLaserShot extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 35.0f; this.minRange = 3.0f; - this.cooldownTicks = 60; + this.cooldownTicks = 0; this.windUpTicks = 15; - this.activeTicks = 1; - this.recoveryTicks = 5; this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; } @@ -101,7 +99,10 @@ public class AbilityLaserShot extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote) return; + if (world.isRemote) { + signalCompletion(); + return; + } double spawnX = npc.posX; double spawnY = npc.posY + npc.getEyeHeight() * 0.8; @@ -120,10 +121,14 @@ public class AbilityLaserShot extends Ability { ); world.spawnEntityInWorld(laser); + + // Laser entity manages itself - ability is done + signalCompletion(); } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // Laser entity manages itself } @Override 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 38ffd8db4..6bc6da964 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 @@ -69,7 +69,7 @@ public class AbilityOrb extends Ability { private AnchorPoint anchorPoint = AnchorPoint.FRONT; // Transient state for orb entity (used during windup charging) - private transient int orbEntityId = -1; + private transient EntityAbilityOrb orbEntity = null; public AbilityOrb() { this.typeId = "ability.cnpc.orb"; @@ -77,10 +77,8 @@ public class AbilityOrb extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 25.0f; this.minRange = 5.0f; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 1; // Minimal - just spawn and done - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; } @@ -108,17 +106,19 @@ public class AbilityOrb extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote) return; - - // Find and start moving the orb that was spawned during windup - if (orbEntityId != -1) { - Entity entity = world.getEntityByID(orbEntityId); - if (entity instanceof EntityAbilityOrb) { - EntityAbilityOrb orb = (EntityAbilityOrb) entity; - orb.startMoving(target); - } - orbEntityId = -1; + if (world.isRemote) { + signalCompletion(); + return; } + + // Start moving the orb that was spawned during windup + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.startMoving(target); + } + orbEntity = null; + + // Orb entity manages itself - ability is done + signalCompletion(); } @Override @@ -128,7 +128,7 @@ public class AbilityOrb extends Ability { // Spawn orb in charging mode on first tick of windup if (tick == 1) { // Create orb in charging mode - follows NPC based on anchor point during windup - EntityAbilityOrb orb = EntityAbilityOrb.createCharging( + orbEntity = EntityAbilityOrb.createCharging( world, npc, target, orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, @@ -141,8 +141,7 @@ public class AbilityOrb extends Ability { windUpTicks // Charge duration = windup duration ); - world.spawnEntityInWorld(orb); - orbEntityId = orb.getEntityId(); + world.spawnEntityInWorld(orbEntity); } } @@ -157,15 +156,12 @@ public class AbilityOrb extends Ability { } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - // Despawn charging orb if interrupted during windup - if (orbEntityId != -1) { - Entity entity = npc.worldObj.getEntityByID(orbEntityId); - if (entity != null) { - entity.setDead(); - } - orbEntityId = -1; + public void cleanup() { + // Despawn orb entity if still alive + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.setDead(); } + orbEntity = null; } @Override 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 8ff9f5410..4a12ae4eb 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 @@ -38,10 +38,8 @@ public class AbilityProjectile extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.minRange = 5.0f; this.maxRange = 20.0f; - this.cooldownTicks = 60; + this.cooldownTicks = 0; this.windUpTicks = 15; - this.activeTicks = 5; - this.recoveryTicks = 10; // No telegraph for projectile - it's a ranged attack this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -122,6 +120,9 @@ public class AbilityProjectile extends Ability { // Spawn projectile particles (visual trail) spawnProjectileParticles(world, npc, target); + + // Projectile is instant + signalCompletion(); } private void spawnProjectileParticles(World world, EntityNPCInterface npc, EntityLivingBase target) { 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 b5a35831f..71d888978 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 @@ -28,19 +28,14 @@ public class AbilityShockwave extends Ability { private float damage = 8.0f; private int maxTargets = 10; - // Runtime state - private transient boolean executed = false; - public AbilityShockwave() { this.typeId = "ability.cnpc.shockwave"; this.name = "Shockwave"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; this.lockMovement = true; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 25; - this.activeTicks = 5; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "random.explode"; this.activeSound = "random.explode"; @@ -74,13 +69,7 @@ public class AbilityShockwave extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - executed = false; - } - - @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (executed) return; - executed = true; + // Shockwave is instant - apply effect immediately after windup // Get all entities in radius AxisAlignedBB box = npc.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); @@ -124,16 +113,14 @@ public class AbilityShockwave extends Ability { applyEffects(entity); } } + + // Shockwave completes instantly + signalCompletion(); } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - executed = false; - } - - @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - executed = false; + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // Shockwave is instant - nothing to do per-tick } @Override 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 d2c17a3e8..dcb914a96 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 @@ -49,11 +49,8 @@ public class AbilitySlam extends Ability { this.name = "Slam"; this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target this.windUpTicks = 30; - this.activeTicks = 60; // Max time in air (used as timeout if waitForCompletion fails) - this.recoveryTicks = 10; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.lockMovement = true; - this.waitForCompletion = true; // Ability completes when NPC lands, not by activeTicks this.minRange = 2.0f; this.maxRange = 15.0f; this.telegraphType = TelegraphType.CIRCLE; 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 b7ca2aa22..ca41ed6e3 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 @@ -54,10 +54,8 @@ public class AbilitySweeper extends Ability { this.maxRange = 15.0f; this.minRange = 0.0f; this.lockMovement = true; - this.cooldownTicks = 200; + this.cooldownTicks = 0; this.windUpTicks = 60; - this.activeTicks = 400; // Long duration - entity controls actual end - this.recoveryTicks = 5; // Short recovery // Circle telegraph to show range this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; @@ -86,36 +84,36 @@ public class AbilitySweeper extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (!world.isRemote) { - LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ); - - // Spawn the entity that handles BOTH visuals AND damage - activeEntity = new EntityAbilitySweeper(world, npc, target, - beamLength, beamWidth, beamHeight, - innerColor, outerColor, outerColorEnabled, outerColorWidth, - sweepSpeed, numberOfRotations, - damage, damageInterval, piercing, - lockOnTarget); - world.spawnEntityInWorld(activeEntity); - - LogWriter.info("[Sweeper] Spawned sweeper entity"); + if (world.isRemote) { + signalCompletion(); + return; } + + LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ); + + // Spawn the entity that handles BOTH visuals AND damage + activeEntity = new EntityAbilitySweeper(world, npc, target, + beamLength, beamWidth, beamHeight, + innerColor, outerColor, outerColorEnabled, outerColorWidth, + sweepSpeed, numberOfRotations, + damage, damageInterval, piercing, + lockOnTarget); + world.spawnEntityInWorld(activeEntity); + + LogWriter.info("[Sweeper] Spawned sweeper entity"); + // Entity manages itself - completion signaled from onActiveTick when entity dies } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Entity handles everything - nothing to do here - // Check if entity died early (entity manages its own death based on rotations) - if (activeEntity != null && activeEntity.isDead) { - // Entity finished, ability can end early - // The ability system will handle the recovery phase + // Entity handles everything - check if entity finished its rotations + if (activeEntity == null || activeEntity.isDead) { + signalCompletion(); } } @Override - public void reset() { - super.reset(); - + public void cleanup() { // Clean up entity if still alive if (activeEntity != null && !activeEntity.isDead) { activeEntity.setDead(); 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 a79223d5d..668b2f04d 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 @@ -59,10 +59,8 @@ public class AbilityTeleport extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 30.0f; this.minRange = 5.0f; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 10; - this.activeTicks = 40; - this.recoveryTicks = 10; // No telegraph for teleport - it's instant repositioning this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -101,7 +99,10 @@ public class AbilityTeleport extends Ability { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; - if (currentBlink >= blinkLimit) return; + if (currentBlink >= blinkLimit) { + signalCompletion(); + return; + } ticksSinceLastBlink++; @@ -109,6 +110,11 @@ public class AbilityTeleport extends Ability { performBlink(npc, target, world); currentBlink++; ticksSinceLastBlink = 0; + + // Check if this was the last blink + if (currentBlink >= blinkLimit) { + signalCompletion(); + } } } 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 77ac4a5fe..dbb7273cd 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 @@ -34,6 +34,7 @@ public class AbilityTrap extends Ability { AHEAD_OF_CASTER } + private int durationTicks = 200; private TrapPlacement placement = TrapPlacement.AT_TARGET; private float placementDistance = 5.0f; private float triggerRadius = 2.0f; @@ -64,10 +65,8 @@ public class AbilityTrap extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; this.lockMovement = true; - this.cooldownTicks = 150; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 200; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; } @@ -180,6 +179,12 @@ public class AbilityTrap extends Ability { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // Check if trap duration has ended + if (tick >= durationTicks) { + signalCompletion(); + return; + } + if (!armed) { if (tick >= armTime) { armed = true; @@ -190,6 +195,7 @@ public class AbilityTrap extends Ability { ticksSinceLastTrigger++; if (maxTriggers > 0 && triggerCount >= maxTriggers) { + signalCompletion(); // All triggers used, trap is done return; } @@ -277,6 +283,7 @@ public class AbilityTrap extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setString("placement", placement.name()); nbt.setFloat("placementDistance", placementDistance); nbt.setFloat("triggerRadius", triggerRadius); @@ -294,6 +301,7 @@ public class AbilityTrap extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 200; try { this.placement = TrapPlacement.valueOf(nbt.getString("placement")); } catch (Exception e) { @@ -314,6 +322,14 @@ public class AbilityTrap extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public TrapPlacement getPlacement() { return placement; } 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 666b19bbc..70c1c96b7 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 @@ -46,10 +46,8 @@ public class AbilityVortex extends Ability { this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; this.lockMovement = true; - this.cooldownTicks = 120; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 40; - this.recoveryTicks = 5; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.ghast.charge"; this.activeSound = "mob.ghast.fireball"; @@ -119,6 +117,7 @@ public class AbilityVortex extends Ability { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { if (pullComplete || pulledEntities.isEmpty()) { + signalCompletion(); return; } @@ -174,6 +173,7 @@ public class AbilityVortex extends Ability { if (!anyStillPulling && pulledEntities.isEmpty()) { pullComplete = true; + signalCompletion(); } } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 5a7e32d28..593c8aa19 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -7,104 +7,87 @@ import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.Frame; import noppes.npcs.controllers.data.FramePart; +import noppes.npcs.entity.EntityCustomNpc; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.entity.data.ModelScalePart; /** * Utility class for calculating world positions based on anchor points. - * Used for positioning charging effects on different body parts. - * Animation-aware: reads arm positions from NPC animation data when available. + * Supports both EntityNPCInterface and EntityPlayer with animation and model size awareness. */ public class AnchorPointHelper { - // ==================== TUNABLE CONSTANTS ==================== - // These can be adjusted to fine-tune positioning + // Default biped model constants (in model units, 1 unit = 1/16 block) + private static final float MODEL_SCALE = 1f / 16f; - // Shoulder position in model units (default biped arm rotation points) - private static final float SHOULDER_X_RIGHT = -5.0f; // Right arm is on the right side (-X in model space) - private static final float SHOULDER_X_LEFT = 5.0f; // Left arm is on the left side (+X in model space) - private static final float SHOULDER_Y = 2.0f; // Shoulder height from model origin - private static final float SHOULDER_Z = 0.0f; // Shoulder depth + // Shoulder position relative to model origin + private static final float SHOULDER_Y = 2f; + private static final float SHOULDER_X_OFFSET = 5f; // Distance from center to shoulder - // Hand offset in model units (where items are held, relative to shoulder after rotation) - // In arm's local space: X = toward/away from body, Y = down the arm, Z = forward/back - private static float HAND_OFFSET_X_RIGHT = -2.0f; // Slightly toward body for right - private static float HAND_OFFSET_X_LEFT = 2.0f; // Slightly toward body for left - private static float HAND_OFFSET_Y = 14.0f; // Down the arm (arm is ~12 units long) - private static float HAND_OFFSET_Z = 0.0f; // Forward from arm + // Palm position (in arm's local space, relative to shoulder) + // Arm is 12 units long; palm is near the end + private static final float PALM_DOWN = 11f; // Distance down the arm to palm center + private static final float PALM_FORWARD = 2f; // Forward offset to palm surface (not center of hand) + private static final float PALM_INWARD = 0f; // Lateral offset (0 = centered on arm) - // Height of shoulder relative to entity position (feet) - private static final float SHOULDER_HEIGHT_BLOCKS = 1.35f; + // Default height offsets as fraction of entity height + private static final float FRONT_HEIGHT = 0.7f; + private static final float CENTER_HEIGHT = 0.5f; + private static final float ARM_HEIGHT = 0.75f; + private static final float ABOVE_HEAD_HEIGHT = 1.2f; + private static final float CHEST_HEIGHT = 0.65f; - // Model scale factor (1/16 - converts model units to blocks) - private static final float MODEL_SCALE = 0.0625f; + // Distance in front for FRONT anchor + private static final float DEFAULT_FRONT_DISTANCE = 1.0f; - // Fallback offsets (in blocks) when no animation - private static final float ARM_LATERAL_OFFSET = 0.35f; - private static final float ARM_FORWARD_OFFSET = 0.3f; - private static final float FRONT_OFFSET = 1.0f; - - // Height multipliers relative to eye height - private static final float FRONT_HEIGHT_MULT = 0.7f; - private static final float CENTER_HEIGHT_MULT = 0.5f; - private static final float ARM_HEIGHT_MULT = 0.75f; - private static final float ABOVE_HEAD_HEIGHT_MULT = 1.3f; - private static final float CHEST_HEIGHT_MULT = 0.5f; + // Fallback arm positioning (when no animation) + private static final float FALLBACK_LATERAL = 0.35f; + private static final float FALLBACK_FORWARD = 0.3f; /** - * Calculate the world position for a given anchor point on an entity. + * Calculate world position for an anchor point on an entity. */ public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor) { - return calculateAnchorPosition(entity, anchor, FRONT_OFFSET); + return calculateAnchorPosition(entity, anchor, DEFAULT_FRONT_DISTANCE); } /** - * Calculate the world position for a given anchor point on an entity. + * Calculate world position for an anchor point with custom front offset. */ public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor, float frontOffset) { + float scale = getModelScale(entity); + float height = entity.height; + double x = entity.posX; double y = entity.posY; double z = entity.posZ; - float eyeHeight = entity.getEyeHeight(); - // Body yaw in radians (used for arm positioning) - // Minecraft: yaw 0 = south (+Z), 90 = west (-X), 180 = north (-Z), 270 = east (+X) - float bodyYawRad = (float) Math.toRadians(entity.renderYawOffset); - float headYawRad = (float) Math.toRadians(entity.rotationYawHead); + float headYaw = (float) Math.toRadians(entity.rotationYawHead); + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); switch (anchor) { case FRONT: - // In front of entity face (use head yaw) - x -= Math.sin(headYawRad) * frontOffset; - y += eyeHeight * FRONT_HEIGHT_MULT; - z += Math.cos(headYawRad) * frontOffset; + x -= Math.sin(headYaw) * frontOffset * scale; + y += height * FRONT_HEIGHT; + z += Math.cos(headYaw) * frontOffset * scale; break; case CENTER: - y += eyeHeight * CENTER_HEIGHT_MULT; + y += height * CENTER_HEIGHT; break; case RIGHT_HAND: - Vec3 rightPos = getAnimatedHandPosition(entity, true); - if (rightPos != null) { - return rightPos; - } - // Fallback - return calculateFallbackHandPosition(entity, bodyYawRad, true); + return calculateHandPosition(entity, true, scale); case LEFT_HAND: - Vec3 leftPos = getAnimatedHandPosition(entity, false); - if (leftPos != null) { - return leftPos; - } - // Fallback - return calculateFallbackHandPosition(entity, bodyYawRad, false); + return calculateHandPosition(entity, false, scale); case ABOVE_HEAD: - y += eyeHeight * ABOVE_HEAD_HEIGHT_MULT; + y += height * ABOVE_HEAD_HEIGHT; break; case CHEST: - y += eyeHeight * CHEST_HEIGHT_MULT; + y += height * CHEST_HEIGHT; break; } @@ -112,17 +95,51 @@ public class AnchorPointHelper { } /** - * Calculate hand position using NPC animation data. - * Uses the same transformation logic as the renderer. + * Get model scale factor for an entity. + * NPCs have configurable model size, players use default scale. */ - private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, boolean rightArm) { - if (!(entity instanceof EntityNPCInterface)) { - return null; + private static float getModelScale(EntityLivingBase entity) { + if (entity instanceof EntityNPCInterface) { + int modelSize = ((EntityNPCInterface) entity).display.modelSize; + return modelSize / 5f; // modelSize 5 = 100% scale + } + return 1f; + } + + // Default arm scale (no scaling) + private static final ModelScalePart DEFAULT_ARM_SCALE = new ModelScalePart(); + + /** + * Get arm scale for an entity. + * EntityCustomNpc has per-part scaling, others use default (1,1,1). + */ + private static ModelScalePart getArmScale(EntityLivingBase entity) { + if (entity instanceof EntityCustomNpc) { + return ((EntityCustomNpc) entity).modelData.modelScale.arms; + } + return DEFAULT_ARM_SCALE; + } + + /** + * Calculate hand position using animation data if available. + */ + private static Vec3 calculateHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { + // Try to get animated position first + Vec3 animatedPos = getAnimatedHandPosition(entity, rightHand, scale); + if (animatedPos != null) { + return animatedPos; } - EntityNPCInterface npc = (EntityNPCInterface) entity; - AnimationData animData = npc.display.animationData; + // Fallback to static position + return calculateFallbackHandPosition(entity, rightHand, scale); + } + /** + * Calculate hand position from animation data. + * Works for both NPCs and Players. + */ + private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { + AnimationData animData = AnimationData.getData(entity); if (animData == null || !animData.isActive() || animData.animation == null) { return null; } @@ -132,146 +149,172 @@ public class AnchorPointHelper { return null; } - EnumAnimationPart armPart = rightArm ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM; - if (!frame.frameParts.containsKey(armPart)) { - return null; - } - + EnumAnimationPart armPart = rightHand ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM; FramePart part = frame.frameParts.get(armPart); if (part == null) { return null; } - // Ensure interpolation is up to date - part.interpolateOffset(); - part.interpolateAngles(); + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(entity); - // === STEP 1: Get shoulder position in model space === - float shoulderX = rightArm ? SHOULDER_X_RIGHT : SHOULDER_X_LEFT; + // Get rotation and pivot values + float[] rotations = getRotations(part, entity); + float[] pivots = getPivots(part, entity); + + // Shoulder position in model space + float shoulderX = rightHand ? -SHOULDER_X_OFFSET : SHOULDER_X_OFFSET; float shoulderY = SHOULDER_Y; - float shoulderZ = SHOULDER_Z; + float shoulderZ = 0f; - // Add animation pivot offsets - if (part.prevPivots != null) { - shoulderX += part.prevPivots[0]; - shoulderY += part.prevPivots[1]; - shoulderZ += part.prevPivots[2]; - } + // Add pivot offsets + shoulderX += pivots[0]; + shoulderY += pivots[1]; + shoulderZ += pivots[2]; - // === STEP 2: Get hand offset in arm's local space === - float handX = rightArm ? HAND_OFFSET_X_RIGHT : HAND_OFFSET_X_LEFT; - float handY = HAND_OFFSET_Y; - float handZ = HAND_OFFSET_Z; + // Palm offset in arm's local space (before rotation) + // Apply arm scaling to the palm position + float palmX = (rightHand ? -PALM_INWARD : PALM_INWARD) * armScale.scaleX; + float palmY = PALM_DOWN * armScale.scaleY; + float palmZ = PALM_FORWARD * armScale.scaleZ; - // === STEP 3: Apply arm rotations to hand offset (Z → Y → X order) === - // Rotations are in radians (already converted by interpolateAngles) - float rotX = part.prevRotations != null ? part.prevRotations[0] : 0; - float rotY = part.prevRotations != null ? part.prevRotations[1] : 0; - float rotZ = part.prevRotations != null ? part.prevRotations[2] : 0; + // Apply arm rotations (order: Z -> Y -> X) + double hx = palmX, hy = palmY, hz = palmZ; - // Start with hand offset - double hx = handX; - double hy = handY; - double hz = handZ; - - // Apply Z rotation first - double cosZ = Math.cos(rotZ); - double sinZ = Math.sin(rotZ); + // Z rotation + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); double newX = hx * cosZ - hy * sinZ; double newY = hx * sinZ + hy * cosZ; hx = newX; hy = newY; - // Apply Y rotation - double cosY = Math.cos(rotY); - double sinY = Math.sin(rotY); + // Y rotation + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); newX = hx * cosY + hz * sinY; double newZ = -hx * sinY + hz * cosY; hx = newX; hz = newZ; - // Apply X rotation - double cosX = Math.cos(rotX); - double sinX = Math.sin(rotX); + // X rotation + double cosX = Math.cos(rotations[0]); + double sinX = Math.sin(rotations[0]); newY = hy * cosX - hz * sinX; newZ = hy * sinX + hz * cosX; hy = newY; hz = newZ; - // === STEP 4: Combine shoulder + rotated hand offset (in model units) === + // Combine shoulder + rotated hand offset double modelX = shoulderX + hx; double modelY = shoulderY + hy; double modelZ = shoulderZ + hz; - // === STEP 5: Convert to blocks === - double blockX = modelX * MODEL_SCALE; - double blockY = modelY * MODEL_SCALE; - double blockZ = modelZ * MODEL_SCALE; + // Convert to blocks and apply model scale + double blockX = modelX * MODEL_SCALE * scale; + double blockY = modelY * MODEL_SCALE * scale; + double blockZ = modelZ * MODEL_SCALE * scale; - // === STEP 6: Apply body yaw rotation === - // In model space: +X = left, -X = right, +Z = back, -Z = front - // Need to rotate to align with entity's facing direction + // Apply body yaw rotation float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); double cosYaw = Math.cos(bodyYaw); double sinYaw = Math.sin(bodyYaw); - // Rotate around Y axis (body rotation) double worldOffsetX = blockX * cosYaw + blockZ * sinYaw; double worldOffsetZ = -blockX * sinYaw + blockZ * cosYaw; - // === STEP 7: Final world position === + // Calculate shoulder height based on entity and scale + float shoulderHeight = entity.height * ARM_HEIGHT; + + // Final world position double worldX = entity.posX + worldOffsetX; - double worldY = entity.posY + SHOULDER_HEIGHT_BLOCKS - blockY; // Subtract because +Y in model is down + double worldY = entity.posY + shoulderHeight - blockY; double worldZ = entity.posZ + worldOffsetZ; return Vec3.createVectorHelper(worldX, worldY, worldZ); } + /** + * Get rotation values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getRotations(FramePart part, EntityLivingBase entity) { + if (entity.worldObj.isRemote) { + // Client side - use interpolated values (already in radians) + part.interpolateAngles(); + return part.prevRotations; + } else { + // Server side - convert raw degrees to radians + float pi = (float) Math.PI / 180f; + return new float[]{ + part.rotation[0] * pi, + part.rotation[1] * pi, + part.rotation[2] * pi + }; + } + } + + /** + * Get pivot values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getPivots(FramePart part, EntityLivingBase entity) { + if (entity.worldObj.isRemote) { + // Client side - use interpolated values + part.interpolateOffset(); + return part.prevPivots; + } else { + // Server side - use raw values + return part.pivot; + } + } + /** * Calculate fallback hand position when no animation is active. */ - private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, float bodyYawRad, boolean rightHand) { - double x = entity.posX; - double y = entity.posY; - double z = entity.posZ; - float eyeHeight = entity.getEyeHeight(); + private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(entity); // Perpendicular direction for lateral offset - float perpYaw = bodyYawRad + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2); + float perpYaw = bodyYaw + (rightHand ? (float) (Math.PI / 2) : (float) (-Math.PI / 2)); - // Lateral offset (perpendicular to body facing) - double lateralX = -Math.sin(perpYaw) * ARM_LATERAL_OFFSET; - double lateralZ = Math.cos(perpYaw) * ARM_LATERAL_OFFSET; + // Calculate offsets scaled by model size and arm scale + double lateralX = -Math.sin(perpYaw) * FALLBACK_LATERAL * scale * armScale.scaleX; + double lateralZ = Math.cos(perpYaw) * FALLBACK_LATERAL * scale * armScale.scaleX; + double forwardX = -Math.sin(bodyYaw) * FALLBACK_FORWARD * scale * armScale.scaleZ; + double forwardZ = Math.cos(bodyYaw) * FALLBACK_FORWARD * scale * armScale.scaleZ; - // Forward offset (in facing direction) - double forwardX = -Math.sin(bodyYawRad) * ARM_FORWARD_OFFSET; - double forwardZ = Math.cos(bodyYawRad) * ARM_FORWARD_OFFSET; + // Arm length affects vertical position + double armLengthOffset = (1 - armScale.scaleY) * 0.3; - x += lateralX + forwardX; - y += eyeHeight * ARM_HEIGHT_MULT; - z += lateralZ + forwardZ; + double x = entity.posX + lateralX + forwardX; + double y = entity.posY + entity.height * ARM_HEIGHT - armLengthOffset; + double z = entity.posZ + lateralZ + forwardZ; return Vec3.createVectorHelper(x, y, z); } /** - * Get the height multiplier for a given anchor point. + * Get default height multiplier for an anchor point. */ public static float getHeightMultiplier(AnchorPoint anchor) { switch (anchor) { case FRONT: - return FRONT_HEIGHT_MULT; + return FRONT_HEIGHT; case CENTER: - return CENTER_HEIGHT_MULT; + return CENTER_HEIGHT; case RIGHT_HAND: case LEFT_HAND: - return ARM_HEIGHT_MULT; + return ARM_HEIGHT; case ABOVE_HEAD: - return ABOVE_HEAD_HEIGHT_MULT; + return ABOVE_HEAD_HEIGHT; case CHEST: - return CHEST_HEIGHT_MULT; + return CHEST_HEIGHT; default: - return FRONT_HEIGHT_MULT; + return FRONT_HEIGHT; } } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index a9c670647..92bb1241c 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -170,8 +170,8 @@ public class DataAbilities { currentAbility.onActiveTick(npc, target, npc.worldObj, currentAbility.getCurrentTick()); break; - case RECOVERY: - // Recovery phase - just wait + case DAZED: + // Dazed phase - NPC cannot attack, just wait for daze to end break; case IDLE: @@ -222,12 +222,10 @@ public class DataAbilities { releaseLockedRotation(); // Calculate cooldown: random(min, max) + ability offset - int baseCooldown = minCooldown; - if (maxCooldown > minCooldown) { - baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); - } - int totalCooldown = baseCooldown + currentAbility.getCooldownTicks(); - cooldownEndTime = npc.worldObj.getTotalWorldTime() + totalCooldown; + int abilityCooldownOffset = currentAbility.getCooldownTicks(); + rollCooldown(); + // Add ability-specific cooldown offset + cooldownEndTime += abilityCooldownOffset; currentAbility = null; lastTarget = null; @@ -539,8 +537,8 @@ public class DataAbilities { /** * Interrupt the currently executing ability. - * The ability will transition to RECOVERY phase (dazed state) and continue ticking. - * When RECOVERY completes, the ability will move to IDLE and onAbilityComplete() is called. + * If interrupted during WINDUP, the ability will transition to DAZED phase. + * When DAZED completes, the ability will move to IDLE and onAbilityComplete() is called. */ public void interruptCurrentAbility(DamageSource source, float damage) { if (currentAbility != null) { @@ -559,10 +557,10 @@ public class DataAbilities { NpcAPI.EVENT_BUS.post(interruptEvent); currentAbility.onInterrupt(npc, source, damage); - currentAbility.interrupt(); // This now transitions to RECOVERY, not IDLE + currentAbility.interrupt(); // Transitions to DAZED if interrupted during WINDUP - // Don't clear currentAbility - let it tick through RECOVERY phase - // When RECOVERY ends and phase becomes IDLE, onAbilityComplete() will be called + // Don't clear currentAbility - let it tick through DAZED phase + // When DAZED ends and phase becomes IDLE, onAbilityComplete() will be called } } @@ -631,10 +629,13 @@ public class DataAbilities { /** * Reset all runtime state (cooldowns, current ability). * Called when NPC respawns or combat ends. + * Rolls cooldown so NPC doesn't immediately use an ability. */ public void reset() { stopCurrentAbility(); - resetCooldown(); + + // Roll cooldown so NPC doesn't immediately attack after reset + rollCooldown(); // Reset execution state on all abilities for (Ability ability : abilities) { @@ -642,6 +643,18 @@ public class DataAbilities { } } + /** + * Roll a new cooldown using the min/max range. + * Called after ability completes and on reset. + */ + private void rollCooldown() { + int baseCooldown = minCooldown; + if (maxCooldown > minCooldown) { + baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); + } + cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown; + } + // ═══════════════════════════════════════════════════════════════════ // ABILITY LIST MANAGEMENT // ═══════════════════════════════════════════════════════════════════ @@ -706,7 +719,7 @@ public class DataAbilities { * Rules: * - WINDUP phase: Block AI if lockMovement is true * - ACTIVE phase: Block AI if lockMovement OR hasAbilityMovement is true - * - RECOVERY phase: Block AI if lockMovement is true + * - DAZED phase: Always block AI (NPC is stunned) */ public boolean isAbilityControllingMovement() { if (currentAbility == null || !currentAbility.isExecuting()) { @@ -727,9 +740,9 @@ public class DataAbilities { // - hasAbilityMovement is true (ability is moving the NPC itself) return currentAbility.isLockMovement() || currentAbility.hasAbilityMovement(); - case RECOVERY: - // During recovery, block if lockMovement is set - return currentAbility.isLockMovement(); + case DAZED: + // During dazed phase, always block - NPC is stunned from interrupt + return true; default: return false; @@ -739,7 +752,7 @@ public class DataAbilities { /** * Check if NPC's look direction should be locked to target. * Only locks during ACTIVE phase when lockMovement is true. - * This allows the NPC to freely track target during WINDUP and RECOVERY. + * This allows the NPC to freely track target during WINDUP and DAZED. */ public boolean shouldLockLookDirection() { if (currentAbility == null || !currentAbility.isExecuting()) { diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 6c5323707..040c999a4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -61,8 +61,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Timing protected int cooldownTicks; protected int windUpTicks; - protected int activeTicks; - protected int recoveryTicks; + protected int dazedTicks; // ═══════════════════════════════════════════════════════════════════════════ // CACHED VALUES - Target Tab @@ -115,8 +114,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Timing this.cooldownTicks = ability.getCooldownTicks(); this.windUpTicks = ability.getWindUpTicks(); - this.activeTicks = ability.getActiveTicks(); - this.recoveryTicks = ability.getRecoveryTicks(); + this.dazedTicks = ability.getDazedTicks(); // Target this.minRange = ability.getMinRange(); @@ -254,42 +252,36 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi y += 24; - // Row 3: Interruptible + // Row 3: Interruptible + Dazed Ticks (only if interruptible) addLabel(new GuiNpcLabel(6, "ability.interruptible", col1LabelX, y + 5)); addButton(new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0)); + // Only show dazed ticks if interruptible is enabled + if (interruptible) { + addLabel(new GuiNpcLabel(12, "ability.dazed", col2LabelX, y + 5)); + GuiNpcTextField dazedField = new GuiNpcTextField(13, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(dazedTicks)); + dazedField.setIntegersOnly(); + dazedField.setMinMaxDefault(0, 1000, 80); + addTextField(dazedField); + } + y += 28; // Separator - Timing section header addLabel(new GuiNpcLabel(7, "ability.timing", col1LabelX, y)); y += 14; - // Row 4: Windup Ticks + Active Ticks + // Row 4: Windup Ticks + Cooldown Ticks addLabel(new GuiNpcLabel(10, "ability.windup", col1LabelX, y + 5)); GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); windupField.setIntegersOnly(); windupField.setMinMaxDefault(0, 1000, 20); addTextField(windupField); - addLabel(new GuiNpcLabel(11, "ability.active", col2LabelX, y + 5)); - GuiNpcTextField activeField = new GuiNpcTextField(12, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(activeTicks)); - activeField.setIntegersOnly(); - activeField.setMinMaxDefault(1, 1000, 10); - addTextField(activeField); - - y += 24; - - // Row 5: Recovery Ticks + Cooldown Ticks - addLabel(new GuiNpcLabel(12, "ability.recovery", col1LabelX, y + 5)); - GuiNpcTextField recoveryField = new GuiNpcTextField(13, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(recoveryTicks)); - recoveryField.setIntegersOnly(); - recoveryField.setMinMaxDefault(0, 1000, 20); - addTextField(recoveryField); - addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5)); GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks)); cooldownField.setIntegersOnly(); - cooldownField.setMinMaxDefault(0, 10000, 100); + cooldownField.setMinMaxDefault(0, 10000, 0); addTextField(cooldownField); } @@ -632,6 +624,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // General tab buttons else if (id == 14) { interruptible = ((GuiNpcButton) guibutton).getValue() == 1; + initGui(); // Refresh to show/hide dazed ticks field } else if (id == 16) { lockMovement = ((GuiNpcButton) guibutton).getValue() == 1; } @@ -764,10 +757,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi cooldownTicks = textField.getInteger(); } else if (id == 11) { windUpTicks = textField.getInteger(); - } else if (id == 12) { - activeTicks = textField.getInteger(); } else if (id == 13) { - recoveryTicks = textField.getInteger(); + dazedTicks = textField.getInteger(); } // Visual-specific fields (200+) - delegate to subclass @@ -865,8 +856,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Timing ability.setCooldownTicks(cooldownTicks); ability.setWindUpTicks(windUpTicks); - ability.setActiveTicks(activeTicks); - ability.setRecoveryTicks(recoveryTicks); + ability.setDazedTicks(dazedTicks); // Target ability.setMinRange(minRange); @@ -914,8 +904,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi this.cooldownTicks = ability.getCooldownTicks(); this.windUpTicks = ability.getWindUpTicks(); - this.activeTicks = ability.getActiveTicks(); - this.recoveryTicks = ability.getRecoveryTicks(); + this.dazedTicks = ability.getDazedTicks(); this.minRange = ability.getMinRange(); this.maxRange = ability.getMaxRange(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java index 10f5ab8c1..f3dbc64c7 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -68,8 +68,15 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { y += 24; - // Row 5: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); + // Row 5: Spin Duration (only for SPIN mode) + Effects button + if (cutter.getSweepMode() == AbilityCutter.SweepMode.SPIN) { + addLabel(new GuiNpcLabel(108, "ability.duration", labelX, y + 5)); + GuiNpcTextField durationField = createIntField(108, fieldX, y, 50, cutter.getSpinDurationTicks()); + durationField.setMinMaxDefault(1, 1000, 60); + addTextField(durationField); + } + + addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); } @Override @@ -78,6 +85,7 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { switch (id) { case 106: cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); + initGui(); // Refresh to show/hide duration field break; case 107: cutter.setPiercing(value == 1); @@ -121,6 +129,9 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { case 105: cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); break; + case 108: + cutter.setSpinDurationTicks(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java index 25383c926..183d66814 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java @@ -31,9 +31,14 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 165; int col2FieldX = guiLeft + 225; - // Row 1: Damage Reduction - addLabel(new GuiNpcLabel(100, "ability.dmgReduce", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, guard.getDamageReduction())); + // Row 1: Duration + Damage Reduction + addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5)); + GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, guard.getDurationTicks()); + durationField.setMinMaxDefault(1, 1000, 60); + addTextField(durationField); + + addLabel(new GuiNpcLabel(100, "ability.dmgReduce", col2LabelX, y + 5)); + addTextField(createFloatField(100, col2FieldX, y, 50, guard.getDamageReduction())); y += 24; @@ -112,6 +117,9 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { + case 99: + guard.setDurationTicks(field.getInteger()); + break; case 100: guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); break; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java index 8a91798d0..c9633c0f9 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -31,14 +31,22 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 145; int col2FieldX = guiLeft + 205; - // Row 1: Shape + Placement - addLabel(new GuiNpcLabel(100, "ability.shape", labelX, y + 5)); - String[] shapes = {"Circle", "Ring", "Cone"}; - addButton(new GuiNpcButton(100, fieldX, y, 55, 20, shapes, hazard.getShape().ordinal())); + // Row 1: Duration + Shape + addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5)); + GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, hazard.getDurationTicks()); + durationField.setMinMaxDefault(1, 2000, 100); + addTextField(durationField); - addLabel(new GuiNpcLabel(101, "ability.placement", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(100, "ability.shape", col2LabelX, y + 5)); + String[] shapes = {"Circle", "Ring", "Cone"}; + addButton(new GuiNpcButton(100, col2FieldX, y, 55, 20, shapes, hazard.getShape().ordinal())); + + y += 24; + + // Row 2: Placement + addLabel(new GuiNpcLabel(101, "ability.placement", labelX, y + 5)); String[] placements = {"Caster", "Target", "FollowCast", "FollowTgt"}; - addButton(new GuiNpcButton(101, col2FieldX, y, 60, 20, placements, hazard.getPlacement().ordinal())); + addButton(new GuiNpcButton(101, fieldX, y, 70, 20, placements, hazard.getPlacement().ordinal())); y += 24; @@ -116,6 +124,9 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { + case 99: + hazard.setDurationTicks(field.getInteger()); + break; case 102: hazard.setRadius(parseFloat(field, hazard.getRadius())); break; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java index 8b679fb25..010da51cf 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java @@ -27,7 +27,20 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 145; int col2FieldX = guiLeft + 205; - // Row 1: Heal Amount + Heal Percent + // Row 1: Instant Heal + Duration (only shown if not instant) + addLabel(new GuiNpcLabel(103, "dialog.instant", labelX, y + 5)); + addButton(new GuiNpcButton(103, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0)); + + if (!heal.isInstantHeal()) { + addLabel(new GuiNpcLabel(99, "ability.duration", col2LabelX, y + 5)); + GuiNpcTextField durationField = createIntField(99, col2FieldX, y, 50, heal.getDurationTicks()); + durationField.setMinMaxDefault(1, 1000, 60); + addTextField(durationField); + } + + y += 24; + + // Row 2: Heal Amount + Heal Percent addLabel(new GuiNpcLabel(100, "ability.healAmount", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, heal.getHealAmount())); @@ -36,16 +49,13 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { y += 24; - // Row 2: Heal Radius + Instant Heal + // Row 3: Heal Radius addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius())); - addLabel(new GuiNpcLabel(103, "dialog.instant", col2LabelX, y + 5)); - addButton(new GuiNpcButton(103, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0)); - y += 24; - // Row 3: Heal Self + Heal Allies + // Row 4: Heal Self + Heal Allies addLabel(new GuiNpcLabel(104, "ability.healSelf", labelX, y + 5)); addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealSelf() ? 1 : 0)); @@ -59,6 +69,7 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { switch (id) { case 103: heal.setInstantHeal(value == 1); + initGui(); // Refresh to show/hide duration field break; case 104: heal.setHealSelf(value == 1); @@ -72,6 +83,9 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { + case 99: + heal.setDurationTicks(field.getInteger()); + break; case 100: heal.setHealAmount(parseFloat(field, heal.getHealAmount())); break; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java index 2c0fc3a68..c846565c4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -31,13 +31,21 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 145; int col2FieldX = guiLeft + 205; - // Row 1: Placement + Trigger Radius - addLabel(new GuiNpcLabel(100, "ability.placement", labelX, y + 5)); - String[] placements = {"Caster", "Target", "Ahead"}; - addButton(new GuiNpcButton(100, fieldX, y, 55, 20, placements, trap.getPlacement().ordinal())); + // Row 1: Duration + Placement + addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5)); + GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, trap.getDurationTicks()); + durationField.setMinMaxDefault(1, 2000, 200); + addTextField(durationField); - addLabel(new GuiNpcLabel(101, "ability.triggerRad", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, trap.getTriggerRadius())); + addLabel(new GuiNpcLabel(100, "ability.placement", col2LabelX, y + 5)); + String[] placements = {"Caster", "Target", "Ahead"}; + addButton(new GuiNpcButton(100, col2FieldX, y, 55, 20, placements, trap.getPlacement().ordinal())); + + y += 24; + + // Row 2: Trigger Radius + addLabel(new GuiNpcLabel(101, "ability.triggerRad", labelX, y + 5)); + addTextField(createFloatField(101, fieldX, y, 50, trap.getTriggerRadius())); y += 24; @@ -90,6 +98,9 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { + case 99: + trap.setDurationTicks(field.getInteger()); + break; case 101: trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); break;