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 = $('