Buff Interceptions
The buff system includes a powerful interception mechanism that allows buffs to intercept, modify, or cancel the application of other buffs.
Interception Structure
Interceptions are defined in the interceptBuffEffects array of a buff. Each entry can match an incoming buff by name, by stat contribution, or both:
interceptBuffEffects?: {
/**
* Buffs this interceptor reacts to. Either a specific buff (matched by
* name) or leave it unset when only `statFilter` should drive matching.
*/
buff?: Buff | string;
/**
* When set, this interceptor reacts to ANY incoming buff whose `stats`
* map evaluates to a non-zero contribution against any of these stats.
* Lets modders build category-wide rules (e.g. "intercept every buff that
* grants Power") without listing every applicable buff by name, so the
* interceptor can also catch buff names introduced by later mods.
*
* Can be combined with `buff` to broaden the criterion: an entry with
* both set matches an incoming buff when EITHER the name OR the stat
* filter is satisfied. At least one of `buff` / `statFilter` must be
* set, since an entry with neither matches nothing.
*
* Each listed stat is matched independently, so `['power', 'defense']`
* matches any buff contributing non-zero to Power OR Defense.
*/
statFilter?: CombatStatistic[];
effects: BuffEffect[];
/**
* How many of the incoming stacks this interceptor blocks before the buff is
* applied. Evaluated once per application (NOT per stack) with `incoming` (the
* remaining incoming stack count) available. The blocked stacks are removed
* from the application, so `{ value: 9999 }` blocks all of them. Omit to block
* nothing (a pure listener that reacts without consuming any stacks).
*
* Either way the `effects` run exactly ONCE, with `intercepted` set to the
* number of stacks this interceptor handled (the blocked count, or the full
* incoming count for a pure listener), so an effect can scale itself per stack
* via `eqn: 'intercepted'` instead of the engine looping the effect per stack.
*/
blockAmount?: Scaling;
}[]
Migration from the old cancelApplication field:
cancelApplication: true→ setblockAmount: { value: 9999 }(blocks all stacks; effects fire once withintercepted = incoming)cancelApplication: false→ omitblockAmountentirely (effects fire as a pure listener without consuming any stacks)
Real Example - Profane Exchange
The best example of interception comes from the Blood school’s Profane Exchange technique:
import { Buff } from 'afnm-types';
import icon from '../assets/buffs/profane-exchange.png';
export const profaneExchangeBuff: Buff = {
name: 'Profane Exchange',
icon: icon,
canStack: false,
stats: undefined,
tooltip:
'You no longer gain {buff}. Instead, lose <num>3%</num> health as <name>True Damage</name> per stack you would have gained.',
onRoundEffects: [],
interceptBuffEffects: [
{
buff: window.modAPI.gameData.techniqueBuffs.blood.bloodCorruption, // Intercept Blood Corruption
effects: [
{
kind: 'damageSelf',
amount: {
value: 0.03,
stat: 'maxhp',
upgradeKey: 'hpCost',
},
damageType: 'true',
},
],
blockAmount: { value: 9999 }, // Block all incoming stacks — cancels the application
},
],
stacks: 1,
priority: -1, // Execute before other buffs
};
This intercept completely transforms how Blood Corruption works:
- Normal behavior: Gain Blood Corruption stacks
- With Profane Exchange: Take 3% max HP as true damage instead
- Result: Blood techniques become high-risk, high-reward
Interception Mechanics
Priority System
Interceptions use the buff’s priority value to determine execution order:
- Lower numbers execute first
- Profane Exchange uses
priority: -1to ensure it intercepts before other effects
Multiple Interceptions
A single buff can intercept multiple different buffs:
interceptBuffEffects: [
{
buff: firstBuff,
effects: [/* effects for first buff */],
blockAmount: { value: 9999 }, // Block all — cancels the application
},
{
buff: secondBuff,
effects: [/* effects for second buff */],
// blockAmount omitted — pure listener, original buff still applies
},
];
Partial Interception (Listener Pattern)
When blockAmount is omitted, the interceptor is a pure listener: the original buff still applies normally, but the effects also fire at the same time:
{
buff: targetBuff,
effects: [
{
kind: 'heal',
amount: { value: 0.1, stat: 'power' }
}
],
// blockAmount omitted — original buff applies + bonus healing fires as a listener
}
Stat Filter — Category-Wide Interception
Use statFilter to intercept any buff that grants a specific stat, without listing every buff by name:
// Intercept every buff that grants Power (catches mod-added buffs too)
interceptBuffEffects: [
{
statFilter: ['power'],
effects: [
{
kind: 'buffSelf',
buff: powerTracker,
amount: { value: 1, stat: undefined },
},
],
blockAmount: { value: 9999 }, // Block the incoming buff entirely
},
];
Combine buff and statFilter to broaden matching — a buff is intercepted when it matches either criterion:
interceptBuffEffects: [
{
buff: specificDebuff, // Match by name
statFilter: ['power'], // OR match any buff that grants Power
effects: [/* intercept effects */],
},
];
Guardian Interception
When a buff declares guardianIntercept, it acts as a damage shield that absorbs a percentage of incoming damage before it reaches the owner. When the guardian’s HP reaches zero, the buff is permanently removed.
Guardian Structure
guardianIntercept?: {
percent: Scaling; // Percentage of incoming damage redirected to guardian HP
maxHp: Scaling; // Maximum HP for the guardian, evaluated at buff creation
refreshMode?: 'refresh' | 'extend'; // How re-application combines with existing HP
target?: 'all' | 'healthOnly'; // Which damage portion the guardian intercepts
canUpgrade?: boolean;
onDestroyed?: BuffEffect[]; // Effects fired on the owner when the guardian is destroyed
};
The target Field
The target field controls which portion of incoming damage the guardian intercepts:
'all'(default): Intercepts the configured percent of total incoming damage at the pre-barrier location. This is the legacy behavior. Damage is intercepted before barrier absorption, so the guardian absorbs raw damage that would otherwise be reduced by the owner’s barrier.'healthOnly': Skips the pre-barrier interception entirely. The guardian instead absorbs a percent of the damage that would actually reach HP, after all other reductions (barrier, other interceptors, damage reduction, temporary health) have been applied. This lets mods build guardians that do not interfere with barrier-gated effects.
// Example: A guardian that only activates after barrier is fully breached
{
name: 'Final Ward',
icon: wardIcon,
canStack: false,
guardianIntercept: {
percent: { value: 0.50 },
maxHp: { value: 0.20, stat: 'maxhp' },
target: 'healthOnly', // Absorbs damage that reaches HP, not raw pre-barrier damage
},
}
The onDestroyed Hook
When a guardian is destroyed (its HP reaches zero), any onDestroyed effects declared on the guardianIntercept block are executed. This is equivalent to adding a triggeredBuffEffects entry with trigger: 'guardianBroken.${name}', but declared directly on the guardian for convenience.
The effects execute with:
source: The guardian’s owner (the character who has the guardian buff)target: The attacker who destroyed the guardian
// Example: A guardian that explodes when destroyed
{
name: 'Ritual Shield',
icon: shieldIcon,
canStack: false,
guardianIntercept: {
percent: { value: 0.50 }, // Absorb 50% of incoming damage
maxHp: { value: 0.30, stat: 'maxhp' }, // Guardian HP = 30% of max HP
onDestroyed: [
{
kind: 'damage',
amount: { value: 0.25, stat: 'power' },
damageType: 'true',
},
],
},
}
When this guardian is destroyed, it deals 25% of the owner’s power as true damage back to the attacker.
Auto-Generated Tooltip
Tooltips for onDestroyed are auto-generated using the “When destroyed, …” prefix. The trigger description will include the effect details automatically. No custom tooltip is required unless you want different wording than the default.
Advanced Interception Patterns
Stack Conversion
Convert incoming buff stacks into different amounts:
interceptBuffEffects: [
{
buff: incomingBuff,
effects: [
{
kind: 'buffSelf',
buff: differentBuff,
amount: { value: 2, stat: undefined }, // 1 incoming = 2 different
},
],
blockAmount: { value: 9999 }, // Block the original application
},
];
Conditional Interception
Combine with effect conditions for situational interceptions:
interceptBuffEffects: [
{
buff: targetBuff,
effects: [
{
kind: 'damage',
amount: { value: 0.5, stat: 'power' },
condition: {
kind: 'hp',
percentage: 50,
mode: 'less',
},
},
],
blockAmount: { value: 9999 },
},
];
Resource Redirection
Redirect resource generation to different pools:
interceptBuffEffects: [
{
buff: normalResource,
effects: [
{
kind: 'buffSelf',
buff: alternativeResource,
amount: { value: 1, stat: undefined },
},
{
kind: 'heal',
amount: { value: 0.05, stat: 'maxhp' },
},
],
blockAmount: { value: 9999 },
},
];
Tooltip Integration
Interceptions do not automatically appear in tooltips, but the variables within them do. Therefore, a custom tooltip must be written to display them.
tooltip: 'You no longer gain {buff}. Instead, lose <num>3%</num> health as <name>True Damage</name> per stack you would have gained.';
The {buff} placeholder is automatically replaced with the intercepted buff’s name in the tooltip system.