Refactor ability handling: replace damage handler with ability extenders for improved lifecycle management and damage routing

This commit is contained in:
Kam committed 2026-02-14 21:06:12 -05:00
1 parent f8c75dd79b
commit 0f774a68fc
7 files changed
+184 -53

No files matched your search

@@ -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) {
@@ -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<String, List<AbilityVariant>> externalVariants = new LinkedHashMap<>();
private final List<IAbilityFieldProvider> fieldProviders = new ArrayList<>();
private IAbilityDamageHandler damageHandler = null;
private final List<IAbilityExtender> extenders = new ArrayList<>();
// ── Derived State ────────────────────────────────────────────────────────
private final Set<String> 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<IAbilityExtender> 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
// ═══════════════════════════════════════════════════════════════════════════
@@ -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);
}
@@ -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.
* <p>
* 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).
* <p>
* 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;
}
}
@@ -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) {
@@ -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);
@@ -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();