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}
-
-${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