Adds docs/HOOK_CONTRACT.md — the spec for hooks-lib v0.2.0. The
v0.1.0 implementation (18 hand-written event handlers, the
scripts/systems/ adapter pair, the encounter.js stub) is the wrong
shape. v0.2.0 is a generic Foundry hook facade:
- Subscribes to every Foundry hook in §6 (lifecycle, document CRUD
generic, combat, chat/canvas/UI, dnd5e v2 roll hooks)
- Emits a uniform {ts, hook, args} envelope with no domain
interpretation
- Provides subscribe / subscribeMany / subscribeAll / unsubscribeAll
primitives
- Registers system adapters (separate repos) via a manifest with
semver range matching for system + Foundry versions
- Dispatches sync for pre-* / combat* / applyActiveEffect /
get*Context hooks; async (microtask) for everything else
- Absorbs Foundry rename churn (renderChatInput vs renderChatLog)
and arity shifts (combatRound signature) so consumers stay stable
No code changed. The contract is the gate for v0.2.0 implementation.
Push: Gitea only (this turn). No GitHub mirror.
Hax's Tools — Hooks Lib
Foundry VTT module (id: hax-hooks-lib) that turns Foundry's hook soup
(dnd5e, combat, token updates) into a clean, normalized event stream.
Library-only — no UI, no settings, no chat output. Designed to be
consumed by any module that wants Foundry events in a stable shape.
Part of the Hax's Tools umbrella. Consumers today:
battle-focus— encounter + journal + summaryIts-Achievable— achievements, rewards, wall, HUD
Status
v0.1.0 — initial extraction from battle-focus v0.5.0-alpha.12. The
scripts/events/ and scripts/systems/ content is a verbatim copy
of the battle-focus versions, with module-id log strings retagged
from battle-focus to hax-hooks-lib.
Stage 1 of the split plan: ship this repo without changing battle-focus's behavior. Stage 2 will flip battle-focus to import from here and delete the local copy.
Event Shape
Every event is a plain object with at minimum:
{
kind: "combat-start", // stable string id
ts: 1719000000000, // epoch ms when handler fired
// ... kind-specific fields
}
Handler functions in scripts/events/core/ and scripts/events/dnd5e/
return the event object (or null to drop it). The registry calls the
consumer's onEvent(event) callback for any non-null return.
Event catalog
Core (system-agnostic, always loaded):
combat-start,combat-end,combat-inactive,combat-turn,combat-roundcombatant-add,combatant-removepre-update-actor,update-actor,pre-update-token,update-token,pre-update-item,update-itemcreate-active-effect,delete-active-effecttoken-avatar-change
D&D 5e (loaded when game.system.id === "dnd5e"):
attack-roll,damage-roll
Public API (on game.modules.get("hax-hooks-lib").api)
loadSystems({ currentSystemId, systemVersion })→ array of active system adapters ([{ id, label, match, events }]). Thecoreadapter is always present; other adapters are filtered bymatch().registerAllEvents(systems, onEvent)→ registers every event from every active system viaHooks.on(...). TheonEventcallback is invoked once per non-null handler return, awaited. Returns the array of registered event defs (for introspection).
Consumers should call registerAllEvents from their ready hook and
provide their own onEvent(event) to drive their downstream pipeline.
Dependencies
None. This is a leaf library.
Architecture notes
- Single chokepoint:
registerEvent()is the only place that wraps a Foundry hook into the normalized shape. Adding a new event type = add one file + one line in the relevant system adapter. - System loader:
scripts/systems/loader.jsfilters system adapters bygame.system.id. Adding a new system = add one file inscripts/systems/+ one import line. - Context helper:
$ctx()returns the active event's metadata for the duration of the handler call. Use it inside a handler to log with the event id, or to read which system emitted it.
Tests
tests/verify-hooks-lib.mjs is a minimal smoke test that exercises
the registry + a single event without booting Foundry. Foundry-load
verification happens in battle-focus's E2E suite (the test driver
registers hooks-lib's events through battle-focus's consumer path
and confirms the event stream flows end-to-end).
npm test