Stage 1 of the Hax's Tools split (plan: .hermes/plans/2026-06-20_040000-hax-tools-split.md).
What's in this repo:
- module.json, package.json, README.md, LICENSE, .gitignore
- scripts/main.js — module entry, re-exports loadSystems / registerAllEvents / $ctx on mod.api
- scripts/events/registry.js — single-chokepoint hook wrapper (MODULE_ID retagged)
- scripts/events/core/ — 16 system-agnostic events
- scripts/events/dnd5e/ — 2 dnd5e events (roll-attack, roll-damage)
- scripts/systems/loader.js + core-system.js + dnd5e-system.js — system-adapter layer
- scripts/encounter.js — STAGE-1-STUB: returns null; real encounter wiring lands in Stage 2
- tests/verify-hooks-lib.mjs — 20-assertion no-Foundry smoke test (all green)
- hooks-lib-0.1.0.zip — release artifact for Foundry's manifest download URL
Behavior changes vs. battle-focus source:
- Module-id log strings retagged: [battle-focus] -> [hax-hooks-lib] in loader.js + registry.js
- Token/item stash flag namespace: battle-focus -> hax-hooks-lib (the stash is
internal to the pre/update pair; consumer-agnostic so we own it)
- Hook calls in hooks-lib's copy are gated on a null encounter (the stub),
so hooks-lib's events don't fire end-to-end. battle-focus's LOCAL copy
of the events is untouched and still works. Stage 2 will wire the real
encounter via registerAllEvents({getActiveEncounter}).
Verified:
- node --check passes on all 26 JS files
- npm test (verify-hooks-lib.mjs) — 20/20 assertions
- python validate-module-json.py module.json — 0 errors, 1 warning (no icon, Stage 2)
Push: Gitea only (git.homelab.local/kaykayyali/hooks-lib). No GitHub mirror.
Refs: battle-focus 8b9db20 (v0.5.0-alpha.12, 220/222 tests)
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