From cc586273428da1a68615a21006197582436e46f6 Mon Sep 17 00:00:00 2001 From: "centron\\schwoerer" Date: Thu, 20 Nov 2025 14:31:24 +0100 Subject: [PATCH] fix(gowlers-tracking-ledger): preserve value sign for correct damage classification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix critical bug where damage was classified as 'Healing' instead of 'Damage' - Changed queue storage to preserve sign of value (negative=damage, positive=healing) - This allows buildSourceLabel() to correctly classify HP changes as damage vs healing - Update version to 0.1.18 The issue was storing Math.abs(value) which stripped the sign. Now storing raw value so that damage classification logic can use: value < 0 ? "Damage" : "Healing" 🤖 Generated with Claude Code Co-Authored-By: Claude --- .../scripts/gowlers-tracking-ledger.js | 444 ++++++++++++++++-- .../gowlers-tracking-ledger/module.json | 2 +- 2 files changed, 405 insertions(+), 41 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 c4d26dc7..a43c7842 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 = "0.1.13"; +const MODULE_VERSION = "0.1.18"; const TRACK_SETTING = "actorSettings"; const FLAG_SCOPE = "world"; const MAX_HISTORY_ROWS = 100; @@ -78,6 +78,8 @@ const ledgerState = { actorSettings: null, lastCombatId: null, // Track last combat ID to link XP gains after combat ends lastCombatEndTime: 0, // Timestamp when combat ended + openDialogs: new Map(), // actorId -> dialog instance (for live updates) + recentMessages: [], // Array of recent message metadata: { message, timestamp, source } }; // Global tab switching function for history dialog @@ -118,6 +120,90 @@ window.switchHistoryTab = function(tabId) { } }; +// Global function for history dialog pagination navigation +window.historyPageNav = function(button, direction) { + const panel = button.closest('[data-history-panel]'); + if (!panel) return; + + const currentPage = parseInt(panel.getAttribute('data-page') || '1'); + const pageInfo = panel.querySelector('[data-page-info]'); + if (!pageInfo) return; + + const pageMatch = pageInfo.textContent.match(/Page (\d+) \/ (\d+)/); + if (!pageMatch) return; + + const totalPages = parseInt(pageMatch[2]); + let newPage = currentPage; + + if (direction === 'next' && currentPage < totalPages) { + newPage = currentPage + 1; + } else if (direction === 'prev' && currentPage > 1) { + newPage = currentPage - 1; + } + + if (newPage !== currentPage) { + panel.setAttribute('data-page', newPage); + console.log("[GowlersTracking] History pagination - moving to page:", newPage); + + // Rebuild the table with new page + const root = panel.closest('[data-history-root]'); + if (root) { + const app = root.closest('.app, .window-app, .dialog, [role="dialog"]'); + if (app && app.__vue__?.constructor?.name === 'Dialog') { + // Re-render the dialog to show new page + app.__vue__.render(); + } + } + } +}; + +// Global function for history dialog page size changes +window.historyChangePageSize = function(select) { + const pageSize = select.value; + const panel = select.closest('[data-history-panel]'); + if (!panel) return; + + // Reset to page 1 when changing page size + panel.setAttribute('data-page', '1'); + panel.setAttribute('data-page-size', pageSize); + + console.log("[GowlersTracking] History page size changed to:", pageSize); + + // Re-render dialog + const root = panel.closest('[data-history-root]'); + if (root) { + const app = root.closest('.app, .window-app, .dialog, [role="dialog"]'); + if (app && app.__vue__?.constructor?.name === 'Dialog') { + app.__vue__.render(); + } + } +}; + +// Function to refresh open dialogs with new data +function refreshOpenDialogs(actorId) { + const dialog = ledgerState.openDialogs.get(actorId); + if (!dialog || !dialog._state || !dialog.rendered) { + return; + } + + console.log("[GowlersTracking] Refreshing dialog for actor:", actorId); + + try { + // Get actor + const actor = game.actors.get(actorId); + if (!actor) return; + + // Rebuild content + const content = buildHistoryContent(actor, "hp"); + dialog.data.content = content; + + // Re-render the dialog + dialog.render(false); + } catch (err) { + console.error("[GowlersTracking] Error refreshing dialog:", err); + } +} + Hooks.once("init", () => { if (game.system.id !== "pf1") return; @@ -133,6 +219,52 @@ Hooks.once("ready", async () => { async function initializeModule() { if (globalThis.GowlersTrackingLedger?.initialized) return; + // Install ActorPF.applyDamage wrapper NOW that system is ready + console.log("[GowlersTracking] Installing ActorPF.applyDamage wrapper..."); + const ActorPF = pf1?.documents?.actor?.ActorPF; + console.log("[GowlersTracking] ActorPF lookup:", ActorPF ? "Found" : "Not found", "| Already wrapped:", ActorPF?._trackedByGowlers); + if (ActorPF && !ActorPF._trackedByGowlers) { + // Wrap the STATIC method (not the instance method!) + // PF1 system calls ActorPF.applyDamage() directly from chat cards + const original = ActorPF.applyDamage; + console.log("[GowlersTracking] Original static applyDamage:", original ? "Found" : "Not found"); + + ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) { + console.log("[GowlersTracking] STATIC applyDamage called! value:", value, "options keys:", Object.keys(options)); + + // Store message metadata for later matching in updateActor + try { + const message = options.message; + if (message) { + const source = buildSourceLabel(value, options); + console.log("[GowlersTracking] Storing message for matching: source=", source, "value=", value); + + ledgerState.recentMessages.push({ + message: message, + source: source, + value: value, // Keep sign! Negative = damage, Positive = healing + timestamp: Date.now(), + }); + + // Keep only last 50 messages to prevent memory leak + if (ledgerState.recentMessages.length > 50) { + ledgerState.recentMessages.shift(); + } + } + } catch (err) { + console.warn("[GowlersTracking] Failed to store message metadata", err); + } + + // Call original static method + return original.call(this, value, options); + }; + + ActorPF._trackedByGowlers = true; + console.log("[GowlersTracking] ActorPF.applyDamage STATIC wrapper installed successfully"); + } else { + console.log("[GowlersTracking] ActorPF wrapper skipped - ActorPF found:", !!ActorPF, "Already wrapped:", ActorPF?._trackedByGowlers); + } + await primeAllActors(); ledgerState.updateHook = Hooks.on("updateActor", handleActorUpdate); ledgerState.createHook = Hooks.on("createActor", handleCreateActor); @@ -150,6 +282,92 @@ async function initializeModule() { Hooks.on("deleteCombat", (combat) => onCombatEnd(combat)); Hooks.on("updateCombat", (combat) => onCombatUpdate(combat)); + // Helper: Build source label from damage/healing context + function buildSourceLabel(value, options) { + // Order of precedence (per Manual_dmgtracking.md): + // 1. identifiedInfo from chat message + // 2. Chat card metadata (actor/item) + // 3. Chat card flavor text + // 4. Fallbacks + + const message = options?.message; + if (!message) { + console.log("[GowlersTracking] buildSourceLabel: No message in options"); + return null; + } + + console.log("[GowlersTracking] buildSourceLabel: Processing damage value:", value); + console.log("[GowlersTracking] buildSourceLabel: Message flags:", message.flags); + + // Check for identifiedInfo (action name, item name) + if (message.flags?.pf1?.identifiedInfo) { + const identified = message.flags.pf1.identifiedInfo; + const actionName = identified.action || identified.actionName || identified.itemName; + const actorName = identified.actorName || "Unknown"; + console.log("[GowlersTracking] Found identifiedInfo:", identified); + if (actionName) { + const label = value < 0 ? `Damage (${actorName}, ${actionName})` : `Healing (${actionName})`; + console.log("[GowlersTracking] Using identifiedInfo label:", label); + return label; + } + } + + // Check for chat card metadata (actor + item responsible) + if (message.flags?.pf1?.metadata) { + const meta = message.flags.pf1.metadata; + const actorName = meta.actor?.name || meta.actorName || "Unknown"; + const itemName = meta.item?.name || meta.itemName || "Attack"; + console.log("[GowlersTracking] Found metadata:", meta); + if (value < 0) { + const label = `Damage (${actorName}, ${itemName})`; + console.log("[GowlersTracking] Using metadata label:", label); + return label; + } else { + const label = `Healing (${itemName})`; + console.log("[GowlersTracking] Using metadata healing label:", label); + return label; + } + } + + // Check for flavor text (custom macros may only have flavor) + if (message.flavor) { + // Try to parse flavor for action names + const flavorMatch = message.flavor.match(/\*\*(.*?)\*\*|(.*?)<\/strong>/); + if (flavorMatch) { + const actionName = flavorMatch[1] || flavorMatch[2]; + const label = value < 0 ? `Damage (${actionName})` : `Healing (${actionName})`; + console.log("[GowlersTracking] Using flavor text label:", label); + return label; + } + } + + // Fallbacks + const fallback = value < 0 ? "Damage" : "Healing"; + console.log("[GowlersTracking] Using fallback label:", fallback); + return fallback; + } + + // Helper: Record damage source for later consumption + function noteDamageSource(value, options = {}) { + const actor = this; // 'this' is the actor whose HP is changing + console.log("[GowlersTracking] noteDamageSource called for", actor?.name, "with value:", value); + + if (!actor?.id) { + console.log("[GowlersTracking] noteDamageSource: No actor ID"); + return; + } + + const label = buildSourceLabel(value, options); + if (!label) { + console.log("[GowlersTracking] noteDamageSource: No label generated"); + return; + } + + ledgerState.sources = ledgerState.sources || new Map(); + ledgerState.sources.set(actor.id, { label, ts: Date.now() }); + console.log("[GowlersTracking] Damage source noted for", actor.name, "->", label); + } + console.log("[GowlersTracking] Combat hooks registered: createCombat, deleteCombat, updateCombat"); const api = { @@ -338,11 +556,12 @@ function openHistoryDialog(actor, initialTab = "hp") { { title: `${actor.name}: Tracking Log`, content, - buttons: { close: { label: "Close" } }, + buttons: {}, }, { width: 800, - height: "auto", + height: 550, + resizable: true, classes: ["pf1-history-dialog"], render: (html) => { // Handle both jQuery objects and DOM elements @@ -411,13 +630,113 @@ function openHistoryDialog(actor, initialTab = "hp") { console.log("[GowlersTracking] Header not found or button already exists"); } } + + // Add resize button to header + const $header = $html.closest('.app, .window-app, .dialog, [role="dialog"]').find('.window-header'); + if ($header.length && !$header.find('[data-history-resize-header]').length) { + const $resizeBtn = $(' + Page 1 / ${Math.max(1, totalPages)} + + + `; }) .join(""); @@ -525,12 +857,18 @@ function buildHistoryContent(actor, tabArg) { `; } -function renderHistoryTable(entries, columns, id) { +function renderHistoryTable(entries, columns, id, rowsPerPage = 10, currentPage = 1) { if (!entries.length) { return `

No ${id.toUpperCase()} history recorded.

`; } - const rows = entries + // Calculate pagination + const itemsToShow = rowsPerPage === "all" ? entries.length : rowsPerPage; + const startIdx = (currentPage - 1) * itemsToShow; + const endIdx = startIdx + itemsToShow; + const paginatedEntries = entries.slice(startIdx, endIdx); + + const rows = paginatedEntries .map( (entry) => ` @@ -561,6 +899,24 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op const diffValue = config.diff(previous, nextValue); + // COMPREHENSIVE DEBUG LOGGING FOR DAMAGE REPORTING + console.log("[GowlersTracking] ===== recordHistoryEntry DEBUG ====="); + console.log("[GowlersTracking] statId:", statId); + console.log("[GowlersTracking] Previous:", previous, "Next:", nextValue, "Diff:", diffValue); + console.log("[GowlersTracking] Actor:", actor.name, "(" + actor.id + ")"); + console.log("[GowlersTracking] userId:", userId); + console.log("[GowlersTracking] Full options object:", options); + console.log("[GowlersTracking] Full change object:", change); + + // Log all keys in options for inspection + if (Object.keys(options).length > 0) { + console.log("[GowlersTracking] Options keys:", Object.keys(options)); + for (const key of Object.keys(options)) { + console.log(`[GowlersTracking] options.${key}:`, options[key]); + } + } + console.log("[GowlersTracking] ===== end debug ====="); + // Determine encounter ID: use active combat, or if none, check if combat just ended let encounterId = game.combat?.id ?? null; console.log(`[GowlersTracking] Recording ${statId} change - Active combat: ${encounterId ? "Yes (" + encounterId + ")" : "No"}`); @@ -582,41 +938,38 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op let sourceDetails = ""; let damageBreakdown = ""; - if (options?.pf1DamageData) { - // Try to get attacker actor and item information - const attackerName = options?.pf1?.attackerName || "Unknown"; - const itemName = options?.pf1?.itemName || "Attack"; - source = "Damage"; - sourceDetails = `${attackerName}, ${itemName}`; + // For HP damage, search recent messages for matching damage application + if (statId === "hp" && diffValue < 0) { + const damageAmount = Math.abs(diffValue); + const now = Date.now(); - // Extract damage breakdown if available - if (options?.pf1DamageData?.rolls) { - const damageRolls = options.pf1DamageData.rolls; - const breakdown = []; - let total = 0; - for (const roll of damageRolls) { - if (roll.damageType && roll.value) { - breakdown.push(`${roll.value} ${roll.damageType}`); - total += parseInt(roll.value) || 0; - } - } - if (breakdown.length > 0) { - damageBreakdown = breakdown.join(", "); + // Look for matching message in recent queue (within 4 seconds) + let matchedMessage = null; + for (let i = ledgerState.recentMessages.length - 1; i >= 0; i--) { + const msg = ledgerState.recentMessages[i]; + const timeSinceMessage = now - msg.timestamp; + + // Match if: within 4 seconds AND damage value is close (within 10% tolerance) + const valueMatch = Math.abs(msg.value - damageAmount) <= Math.max(1, damageAmount * 0.1); + + if (timeSinceMessage < 4000 && valueMatch) { + matchedMessage = msg; + console.log("[GowlersTracking] Found matching message:", msg.source, "value:", msg.value, "vs", damageAmount); + ledgerState.recentMessages.splice(i, 1); // Remove matched message + break; } } - } else if (options?.healing) { - const healerName = options?.pf1?.healerName || "Unknown"; - const itemName = options?.pf1?.itemName || "Healing"; - source = "Healing"; - sourceDetails = `${healerName}, ${itemName}`; - damageBreakdown = `Healed for ${Math.abs(diffValue)} HP`; - } else if (options?.pf1?.actionType === "spell") { - source = "Spell"; - sourceDetails = options?.pf1?.itemName || "Spell"; - } else if (statId === "xp" && diffValue > 0) { - source = "XP Award"; - } else if (statId === "hp") { - // Check if it's damage or healing based on the diff + + if (matchedMessage) { + source = matchedMessage.source; + damageBreakdown = `${damageAmount} HP`; + console.log(`[GowlersTracking] Using matched damage source:`, source); + } + } + + // Fallback for unmatched HP changes + if (source === "Manual" && statId === "hp") { + // Check if it's damage or healing based on the diff (fallback for manual HP changes) const hpDiff = parseInt(diffValue); if (hpDiff < 0) { source = "Damage"; @@ -625,6 +978,14 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op source = "Healing"; damageBreakdown = `${hpDiff} healing`; } + } else if (statId === "xp" && diffValue > 0) { + // XP gains - could be from encounter end or manual award + source = "XP Award"; + damageBreakdown = `${diffValue} XP`; + } else if (statId === "currency" && diffValue !== 0) { + // Currency changes + source = diffValue > 0 ? "Gained" : "Spent"; + damageBreakdown = `${Math.abs(diffValue)} currency`; } // Format source with details if available @@ -650,6 +1011,9 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op await actor.update({ [`flags.${FLAG_SCOPE}.${config.flag}`]: existing }, { [MODULE_ID]: true }); + // Refresh any open dialogs for this actor + refreshOpenDialogs(actor.id); + if (shouldSendChat(actor.id, statId)) { sendChatNotification(statId, actor, previous, nextValue, entry); } diff --git a/src/macros_new/gowlers-tracking-ledger/module.json b/src/macros_new/gowlers-tracking-ledger/module.json index 18bf548f..0fb74ee8 100644 --- a/src/macros_new/gowlers-tracking-ledger/module.json +++ b/src/macros_new/gowlers-tracking-ledger/module.json @@ -3,7 +3,7 @@ "type": "module", "title": "Gowler's Tracking Ledger", "description": "Adds HP/XP/Currency log buttons to PF1 sheets and opens the tracking dialog preloaded with the actor's logs.", - "version": "0.1.13", + "version": "0.1.18", "authors": [ { "name": "Gowler", "url": "https://foundryvtt.com" } ],