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.
159 lines
4.9 KiB
JavaScript
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 }; |