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 1878337b8..f755bc370 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -329,13 +329,10 @@ public abstract class Ability implements IAbility { // Apply damage if (damage > 0) { - // Check for external damage handler first (e.g., DBC Addon) - IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler(); - boolean handled = false; - if (handler != null) { - handled = handler.handleDamage(this, caster, hitEntity, damage, knockback, knockbackUp, - knockbackDirX, knockbackDirZ); - } + // Check for ability extenders (e.g., DBC Addon damage routing) + boolean handled = AbilityController.Instance.fireOnAbilityDamage( + this, caster, hitEntity, damage, knockback, knockbackUp, + knockbackDirX, knockbackDirZ); if (!handled) { // Default damage path if (caster instanceof EntityNPCInterface) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index b65276211..c86e3e623 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -2,6 +2,7 @@ package kamkeel.npcs.controllers.data.ability; import kamkeel.npcs.controllers.data.ability.type.*; import kamkeel.npcs.controllers.SyncController; +import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -24,7 +25,7 @@ public class AbilityController implements IAbilityHandler { // ── Extension Points ───────────────────────────────────────────────────── private final Map> externalVariants = new LinkedHashMap<>(); private final List fieldProviders = new ArrayList<>(); - private IAbilityDamageHandler damageHandler = null; + private final List extenders = new ArrayList<>(); // ── Derived State ──────────────────────────────────────────────────────── private final Set builtInTypeIds = new HashSet<>(); @@ -350,14 +351,6 @@ public class AbilityController implements IAbilityHandler { return result; } - public void registerDamageHandler(IAbilityDamageHandler handler) { - this.damageHandler = handler; - } - - public IAbilityDamageHandler getDamageHandler() { - return damageHandler; - } - public void registerFieldProvider(IAbilityFieldProvider provider) { fieldProviders.add(provider); } @@ -366,6 +359,64 @@ public class AbilityController implements IAbilityHandler { return fieldProviders; } + public void registerExtender(IAbilityExtender extender) { + extenders.add(extender); + } + + public List getExtenders() { + return extenders; + } + + /** + * Fire onAbilityStart on all extenders. Returns false if ANY extender cancels. + */ + public boolean fireOnAbilityStart(Ability ability, EntityLivingBase caster, EntityLivingBase target) { + for (IAbilityExtender ext : extenders) { + if (!ext.onAbilityStart(ability, caster, target)) { + return false; + } + } + return true; + } + + /** + * Fire onAbilityTick on all extenders. Returns false if ANY extender says to interrupt. + */ + public boolean fireOnAbilityTick(Ability ability, EntityLivingBase caster, EntityLivingBase target, + AbilityPhase phase, int tick) { + for (IAbilityExtender ext : extenders) { + if (!ext.onAbilityTick(ability, caster, target, phase, tick)) { + return false; + } + } + return true; + } + + /** + * Fire onAbilityComplete on all extenders. + */ + public void fireOnAbilityComplete(Ability ability, EntityLivingBase caster, EntityLivingBase target, + boolean interrupted) { + for (IAbilityExtender ext : extenders) { + ext.onAbilityComplete(ability, caster, target, interrupted); + } + } + + /** + * Fire onAbilityDamage on all extenders. Chain of responsibility — first true wins. + */ + public boolean fireOnAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ) { + for (IAbilityExtender ext : extenders) { + if (ext.onAbilityDamage(ability, caster, target, damage, knockback, knockbackUp, + knockbackDirX, knockbackDirZ)) { + return true; + } + } + return false; + } + // ═══════════════════════════════════════════════════════════════════════════ // IAbilityHandler // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java deleted file mode 100644 index 9a7f353c1..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package kamkeel.npcs.controllers.data.ability; - -import net.minecraft.entity.EntityLivingBase; - -/** - * External damage handler for abilities. - * When registered, called AFTER script events but BEFORE default attackEntityFrom. - * If handleDamage returns true, default damage is skipped. - */ -public interface IAbilityDamageHandler { - /** - * Handle damage from an ability. - * - * @param ability The ability dealing damage - * @param caster The entity executing the ability - * @param target The entity being hit - * @param damage The damage amount (after script event modifications) - * @param knockback The horizontal knockback - * @param knockbackUp The vertical knockback - * @param knockbackDirX The X component of knockback direction - * @param knockbackDirZ The Z component of knockback direction - * @return true if damage was handled (skip default), false to use default damage - */ - boolean handleDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, - float damage, float knockback, float knockbackUp, - double knockbackDirX, double knockbackDirZ); -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityExtender.java b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityExtender.java new file mode 100644 index 000000000..e3e7e5c0b --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityExtender.java @@ -0,0 +1,76 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.entity.EntityLivingBase; + +/** + * General-purpose extension point for abilities. Multiple extenders can be registered. + * Covers lifecycle hooks (start/tick/complete) and damage handling. + *

+ * All methods have no-op defaults so extenders only override what they need. + */ +public interface IAbilityExtender { + + /** + * Called before an ability starts executing. + * Return false to cancel the ability (e.g., insufficient resources). + * + * @param ability The ability about to start + * @param caster The entity executing the ability (NPC or Player) + * @param target The target entity (may be null for player self-cast) + * @return true to allow, false to cancel + */ + default boolean onAbilityStart(Ability ability, EntityLivingBase caster, EntityLivingBase target) { + return true; + } + + /** + * Called each tick during ability execution. + * Return false to interrupt the ability (e.g., resource depleted). + * + * @param ability The executing ability + * @param caster The entity executing the ability + * @param target The target entity (may be null) + * @param phase The current ability phase (WINDUP, ACTIVE, DAZED, BURST_DELAY) + * @param tick The current tick within the phase + * @return true to continue, false to interrupt + */ + default boolean onAbilityTick(Ability ability, EntityLivingBase caster, EntityLivingBase target, + AbilityPhase phase, int tick) { + return true; + } + + /** + * Called when an ability completes (normally or via interruption). + * + * @param ability The completed ability + * @param caster The entity that was executing the ability + * @param target The target entity (may be null) + * @param interrupted true if the ability was interrupted, false if it completed normally + */ + default void onAbilityComplete(Ability ability, EntityLivingBase caster, EntityLivingBase target, + boolean interrupted) { + } + + /** + * Called when an ability deals damage, AFTER script events but BEFORE default attackEntityFrom. + * Return true to handle the damage (skips default damage application). + *

+ * Chain of responsibility: extenders are called in registration order. + * The first extender returning true claims the damage; remaining extenders are skipped. + * + * @param ability The ability dealing damage + * @param caster The entity executing the ability + * @param target The entity being hit + * @param damage The damage amount (after script event modifications) + * @param knockback The horizontal knockback + * @param knockbackUp The vertical knockback + * @param knockbackDirX The X component of knockback direction + * @param knockbackDirZ The Z component of knockback direction + * @return true if damage was handled (skip default), false to pass to next extender or default + */ + default boolean onAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ) { + return false; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 9b30abab6..dee2ed292 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -8,7 +8,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.IAbilityDamageHandler; + import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; @@ -326,17 +326,15 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP if (previewMode) return; // Skip damage in preview mode Entity owner = getOwnerEntity(); - // Check for external damage handler (e.g., DBC Addon) + // Check for ability extenders (e.g., DBC Addon damage routing) boolean handled = false; if (sourceAbility != null && owner instanceof EntityLivingBase) { - IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler(); - if (handler != null) { - double dx = target.posX - posX; - double dz = target.posZ - posZ; - float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f; - handled = handler.handleDamage(sourceAbility, (EntityLivingBase) owner, target, - dmg, kb, kbUp, dx, dz); - } + double dx = target.posX - posX; + double dz = target.posZ - posZ; + float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f; + handled = AbilityController.Instance.fireOnAbilityDamage( + sourceAbility, (EntityLivingBase) owner, target, + dmg, kb, kbUp, dx, dz); } if (!handled) { diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 8edcba628..eb3082a59 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -171,6 +171,13 @@ public class DataAbilities { // Fire tick event for scripts fireTickEvent(currentAbility, target); + // Fire extender tick hook (e.g., per-tick resource drain) + if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, npc, target, + currentAbility.getPhase(), currentAbility.getCurrentTick())) { + interruptCurrentAbility(null, 0); + return; + } + // Handle phase-specific logic switch (currentAbility.getPhase()) { case WINDUP: @@ -342,6 +349,9 @@ public class DataAbilities { // Remove any remaining telegraphs (for abilities that keep telegraph during active) removeTelegraph(currentAbility); + // Fire extender complete hook + AbilityController.Instance.fireOnAbilityComplete(currentAbility, npc, target, false); + // Call onComplete callback currentAbility.onComplete(npc, target); @@ -501,6 +511,11 @@ public class DataAbilities { return false; } + // Fire extender start hook (e.g., resource cost checks) + if (!AbilityController.Instance.fireOnAbilityStart(ability, npc, target)) { + return false; + } + currentAbility = ability; lastTarget = target; ability.start(target); @@ -750,6 +765,9 @@ public class DataAbilities { releaseRotationControl(); releaseLockedPosition(); + // Fire extender complete hook (interrupted) + AbilityController.Instance.fireOnAbilityComplete(currentAbility, npc, lastTarget, true); + // Fire interrupt event AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent( npc.wrappedNPC, currentAbility, lastTarget, source, damage); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 0f87ddf5a..aa8d4301d 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -147,6 +147,13 @@ public class PlayerAbilityData implements IPlayerAbilityData { // Tick advances time and possibly changes phase boolean phaseChanged = currentAbility.tick(); + // Fire extender tick hook (e.g., per-tick resource drain) + if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, player, currentTarget, + currentAbility.getPhase(), currentAbility.getCurrentTick())) { + interruptCurrentAbility(); + return; + } + switch (currentAbility.getPhase()) { case WINDUP: if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) { @@ -249,6 +256,9 @@ public class PlayerAbilityData implements IPlayerAbilityData { private void handleAbilityCompletion(EntityPlayer player) { if (currentAbility == null) return; + // Fire extender complete hook + AbilityController.Instance.fireOnAbilityComplete(currentAbility, player, currentTarget, false); + currentAbility.onComplete(player, currentTarget); // Release all locks @@ -318,6 +328,11 @@ public class PlayerAbilityData implements IPlayerAbilityData { // Check conditions (skip target-requiring ones) if (!ability.checkConditionsForPlayer(player)) return false; + // Fire extender start hook (e.g., resource cost checks) + if (!AbilityController.Instance.fireOnAbilityStart(ability, player, null)) { + return false; + } + // Start the ability currentAbility = ability; currentAbilityKey = key; @@ -517,6 +532,9 @@ public class PlayerAbilityData implements IPlayerAbilityData { public void interruptCurrentAbility() { if (currentAbility != null && currentAbility.isExecuting()) { + // Fire extender complete hook (interrupted) + AbilityController.Instance.fireOnAbilityComplete(currentAbility, playerData.player, currentTarget, true); + currentAbility.interrupt(); stopAbilityAnimation(); releaseRotationControl();