/**
* 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 ``;
})
.join("");
return `
`;
}
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) => `| ${col.label} | `).join("")}
${rows}
`;
}
function formatDate(ts) {
return new Date(ts).toLocaleString();
}