Skip to content

Ability hooks and scope reference

This reference describes the ability hooks and their callers. The definition and identity notes were updated on 2026-11-02; check source before changing other hook details. The test records describe observed checks and their limits. These checks do not certify every hook, ability, stage or player build.

How the pieces connect

  1. A character, upgrade or player binding provides an internal key. AbilityDefinitionLookup resolves the AB_ definition, which creates a behavior for that player.
  2. The runtime creates a scope and calls the behavior's Connect method. The behavior registers the hooks it needs; each player has a separate hook hub.
  3. Gameplay publishes an event, requests a value change or asks for a decision. The hub calls the registered methods in order.
  4. Removing the last entry using a definition class, or disposing the runtime, disposes the scope. This removes its subscriptions and runs cleanup.

The runtime stores mutable PlayerAbilityState by internal definition key and groups behaviors and scopes by concrete AB_ class. Reacquiring the same definition reuses that player's state and resets its cooldown; other players have separate states and behaviors. Activation chooses the first acquired entry of the requested class. Some abilities have separate definition keys on the same class, including Concussive/Deadly Impact and Super Magnet/GKCrown. If a runtime has not been created, gameplay callers generally skip its hooks.

Authoring and startup

  • Put metadata and behavior in an attributed, preserved AB_ class. Use named values such as name, cooldown and staminaCost; omitted numeric values default to zero. Preserve existing four-character upgrade codes; Dash and automatic character abilities can omit one.
  • After a new definition compiles, use Tools > Tiny Chaos > Register Ability IDs. It generates a stable ID in the separate registry; do not edit the generated record by hand. Keep a script's .meta file with it when moving or renaming it. For multiple definitions on one class, keep the variant labels stable.
  • Keep reward identity, icon and upgrade values in the existing Upgrade asset. Its ability key selects the code definition; an upgrade ID is not an ability key.
  • Store player-specific counters, charges and timing on the behavior or PlayerAbilityState, never on a shared definition. Register only the hooks the behavior uses, and clean up subscriptions and owned effects when it is removed.
  • Released scenes use GameInitialization.PreparePlayer: await character weapon/stat setup, initialize the player's ability host with prefab starting keys, acquire the character definition, then set dash speed and register camera/UI ownership. Player.Update polls scene Dash input; PlayerAbilityHost.Update publishes ability ticks.
  • The separate development run participant has its own preparation path. Checks using its synthetic fixtures do not establish that the released scene path works. FixtureAbilities is compatibility data for older checks, not the production starting list.

Scope operations

The implementation is in PlayerHookHub.cs.

Ability-side method Handler form Use Caller-side operation
scope.Listen(key, handler, order, id) void Handler(T context) React to a notification. All subscribers in the stage run in order. hub.Publish(key, context)
scope.Modify(key, handler, order, id) T Handler(T value) Change a value, such as dash cost. Each handler receives the previous result. hub.Modify(key, value)
scope.Modify(key, handler, order, id) TValue Handler(TContext context, TValue value) Change a value while receiving extra context, such as incoming damage and its hit information. hub.Modify(key, context, value)
scope.Decide(key, handler, order, id) bool Handler(T context) A true result handles/accepts the stage and stops later handlers. The meaning of true depends on the specific hook below. hub.Any(key, context)
scope.OnDispose(cleanup) void Cleanup() Release or reset behavior-owned state when the scope is disposed. Runtime disposes the scope.

order is required in the current API: choose a group from 0 to 99. Lower groups run first and 99 is last; multiple handlers may share a group. Within the same group the current implementation sorts by behavior class's full name and subscription id, using ordinal comparison. This is deterministic for unchanged names but is not yet a durable identity. Use different groups if the order matters. An ability may register several hooks or multiple uniquely named subscriptions; the same behavior identity and id cannot be registered twice on the same key. The scope owns the resulting registrations; an ability should not create its own scope.

Each call takes an ordered snapshot. Registration/removal during a dispatch takes effect on the next dispatch, including a reentrant call; handlers already in the current snapshot may still run. A handler exception stops the dispatch and propagates. OnDispose callbacks and registrations are disposed in reverse registration order; cleanup attempts all entries and reports exceptions afterward. On final removal, the runtime separately cancels work and cooldowns started for that ability script type. Disposing the scope alone does not cancel work, and already-created attack effects remain on their attacks. General player-owned work started with Run is not tied to an individual ability.

Example from AB_SurvivalFitness.cs:

public void Connect(PlayerHookScope scope)
{
    scope.Modify(MovementAbilityHooks.DashCost, ModifyDashCost, 0, "dash-cost");
    scope.Listen(CombatAbilityHooks.DamageReaction, OnDamageReaction, 0, "damage-reaction");
}

public float ModifyDashCost(float cost) => cost - 5;
public void OnDamageReaction(PlayerDamageHit hit) => player.currentStamina += 20;

The number is the priority group, not an absolute position. The string is a subscription identifier, not the ability's display name.

Hook list

Listen, Modify, and Decide in the tables below refer to the ability-side scope method. They are not all interchangeable: check the input and result before choosing a stage.

Movement

Defined in MovementAbilityHooks.cs; called by the scene dash adapter and movement-driver dash controller.

Hook Scope Input / result When it runs
BeforeDash Listen PlayerDashFeedback After dash admission and stamina check, before launch.
DashCost Modify float → float When dash stamina cost is calculated, before checking available stamina.

Player damage and combat

Defined in CombatAbilityHooks.cs; called by PlayerDamageHooks.cs, except thorn retaliation, which Electrical Thorn Armor publishes.

Hook Scope Input / result When it runs
BeforeImmunity Listen PlayerDamageHit Before the damage resolver checks invulnerability.
AbsorbShield Decide PlayerDamageHit → bool After immunity; true means this shield absorbed the hit, so later shields do not run.
HealthDamage Modify PlayerDamageHit, float → float After the general damage-taken calculation, before health subtraction.
DamageReflection Decide PlayerDamageHit → bool During damage reactions; current reflection abilities return true after handling so only the first runs.
DamageReaction Listen PlayerDamageHit After health-damage handling and damage reflection, before lethal resolution.
PreventDeath Decide Player → bool At lethal resolution; true means death was prevented/handled.
ThornRetaliation Listen Player Published by Electrical Thorn Armor's retaliation effect; currently used by Judgement.

PlayerDamageHit contains the amount, source entity, attack source, knockback, damage type and knockback direction. The damage resolver has additional general steps outside these hooks, including checking immunity and applying invulnerability.

Pickups

Defined in CollectibleAbilityHooks.cs; called by PlayerCollectItem.cs.

Hook Scope Input / result When it runs
EffectCount Modify int → int Calculates how many times applicable collectible effects run.
ChestCollected Listen ICollectible When a normal or boss chest is handled, before rewards are enqueued.
CoinCollected Listen ICollectible After the coin's gold, experience and collection count are applied.
PotionCollected Listen ICollectible Once when potion handling begins, before its effect loop.
PotionApplied Listen ICollectible For each application of a regular or greater potion after its resources are added; it is not called for every potion subtype.

Companions

Defined in CompanionAbilityHooks.cs.

Hook Scope Input / result When it runs
AttackPrepared Listen CompanionAttackContext During Companion.AttackModify, before the companion's damage and SpellAmp assignments. Context has Companion and Attack.
AttackInterval Modify Companion, float → float During the drone's attack-interval calculation.
ExtraCompanionsRequested Listen Player Multishot publishes this after an upgrade equips it; character abilities decide how to add companions.

Not every companion uses the same attack preparation path. Sword and orb attacks can go through PlayerWeapon.AttackModify; trace the specific caller before assuming this stage covers them.

Weapons and attack preparation

Defined in WeaponAbilityHooks.cs; mostly called by PlayerWeapon.cs. The three attack-preparation stages are separate because spell amplification, critical selection and damage calculation occur between them.

Hook Scope Input / result When it runs
Started Listen PlayerWeapon Weapon startup.
Destroyed Listen PlayerWeapon Weapon destruction.
Reload Listen PlayerWeapon At the existing reload stage before reload state/audio processing.
HoldFire Decide WeaponChargeContext → bool Before a shot; true holds fire. Context contains Weapon and DeltaTime.
BeforeFire Listen WeaponFireContext Before projectile spawning. Context contains Weapon and FireWeapon; this stage also runs for the existing activation-only call.
ShotCompleted Listen PlayerWeapon After the weapon's normal fire call completes.
ExtraShots Listen PlayerWeapon After the main projectile is spawned.
BeforeSpellAmp Listen AttackPreparationContext During weapon attack preparation, before SpellAmp is assigned. Context contains Weapon and Attack.
BeforeCriticalRoll Listen AttackPreparationContext After SpellAmp assignment, before the critical roll.
GuaranteedCritical Decide PlayerWeapon → bool Critical selection; true guarantees a critical. Soul Wager also consults it.
ForcedCritical Decide PlayerWeapon → bool Critical selection; true forces a critical. Soul Wager does not consult this stage.
BeforeCriticalReset Listen AttackPreparationContext On a critical, before the guaranteed-critical reset.
CriticalResolved Listen PlayerWeapon On a critical, at the guaranteed-critical reset stage.
AfterCriticalReset Listen AttackPreparationContext On a critical, after that reset.
BaseDamage Modify PlayerWeapon, float → float During weapon damage calculation, before fixed damage and critical calculation.
Damage Listen WeaponDamageContext After critical/noncritical damage calculation. Context has Weapon, Attack, and CriticalRoll; handlers may change the attack.
AfterDamage Listen AttackPreparationContext After the Damage stage during attack preparation.
CarrierAttackPrepared Listen PlayerAttack When PlayerAttackCarrier creates its explosion attack.
Excavation Listen Player When Ground Breaker triggers its excavation event; currently used by Lucky Find.

Player progression

Defined in PlayerProgressionHooks.cs; called by PlayerManager after scheduling a level-up reward and before increasing the level. Each phase is sent to each player separately.

Hook Scope Input / result When it runs
BeforeLevelUp Listen Player First level-up phase; Future Pills applies its limited permanent stat additions here.
LevelUp Listen Player Second phase; RPG Buff applies recovery and one random stat bonus.

Upgrade values

Defined in UpgradeAbilityHooks.cs; called by UpgradeFunction before applying an additive stat upgrade. A player ability runtime must already be prepared.

Hook Scope Input / result When it runs
AdditiveValue Modify Upgrade.UpgradeList, float → float For additive damage, attack speed, critical damage/chance, reload, movement, ammo, health, stamina and skill amplification upgrades.

Adept Learner runs first, then Mage Apprentice, Proto Barrier, Ground Breaker, Rocket Barrage and Sky Ying Sword in that order. Each changes only its applicable upgrade values. This does not replace the central calculation of final player stats.

For a one-time ability-specific upgrade operation, a definition can implement IAbilityUpgradeOperation. UpgradeFunction calls its ApplyUpgradeOperation(player, operation, value) at that operation's position in the upgrade list. Return true when handled. This is separate from the player hook scope and can run before the ability is equipped, as Karmayana's existing upgrade requires.

Reward card count

Defined in RewardAbilityHooks.cs. CardCount lets an acquired player ability modify the default of three cards for special and boss rewards. The highest player result is used. No current ability subscribes; Quartet remains unfinished development content.

Health recovery

Defined in HealthRecoveryHooks.cs; called by Player.RegenerateHealth.

Hook Scope Input / result When it runs
BlockRecovery Decide Player → bool Before a potion is consumed; true blocks this recovery cycle.
BlockDirectHealing Decide Player → bool Checked by direct healing effects; true blocks healing. RPG Buff retains its existing Last Stand exception.
Contributions Listen HealthRecoveryContext After potion consumption; vitamin and petrification bonuses are added here.
AfterContributions Listen HealthRecoveryContext Adds Survival Instinct recovery, then Meditate recovery.

The context holds the recovery amount. AddOnce lets related effects share one contribution: Petrified Skin and De-Petrify use the same key so they add their bonus only once per cycle.

Ability lifecycle

Hook Scope Input / result When it runs
AbilityLifecycleHooks.Tick Listen Player The active player ability runtime publishes it each frame. The owner/runtime lifecycle controls whether it is published.
AbilityLifecycleHooks.Equipped Listen PlayerAbilityState Published after an upgrade equips an ability, including a queued equip. Starting abilities and ordinary acquisitions do not publish it.

Defined in AbilityLifecycleHooks.cs and published by PlayerAbilityRuntime.cs.

Effects attached to individual attacks

These are a separate mechanism from PlayerHookScope and the player-level hook list. An ability can attach an impact handler to a specific PlayerAttack:

attack.AddHitEffect("freezing-pendant", 0, Freeze, true, true);

The arguments are: effect ID, priority group, handler, whether it transfers to child projectiles, and whether it transfers to explosions. The last two arguments default to false. The handler receives PlayerAttack, Enemy and the hit position. Effects are sorted by group then ID, run on enemy impact, and are cleared when the pooled attack resets. An attached effect can remain on an already-created attack after its ability is removed. CopySecondaryHitEffectsTo copies only effects whose transfer setting permits the destination.

attack.AddDamageEffect(id, order, handler) attaches an ordered damage modifier for a particular attack. Its handler receives the enemy and PlayerAttack.HitDamage, then returns the updated value. AB_ProtoBarrier is the current example. The attack effect lists are separate from the player hook hub; a combined ordering between them has not been implemented.

Some handlers still read live player or attack values at hit time. Captured projectile ownership, pooling and removed/destroyed-player cases are source-level work and need later Unity verification.

If an ability needs a new hook

First check the lists above for a stage with the correct timing and input. If none exists, the gameplay owner must declare a typed key beside its other hooks and call the hub at the point where the event actually occurs. Then the ability can subscribe in its own Connect method. Do not add an ability-name branch or a separate forwarding method to PlayerAbilityRuntime for the new hook.