Files
combat-hud-hub/README.md
Kaysser Taylor b1121651dc v0.2.5: 1Hz tick keeps timer counting when no envelope event fires
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.
2026-06-22 19:21:30 -04:00

2.5 KiB

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 via subscribeMany for the combat envelope stream (combatStart, combatEnd, combatRound, combatTurn, createCombatant, deleteCombatant, dnd5e.rollAttackV2, dnd5e.rollDamageV2).
  • Soft-dep battle-focus — reads the active encounter via battle-focus.api.getActiveEncounter().
  • Public APIgame.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/clearFeed read 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