Bug: timeSinceStart only updated when an envelope event fired (combatStart, attack-roll, etc.). If the combat was idle — a player thinking about their turn, between turns — the timer froze. Fix: 1Hz self-rescheduling setTimeout in the constructor that bumps timeSinceStart and triggers a throttled render while _state.isActive is true. Stopped on unwireHooks (combat end). TDD: Section O (5 assertions) added BEFORE the fix. - O.1 initial timeSinceStart reflects _combatStartedAt - O.2 advances without an envelope event - O.3 tick interval is ~1s - O.4 timer does not tick when combat is inactive - O.5 timer resumes when combat becomes active again Tests: 65/65 passing in ~2s. Playwright 31/31.
Combat HUD Hub
A generic combat HUD host for Foundry VTT v14. Other modules register sections via the public API; the hub ships built-in core sections (round, current turn, per-PC damage, dice streak) that light up when foundry-hooks-lib and battle-focus are present. Consumer-registered sections work even when both are missing.
Architecture
foundry-hooks-lib ─┐
├──▶ combat-hud-hub ◀── its-achievable (Pinned Achievements)
battle-focus ─┘ ◀── <future modules>
- Soft-dep
foundry-hooks-lib— subscribed viasubscribeManyfor the combat envelope stream (combatStart,combatEnd,combatRound,combatTurn,createCombatant,deleteCombatant,dnd5e.rollAttackV2,dnd5e.rollDamageV2). - Soft-dep
battle-focus— reads the active encounter viabattle-focus.api.getActiveEncounter(). - Public API —
game.modules.get("combat-hud-hub").api:addSection({ id, label, render })— register a slot.render(ctx)receives{ feed, section, round, turn, combatants, ... }.removeSection(id)pushFeedEntry(sectionId, entry)— append an entry to a section's feed.listSections()— snapshot of registered sections.getHud(),openHud(),closeHud().
Consumer example
// its-achievable wants to surface unlocks in the HUD
Hooks.once("ready", () => {
const hub = game.modules.get("combat-hud-hub")?.api;
if (!hub) return; // soft dep; hub not installed
hub.addSection({
id: "pinned-achievements",
label: "Pinned Achievements",
render: (ctx) => (ctx.feed ?? []).slice(-5).map(e =>
`<li>${e.icon ?? "🏆"} ${e.name}</li>`).join(""),
});
});
// later, on unlock:
hub.pushFeedEntry("pinned-achievements", { name: "Critical Hit!", icon: "🎯" });
Status
- v0.2.5 — 1Hz tick keeps the timer counting when no envelope event fires. 65/65 smoke tests.
- v0.2.4 — current-turn updates on combatTurn + session-open auto-open. 60/60 smoke tests.
- v0.2.3 — added
isReady()and corrected 3-place version drift. 50/50 smoke tests. - v0.2.2 — added
getFeed/clearFeedread accessors. 50/50 smoke tests. - v0.2.1 — current-turn indicator; encounter-as-source-of-truth for currentTurn. 46/46 smoke tests.
- v0.2.0 — ported HUD from its-achievable (section-based rendering, throttled to 1Hz). 42/42 smoke tests.
- v0.1.0 — public API + soft-dep wiring (initial scaffolding).
Tests
npm test
Languages
JavaScript
91%
CSS
7.6%
HTML
1.4%