diff --git a/src/macro_activate-currency-history.js b/src/macro_activate-currency-history.js
new file mode 100644
index 00000000..b863d770
--- /dev/null
+++ b/src/macro_activate-currency-history.js
@@ -0,0 +1,300 @@
+/**
+ * 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.js b/src/macro_activate-hp-history.js
new file mode 100644
index 00000000..7c1dc396
--- /dev/null
+++ b/src/macro_activate-hp-history.js
@@ -0,0 +1,369 @@
+/**
+ * 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