From 0a076ae5ee47346125ca1c69c1badffd07f23e0a Mon Sep 17 00:00:00 2001 From: "centron\\schwoerer" Date: Sat, 22 Nov 2025 22:25:28 +0100 Subject: [PATCH] Damage Meter auto-refresh, NPC toggle, and iconized breakdown --- .../scripts/gowlers-tracking-ledger.js | 212 +++++++++++++++++- 1 file changed, 207 insertions(+), 5 deletions(-) diff --git a/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js b/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js index b3eb33fb..ae164061 100644 --- a/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js +++ b/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js @@ -1,6 +1,6 @@ const MODULE_ID = "gowlers-tracking-ledger"; -const MODULE_VERSION = "1.1.0"; +const MODULE_VERSION = "1.1.1"; const TRACK_SETTING = "actorSettings"; const FLAG_SCOPE = "world"; const MAX_HISTORY_ROWS = 100; @@ -83,6 +83,8 @@ const ledgerState = { recentMessages: [], // Array of recent message metadata: { message, timestamp, source } historyPageState: new Map(), // actorId -> { [tabId]: { page, pageSize } } historyTabState: new Map(), // actorId -> active tab id + damageOverlay: null, + damageMeterIncludeNPCs: true, }; function getHistoryPageState(actorId, tabId) { @@ -267,6 +269,12 @@ Hooks.once("init", () => { Hooks.once("ready", async () => { if (game.system.id !== "pf1") return; await initializeModule(); + registerSceneControls(); + // Expose NPC toggle helper for damage meter checkbox + window.GowlersTrackingDamageMeterToggleNPCs = (checked) => { + ledgerState.damageMeterIncludeNPCs = !!checked; + refreshDamageMeterOverlay(); + }; }); async function initializeModule() { @@ -538,6 +546,7 @@ async function initializeModule() { openConfigForActor: (actorId) => TrackingLedgerConfig.openForActor(actorId), setActorTracking: setActorTracking, getActorTracking, + openDamageMeterOverlay: () => openDamageMeterOverlay(), }; game.modules.get(MODULE_ID).api = api; globalThis.GowlersTrackingLedger = api; @@ -563,6 +572,57 @@ function registerSettingsMenu() { }); } +function registerSceneControls() { + Hooks.on("getSceneControlButtons", (controls) => { + const tokenControls = controls.find((c) => c.name === "token"); + if (!tokenControls) return; + + const existing = tokenControls.tools.find((t) => t.name === "damage-meter"); + if (existing) return; + + tokenControls.tools.push({ + name: "damage-meter", + title: "Damage Meter", + icon: "fas fa-bolt", + button: true, + onClick: () => openDamageMeterOverlay(), + }); + }); +} + +function openDamageMeterOverlay() { + try { + if (ledgerState.damageOverlay?.rendered) { + const content = buildDamageMeterContent(); + ledgerState.damageOverlay.data.content = content; + ledgerState.damageOverlay.render(false); + ledgerState.damageOverlay.bringToTop(); + return; + } + + const content = buildDamageMeterContent(); + + const dlg = new Dialog({ + title: "Damage Meter", + content, + buttons: {}, + close: () => { + ledgerState.damageOverlay = null; + }, + }, { + id: `${MODULE_ID}-damage-meter`, + width: 300, + height: "auto", + resizable: true, + }); + + ledgerState.damageOverlay = dlg; + dlg.render(true); + } catch (err) { + console.error("[GowlersTracking] Failed to open Damage Meter overlay:", err); + } +} + async function handleCreateActor(actor) { await ensureActorConfig(actor); primeActor(actor); @@ -933,6 +993,9 @@ function buildHistoryContent(actor, tabArg) { { label: "Details", render: (entry) => { + if (entry.damageDetails?.parts?.length) { + return formatDamagePartsWithIcons(entry.damageDetails.parts); + } if (entry.breakdown) return entry.breakdown; if (entry.amount != null) return `${entry.amount} damage`; return ""; @@ -1066,11 +1129,11 @@ function renderHistoryTable(entries, columns, id, rowsPerPage = 10, currentPage const itemsToShow = rowsPerPage === "all" ? entries.length : rowsPerPage; const startIdx = (currentPage - 1) * itemsToShow; const endIdx = startIdx + itemsToShow; - const paginatedEntries = entries.slice(startIdx, endIdx); + const paginatedEntries = entries.slice(startIdx, endIdx); - const rows = paginatedEntries - .map( - (entry) => ` + const rows = paginatedEntries + .map( + (entry) => ` { const rowTitle = columns.map((col) => (col.getTitle ? col.getTitle(entry) : "")).find((t) => t); return rowTitle ? ` title="${rowTitle}"` : ""; @@ -1228,6 +1291,7 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op source: source, breakdown: damageBreakdown, encounterId: encounterId, + damageDetails: matchedMessage.damageDetails ?? null, }; recordDamageDealt(attacker, dealtEntry); } @@ -1289,6 +1353,7 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op // Refresh any open dialogs for this actor refreshOpenDialogs(actor.id); + refreshDamageMeterOverlay(); if (shouldSendChat(actor.id, statId)) { sendChatNotification(statId, actor, previous, nextValue, entry); @@ -1446,6 +1511,35 @@ function isNonlethalType(type) { return String(type ?? "").toLowerCase() === "nonlethal"; } +function formatDamagePartsWithIcons(parts) { + if (!Array.isArray(parts) || !parts.length) return ""; + const iconMap = { + slashing: { icon: "ra ra-sword", color: "#e3c000" }, + piercing: { icon: "ra ra-spear-head", color: "#2c7be5" }, + bludgeoning: { icon: "ra ra-large-hammer", color: "#e03131" }, + fire: { icon: "ra ra-fire", color: "#f76707" }, + cold: { icon: "ra ra-snowflake", color: "#3bc9db" }, + electricity: { icon: "ra ra-lightning-bolt", color: "#f0c419" }, + acid: { icon: "ra ra-round-bottom-flask", color: "#2f9e44" }, + sonic: { icon: "ra ra-megaphone", color: "#22b8cf" }, + force: { icon: "ra ra-crystal-ball", color: "#845ef7" }, + negative: { icon: "ra ra-skull", color: "#7950f2" }, + positive: { icon: "ra ra-sun", color: "#fab005" }, + precision: { icon: "ra ra-target-arrows", color: "#000" }, + nonlethal: { icon: "ra ra-hand", color: "#000" }, + untyped: { icon: "ra ra-uncertainty", color: "#666" }, + }; + return parts + .map((p) => { + const baseType = (p.types && p.types[0]) || p.customTypes?.[0] || p.materials?.[0] || "untyped"; + const mapEntry = iconMap[baseType?.toLowerCase?.()] ?? iconMap.untyped; + const icon = ``; + const amt = Number.isFinite(p.total) ? p.total : p.formula ?? "?"; + return `${icon}${amt}`; + }) + .join(" "); +} + function resolveParticipantName(participant) { if (!participant) return "Unknown"; @@ -1549,6 +1643,114 @@ function buildEncounterXpTooltip(encounter) { } } +function refreshDamageMeterOverlay() { + const dlg = ledgerState.damageOverlay; + if (!dlg?.rendered) return; + try { + const content = buildDamageMeterContent(); + dlg.data.content = content; + dlg.render(false); + } catch (err) { + console.warn("[GowlersTracking] Failed to refresh Damage Meter overlay:", err); + } +} + +function computeDamageMeterData() { + const currentEncounterId = game.combat?.id ?? ledgerState.lastCombatId ?? null; + const actors = game.actors.contents ?? []; + const totals = []; + const includeNPCs = ledgerState.damageMeterIncludeNPCs; + + for (const actor of actors) { + const isNPC = actor.type !== "character" && !actor.hasPlayerOwner; + if (!includeNPCs && isNPC) continue; + + const entries = actor.getFlag(FLAG_SCOPE, DAMAGE_DEALT_FLAG) ?? []; + if (!entries.length) continue; + + const filtered = currentEncounterId ? entries.filter((e) => e.encounterId === currentEncounterId) : entries; + if (!filtered.length) continue; + + const total = filtered.reduce((sum, e) => sum + (Number(e.amount) || 0), 0); + totals.push({ + actorId: actor.id, + name: actor.name, + total, + hits: filtered.length, + last: filtered[0], + }); + } + + totals.sort((a, b) => b.total - a.total); + const grandTotal = totals.reduce((s, t) => s + t.total, 0); + return { totals, grandTotal, encounterId: currentEncounterId }; +} + +function buildDamageMeterContent() { + const { totals, grandTotal, encounterId } = computeDamageMeterData(); + const encounterLabel = encounterId ? `Encounter: ${encounterId.slice(0, 8)}` : "All history"; + const includeNPCs = ledgerState.damageMeterIncludeNPCs; + + const rows = totals.length + ? totals + .map( + (t, idx) => { + const max = totals[0]?.total || 1; + const pct = Math.max(1, Math.round((t.total / max) * 100)); + return ` + + ${idx + 1} + ${t.name} + ${t.total} + ${t.hits} + +
+
+
+ ${t.last?.breakdown ?? ""} + + `; + } + ) + .join("") + : `No damage recorded${encounterId ? " for this encounter" : ""}.`; + + const npcToggle = ``; + + return ` +
+
Damage Meter
+
${encounterLabel}${npcToggle} v${MODULE_VERSION}
+ + + + + + + + + + + + ${rows} + + + + + + + + + + +
#ActorTotalHitsLast Breakdown
Total${grandTotal}
+
+ `; +} + function checkboxValue(value, fallback = false) { if (value === undefined || value === null) return fallback; if (Array.isArray(value)) value = value[value.length - 1];