diff --git a/src/macro_activate-currency-history-tab.js b/src/macro_activate-currency-history-tab.js deleted file mode 100644 index d6daab10..00000000 --- a/src/macro_activate-currency-history-tab.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Activate currency history tracking that stores entries in flags only. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; -const CURRENCY_PATH = "system.currency"; -const FLAG_SCOPE = "world"; -const FLAG_KEY = "pf1CurrencyHistory"; -const MAX_ROWS = 50; -const COIN_ORDER = ["pp", "gp", "sp", "cp"]; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -game.pf1 ??= {}; -game.pf1.currencyHistoryFlags ??= { current: {}, hooks: {} }; -const state = game.pf1.currencyHistoryFlags; - -const logKey = `pf1-currency-history-flags-${ACTOR_ID}`; -if (state.hooks[logKey]) { - Hooks.off("updateActor", state.hooks[logKey]); - delete state.hooks[logKey]; -} - -primeSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR); - -state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => { - if (actor.id !== ACTOR_ID) return; - - const newCurrency = readCurrency(actor); - if (!newCurrency) return; - - const previous = state.current[ACTOR_ID]; - if (!previous) { - state.current[ACTOR_ID] = newCurrency; - return; - } - if (currencyEquals(previous, newCurrency)) return; - - const diff = diffCurrency(previous, newCurrency); - const user = game.users.get(userId); - - state.current[ACTOR_ID] = newCurrency; - - appendHistoryEntry(actor, { - value: formatCurrency(newCurrency), - diff: formatCurrency(diff, true), - user: user?.name ?? "System", - }).catch((err) => console.error("Currency History Flags | Failed to append entry", err)); -}); - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`Currency flag history tracking active for ${actorName}.`); - -async function appendHistoryEntry(actor, entry) { - const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? []; - existing.unshift({ - timestamp: Date.now(), - value: entry.value, - diff: entry.diff, - user: entry.user, - }); - if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS); - - await actor.setFlag(FLAG_SCOPE, FLAG_KEY, existing); -} - -function primeSnapshot(actor) { - const currency = readCurrency(actor); - if (!currency) return; - state.current[ACTOR_ID] = currency; -} - -function readCurrency(actor) { - if (!actor) return null; - const base = foundry.utils.getProperty(actor, CURRENCY_PATH) ?? actor.system?.currency; - if (!base) return null; - const clone = {}; - for (const coin of COIN_ORDER) clone[coin] = Number(base[coin] ?? 0); - return clone; -} - -function currencyEquals(a, b) { - return COIN_ORDER.every((coin) => (a[coin] ?? 0) === (b[coin] ?? 0)); -} - -function diffCurrency(prev, next) { - const diff = {}; - for (const coin of COIN_ORDER) diff[coin] = (next[coin] ?? 0) - (prev[coin] ?? 0); - return diff; -} - -function formatCurrency(obj, skipZero = false) { - return COIN_ORDER.map((coin) => obj[coin] ?? 0) - .map((value, idx) => { - if (skipZero && value === 0) return null; - const label = COIN_ORDER[idx]; - const v = skipZero && value > 0 ? `+${value}` : `${value}`; - return `${label}:${v}`; - }) - .filter(Boolean) - .join(" "); -} diff --git a/src/macro_activate-currency-history.js b/src/macro_activate-currency-history.js deleted file mode 100644 index b863d770..00000000 --- a/src/macro_activate-currency-history.js +++ /dev/null @@ -1,300 +0,0 @@ -/** - * Track currency changes for a single actor and store them in ===Currency History=== notes. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; -const CURRENCY_PATH = "system.currency"; -const UPDATE_CONTEXT_FLAG = "currencyHistory"; -const FLAG_SCOPE = "world"; -const FLAG_KEY = "pf1CurrencyHistory"; -const BLOCK_NAME = "pf1-currency-history"; -const MAX_ROWS = 50; -const BLOCK_START = ""; -const BLOCK_END = ""; -const COIN_ORDER = ["pp", "gp", "sp", "cp"]; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -game.pf1 ??= {}; -game.pf1.currencyHistory ??= {}; -const state = game.pf1.currencyHistory; -state.sources ??= new Map(); -state.current ??= {}; -state.hooks ??= {}; - -const logKey = `pf1-currency-history-${ACTOR_ID}`; - -if (state.hooks[logKey]) { - Hooks.off("updateActor", state.hooks[logKey]); - delete state.hooks[logKey]; -} - -ensureDamageSourceTracking(); -primeCurrencySnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR); - -state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => { - if (actor.id !== ACTOR_ID) return; - if (options?.[UPDATE_CONTEXT_FLAG]) return; - - const newCurrency = readCurrencyValue(actor); - if (!newCurrency) return; - - const previous = state.current[ACTOR_ID]; - if (!previous) { - state.current[ACTOR_ID] = newCurrency; - return; - } - - if (currencyEquals(previous, newCurrency)) return; - - const diff = diffCurrency(previous, newCurrency); - const diffText = formatCurrency(diff, true); - const user = game.users.get(userId); - const source = consumeDamageSource(actor.id) ?? "Manual change"; - - state.current[ACTOR_ID] = newCurrency; - - appendHistoryRow(actor, { - value: formatCurrency(newCurrency), - diffText, - user, - source, - snapshot: newCurrency, - }).catch((err) => console.error("Currency History | Failed to append entry", err)); -}); - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`Currency history tracking active for ${actorName}.`); - -async function appendHistoryRow(actor, entry) { - const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? []; - const newEntry = { - timestamp: Date.now(), - value: entry.value, - diff: entry.diffText, - user: entry.user?.name ?? "System", - source: entry.source ?? "", - }; - existing.unshift(newEntry); - if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS); - - const notes = actor.system.details?.notes?.value ?? ""; - const block = renderHistoryBlock(existing); - const updatedNotes = injectHistoryBlock(notes, block); - - await actor.update( - { - [`flags.${FLAG_SCOPE}.${FLAG_KEY}`]: existing, - "system.details.notes.value": updatedNotes, - }, - { [UPDATE_CONTEXT_FLAG]: true } - ); -} - -function renderHistoryBlock(entries) { - const rows = entries - .map( - (e) => ` - - ${new Date(e.timestamp).toLocaleString()} - ${e.value} - ${e.diff} - ${e.user ?? ""} - ${e.source ?? ""} - ` - ) - .join(""); - - const blockId = `${BLOCK_NAME}-${ACTOR_ID}`; - return ` -${BLOCK_START} -
-
- ===Currency History=== (click to collapse) -
- - -
- - - - - - - - - - - - ${rows} - -
TimestampTotalsΔUserSource
-
- -
-${BLOCK_END}`.trim(); -} - -function injectHistoryBlock(notes, block) { - const blockPattern = new RegExp(`${escapeRegExp(BLOCK_START)}[\\s\\S]*?${escapeRegExp(BLOCK_END)}`, "g"); - let cleaned = notes.replace(blockPattern, ""); - if (cleaned === notes) { - const sectionPattern = new RegExp(`]*data-history-block="${BLOCK_NAME}"[^>]*>[\\s\\S]*?<\\/section>`, "g"); - cleaned = notes.replace(sectionPattern, ""); - } - cleaned = cleaned.trim(); - const separator = cleaned.length && !cleaned.endsWith("\n") ? "\n\n" : ""; - return `${cleaned}${separator}${block}`; -} - -function consumeDamageSource(actorId) { - const entry = state.sources.get(actorId); - if (!entry) return null; - state.sources.delete(actorId); - return entry.label; -} - -function ensureDamageSourceTracking() { - if (state.applyDamageWrapped) return; - - const ActorPF = pf1?.documents?.actor?.ActorPF; - if (!ActorPF?.applyDamage) return; - - const original = ActorPF.applyDamage; - ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) { - try { - noteDamageSource(value, options); - } catch (err) { - console.warn("Currency History | Failed to record source", err); - } - return original.call(this, value, options); - }; - - state.applyDamageWrapped = true; -} - -function noteDamageSource(value, options) { - const actors = resolveActorTargets(options?.targets); - if (!actors.some((a) => a?.id === ACTOR_ID)) return; - - const label = buildSourceLabel(value, options); - if (!label) return; - state.sources.set(ACTOR_ID, { label, ts: Date.now() }); -} - -function buildSourceLabel(value, options = {}) { - const info = options.message?.flags?.pf1?.identifiedInfo ?? {}; - const metadata = options.message?.flags?.pf1?.metadata ?? {}; - const fromChatFlavor = options.message?.flavor?.trim(); - - const actorDoc = resolveActorFromMetadata(metadata); - const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null; - const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null; - const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null; - - if (actorName && actionName) return `${actorName} -> ${actionName}`; - if (actionName) return actionName; - if (actorName) return actorName; - return null; -} - -function primeCurrencySnapshot(actor) { - const currency = readCurrencyValue(actor); - if (!currency) return; - state.current[ACTOR_ID] = currency; -} - -function resolveActorTargets(targets) { - let list = []; - if (Array.isArray(targets) && targets.length) list = targets; - else list = canvas?.tokens?.controlled ?? []; - - return list - .map((entry) => { - if (!entry) return null; - if (entry instanceof Actor) return entry; - if (entry.actor) return entry.actor; - if (entry.document?.actor) return entry.document.actor; - return null; - }) - .filter((actor) => actor instanceof Actor && actor.id); -} - -function readCurrencyValue(actor) { - if (!actor) return null; - const base = foundry.utils.getProperty(actor, CURRENCY_PATH) ?? actor.system?.currency; - if (!base) return null; - const clone = {}; - for (const coin of COIN_ORDER) clone[coin] = Number(base[coin] ?? 0); - return clone; -} - -function currencyEquals(a, b) { - return COIN_ORDER.every((coin) => (a[coin] ?? 0) === (b[coin] ?? 0)); -} - -function diffCurrency(prev, next) { - const diff = {}; - for (const coin of COIN_ORDER) diff[coin] = (next[coin] ?? 0) - (prev[coin] ?? 0); - return diff; -} - -function formatCurrency(obj, skipZero = false) { - return COIN_ORDER.map((coin) => obj[coin] ?? 0) - .map((value, idx) => { - if (skipZero && value === 0) return null; - const label = COIN_ORDER[idx]; - const v = skipZero ? (value > 0 ? `+${value}` : `${value}`) : value; - return `${label}:${v}`; - }) - .filter(Boolean) - .join(" "); -} - -function resolveActorFromMetadata(metadata = {}) { - if (!metadata.actor) return null; - - if (typeof fromUuidSync === "function") { - try { - const doc = fromUuidSync(metadata.actor); - if (doc instanceof Actor) return doc; - } catch (err) { - console.warn("Currency History | Failed to resolve actor UUID", err); - } - } - - const id = metadata.actor.split(".").pop(); - return game.actors.get(id) ?? null; -} - -function resolveItemFromMetadata(actor, metadata = {}) { - if (!metadata.item || !(actor?.items instanceof Collection)) return null; - return actor.items.get(metadata.item) ?? null; -} - -function escapeRegExp(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} diff --git a/src/macro_activate-hp-history-tab.js b/src/macro_activate-hp-history-tab.js deleted file mode 100644 index b517f44e..00000000 --- a/src/macro_activate-hp-history-tab.js +++ /dev/null @@ -1,208 +0,0 @@ -/** - * Activate HP history tracking that stores entries in flags only. - * Pair with macro_enable-history-tab.js to view the data inside a History tab. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; -const HP_PATH = "system.attributes.hp.value"; -const FLAG_SCOPE = "world"; -const FLAG_KEY = "pf1HpHistory"; -const UPDATE_CONTEXT_FLAG = "hpHistoryFlags"; -const MAX_ROWS = 50; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -game.pf1 ??= {}; -game.pf1.hpHistoryFlags ??= { sources: new Map(), current: {}, hooks: {} }; -const state = game.pf1.hpHistoryFlags; - -const logKey = `pf1-hp-history-flags-${ACTOR_ID}`; -if (state.hooks[logKey]) { - Hooks.off("updateActor", state.hooks[logKey]); - delete state.hooks[logKey]; -} - -ensureDamageSourceTracking(); -primeSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR); - -state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => { - if (actor.id !== ACTOR_ID) return; - if (options?.[UPDATE_CONTEXT_FLAG]) return; - - const newHP = readHpValue(actor); - if (newHP === null) return; - - const previous = state.current[ACTOR_ID]; - if (previous === undefined) { - state.current[ACTOR_ID] = newHP; - return; - } - if (Object.is(newHP, previous)) return; - - const diff = newHP - previous; - const diffText = diff >= 0 ? `+${diff}` : `${diff}`; - const user = game.users.get(userId); - const source = consumeDamageSource(actor.id) ?? inferManualSource(diff); - - state.current[ACTOR_ID] = newHP; - - appendHistoryEntry(actor, { - hp: newHP, - diff: diffText, - user: user?.name ?? "System", - source: source ?? "", - }).catch((err) => console.error("HP History Flags | Failed to append entry", err)); -}); - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`HP flag history tracking active for ${actorName}.`); - -async function appendHistoryEntry(actor, entry) { - const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? []; - existing.unshift({ - timestamp: Date.now(), - hp: entry.hp, - diff: entry.diff, - user: entry.user, - source: entry.source, - }); - if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS); - - await actor.setFlag(FLAG_SCOPE, FLAG_KEY, existing); -} - -function consumeDamageSource(actorId) { - const entry = state.sources.get(actorId); - if (!entry) return null; - state.sources.delete(actorId); - return entry.label; -} - -function inferManualSource(diff) { - if (!Number.isFinite(diff) || diff === 0) return null; - return diff > 0 ? "Manual healing" : "Manual damage"; -} - -function ensureDamageSourceTracking() { - if (state.applyDamageWrapped) return; - - const ActorPF = pf1?.documents?.actor?.ActorPF; - if (!ActorPF?.applyDamage) return; - - const original = ActorPF.applyDamage; - ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) { - try { - noteDamageSource(value, options); - } catch (err) { - console.warn("HP History Flags | Failed to record source", err); - } - return original.call(this, value, options); - }; - - state.applyDamageWrapped = true; -} - -function noteDamageSource(value, options) { - const actors = resolveActorTargets(options?.targets); - if (!actors.some((a) => a?.id === ACTOR_ID)) return; - - const label = buildSourceLabel(value, options); - if (!label) return; - state.sources.set(ACTOR_ID, { label, ts: Date.now() }); -} - -function buildSourceLabel(value, options = {}) { - const info = options.message?.flags?.pf1?.identifiedInfo ?? {}; - const metadata = options.message?.flags?.pf1?.metadata ?? {}; - const fromChatFlavor = options.message?.flavor?.trim(); - - const actorDoc = resolveActorFromMetadata(metadata); - const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null; - const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null; - const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null; - - let label = null; - if (actorName && actionName) label = `${actorName} -> ${actionName}`; - else if (actionName) label = actionName; - else if (actorName) label = actorName; - else label = value < 0 ? "Healing" : "Damage"; - - if (options.isCritical) label += " (Critical)"; - if (options.asNonlethal) label += " [Nonlethal]"; - - return label; -} - -function primeSnapshot(actor) { - const hp = readHpValue(actor); - if (hp === null) return; - state.current[ACTOR_ID] = hp; -} - -function resolveActorTargets(targets) { - let list = []; - if (Array.isArray(targets) && targets.length) list = targets; - else list = canvas?.tokens?.controlled ?? []; - - return list - .map((entry) => { - if (!entry) return null; - if (entry instanceof Actor) return entry; - if (entry.actor) return entry.actor; - if (entry.document?.actor) return entry.document.actor; - return null; - }) - .filter((actor) => actor instanceof Actor && actor.id); -} - -function readHpValue(actor) { - if (!actor) return null; - - const candidates = [ - HP_PATH, - HP_PATH.replace(/^system\./, "data."), - HP_PATH.replace(/^system\./, ""), - ]; - - for (const path of candidates) { - const value = foundry.utils.getProperty(actor, path); - if (value !== undefined) { - const numeric = Number(value); - return Number.isFinite(numeric) ? numeric : null; - } - if (actor.system) { - const trimmed = path.startsWith("system.") ? path.slice(7) : path; - const systemValue = foundry.utils.getProperty(actor.system, trimmed); - if (systemValue !== undefined) { - const numeric = Number(systemValue); - return Number.isFinite(numeric) ? numeric : null; - } - } - } - - return null; -} - -function resolveActorFromMetadata(metadata = {}) { - if (!metadata.actor) return null; - - if (typeof fromUuidSync === "function") { - try { - const doc = fromUuidSync(metadata.actor); - if (doc instanceof Actor) return doc; - } catch (err) { - console.warn("HP History Flags | Failed to resolve actor UUID", err); - } - } - - const id = metadata.actor.split(".").pop(); - return game.actors.get(id) ?? null; -} - -function resolveItemFromMetadata(actor, metadata = {}) { - if (!metadata.item || !(actor?.items instanceof Collection)) return null; - return actor.items.get(metadata.item) ?? null; -} diff --git a/src/macro_activate-hp-history.js b/src/macro_activate-hp-history.js deleted file mode 100644 index 7c1dc396..00000000 --- a/src/macro_activate-hp-history.js +++ /dev/null @@ -1,369 +0,0 @@ -/** - * Activate HP history tracking for a single actor. - * Instead of sending chat messages, HP changes are written into an - * ===HP History=== table inside the actor's Notes field. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; -const HP_PATH = "system.attributes.hp.value"; -const UPDATE_CONTEXT_FLAG = "hpHistory"; -const FLAG_SCOPE = "world"; -const FLAG_KEY = "pf1HpHistory"; -const BLOCK_NAME = "pf1-hp-history"; -const MAX_ROWS = 50; -const BLOCK_START = ""; -const BLOCK_END = ""; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -game.pf1 ??= {}; -game.pf1.hpHistory ??= {}; -const state = game.pf1.hpHistory; -state.sources ??= new Map(); -state.current ??= {}; -state.hooks ??= {}; - -const logKey = `pf1-hp-history-${ACTOR_ID}`; - -// Remove any previous hook for this actor so the macro is idempotent. -if (state.hooks[logKey]) { - Hooks.off("updateActor", state.hooks[logKey]); - delete state.hooks[logKey]; -} - -ensureDamageSourceTracking(); -primeHpSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR); - -state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => { - if (actor.id !== ACTOR_ID) return; - if (options?.[UPDATE_CONTEXT_FLAG]) return; // Ignore our own notes updates. - - const newHP = readHpValue(actor); - if (newHP === null) return; - - const previous = state.current[ACTOR_ID]; - if (previous === undefined) { - state.current[ACTOR_ID] = newHP; - return; - } - if (Object.is(newHP, previous)) return; - - const diff = newHP - previous; - const diffText = diff >= 0 ? `+${diff}` : `${diff}`; - const user = game.users.get(userId); - const source = consumeDamageSource(actor.id) ?? inferManualSource(diff); - - state.current[ACTOR_ID] = newHP; - - appendHistoryRow(actor, { - previous, - newHP, - diff, - diffText, - user, - source, - }).catch((err) => console.error("HP History | Failed to append entry", err)); -}); - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`HP history tracking active for ${actorName}.`); - -/** - * Append an entry to the Notes table. - * @param {Actor} actor - * @param {object} entry - */ -async function appendHistoryRow(actor, entry) { - const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? []; - const newEntry = { - timestamp: Date.now(), - hp: entry.newHP, - diff: entry.diffText, - user: entry.user?.name ?? "System", - source: entry.source ?? "", - }; - existing.unshift(newEntry); - if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS); - - const notes = actor.system.details?.notes?.value ?? ""; - const block = renderHistoryBlock(existing); - const updatedNotes = injectHistoryBlock(notes, block); - - await actor.update( - { - [`flags.${FLAG_SCOPE}.${FLAG_KEY}`]: existing, - "system.details.notes.value": updatedNotes, - }, - { [UPDATE_CONTEXT_FLAG]: true } - ); -} - -/** - * Consume and clear the stored damage source for this actor, if any. - * @param {string} actorId - */ -function consumeDamageSource(actorId) { - const entry = state.sources.get(actorId); - if (!entry) return null; - state.sources.delete(actorId); - return entry.label; -} - -/** - * Provide a fallback label when the change was manual or unidentified. - * @param {number} diff - */ -function inferManualSource(diff) { - if (!Number.isFinite(diff) || diff === 0) return null; - return diff > 0 ? "Manual healing" : "Manual damage"; -} - -/** - * Ensure ActorPF.applyDamage is wrapped so we can capture originating chat data. - */ -function ensureDamageSourceTracking() { - if (state.applyDamageWrapped) return; - - const ActorPF = pf1?.documents?.actor?.ActorPF; - if (!ActorPF?.applyDamage) return; - - const original = ActorPF.applyDamage; - ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) { - try { - noteDamageSource(value, options); - } catch (err) { - console.warn("HP History | Failed to record damage source", err); - } - return original.call(this, value, options); - }; - - state.applyDamageWrapped = true; -} - -/** - * Record the best-guess label for the HP change if the tracked actor is among the targets. - * @param {number} value - * @param {object} options - */ -function noteDamageSource(value, options) { - const actors = resolveActorTargets(options?.targets); - if (!actors.some((a) => a?.id === ACTOR_ID)) return; - - const label = buildSourceLabel(value, options); - if (!label) return; - state.sources.set(ACTOR_ID, { label, ts: Date.now() }); -} - -/** - * Try to describe where the HP change originated. - * @param {number} value - * @param {object} options - */ -function buildSourceLabel(value, options = {}) { - const info = options.message?.flags?.pf1?.identifiedInfo ?? {}; - const metadata = options.message?.flags?.pf1?.metadata ?? {}; - const fromChatFlavor = options.message?.flavor?.trim(); - - const actorDoc = resolveActorFromMetadata(metadata); - const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null; - const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null; - const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null; - - let label = null; - if (actorName && actionName) label = `${actorName} -> ${actionName}`; - else if (actionName) label = actionName; - else if (actorName) label = actorName; - else label = value < 0 ? "Healing" : "Damage"; - - if (options.isCritical) label += " (Critical)"; - if (options.asNonlethal) label += " [Nonlethal]"; - - return label; -} - -/** - * Store the actor's current HP so the first change has a baseline. - * @param {Actor|null} actor - */ -function primeHpSnapshot(actor) { - const hp = readHpValue(actor); - if (hp === null) return; - state.current[ACTOR_ID] = hp; -} - -/** - * Resolve actors targeted by the damage application. - * Falls back to currently controlled tokens if no explicit targets were supplied. - * @param {Array} targets - * @returns {Actor[]} - */ -function resolveActorTargets(targets) { - let list = []; - if (Array.isArray(targets) && targets.length) list = targets; - else list = canvas?.tokens?.controlled ?? []; - - return list - .map((entry) => { - if (!entry) return null; - if (entry instanceof Actor) return entry; - if (entry.actor) return entry.actor; - if (entry.document?.actor) return entry.document.actor; - return null; - }) - .filter((actor) => actor instanceof Actor && actor.id); -} - -/** - * Read HP from the actor using several possible data paths. - * @param {Actor|null} actor - * @returns {number|null} - */ -function readHpValue(actor) { - if (!actor) return null; - - const candidates = [ - HP_PATH, - HP_PATH.replace(/^system\./, "data."), - HP_PATH.replace(/^system\./, ""), - ]; - - for (const path of candidates) { - const value = foundry.utils.getProperty(actor, path); - if (value !== undefined) { - const numeric = Number(value); - return Number.isFinite(numeric) ? numeric : null; - } - if (actor.system) { - const trimmed = path.startsWith("system.") ? path.slice(7) : path; - const systemValue = foundry.utils.getProperty(actor.system, trimmed); - if (systemValue !== undefined) { - const numeric = Number(systemValue); - return Number.isFinite(numeric) ? numeric : null; - } - } - } - - return null; -} - -/** - * Fetch the actor referenced in the chat card metadata, if any. - * @param {object} metadata - * @returns {Actor|null} - */ -function resolveActorFromMetadata(metadata = {}) { - if (!metadata.actor) return null; - - if (typeof fromUuidSync === "function") { - try { - const doc = fromUuidSync(metadata.actor); - if (doc instanceof Actor) return doc; - } catch (err) { - console.warn("HP History | Failed to resolve actor UUID", err); - } - } - - const id = metadata.actor.split(".").pop(); - return game.actors.get(id) ?? null; -} - -/** - * Fetch the item referenced in the chat card metadata from the given actor. - * @param {Actor} actor - * @param {object} metadata - * @returns {Item|null} - */ -function resolveItemFromMetadata(actor, metadata = {}) { - if (!metadata.item || !(actor?.items instanceof Collection)) return null; - return actor.items.get(metadata.item) ?? null; -} - -/** - * Render the HP history heading and table HTML. - * @param {Array} entries - * @returns {string} - */ -function renderHistoryBlock(entries) { - const rows = entries - .map( - (e) => ` - - ${new Date(e.timestamp).toLocaleString()} - ${e.hp} - ${e.diff} - ${e.user ?? ""} - ${e.source ?? ""} - ` - ) - .join(""); - - const blockId = `${BLOCK_NAME}-${ACTOR_ID}`; - const table = ` -${BLOCK_START} -
-
- ===HP History=== (click to collapse) -
- - -
- - - - - - - - - - - - ${rows} - -
TimestampHPΔUserSource
-
- -
-${BLOCK_END}`.trim(); - - return table; -} - -function injectHistoryBlock(notes, block) { - const blockPattern = new RegExp(`${escapeRegExp(BLOCK_START)}[\\s\\S]*?${escapeRegExp(BLOCK_END)}`, "g"); - let cleaned = notes.replace(blockPattern, ""); - if (cleaned === notes) { - const sectionPattern = new RegExp(`]*data-history-block="${BLOCK_NAME}"[^>]*>[\\s\\S]*?<\\/section>`, "g"); - cleaned = notes.replace(sectionPattern, ""); - } - cleaned = cleaned.trim(); - const separator = cleaned.length && !cleaned.endsWith("\n") ? "\n\n" : ""; - return `${cleaned}${separator}${block}`; -} - -function escapeRegExp(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} diff --git a/src/macro_activate-hp-tracking.js b/src/macro_activate-hp-tracking.js deleted file mode 100644 index d1b1ef93..00000000 --- a/src/macro_activate-hp-tracking.js +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Activate HP tracking for a single actor. - * Drop this script into a Foundry macro. - */ - -// Change this lookup (or set ACTOR_ID directly) to match the actor you want to monitor. -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; // e.g. replace string with actor id -const HP_PATH = "system.attributes.hp.value"; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -game.pf1 ??= {}; -game.pf1.hpLogger ??= {}; -game.pf1.hpLogger.sources ??= new Map(); -game.pf1.hpLogger.current ??= {}; - -const logKey = `pf1-hp-log-${ACTOR_ID}`; - -// Remove existing hook for the same actor so re-running stays idempotent. -if (game.pf1.hpLogger[logKey]) { - Hooks.off("updateActor", game.pf1.hpLogger[logKey]); - delete game.pf1.hpLogger[logKey]; -} - -ensureDamageSourceTracking(); -primeHpSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR); - -game.pf1.hpLogger[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => { - if (actor.id !== ACTOR_ID) return; - - const newHP = readHpValue(actor); - if (newHP === null) return; - - const previous = game.pf1.hpLogger.current[ACTOR_ID]; - if (previous === undefined) { - game.pf1.hpLogger.current[ACTOR_ID] = newHP; - return; - } - if (Object.is(newHP, previous)) return; // No HP change. - - const diff = newHP - previous; - const diffText = diff >= 0 ? `+${diff}` : `${diff}`; - const user = game.users.get(userId); - const source = consumeDamageSource(actor.id) ?? inferManualSource(diff); - const sourceLine = source ? `
Source: ${source}` : ""; - - ChatMessage.create({ - content: `HP Monitor
${actor.name} HP: ${previous} -> ${newHP} (${diffText})${user ? ` by ${user.name}` : ""}${sourceLine}`, - speaker: { alias: "System" }, - }); - - game.pf1.hpLogger.current[ACTOR_ID] = newHP; -}); - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`HP change hook active for ${actorName}.`); - -/** - * Consume and clear the stored damage source for this actor, if any. - * @param {string} actorId - */ -function consumeDamageSource(actorId) { - const entry = game.pf1.hpLogger.sources.get(actorId); - if (!entry) return null; - game.pf1.hpLogger.sources.delete(actorId); - return entry.label; -} - -/** - * Provide a fallback label when the change was manual or unidentified. - * @param {number} diff - */ -function inferManualSource(diff) { - if (!Number.isFinite(diff) || diff === 0) return null; - return diff > 0 ? "Manual healing" : "Manual damage"; -} - -/** - * Ensure ActorPF.applyDamage is wrapped so we can capture the originating chat card. - */ -function ensureDamageSourceTracking() { - const state = game.pf1.hpLogger; - if (state.applyDamageWrapped) return; - - const ActorPF = pf1?.documents?.actor?.ActorPF; - if (!ActorPF?.applyDamage) return; - - const original = ActorPF.applyDamage; - ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) { - try { - noteDamageSource(value, options); - } catch (err) { - console.warn("HP Logger | Failed to record damage source", err); - } - return original.call(this, value, options); - }; - - state.applyDamageWrapped = true; -} - -/** - * Record the best-guess label for the HP change if the tracked actor is among the targets. - * @param {number} value - * @param {object} options - */ -function noteDamageSource(value, options) { - const actors = resolveActorTargets(options?.targets); - if (!actors.some((a) => a?.id === ACTOR_ID)) return; - - const label = buildSourceLabel(value, options); - if (!label) return; - game.pf1.hpLogger.sources.set(ACTOR_ID, { label, ts: Date.now() }); -} - -/** - * Try to describe where the HP change originated. - * @param {number} value - * @param {object} options - */ -function buildSourceLabel(value, options = {}) { - const info = options.message?.flags?.pf1?.identifiedInfo ?? {}; - const metadata = options.message?.flags?.pf1?.metadata ?? {}; - const fromChatFlavor = options.message?.flavor?.trim(); - - const actorDoc = resolveActorFromMetadata(metadata); - const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null; - const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null; - const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null; - - let label = null; - if (actorName && actionName) label = `${actorName} -> ${actionName}`; - else if (actionName) label = actionName; - else if (actorName) label = actorName; - else label = value < 0 ? "Healing" : "Damage"; - - if (options.isCritical) label += " (Critical)"; - if (options.asNonlethal) label += " [Nonlethal]"; - - return label; -} - -/** - * Store the actor's current HP so the first change has a baseline. - * @param {Actor|null} actor - */ -function primeHpSnapshot(actor) { - const hp = readHpValue(actor); - if (hp === null) return; - game.pf1.hpLogger.current[ACTOR_ID] = hp; -} - -/** - * Resolve actors targeted by the damage application. - * Falls back to currently controlled tokens if no explicit targets were supplied. - * @param {Array} targets - * @returns {Actor[]} - */ -function resolveActorTargets(targets) { - let list = []; - if (Array.isArray(targets) && targets.length) list = targets; - else list = canvas?.tokens?.controlled ?? []; - - return list - .map((entry) => { - if (!entry) return null; - if (entry instanceof Actor) return entry; - if (entry.actor) return entry.actor; - if (entry.document?.actor) return entry.document.actor; - return null; - }) - .filter((actor) => actor instanceof Actor && actor.id); -} - -/** - * Read HP from the actor using several possible data paths. - * @param {Actor|null} actor - * @returns {number|null} - */ -function readHpValue(actor) { - if (!actor) return null; - - const candidates = [ - HP_PATH, - HP_PATH.replace(/^system\./, "data."), - HP_PATH.replace(/^system\./, ""), - ]; - - for (const path of candidates) { - const value = foundry.utils.getProperty(actor, path); - if (value !== undefined) { - const numeric = Number(value); - return Number.isFinite(numeric) ? numeric : null; - } - if (actor.system) { - const trimmed = path.startsWith("system.") ? path.slice(7) : path; - const systemValue = foundry.utils.getProperty(actor.system, trimmed); - if (systemValue !== undefined) { - const numeric = Number(systemValue); - return Number.isFinite(numeric) ? numeric : null; - } - } - } - - return null; -} - -/** - * Fetch the actor referenced in the chat card metadata, if any. - * @param {object} metadata - * @returns {Actor|null} - */ -function resolveActorFromMetadata(metadata = {}) { - if (!metadata.actor) return null; - - if (typeof fromUuidSync === "function") { - try { - const doc = fromUuidSync(metadata.actor); - if (doc instanceof Actor) return doc; - } catch (err) { - console.warn("HP Logger | Failed to resolve actor UUID", err); - } - } - - // Fallback: assume uuid ends with actor id - const id = metadata.actor.split(".").pop(); - return game.actors.get(id) ?? null; -} - -/** - * Fetch the item referenced in the chat card metadata from the given actor. - * @param {Actor} actor - * @param {object} metadata - * @returns {Item|null} - */ -function resolveItemFromMetadata(actor, metadata = {}) { - if (!metadata.item || !(actor?.items instanceof Collection)) return null; - return actor.items.get(metadata.item) ?? null; -} diff --git a/src/macro_activate-xp-history-tab.js b/src/macro_activate-xp-history-tab.js deleted file mode 100644 index 52ff1b6a..00000000 --- a/src/macro_activate-xp-history-tab.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Activate XP history tracking that stores entries in flags only. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; -const XP_PATH = "system.details.xp.value"; -const FLAG_SCOPE = "world"; -const FLAG_KEY = "pf1XpHistory"; -const MAX_ROWS = 50; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -game.pf1 ??= {}; -game.pf1.xpHistoryFlags ??= { current: {}, hooks: {} }; -const state = game.pf1.xpHistoryFlags; - -const logKey = `pf1-xp-history-flags-${ACTOR_ID}`; -if (state.hooks[logKey]) { - Hooks.off("updateActor", state.hooks[logKey]); - delete state.hooks[logKey]; -} - -primeSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR); - -state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => { - if (actor.id !== ACTOR_ID) return; - - const newXP = readXpValue(actor); - if (newXP === null) return; - - const previous = state.current[ACTOR_ID]; - if (previous === undefined) { - state.current[ACTOR_ID] = newXP; - return; - } - if (Object.is(newXP, previous)) return; - - const diff = newXP - previous; - const diffText = diff >= 0 ? `+${diff}` : `${diff}`; - const user = game.users.get(userId); - - state.current[ACTOR_ID] = newXP; - - appendHistoryEntry(actor, { - value: newXP, - diff: diffText, - user: user?.name ?? "System", - }).catch((err) => console.error("XP History Flags | Failed to append entry", err)); -}); - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`XP flag history tracking active for ${actorName}.`); - -async function appendHistoryEntry(actor, entry) { - const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? []; - existing.unshift({ - timestamp: Date.now(), - value: entry.value, - diff: entry.diff, - user: entry.user, - }); - if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS); - - await actor.setFlag(FLAG_SCOPE, FLAG_KEY, existing); -} - -function primeSnapshot(actor) { - const xp = readXpValue(actor); - if (xp === null) return; - state.current[ACTOR_ID] = xp; -} - -function readXpValue(actor) { - if (!actor) return null; - const direct = foundry.utils.getProperty(actor, XP_PATH); - if (direct !== undefined) { - const numeric = Number(direct); - return Number.isFinite(numeric) ? numeric : null; - } - const sys = foundry.utils.getProperty(actor.system ?? {}, XP_PATH.replace(/^system\./, "")); - if (sys !== undefined) { - const numeric = Number(sys); - return Number.isFinite(numeric) ? numeric : null; - } - return null; -} diff --git a/src/macro_activate-xp-history.js b/src/macro_activate-xp-history.js deleted file mode 100644 index 27d3e5a2..00000000 --- a/src/macro_activate-xp-history.js +++ /dev/null @@ -1,285 +0,0 @@ -/** - * Track XP changes for a single actor and record them inside ===XP History=== notes. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; -const XP_PATH = "system.details.xp.value"; -const UPDATE_CONTEXT_FLAG = "xpHistory"; -const FLAG_SCOPE = "world"; -const FLAG_KEY = "pf1XpHistory"; -const BLOCK_NAME = "pf1-xp-history"; -const MAX_ROWS = 50; -const BLOCK_START = ""; -const BLOCK_END = ""; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -game.pf1 ??= {}; -game.pf1.xpHistory ??= {}; -const state = game.pf1.xpHistory; -state.sources ??= new Map(); -state.current ??= {}; -state.hooks ??= {}; - -const logKey = `pf1-xp-history-${ACTOR_ID}`; - -if (state.hooks[logKey]) { - Hooks.off("updateActor", state.hooks[logKey]); - delete state.hooks[logKey]; -} - -ensureDamageSourceTracking(); -primeSnapshot(game.actors.get(ACTOR_ID) ?? TARGET_ACTOR); - -state.hooks[logKey] = Hooks.on("updateActor", (actor, change, options, userId) => { - if (actor.id !== ACTOR_ID) return; - if (options?.[UPDATE_CONTEXT_FLAG]) return; - - const newXP = readXpValue(actor); - if (newXP === null) return; - - const previous = state.current[ACTOR_ID]; - if (previous === undefined) { - state.current[ACTOR_ID] = newXP; - return; - } - if (Object.is(previous, newXP)) return; - - const diff = newXP - previous; - const diffText = diff >= 0 ? `+${diff}` : `${diff}`; - const user = game.users.get(userId); - const source = consumeDamageSource(actor.id) ?? inferManualSource(diff); - - state.current[ACTOR_ID] = newXP; - - appendHistoryRow(actor, { - value: newXP, - diffText, - user, - source, - }).catch((err) => console.error("XP History | Failed to append entry", err)); -}); - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`XP history tracking active for ${actorName}.`); - -async function appendHistoryRow(actor, entry) { - const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? []; - const newEntry = { - timestamp: Date.now(), - value: entry.value, - diff: entry.diffText, - user: entry.user?.name ?? "System", - source: entry.source ?? "", - }; - existing.unshift(newEntry); - if (existing.length > MAX_ROWS) existing.splice(MAX_ROWS); - - const notes = actor.system.details?.notes?.value ?? ""; - const block = renderHistoryBlock(existing); - const updatedNotes = injectHistoryBlock(notes, block); - - await actor.update( - { - [`flags.${FLAG_SCOPE}.${FLAG_KEY}`]: existing, - "system.details.notes.value": updatedNotes, - }, - { [UPDATE_CONTEXT_FLAG]: true } - ); -} - -function renderHistoryBlock(entries) { - const rows = entries - .map( - (e) => ` - - ${new Date(e.timestamp).toLocaleString()} - ${e.value} - ${e.diff} - ${e.user ?? ""} - ${e.source ?? ""} - ` - ) - .join(""); - - const blockId = `${BLOCK_NAME}-${ACTOR_ID}`; - return ` -${BLOCK_START} -
-
- ===XP History=== (click to collapse) -
- - -
- - - - - - - - - - - - ${rows} - -
TimestampXPΔUserSource
-
- -
-${BLOCK_END}`.trim(); -} - -function injectHistoryBlock(notes, block) { - const blockPattern = new RegExp(`${escapeRegExp(BLOCK_START)}[\\s\\S]*?${escapeRegExp(BLOCK_END)}`, "g"); - let cleaned = notes.replace(blockPattern, ""); - if (cleaned === notes) { - const sectionPattern = new RegExp(`]*data-history-block="${BLOCK_NAME}"[^>]*>[\\s\\S]*?<\\/section>`, "g"); - cleaned = notes.replace(sectionPattern, ""); - } - cleaned = cleaned.trim(); - const separator = cleaned.length && !cleaned.endsWith("\n") ? "\n\n" : ""; - return `${cleaned}${separator}${block}`; -} - -function consumeDamageSource(actorId) { - const entry = state.sources.get(actorId); - if (!entry) return null; - state.sources.delete(actorId); - return entry.label; -} - -function inferManualSource(diff) { - if (!Number.isFinite(diff) || diff === 0) return null; - return diff > 0 ? "Manual increase" : "Manual decrease"; -} - -function ensureDamageSourceTracking() { - if (state.applyDamageWrapped) return; - - const ActorPF = pf1?.documents?.actor?.ActorPF; - if (!ActorPF?.applyDamage) return; - - const original = ActorPF.applyDamage; - ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) { - try { - noteDamageSource(value, options); - } catch (err) { - console.warn("XP History | Failed to record source", err); - } - return original.call(this, value, options); - }; - - state.applyDamageWrapped = true; -} - -function noteDamageSource(value, options) { - const actors = resolveActorTargets(options?.targets); - if (!actors.some((a) => a?.id === ACTOR_ID)) return; - - const label = buildSourceLabel(value, options); - if (!label) return; - state.sources.set(ACTOR_ID, { label, ts: Date.now() }); -} - -function buildSourceLabel(value, options = {}) { - const info = options.message?.flags?.pf1?.identifiedInfo ?? {}; - const metadata = options.message?.flags?.pf1?.metadata ?? {}; - const fromChatFlavor = options.message?.flavor?.trim(); - - const actorDoc = resolveActorFromMetadata(metadata); - const itemDoc = actorDoc ? resolveItemFromMetadata(actorDoc, metadata) : null; - const actorName = actorDoc?.name ?? options.message?.speaker?.alias ?? null; - const actionName = info.actionName ?? info.name ?? itemDoc?.name ?? fromChatFlavor ?? null; - - let label = null; - if (actorName && actionName) label = `${actorName} -> ${actionName}`; - else if (actionName) label = actionName; - else if (actorName) label = actorName; - else label = value < 0 ? "Decrease" : "Increase"; - - if (options.isCritical) label += " (Critical)"; - if (options.asNonlethal) label += " [Nonlethal]"; - - return label; -} - -function primeSnapshot(actor) { - const xp = readXpValue(actor); - if (xp === null) return; - state.current[ACTOR_ID] = xp; -} - -function resolveActorTargets(targets) { - let list = []; - if (Array.isArray(targets) && targets.length) list = targets; - else list = canvas?.tokens?.controlled ?? []; - - return list - .map((entry) => { - if (!entry) return null; - if (entry instanceof Actor) return entry; - if (entry.actor) return entry.actor; - if (entry.document?.actor) return entry.document.actor; - return null; - }) - .filter((actor) => actor instanceof Actor && actor.id); -} - -function readXpValue(actor) { - if (!actor) return null; - const value = foundry.utils.getProperty(actor, XP_PATH) ?? foundry.utils.getProperty(actor.system ?? {}, XP_PATH.replace(/^system\./, "")); - if (value === undefined) return null; - const numeric = Number(value); - return Number.isFinite(numeric) ? numeric : null; -} - -function resolveActorFromMetadata(metadata = {}) { - if (!metadata.actor) return null; - - if (typeof fromUuidSync === "function") { - try { - const doc = fromUuidSync(metadata.actor); - if (doc instanceof Actor) return doc; - } catch (err) { - console.warn("XP History | Failed to resolve actor UUID", err); - } - } - - const id = metadata.actor.split(".").pop(); - return game.actors.get(id) ?? null; -} - -function resolveItemFromMetadata(actor, metadata = {}) { - if (!metadata.item || !(actor?.items instanceof Collection)) return null; - return actor.items.get(metadata.item) ?? null; -} - -function escapeRegExp(str) { - return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} diff --git a/src/macro_disable-history-dialog.js b/src/macro_disable-history-dialog.js deleted file mode 100644 index 45de4dd2..00000000 --- a/src/macro_disable-history-dialog.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Remove the HP history button hooks added by macro_enable-history-dialog.js. - */ - -const SHEET_EVENTS = [ - "renderActorSheetPFCharacter", - "renderActorSheetPFNPC", - "renderActorSheetPFNPCLoot", - "renderActorSheetPFNPCLite", - "renderActorSheetPFTrap", - "renderActorSheetPFVehicle", - "renderActorSheetPFHaunt", - "renderActorSheetPFBasic", -]; - -const hooks = game.pf1?.historyDialog?.renderHooks ?? []; -if (!hooks.length) { - return ui.notifications.info("History dialog hooks are not active."); -} - -hooks.forEach((hook, idx) => { - if (hook == null) return; - const isObject = typeof hook === "object"; - const event = isObject ? hook.event : SHEET_EVENTS[idx]; - const id = isObject ? hook.id : hook; - if (event && id !== undefined) Hooks.off(event, id); -}); - -delete game.pf1.historyDialog.renderHooks; -if (game.pf1?.historyDialog?.observers instanceof Map) { - for (const observer of game.pf1.historyDialog.observers.values()) { - observer?.disconnect(); - } - game.pf1.historyDialog.observers.clear(); -} -game.pf1.historyDialog.observers = new Map(); - -ui.notifications.info("History dialog hooks disabled. Reopen actor sheets to remove the button."); diff --git a/src/macro_disable-history-tab.js b/src/macro_disable-history-tab.js deleted file mode 100644 index d2d5d9e3..00000000 --- a/src/macro_disable-history-tab.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Remove the History tab hooks added by macro_enable-history-tab.js. - */ - -const hooks = game.pf1?.historyTab?.renderHooks ?? (game.pf1?.historyTab?.renderHook ? [{ event: null, id: game.pf1.historyTab.renderHook }] : []); -if (!hooks.length) { - return ui.notifications.info("History tab hooks are not active."); -} - -for (const hook of hooks) { - if (!hook) continue; - Hooks.off(hook.event ?? "renderActorSheet", hook.id ?? hook); -} - -delete game.pf1.historyTab.renderHooks; -delete game.pf1.historyTab.renderHook; - -if (game.pf1.historyTab.observers instanceof Map) { - for (const observer of game.pf1.historyTab.observers.values()) { - observer?.disconnect(); - } - delete game.pf1.historyTab.observers; -} - -ui.notifications.info("History tab hooks disabled. Close and reopen actor sheets to remove the tab."); diff --git a/src/macro_enable-history-dialog.js b/src/macro_enable-history-dialog.js deleted file mode 100644 index 6bd7e750..00000000 --- a/src/macro_enable-history-dialog.js +++ /dev/null @@ -1,239 +0,0 @@ -/** - * Adds small log buttons to PF1 actor sheets (HP, XP, Currency headers). - * Buttons open a modal showing HP / XP / Currency logs stored in world flags. - */ - -const HISTORY_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 LOG_TARGETS = [ - { key: "hp", tab: "hp", finder: findHpContainer }, - { key: "xp", tab: "xp", finder: findXpContainer }, - { key: "currency", tab: "currency", finder: findCurrencyContainer }, -]; - -const SHEET_EVENTS = [ - "renderActorSheetPFCharacter", - "renderActorSheetPFNPC", - "renderActorSheetPFNPCLoot", - "renderActorSheetPFNPCLite", - "renderActorSheetPFTrap", - "renderActorSheetPFVehicle", - "renderActorSheetPFHaunt", - "renderActorSheetPFBasic", -]; - -game.pf1 ??= {}; -game.pf1.historyDialog ??= {}; -game.pf1.historyDialog.renderHooks ??= []; -game.pf1.historyDialog.observers ??= new Map(); - -if (game.pf1.historyDialog.renderHooks.length) { - return ui.notifications.info("Log buttons already active."); -} - -game.pf1.historyDialog.renderHooks = SHEET_EVENTS.map((event) => { - const id = Hooks.on(event, (sheet) => { - const delays = [0, 100, 250]; - delays.forEach((delay) => - setTimeout(() => { - attachButtons(sheet); - }, delay) - ); - }); - return { event, id }; -}); - -ui.notifications.info("PF1 log buttons enabled. Check the HP/XP/Currency headers."); - -function attachButtons(sheet) { - for (const targetCfg of LOG_TARGETS) { - const container = targetCfg.finder(sheet.element); - if (!container?.length) continue; - addButton(container, sheet, targetCfg.tab, `${sheet.id}-${targetCfg.key}`); - } -} - -function addButton(container, sheet, tab, observerKey) { - if (!container.length) return; - if (container.find(`.${HISTORY_BUTTON_CLASS}[data-log-tab="${tab}"]`).length) return; - - if (container.css("position") === "static") container.css("position", "relative"); - - const button = $(` - - `); - button.on("click", () => openHistoryDialog(sheet.actor, tab)); - container.append(button); - - observeContainer(container, sheet, tab, observerKey); -} - -function observeContainer(container, sheet, tab, key) { - if (game.pf1.historyDialog.observers.has(key)) return; - - const observer = new MutationObserver(() => addButton(container, sheet, tab, key)); - observer.observe(container[0], { childList: true }); - game.pf1.historyDialog.observers.set(key, observer); - - const cleanup = () => { - observer.disconnect(); - game.pf1.historyDialog.observers.delete(key); - sheet.element.off("remove", cleanup); - }; - sheet.element.on("remove", cleanup); -} - -function findHpContainer(root) { - const header = root - .find("h3, label") - .filter((_, el) => el.textContent.trim() === "Hit Points") - .first(); - if (!header.length) return null; - return header.parent().length ? header.parent() : header.closest(".attribute.hitpoints").first(); -} - -function findXpContainer(root) { - const box = root.find(".info-box.experience").first(); - if (box.length) return box; - const header = root - .find("h5, label") - .filter((_, el) => el.textContent.trim().toLowerCase() === "experience") - .first(); - return header.length ? header.parent() : null; -} - -function findCurrencyContainer(root) { - const header = root - .find("h3, label") - .filter((_, el) => el.textContent.trim().toLowerCase() === "currency") - .first(); - if (header.length) return header.parent().length ? header.parent() : header; - return root.find(".currency").first(); -} - -function openHistoryDialog(actor, initialTab = "hp") { - if (!actor) return; - const content = buildHistoryContent(actor); - const dlg = new Dialog( - { - title: `${actor.name}: Log`, - content, - buttons: { close: { label: "Close" } }, - render: (html) => setupTabs(html, initialTab), - }, - { - width: 720, - classes: ["pf1-history-dialog"], - } - ); - dlg.render(true); -} - -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 tabs = configs - .map((cfg) => `${cfg.label}`) - .join(""); - - const panels = configs - .map((cfg) => { - const entries = actor.getFlag("world", cfg.flag) ?? []; - const table = renderHistoryTable(entries, cfg.columns, cfg.id); - return `
${table}
`; - }) - .join(""); - - return ` -
- - -
${panels}
-
`; -} - -function setupTabs(html, initialTab) { - const buttons = Array.from(html[0].querySelectorAll(".history-tab")); - const panels = Array.from(html[0].querySelectorAll(".history-panel")); - const activate = (target) => { - buttons.forEach((btn) => btn.classList.toggle("active", btn.dataset.historyTab === target)); - panels.forEach((panel) => (panel.style.display = panel.dataset.historyPanel === target ? "block" : "none")); - }; - buttons.forEach((btn) => btn.addEventListener("click", () => activate(btn.dataset.historyTab))); - activate(initialTab); -} - -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 ts ? new Date(ts).toLocaleString() : ""; -} diff --git a/src/macro_enable-history-tab.js b/src/macro_enable-history-tab.js deleted file mode 100644 index 2d1a8899..00000000 --- a/src/macro_enable-history-tab.js +++ /dev/null @@ -1,226 +0,0 @@ -/** - * 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(); -} diff --git a/src/macro_stop-currency-history-tab.js b/src/macro_stop-currency-history-tab.js deleted file mode 100644 index a79aef6c..00000000 --- a/src/macro_stop-currency-history-tab.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Stop currency flag history tracking (tab-based version). - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -const state = game.pf1?.currencyHistoryFlags; -if (!state) return ui.notifications.info("Currency flag history tracking is not active."); - -const logKey = `pf1-currency-history-flags-${ACTOR_ID}`; -const handler = state.hooks?.[logKey]; - -if (!handler) { - return ui.notifications.info(`No currency flag history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`); -} - -Hooks.off("updateActor", handler); -delete state.hooks[logKey]; -if (state.current) delete state.current[ACTOR_ID]; - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`Currency flag history tracking disabled for ${actorName}.`); diff --git a/src/macro_stop-currency-history.js b/src/macro_stop-currency-history.js deleted file mode 100644 index 63f01f9e..00000000 --- a/src/macro_stop-currency-history.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Stop currency history tracking for the configured actor. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -const state = game.pf1?.currencyHistory; -if (!state) return ui.notifications.info("Currency history tracking is not active."); - -const logKey = `pf1-currency-history-${ACTOR_ID}`; -const handler = state.hooks?.[logKey]; - -if (!handler) { - return ui.notifications.info(`No currency history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`); -} - -Hooks.off("updateActor", handler); -delete state.hooks[logKey]; -state.sources?.delete(ACTOR_ID); -if (state.current) delete state.current[ACTOR_ID]; - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`Currency history tracking disabled for ${actorName}.`); diff --git a/src/macro_stop-hp-history-tab.js b/src/macro_stop-hp-history-tab.js deleted file mode 100644 index 154310ff..00000000 --- a/src/macro_stop-hp-history-tab.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Stop HP flag history tracking (tab-based version). - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -const state = game.pf1?.hpHistoryFlags; -if (!state) return ui.notifications.info("HP flag history tracking is not active."); - -const logKey = `pf1-hp-history-flags-${ACTOR_ID}`; -const handler = state.hooks?.[logKey]; - -if (!handler) { - return ui.notifications.info(`No HP flag history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`); -} - -Hooks.off("updateActor", handler); -delete state.hooks[logKey]; -state.sources?.delete(ACTOR_ID); -if (state.current) delete state.current[ACTOR_ID]; - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`HP flag history tracking disabled for ${actorName}.`); diff --git a/src/macro_stop-hp-history.js b/src/macro_stop-hp-history.js deleted file mode 100644 index 8fef6514..00000000 --- a/src/macro_stop-hp-history.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Deactivate the HP history tracker and stop writing to the Notes table. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -game.pf1 ??= {}; -const state = game.pf1.hpHistory; - -if (!state) { - return ui.notifications.info("HP history tracking is not active."); -} - -const logKey = `pf1-hp-history-${ACTOR_ID}`; -const handler = state.hooks?.[logKey]; - -if (!handler) { - return ui.notifications.info(`No HP history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`); -} - -Hooks.off("updateActor", handler); -delete state.hooks[logKey]; -state.sources?.delete(ACTOR_ID); -if (state.current) delete state.current[ACTOR_ID]; - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`HP history tracking disabled for ${actorName}.`); diff --git a/src/macro_stop-hp-tracking.js b/src/macro_stop-hp-tracking.js deleted file mode 100644 index 6f648040..00000000 --- a/src/macro_stop-hp-tracking.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Deactivate HP tracking for the configured actor. - * Use after running macro_activate-hp-tracking.js to stop logging. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -game.pf1 ??= {}; -game.pf1.hpLogger ??= {}; - -const logKey = `pf1-hp-log-${ACTOR_ID}`; -const handler = game.pf1.hpLogger[logKey]; - -if (!handler) { - return ui.notifications.info(`No HP tracking hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`); -} - -Hooks.off("updateActor", handler); -delete game.pf1.hpLogger[logKey]; - -if (game.pf1.hpLogger.sources instanceof Map) { - game.pf1.hpLogger.sources.delete(ACTOR_ID); -} -if (game.pf1.hpLogger.current) { - delete game.pf1.hpLogger.current[ACTOR_ID]; -} - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`HP tracking disabled for ${actorName}.`); diff --git a/src/macro_stop-xp-history-tab.js b/src/macro_stop-xp-history-tab.js deleted file mode 100644 index 576cbab5..00000000 --- a/src/macro_stop-xp-history-tab.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Stop XP flag history tracking (tab-based version). - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -const state = game.pf1?.xpHistoryFlags; -if (!state) return ui.notifications.info("XP flag history tracking is not active."); - -const logKey = `pf1-xp-history-flags-${ACTOR_ID}`; -const handler = state.hooks?.[logKey]; - -if (!handler) { - return ui.notifications.info(`No XP flag history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`); -} - -Hooks.off("updateActor", handler); -delete state.hooks[logKey]; -if (state.current) delete state.current[ACTOR_ID]; - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`XP flag history tracking disabled for ${actorName}.`); diff --git a/src/macro_stop-xp-history.js b/src/macro_stop-xp-history.js deleted file mode 100644 index 715322c3..00000000 --- a/src/macro_stop-xp-history.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Stop XP history tracking for the configured actor. - */ - -const TARGET_ACTOR = game.actors.getName("Zeratal") ?? null; -const ACTOR_ID = TARGET_ACTOR?.id ?? "put-actor-id-here"; - -if (!ACTOR_ID || ACTOR_ID === "put-actor-id-here") { - return ui.notifications.warn("Set ACTOR_ID before running the macro."); -} - -const state = game.pf1?.xpHistory; -if (!state) return ui.notifications.info("XP history tracking is not active."); - -const logKey = `pf1-xp-history-${ACTOR_ID}`; -const handler = state.hooks?.[logKey]; - -if (!handler) { - return ui.notifications.info(`No XP history hook found for ${TARGET_ACTOR?.name ?? ACTOR_ID}.`); -} - -Hooks.off("updateActor", handler); -delete state.hooks[logKey]; -state.sources?.delete(ACTOR_ID); -if (state.current) delete state.current[ACTOR_ID]; - -const actorName = game.actors.get(ACTOR_ID)?.name ?? TARGET_ACTOR?.name ?? ACTOR_ID; -ui.notifications.info(`XP history tracking disabled for ${actorName}.`); 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 index 4b7a24f1..25cfa43d 100644 --- a/src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js +++ b/src/macros_new/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js @@ -329,8 +329,9 @@ async function initializeModule() { try { const message = options.message; if (message) { + const storedValue = computeDamageValue(value, options); const source = buildSourceLabel(value, options); - console.log("[GowlersTracking] Storing message for matching: source=", source, "value=", value); + console.log("[GowlersTracking] Storing message for matching: source=", source, "value=", storedValue); // Extract damage details for breakdown information const damageDetails = extractDamageDetails(value, options); @@ -339,7 +340,7 @@ async function initializeModule() { ledgerState.recentMessages.push({ message: message, source: source, - value: value, // Keep sign! Negative = damage, Positive = healing + value: storedValue, // Keep sign! Negative = damage, Positive = healing damageDetails: damageDetails, timestamp: Date.now(), }); @@ -645,6 +646,18 @@ function registerSceneControls(forceRefresh = false) { } } +function computeDamageValue(value, options = {}) { + const base = Number(value) || 0; + if (!Array.isArray(options.instances) || !options.instances.length) return base; + const sum = options.instances.reduce((total, inst) => { + const parts = [inst.total, inst.value, inst.amount].filter((v) => Number.isFinite(v)); + return total + (parts.length ? parts[0] : 0); + }, 0); + if (!sum) return base; + const sign = base < 0 ? -1 : 1; + return sign * Math.abs(sum); +} + function openDamageMeterOverlay() { try { if (ledgerState.damageOverlay?.rendered) {