4.4 KiB
PF1 Tracking Ledger: Damage Source Logging
This document describes how the tracking-ledger module captures detailed HP change metadata (damage source, damage type, encounter ID, etc.) so the ledger can show where every hit point delta came from.
Overview
The module instruments every HP update by:
- Wrapping the PF1 system's
ActorPF.applyDamagemethod to intercept the combat context (chat card, damage/healing roll, target list, action name). - Buffering the captured metadata (source label, type, encounter) in a temporary cache keyed by actor ID.
- Listening to the
updateActorhook for the specific actor, detecting HP deltas, and consuming the buffered metadata while persisting it into the world flag (flags.world.pf1HpHistory).
This preserves full provenance of each HP change without requiring upstream system modifications.
Hooking ActorPF.applyDamage
const ActorPF = pf1?.documents?.actor?.ActorPF;
const original = ActorPF.applyDamage;
ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) {
try {
noteDamageSource(value, options);
} catch (err) {
console.warn("Tracking Ledger | Failed to record damage source", err);
}
return original.call(this, value, options);
};
- Run once, guarded to avoid double wrapping.
valueis the delta applied to each target (negative for damage, positive for healing);optionscarries metadata from PF1's roll flow.
Recording the source details
function noteDamageSource(value, options) {
const actors = resolveActorTargets(options?.targets);
if (!actors.length) return;
const label = buildSourceLabel(value, options);
if (!label) return;
for (const actor of actors) {
trackingState.sources.set(actor.id, { label, ts: Date.now() });
}
}
resolveActorTargetsconverts the PF1 target collection (tokens or actors) into concreteActorinstances.buildSourceLabelinspectsoptions.message.flags.pf1(identified action info, metadata, flavor text) to construct a human-readable source such as"Goblin -> Scimitar Slash"or"Manual healing".- The resulting label is cached in
trackingState.sourcesuntil the corresponding HP delta is observed on that actor.
Source label construction
Order of precedence when building the label:
options.message.flags.pf1.identifiedInfo(action name, item name).- Chat card metadata (
flags.pf1.metadata.actor/.item) to resolve the actor+item pair responsible. - Chat card flavor text (
message.flavor). - Fallbacks:
"Healing"or"Damage"depending on delta sign.
This ensures we always have some context, even for custom macros.
Consuming the buffered metadata
Inside the updateActor hook, every time system.attributes.hp.value changes, we:
const diff = newValue - previous;
const label = consumeDamageSource(actor.id) ?? inferManualSource(diff);
const entry = {
timestamp: Date.now(),
hp: newValue,
diff: diff >= 0 ? `+${diff}` : `${diff}`,
user: game.users.get(userId)?.name ?? "System",
source: label ?? "",
};
consumeDamageSourcepulls (and deletes) the cached metadata for that actor if present.inferManualSourcesupplies"Manual healing"or"Manual damage"when the HP change didn't originate from a wrappedapplyDamagecall (e.g., direct sheet edits).- The entry is stored in
flags.world.pf1HpHistorywith a max history size of 50 rows.
Encounter tagging
Additionally, every recorded entrée is tagged with the active encounter:
entry.encounterId = game.combats?.active?.id ?? null;
updateEncounterSummary maintains flags.world.pf1EncounterHistory, aggregating:
- Encounter ID
- Start/end timestamps
- Participants (actor names captured per HP/XP change)
- XP gained per actor (sum of positive XP deltas)
The history dialog exposes this as a dedicated "Encounters" tab that updates in real time as HP/XP changes occur within the same combat.
Summary
- Damage source detection: Done by wrapping
ActorPF.applyDamageto capture chat metadata. - Storage: Cached per-actor, consumed when HP changes fire in
updateActor, saved toflags.world.pf1HpHistory. - Encounter tracking: Each HP/XP change is tagged with
game.combats.active.idand aggregated intoflags.world.pf1EncounterHistory.
This instrumentation gives the ledger full visibility into who dealt which damage (and why), while remaining self-contained in the module.