v0.2.1: current-turn indicator + encounter-as-source-of-truth
- Combatants row whose tokenId matches encounter's current combatant gets data-chh-current-turn=true, an accent-color border + tint, an outlined portrait, and a '▶' marker before the name. - Header section prepends '▶' before the turn name. - buildRenderContext now resolves currentTurn from the encounter singleton (encounter.currentTurn / encounter.combatantId) instead of reading game.combat directly. battle-focus owns the encounter. - Tests: 46/46 in <1s, new section L covers the indicator.
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
All notable changes to Combat HUD Hub are documented here.
|
||||
|
||||
## [0.2.1] — 2026-06-22 (current-turn indicator)
|
||||
|
||||
- **Current-turn indicator.** The combatants row whose `tokenId` matches the encounter's current combatant gets `data-chh-current-turn="true"`, a `.chh-section-combatants-row--current` class, an accent-color left border + subtle background tint, an outlined portrait, and a "▶" marker before the name. The header section also prepends "▶" before the turn name.
|
||||
- **Encounter-as-source-of-truth for currentTurn.** `buildRenderContext` now resolves the current turn from the encounter singleton (`encounter.currentTurn` or `encounter.combatantId → encounter.combatants.get(...)`) instead of reading `game.combat` directly. battle-focus owns the encounter; the hub reads it.
|
||||
- Tests: 46/46 passing in <1s covering sections A-L.
|
||||
|
||||
## [0.2.0] — 2026-06-20 (port HUD from its-achievable)
|
||||
|
||||
- Ported `scripts/hud.js`, `scripts/event-translation.js`, `templates/hud.html`, `styles/hud.css` from its-achievable v0.2.0.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "combat-hud-hub",
|
||||
"title": "Combat HUD Hub",
|
||||
"description": "Foundry VTT v14 module: a generic combat HUD host. 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 + battle-focus are present. Soft-deps on foundry-hooks-lib and battle-focus; consumer-registered sections work even when both are missing.",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"library": false,
|
||||
"manifestPlusVersion": "1.2.0",
|
||||
"authors": [
|
||||
@@ -41,7 +41,7 @@
|
||||
"styles": ["styles/hud.css"],
|
||||
"url": "https://git.homelab.local/kaykayyali/combat-hud-hub",
|
||||
"manifest": "https://git.homelab.local/kaykayyali/combat-hud-hub/raw/branch/main/module.json",
|
||||
"download": "https://git.homelab.local/kaykayyali/combat-hud-hub/raw/branch/main/combat-hud-hub-0.2.0.zip",
|
||||
"download": "https://git.homelab.local/kaykayyali/combat-hud-hub/raw/branch/main/combat-hud-hub-0.2.1.zip",
|
||||
"readme": "https://git.homelab.local/kaykayyali/combat-hud-hub/blob/main/README.md",
|
||||
"changelog": "https://git.homelab.local/kaykayyali/combat-hud-hub/commits/main",
|
||||
"bugs": "https://git.homelab.local/kaykayyali/combat-hud-hub/issues",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "combat-hud-hub",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "Foundry VTT v14 module: generic combat HUD host. Other modules register sections via the public API; built-in core sections render round/turn/damage/dice-streak. Soft-dep on foundry-hooks-lib and battle-focus.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -154,18 +154,43 @@ function buildRenderContext(state, encounter, event) {
|
||||
? allCombatants.filter(c => !c.isPlayer || c.actorId === playerCharId)
|
||||
: allCombatants;
|
||||
|
||||
// Current turn: prefer the encounter's current combatant (if it has
|
||||
// one), fall back to game.combat for legacy callers. The encounter's
|
||||
// authoritative source — battle-focus owns the encounter singleton.
|
||||
let currentTurn = null;
|
||||
try {
|
||||
const cc = game.combat?.combatant;
|
||||
const tokDoc = cc?.token ?? (cc?.tokenId ? canvas?.tokens?.get(cc.tokenId)?.document : null);
|
||||
if (encounter?.currentTurn && typeof encounter.currentTurn === "object") {
|
||||
const ec = encounter.currentTurn;
|
||||
currentTurn = { name: ec.name, tokenId: ec.tokenId ?? null, portrait: ec.portrait ?? null };
|
||||
} else if (encounter?.combatantId && encounter.combatants?.get) {
|
||||
// Some encounter shapes store the current combatant id and a
|
||||
// combatants map; resolve through the map.
|
||||
const cc = encounter.combatants.get(encounter.combatantId);
|
||||
if (cc) {
|
||||
const tok = (() => {
|
||||
try { return canvas?.tokens?.get(cc.tokenId)?.document ?? null; }
|
||||
catch (_) { return null; }
|
||||
})();
|
||||
const actor = cc.actorId ? game.actors?.get(cc.actorId) : null;
|
||||
currentTurn = {
|
||||
name: cc.name ?? cc.actor?.name ?? "(unknown)",
|
||||
tokenId: cc.tokenId ?? null,
|
||||
portrait: resolvePortrait(tokDoc, cc.actor),
|
||||
portrait: resolvePortrait(tok, actor),
|
||||
};
|
||||
}
|
||||
} catch (_) { /* no combat */ }
|
||||
} else {
|
||||
// Legacy fallback: game.combat global.
|
||||
try {
|
||||
const cc = game.combat?.combatant;
|
||||
const tokDoc = cc?.token ?? (cc?.tokenId ? canvas?.tokens?.get(cc.tokenId)?.document : null);
|
||||
if (cc) {
|
||||
currentTurn = {
|
||||
name: cc.name ?? cc.actor?.name ?? "(unknown)",
|
||||
tokenId: cc.tokenId ?? null,
|
||||
portrait: resolvePortrait(tokDoc, cc.actor),
|
||||
};
|
||||
}
|
||||
} catch (_) { /* no combat */ }
|
||||
}
|
||||
|
||||
return {
|
||||
round: encounter?.currentRound ?? state.round ?? 0,
|
||||
@@ -193,19 +218,24 @@ function renderCoreHeader(ctx) {
|
||||
const turn = ct
|
||||
? `<span class="chh-section-header-turn" title="Current turn">
|
||||
<img class="chh-section-header-portrait" src="${ct.portrait ?? ""}" alt="${ct.name ?? ""}" />
|
||||
<span class="chh-section-header-turn-name">${ct.name ?? ""}</span>
|
||||
<span class="chh-section-header-turn-name">▶ ${ct.name ?? ""}</span>
|
||||
</span>`
|
||||
: `<span class="chh-section-header-turn" title="Current turn">Turn ${t}</span>`;
|
||||
return `<div class="chh-section-header-content">${round}${turn}</div>`;
|
||||
}
|
||||
|
||||
function renderCoreCombatants(ctx) {
|
||||
const rows = (ctx.combatants ?? []).map(c => `
|
||||
<li class="chh-section-combatants-row chh-section-combatants-row--${c.side}"
|
||||
data-token-id="${c.tokenId ?? ""}">
|
||||
${c.portrait ? `<img class="chh-section-combatants-portrait" src="${c.portrait}" alt="${c.name ?? ""}" />` : ""}
|
||||
const currentTokenId = ctx.currentTurn?.tokenId ?? null;
|
||||
const rows = (ctx.combatants ?? []).map(c => {
|
||||
const isCurrent = currentTokenId != null && c.tokenId === currentTokenId;
|
||||
return `
|
||||
<li class="chh-section-combatants-row chh-section-combatants-row--${c.side}${isCurrent ? " chh-section-combatants-row--current" : ""}"
|
||||
data-token-id="${c.tokenId ?? ""}"
|
||||
${isCurrent ? 'data-chh-current-turn="true"' : ""}>
|
||||
${c.portrait ? `<img class="chh-section-combatants-portrait${isCurrent ? " chh-section-combatants-portrait--current" : ""}" src="${c.portrait}" alt="${c.name ?? ""}" />` : ""}
|
||||
<div class="chh-section-combatants-body">
|
||||
<div class="chh-section-combatants-name">
|
||||
${isCurrent ? '<span class="chh-section-combatants-current-marker" title="Current turn">▶</span>' : ""}
|
||||
${c.name ?? "(unnamed)"}
|
||||
${c.isPlayer
|
||||
? `<span class="chh-section-combatants-tag">PC</span>`
|
||||
@@ -223,7 +253,8 @@ function renderCoreCombatants(ctx) {
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</li>`).join("");
|
||||
</li>`;
|
||||
}).join("");
|
||||
return rows
|
||||
? `<ul class="chh-section-combatants-list">${rows}</ul>`
|
||||
: `<p class="chh-hud-empty">No combatants yet.</p>`;
|
||||
|
||||
@@ -172,6 +172,34 @@
|
||||
border-left-color: var(--chh-hud-foe);
|
||||
}
|
||||
|
||||
/* Current-turn indicator (v0.2.1). The row whose data-token-id
|
||||
* matches ctx.currentTurn.tokenId gets an accent-color border +
|
||||
* subtle tint + outlined portrait + ▶ marker. The intent is "this
|
||||
* is whose turn it is, at a glance" without redesigning the row.
|
||||
*/
|
||||
.chh-section-combatants-row--current {
|
||||
border-left-color: var(--chh-hud-accent);
|
||||
background: rgba(201, 122, 74, 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(201, 122, 74, 0.25);
|
||||
}
|
||||
|
||||
.chh-section-combatants-row--current.chh-section-combatants-row--party,
|
||||
.chh-section-combatants-row--current.chh-section-combatants-row--foe {
|
||||
/* Override the party/foe border for the current row. */
|
||||
border-left-color: var(--chh-hud-accent);
|
||||
}
|
||||
|
||||
.chh-section-combatants-portrait--current {
|
||||
outline: 2px solid var(--chh-hud-accent);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.chh-section-combatants-current-marker {
|
||||
color: var(--chh-hud-accent);
|
||||
font-weight: 700;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.chh-section-combatants-portrait {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
@@ -200,6 +200,47 @@ mod.api.addSection({
|
||||
mod.api.getHud().forceRender();
|
||||
assert("F.1 forceRender triggers section render", renderCount >= 1);
|
||||
|
||||
// ── Section L — current-turn indicator (v0.2.1) ───────────────────────
|
||||
console.log("[L] current-turn indicator");
|
||||
// Install battle-focus with an encounter that has a current combatant
|
||||
// tokenId, then verify the combatants row renders
|
||||
// data-chh-current-turn="true" on the matching row.
|
||||
game.modules.set("battle-focus", {
|
||||
id: "battle-focus",
|
||||
active: true,
|
||||
api: {
|
||||
version: "0.7.0",
|
||||
getActiveEncounter: () => ({
|
||||
id: "enc1",
|
||||
currentRound: 1,
|
||||
currentTurn: 0,
|
||||
combatantId: "t-bard", // <-- current combatant key for buildRenderContext fallback
|
||||
startedAt: Date.now() - 10000,
|
||||
combatants: new Map([
|
||||
["t-bard", { tokenId: "t-bard", actorId: "a-bard", name: "Bard", isPlayer: true, status: "active" }],
|
||||
["t-goblin", { tokenId: "t-goblin", actorId: "a-goblin", name: "Goblin", isPlayer: false, status: "active" }],
|
||||
]),
|
||||
statsByRound: new Map(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
// Force the encounter into the HUD's render ctx.
|
||||
const hud2 = mod.api.getHud();
|
||||
hud2._encounter = game.modules.get("battle-focus").api.getActiveEncounter();
|
||||
await hud2.forceRender();
|
||||
|
||||
// Inspect the last render HTML (the stub stores it).
|
||||
const html = hud2._lastHtml ?? "";
|
||||
const currentRow = html.match(/<li[^>]*data-chh-current-turn="true"[^>]*>([\s\S]*?)<\/li>/);
|
||||
assert("L.1 combatants row has data-chh-current-turn=\"true\"", !!currentRow);
|
||||
assert("L.2 current-turn row references the current combatant tokenId",
|
||||
currentRow && currentRow[0].includes('data-token-id="t-bard"'));
|
||||
// Verify the header section also shows the "▶" indicator before the turn name.
|
||||
const headerSection = html.match(/<section[^>]*data-section-id="core-header"[^>]*>([\s\S]*?)<\/section>/);
|
||||
assert("L.3 header section rendered", !!headerSection);
|
||||
assert("L.4 header section has ▶ indicator before turn name",
|
||||
headerSection && /▶\s*[^<]*<\/span>/.test(headerSection[1] ?? ""));
|
||||
|
||||
// ── Section G — public API integrity ───────────────────────────────────
|
||||
console.log("[G] public API integrity");
|
||||
const apiKeys = Object.keys(mod.api).sort();
|
||||
|
||||
Reference in New Issue
Block a user