From 8646ebd3e1cc0a9e9c4c849f9603e1e279600332 Mon Sep 17 00:00:00 2001 From: "centron\\schwoerer" Date: Wed, 19 Nov 2025 15:44:51 +0100 Subject: [PATCH] fix(gowlers-tracking-ledger): link XP gains to encounters after combat ends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add lastCombatId and lastCombatEndTime tracking to preserve encounter data - Update recordHistoryEntry to link XP within 5 seconds of combat end to last encounter - Change encounter status from 'ended' to 'finished' for clarity - Store encounter ID when combat ends to catch post-combat XP awards Fixes issue where XP gained after combat ends was not linked to the encounter, and encounters were not being marked as finished in the history. Update version to 0.1.8 🤖 Generated with Claude Code Co-Authored-By: Claude --- .../scripts/gowlers-tracking-ledger.js | 28 +- .../gowlers-tracking-ledger/module.json | 2 +- .../scripts/gowlers-tracking-ledger.js | 995 ------------------ 3 files changed, 26 insertions(+), 999 deletions(-) delete mode 100644 src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js 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 6b8a65ff..eb9a4ce6 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.7"; +const MODULE_VERSION = "0.1.8"; const TRACK_SETTING = "actorSettings"; const FLAG_SCOPE = "world"; const MAX_HISTORY_ROWS = 100; @@ -76,6 +76,8 @@ const ledgerState = { createHook: null, deleteHook: null, actorSettings: null, + lastCombatId: null, // Track last combat ID to link XP gains after combat ends + lastCombatEndTime: 0, // Timestamp when combat ended }; // Global tab switching function for history dialog @@ -542,13 +544,27 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId) { if (!config) return; const diffValue = config.diff(previous, nextValue); + + // Determine encounter ID: use active combat, or if none, check if combat just ended + let encounterId = game.combat?.id ?? null; + + // If no active combat but XP is being recorded shortly after combat ended, link to last encounter + if (!encounterId && statId === "xp" && ledgerState.lastCombatId) { + const timeSinceEnd = Date.now() - ledgerState.lastCombatEndTime; + // Link to last encounter if within 5 seconds (allows time for XP award after combat ends) + if (timeSinceEnd < 5000) { + encounterId = ledgerState.lastCombatId; + console.log("[GowlersTracking] Linking XP entry to last encounter:", encounterId); + } + } + const entry = { timestamp: Date.now(), value: config.formatValue(nextValue), diff: config.formatDiff(diffValue), user: game.users.get(userId)?.name ?? "System", source: "", - encounterId: game.combat?.id ?? null, // Track which encounter this change occurred in + encounterId: encounterId, }; const existing = (await actor.getFlag(FLAG_SCOPE, config.flag)) ?? []; @@ -959,10 +975,16 @@ async function onCombatUpdate(combat) { */ async function onCombatEnd(combat) { if (!combat) return; + + // Store combat ID to link XP gains that occur after combat ends + ledgerState.lastCombatId = combat.id; + ledgerState.lastCombatEndTime = Date.now(); + console.log("[GowlersTracking] Combat ended, storing encounter ID:", combat.id); + for (const combatant of combat.combatants) { const actor = combatant.actor; if (!actor) continue; - await updateEncounterSummary(actor, combat, "ended"); + await updateEncounterSummary(actor, combat, "finished"); } } diff --git a/src/macros_new/gowlers-tracking-ledger/module.json b/src/macros_new/gowlers-tracking-ledger/module.json index 72394410..732e52f3 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.7", + "version": "0.1.8", "authors": [ { "name": "Gowler", "url": "https://foundryvtt.com" } ], diff --git a/src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js b/src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js deleted file mode 100644 index 2d6c88e2..00000000 --- a/src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js +++ /dev/null @@ -1,995 +0,0 @@ - -const MODULE_ID = "gowlers-tracking-ledger"; -const MODULE_VERSION = "0.1.3"; -const TRACK_SETTING = "actorSettings"; -const FLAG_SCOPE = "world"; -const MAX_HISTORY_ROWS = 100; -const BUTTON_CLASS = "pf1-history-btn"; -const BUTTON_TITLE = "Open Log"; -const BUTTON_ICON = ''; -const BUTTON_STYLE = - "position:absolute;right:6px;top:4px;border:none;background:transparent;padding:0;width:18px;height:18px;display:flex;align-items:center;justify-content:center;color:inherit;cursor:pointer;"; -const DEFAULT_TRACKING = Object.freeze({ hp: true, xp: true, currency: true }); -const DEFAULT_CHAT = Object.freeze({ hp: false, xp: false, currency: false }); -const SETTINGS_VERSION = 2; -const COIN_ORDER = ["pp", "gp", "sp", "cp"]; -const ENCOUNTER_FLAG = "pf1EncounterHistory"; - -const STAT_CONFIGS = { - hp: { - id: "hp", - label: "HP", - flag: "pf1HpHistory", - getter: (actor) => readNumericProperty(actor, "system.attributes.hp.value"), - equals: (a, b) => Object.is(a, b), - diff: (prev, next) => next - prev, - formatValue: (value) => `${value}`, - formatDiff: (diff) => (diff >= 0 ? `+${diff}` : `${diff}`), - clone: (value) => value, - }, - xp: { - id: "xp", - label: "XP", - flag: "pf1XpHistory", - getter: (actor) => readNumericProperty(actor, "system.details.xp.value"), - equals: (a, b) => Object.is(a, b), - diff: (prev, next) => next - prev, - formatValue: (value) => `${value}`, - formatDiff: (diff) => (diff >= 0 ? `+${diff}` : `${diff}`), - clone: (value) => value, - }, - currency: { - id: "currency", - label: "Currency", - flag: "pf1CurrencyHistory", - getter: (actor) => readCurrency(actor), - equals: currencyEquals, - diff: diffCurrency, - formatValue: (value) => formatCurrency(value, false), - formatDiff: (value) => formatCurrency(value, true), - clone: (value) => (value ? { ...value } : value), - }, -}; - -const LOG_TARGETS = [ - { id: "hp", finder: findHpHeader }, - { id: "xp", finder: findXpHeader }, - { id: "currency", finder: findCurrencyHeader }, -]; - -const SHEET_EVENTS = [ - "renderActorSheetPFCharacter", - "renderActorSheetPFNPC", - "renderActorSheetPFNPCLoot", - "renderActorSheetPFNPCLite", - "renderActorSheetPFTrap", - "renderActorSheetPFVehicle", - "renderActorSheetPFHaunt", - "renderActorSheetPFBasic", -]; - -const ledgerState = { - baselines: new Map(), // actorId -> { hp, xp, currency } - sheetObservers: new Map(), // `${sheetId}-${stat}` -> observer - sheetHooks: [], - updateHook: null, - createHook: null, - deleteHook: null, - actorSettings: null, -}; - -Hooks.once("init", () => { - if (game.system.id !== "pf1") return; - - registerSettings(); - registerSettingsMenu(); -}); - -Hooks.once("ready", async () => { - if (game.system.id !== "pf1") return; - await initializeModule(); -}); - -async function initializeModule() { - if (globalThis.GowlersTrackingLedger?.initialized) return; - - await primeAllActors(); - ledgerState.updateHook = Hooks.on("updateActor", handleActorUpdate); - ledgerState.createHook = Hooks.on("createActor", handleCreateActor); - ledgerState.deleteHook = Hooks.on("deleteActor", (actor) => ledgerState.baselines.delete(actor.id)); - - ledgerState.sheetHooks = SHEET_EVENTS.map((event) => - Hooks.on(event, (sheet) => { - const delays = [0, 100, 250]; - delays.forEach((delay) => setTimeout(() => attachButtons(sheet), delay)); - }) - ); - - // Track combat encounters - Hooks.on("combatStart", (combat) => onCombatStart(combat)); - Hooks.on("combatEnd", (combat) => onCombatEnd(combat)); - Hooks.on("updateCombat", (combat) => onCombatUpdate(combat)); - - const api = { - initialized: true, - openConfig: () => new TrackingLedgerConfig().render(true), - openConfigForActor: (actorId) => TrackingLedgerConfig.openForActor(actorId), - setActorTracking: setActorTracking, - getActorTracking, - }; - game.modules.get(MODULE_ID).api = api; - globalThis.GowlersTrackingLedger = api; -} - -function registerSettings() { - game.settings.register(MODULE_ID, TRACK_SETTING, { - scope: "world", - config: false, - type: Object, - default: {}, - }); -} - -function registerSettingsMenu() { - game.settings.registerMenu(MODULE_ID, "config", { - name: "Gowler's Tracking Ledger", - label: "Configure Tracking", - hint: "Choose which actors should track HP/XP/Currency logs.", - type: TrackingLedgerConfig, - restricted: true, - icon: "fas fa-book", - }); -} - -async function handleCreateActor(actor) { - await ensureActorConfig(actor); - primeActor(actor); -} - -function handleActorUpdate(actor, change, options, userId) { - if (options?.[MODULE_ID]) return; - if (!(actor instanceof Actor)) return; - - const tracking = getActorTracking(actor.id); - if (!tracking) return; - - const baselines = ledgerState.baselines.get(actor.id) ?? {}; - let changed = false; - - for (const statId of Object.keys(STAT_CONFIGS)) { - if (!tracking[statId]) continue; - const config = STAT_CONFIGS[statId]; - const nextValue = config.getter(actor); - if (nextValue === null || nextValue === undefined) continue; - - if (baselines[statId] === undefined) { - baselines[statId] = config.clone(nextValue); - continue; - } - - if (config.equals(baselines[statId], nextValue)) continue; - - const previous = baselines[statId]; - baselines[statId] = config.clone(nextValue); - changed = true; - recordHistoryEntry(actor, statId, previous, nextValue, userId).catch((err) => - console.error(`Tracking Ledger | Failed to append ${statId} entry`, err) - ); - } - - if (changed) ledgerState.baselines.set(actor.id, baselines); -} - -async function primeAllActors() { - const settings = getSettingsCache(); - let dirty = false; - for (const actor of game.actors.contents) { - if (!settings[actor.id]) { - settings[actor.id] = createActorConfig(); - dirty = true; - } - primeActor(actor); - } - if (dirty) { - await saveActorSettings(settings); - } -} - -function primeActor(actor, stats = Object.keys(STAT_CONFIGS)) { - if (!(actor instanceof Actor)) return; - const existing = ledgerState.baselines.get(actor.id) ?? {}; - for (const statId of stats) { - const config = STAT_CONFIGS[statId]; - const value = config.getter(actor); - if (value === null || value === undefined) continue; - existing[statId] = config.clone(value); - } - ledgerState.baselines.set(actor.id, existing); -} - -function attachButtons(sheet) { - if (!sheet?.element?.length || !sheet.actor) return; - for (const target of LOG_TARGETS) { - try { - const container = target.finder(sheet.element); - if (!container?.length) continue; - addButton(container, sheet, target.id); - } catch (err) { - console.warn(`Tracking Ledger | Failed to add ${target.id} button`, err); - } - } -} - -function addButton(container, sheet, statId) { - if (!container || !container.length || !sheet.actor) return; - if (container.find(`.${BUTTON_CLASS}[data-log-target="${statId}"]`).length) return; - - if (container.css("position") === "static") container.css("position", "relative"); - - const label = STAT_CONFIGS[statId]?.label ?? statId.toUpperCase(); - const button = $(` - - `); - button.on("click", (event) => { - event.preventDefault(); - event.stopPropagation(); - openHistoryDialog(sheet.actor, statId); - }); - - container.append(button); - observeContainer(container, sheet, statId); -} - -function observeContainer(container, sheet, statId) { - const key = `${sheet.id}-${statId}`; - if (ledgerState.sheetObservers.has(key)) return; - if (!container[0]) return; - - const observer = new MutationObserver(() => addButton(container, sheet, statId)); - observer.observe(container[0], { childList: true }); - ledgerState.sheetObservers.set(key, observer); - - const cleanup = () => { - observer.disconnect(); - ledgerState.sheetObservers.delete(key); - sheet.element.off("remove", cleanup); - }; - sheet.element.on("remove", cleanup); -} - -function findHpHeader(root) { - let header = root.find(".info-box-header.health-details h3").first(); - if (header.length) return header; - header = root.find('[data-tooltip-extended="hit-points"] h3').first(); - if (header.length) return header; - return findHeaderByText(root, "Hit Points"); -} - -function findXpHeader(root) { - const experienceHeader = root.find(".info-box.experience h5").first(); - if (experienceHeader.length) return experienceHeader; - return findHeaderByText(root, "Experience"); -} - -function findCurrencyHeader(root) { - const currencySection = root.find('.tab.inventory ol.currency h3').first(); - if (currencySection.length) return currencySection; - return findHeaderByText(root, "Currency"); -} - -function findHeaderByText(root, text) { - const lower = text.toLowerCase(); - return root - .find("h1, h2, h3, h4, h5, label") - .filter((_, el) => el.textContent?.trim().toLowerCase() === lower) - .first(); -} - -function openHistoryDialog(actor, initialTab = "hp") { - if (!actor) return; - const content = buildHistoryContent(actor, initialTab); - - const dialog = new Dialog( - { - title: `${actor.name}: Tracking Log`, - content, - buttons: { close: { label: "Close" } }, - }, - { - width: 800, - height: "auto", - classes: ["pf1-history-dialog"], - render: (html) => { - // Handle both jQuery objects and DOM elements - const $html = $(html); - const $root = $html.find('[data-history-root]'); - - if (!$root.length) { - console.warn("[GowlersTracking] Root not found"); - return; - } - - // Tab switching - $root.find('[data-history-tab]').off('click').on('click', function(e) { - e.preventDefault(); - const tabId = $(this).attr('data-history-tab'); - console.log("[GowlersTracking] Tab clicked:", tabId); - - // Remove active from all tabs and hide all panels - $root.find('[data-history-tab]').removeClass('active'); - $root.find('[data-history-panel]').hide(); - - // Add active to clicked tab and show its panel - $root.find(`[data-history-tab="${tabId}"]`).addClass('active'); - $root.find(`[data-history-panel="${tabId}"]`).show(); - }); - - // Add config icon to dialog header (GM only) - if (game.user?.isGM) { - const $header = $html.closest('.dialog').find('.dialog-header'); - if ($header.length && !$header.find('[data-history-config-header]').length) { - const $configBtn = $('