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}
+
+
+
+ | # |
+ Actor |
+ Total |
+ Hits |
+ Last Breakdown |
+
+
+
+ ${rows}
+
+
+
+ |
+ Total |
+ ${grandTotal} |
+ |
+ |
+
+
+
+
+ `;
+}
+
function checkboxValue(value, fallback = false) {
if (value === undefined || value === null) return fallback;
if (Array.isArray(value)) value = value[value.length - 1];