/** * Add a History tab to PF1 actor sheets with HP/XP/Currency subtabs. * Requires the corresponding flag-tracking macros to populate data. */ const TAB_ID = "pf1-history"; const TAB_LABEL = "History"; if (!game.system.id.includes("pf1")) { ui.notifications.warn("This macro is intended for the PF1 system."); return; } game.pf1 ??= {}; game.pf1.historyTab ??= {}; const PF1_SHEET_HOOKS = [ "renderActorSheetPFCharacter", "renderActorSheetPFNPC", "renderActorSheetPFNPCLoot", "renderActorSheetPFNPCLite", "renderActorSheetPFTrap", "renderActorSheetPFVehicle", "renderActorSheetPFHaunt", "renderActorSheetPFBasic", ]; game.pf1.historyTab.renderHooks ??= []; if (game.pf1.historyTab.renderHooks.length) { return ui.notifications.info("History tab hooks already active."); } for (const hook of PF1_SHEET_HOOKS) { const id = Hooks.on(hook, (sheet, html) => { const delays = [0, 50, 200]; delays.forEach((delay) => setTimeout(() => { try { injectHistoryTab(sheet); } catch (err) { console.error("PF1 History Tab | Failed to render history tab", err); } }, delay) ); }); game.pf1.historyTab.renderHooks.push({ event: hook, id }); } ui.notifications.info("PF1 History tab enabled. Reopen actor sheets to see the new tab."); function injectHistoryTab(sheet) { if (!sheet?.element?.length) return; console.log("PF1 History Tab | Injecting for", sheet.actor?.name, sheet.id); const actor = sheet.actor; const nav = sheet.element.find('.sheet-navigation.tabs[data-group="primary"]').first(); const body = sheet.element.find(".sheet-body").first(); if (!nav.length || !body.length) return; ensureHistoryTab(nav); const existing = body.find(`.tab[data-tab="${TAB_ID}"]`).first(); const content = buildHistoryContent(actor); if (existing.length) existing.html(content); else body.append(`
${content}
`); sheet._tabs?.forEach((tabs) => tabs.bind(sheet.element[0])); setupObserver(sheet, nav); } function ensureHistoryTab(nav) { if (nav.find(`[data-tab="${TAB_ID}"]`).length) return; nav.append(`${TAB_LABEL}`); } function setupObserver(sheet, nav) { game.pf1.historyTab.observers ??= new Map(); const key = sheet.id; if (game.pf1.historyTab.observers.has(key)) return; const observer = new MutationObserver(() => { const currentNav = sheet.element.find('.sheet-navigation.tabs[data-group="primary"]').first(); if (!currentNav.length) return; ensureHistoryTab(currentNav); }); observer.observe(sheet.element[0], { childList: true, subtree: true }); game.pf1.historyTab.observers.set(key, observer); const closeHandler = () => { observer.disconnect(); game.pf1.historyTab.observers.delete(key); sheet.element.off("remove", closeHandler); }; sheet.element.on("remove", closeHandler); } function buildHistoryContent(actor) { const configs = [ { id: "hp", label: "HP", flag: "pf1HpHistory", columns: [ { label: "Timestamp", render: (e) => formatDate(e.timestamp) }, { label: "HP", render: (e) => e.hp }, { label: "Δ", render: (e) => e.diff }, { label: "User", render: (e) => e.user ?? "" }, { label: "Source", render: (e) => e.source ?? "" }, ], }, { id: "xp", label: "XP", flag: "pf1XpHistory", columns: [ { label: "Timestamp", render: (e) => formatDate(e.timestamp) }, { label: "XP", render: (e) => e.value }, { label: "Δ", render: (e) => e.diff }, { label: "User", render: (e) => e.user ?? "" }, ], }, { id: "currency", label: "Currency", flag: "pf1CurrencyHistory", columns: [ { label: "Timestamp", render: (e) => formatDate(e.timestamp) }, { label: "Totals", render: (e) => e.value }, { label: "Δ", render: (e) => e.diff }, { label: "User", render: (e) => e.user ?? "" }, ], }, ]; const nav = configs .map((cfg, idx) => `${cfg.label}`) .join(""); const panels = configs .map((cfg, idx) => { const entries = actor.getFlag("world", cfg.flag) ?? []; const table = renderHistoryTable(entries, cfg.columns, cfg.id); return `
${table}
`; }) .join(""); return `
${panels}
`; } function renderHistoryTable(entries, columns, id) { if (!entries.length) return `

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

`; const rows = entries .map( (entry) => ` ${columns.map((col) => `${col.render(entry) ?? ""}`).join("")} ` ) .join(""); return `
${columns.map((col) => ``).join("")} ${rows}
${col.label}
`; } function formatDate(ts) { return new Date(ts).toLocaleString(); }