Files
combat-hud-hub/scripts/main.js
Kaysser Taylor e6531ec569 v0.2.2: bump MODULE_VERSION in main.js to match module.json/package.json
Missed the 3-place version rule on the v0.2.2 bump. MODULE_VERSION
was still v0.2.0 in scripts/main.js, so Foundry's init log showed
the old version and the live system didn't have getFeed/clearFeed.
Caught during the e2e Playwright run when Foundry loaded v0.2.0
of the main.js but v0.2.2 of the module.json.
2026-06-22 17:06:35 -04:00

159 lines
4.9 KiB
JavaScript

// combat-hud-hub — module entry point (v0.2.2).
//
// Generic combat HUD host. Consumer modules register sections via
// the public API; the hub aggregates built-in core sections +
// consumer-registered feeds.
//
// Soft dependencies (both optional): foundry-hooks-lib (envelope
// stream), battle-focus (encounter seam). Core sections register
// when battle-focus is present; consumer-registered sections work
// whether or not either is present.
import { CombatHudHubApp, getHud, registerCoreSections } from "./hud.js";
const MODULE_ID = "combat-hud-hub";
const MODULE_VERSION = "0.2.2";
const COMBAT_HOOK_NAMES = [
"combatStart",
"combatEnd",
"combatRound",
"combatTurn",
"createCombatant",
"deleteCombatant",
"dnd5e.rollAttackV2",
"dnd5e.rollDamageV2",
];
// ── Settings registration ─────────────────────────────────────────────
function registerSettings() {
if (typeof game === "undefined" || !game?.settings?.register) return;
game.settings.register(MODULE_ID, "hudPosition", {
name: "HUD Position",
hint: "Where the combat HUD sits on the canvas.",
scope: "user",
config: true,
type: String,
choices: { top: "Top", bottom: "Bottom", left: "Left", right: "Right" },
default: "top",
});
}
// ── Section registry ───────────────────────────────────────────────────
const _sections = new Map();
const _feedBySection = new Map();
function _syncHudMirror() {
const hud = hudInstance;
if (!hud?._syncSections) return;
hud._syncSections(
Array.from(_sections.values()),
Array.from(_feedBySection.entries())
);
if (hud.isOpen()) hud.forceRender();
}
const api = {
id: MODULE_ID,
version: MODULE_VERSION,
addSection(spec) {
if (!spec || typeof spec !== "object") throw new Error("addSection: spec required");
if (!spec.id) throw new Error("addSection: spec.id required");
if (typeof spec.render !== "function") throw new Error("addSection: spec.render must be a function");
const id = spec.id;
const wrappedRender = (ctx) => {
const feed = _feedBySection.get(id) ?? [];
return spec.render({ ...(ctx ?? {}), feed, section: _sections.get(id) });
};
_sections.set(id, { id, label: spec.label ?? id, render: wrappedRender });
if (!_feedBySection.has(id)) _feedBySection.set(id, []);
_syncHudMirror();
return id;
},
removeSection(id) {
_sections.delete(id);
_feedBySection.delete(id);
_syncHudMirror();
},
listSections() {
return Array.from(_sections.values()).map(s => ({
id: s.id, label: s.label, render: s.render,
}));
},
pushFeedEntry(sectionId, entry) {
if (!_feedBySection.has(sectionId)) _feedBySection.set(sectionId, []);
_feedBySection.get(sectionId).push(entry);
_syncHudMirror();
},
getFeed(sectionId) {
return [...(_feedBySection.get(sectionId) ?? [])];
},
clearFeed(sectionId) {
if (_feedBySection.has(sectionId)) _feedBySection.set(sectionId, []);
_syncHudMirror();
},
getHud() {
return hudInstance;
},
openHud() {
if (!hudInstance) return;
return hudInstance.open();
},
closeHud() {
if (!hudInstance) return;
return hudInstance.close();
},
};
// ── Lifecycle ──────────────────────────────────────────────────────────
let hudInstance = null;
Hooks.once("init", () => {
const mod = game.modules.get(MODULE_ID);
mod.api = api;
registerSettings();
// Construct the singleton HUD so the api is reachable even before ready.
hudInstance = getHud();
console.log(`[${MODULE_ID} v${MODULE_VERSION}] init`);
});
Hooks.once("ready", () => {
const hooksLibMod = game.modules.get("foundry-hooks-lib");
const battleFocusMod = game.modules.get("battle-focus");
const hooksLibActive = !!hooksLibMod?.active;
const battleFocusActive = !!battleFocusMod?.active;
const hooksLibApi = hooksLibActive ? hooksLibMod.api : null;
// Register core sections when battle-focus is available.
let coreRegistered = false;
if (battleFocusActive) {
registerCoreSections(api);
coreRegistered = true;
}
// Subscribe to envelope stream.
if (hooksLibApi?.subscribeMany) {
hudInstance.wireHooks(hooksLibApi);
} else {
console.warn(`[${MODULE_ID}] foundry-hooks-lib not installed or not active; the HUD will not receive live updates.`);
}
console.log(
`[${MODULE_ID} v${MODULE_VERSION}] ready ` +
`(core sections: ${coreRegistered}, hooks-lib: ${hooksLibActive})`
);
});
Hooks.on("unregisterModule", (moduleId) => {
if (moduleId === MODULE_ID) {
try { hudInstance?.unwireHooks?.(); } catch (e) {
console.warn(`[${MODULE_ID}] unwireHooks failed:`, e);
}
console.log(`[${MODULE_ID}] unregisterModule: cleaned up`);
}
});
export { api };