Stage 2 of the Hax's Tools split. its-achievable ships as a standalone module that subscribes to hax-hooks-lib's envelope stream and provides achievements + custom rules + rewards + achievement wall + combat HUD. ## What's new scripts/ — moved from battle-focus/scripts/, MODULE_ID retagged battle-focus → its-achievable: - achievement-rules.js (323 lines) — rule engine: OPERATORS, TRIGGER_TYPES, evaluateCondition(s), testRule, evaluateRulesFor* - achievements.js (1150 lines) — 24-entry catalog + award path, per-event evaluators, encounter-end + career-update evaluation - achievement-wall.js (333 lines) — renderAchievementWall, getAchievementWallProgress, renderAchievementPopover - custom-achievements-app.js (270 lines) — GM FormApplication for editing custom rules - hud.js (624 lines) — combat HUD (ApplicationV2 + HandlebarsApplicationMixin); removed dead import of battle-focus's encounter.js (it was unused even in the original) scripts/main.js — Foundry entry point. Registers settings at its-achievable.* namespace; exposes the public API on mod.api; registers chatBubble popover listener + HUD singleton on ready. templates/ + styles/ — moved verbatim. tests/PLAN.md — per-project test plan (sections A-F). tests/test-helpers.mjs — Foundry stub. tests/verify-achievable-v1.mjs — smoke test, 75 assertions covering rule engine, catalog, awards, hooks-lib wiring, HUD payload derivation, and wall/popover rendering. Runs in <2s. ## Architecture - **Settings namespace**: its-achievable.* (was battle-focus.*). No migration (per Kaysser's decision); users with existing worlds re-create their custom rules. Documented in README. - **HUD derives its own state from hooks-lib envelopes.** Stage 2 keeps the legacy battle-focus:hud-update broadcast subscription for now (battle-focus still emits it); Stage 3 will switch the HUD to subscribe to hooks-lib directly and remove the battle-focus broadcasts. - **Encounter singleton**: accessed via battle-focus's public api.getActiveEncounter() — no direct import of battle-focus's encounter.js. ## Dependencies - hax-hooks-lib ^0.2.0 (declared in module.json relationships). - battle-focus (soft, runtime) — provides the encounter singleton. ## Tests - 75/75 smoke assertions pass in 0.07s. - Module manifest validates: 0 errors, 1 warning (no icon — Stage 2+ work). Push: Gitea only.
3.5 KiB
Hax's Tools — It's Achievable (its-achievable)
Foundry VTT module: achievements engine, custom rules, rewards,
achievement wall, and combat HUD. Consumes the generic Foundry hook
facade from hax-hooks-lib and the encounter state from battle-focus.
Status
v0.1.0 — first release as a standalone repo. Sourced from
battle-focus/scripts/{achievements.js, achievement-wall.js, custom-achievements-app.js, hud.js, achievement-rules.js} at
battle-focus v0.5.0-alpha.12 (99cf757 on Gitea).
Stage 2 of the Hax's Tools split. The achievement code is now an independent module with its own:
- Settings namespace (
its-achievable.*) - Module id (
its-achievable, lowercase kebab) - Achievements catalog and rule engine
- HUD subscription to hooks-lib's v0.2.0 envelope stream
- Chat-bar popover and form application
battle-focus retains its own copies of these files until Stage 3 of
the split ships (which will delete the copies and add its-achievable
as a soft dependency of battle-focus).
Dependencies
hax-hooks-lib(≥0.2.0) — provides the Foundry event stream via the generic{ts, hook, args}envelope facade. Seehooks-lib/docs/HOOK_CONTRACT.md.battle-focus(soft) — provides the encounter singleton viagame.modules.get("battle-focus").api.getActiveEncounter().
If either is missing, its-achievable logs a warning and degrades gracefully (achievements inactive).
Public API (on game.modules.get("its-achievable").api)
const api = game.modules.get("its-achievable").api;
// Catalog
api.getAchievementCatalog();
api.getActorAchievements(actorKey);
// Rule engine
api.evaluateRulesForEvent(event, encounter, actor, targetActor);
api.evaluateRulesForEncounterEnd(encounter);
api.evaluateRulesForCareerUpdate(career);
// Awarding
api.processEventForAchievements(event, encounter);
api.evaluateCombatAchievements(stats);
api.evaluateCareerAchievements(pcName, career, encounterId);
api.awardAchievement(actorKey, achievementId, encounterId);
// Wall + HUD
api.renderAchievementWall(actorId, actorName, opts);
api.getAchievementWallProgress(actorId, actorName);
api.renderAchievementPopover(unlocks, viewerName);
api.buildHudUpdatePayload(encounter, event);
api.openCustomAchievementsApp();
Settings namespace
Old (battle-focus.*) |
New (its-achievable.*) |
|---|---|
achievementsByActor |
achievementsByActor |
customAchievementRules |
customAchievementRules |
enableRewards |
enableRewards |
No automatic migration. Per the Stage 2 decision, users with existing worlds will need to re-create their custom rules and re-trigger any past achievement awards. Documented in CHANGELOG.
Tests
npm test # smoke test, no Foundry needed
tests/PLAN.md describes what we test and what we don't.
Real-Foundry integration testing happens when battle-focus migrates
in Stage 3 and the existing E2E suite exercises the moved code.
Architecture notes
- Hooks-lib first. its-achievable subscribes to
hax-hooks-lib's envelope stream. Combat lifecycle, document CRUD, dnd5e rolls — all viahooksLib.api.subscribeMany({...}). No directHooks.on(...)calls. - Encounter via battle-focus's API. its-achievable does not import
battle-focus's
encounter.jsdirectly; it callsbattle-focus.api.getActiveEncounter()to resolve the singleton. - HUD derives its payload locally.
buildHudUpdatePayloadlives in its-achievable (moved from battle-focus). It derives the HUD state from hooks-lib envelopes, not from a battle-focus broadcast.