diff --git a/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js b/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js
index 26b153c1..8d1d30c8 100644
--- a/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js
+++ b/src/macros_new/gowlers-tracking-ledger/foundry.gowlershome.dyndns.org/modules/gowlers-tracking-ledger/scripts/gowlers-tracking-ledger.js
@@ -1,6 +1,6 @@
const MODULE_ID = "gowlers-tracking-ledger";
-const MODULE_VERSION = "0.1.21";
+const MODULE_VERSION = "0.1.23";
const TRACK_SETTING = "actorSettings";
const FLAG_SCOPE = "world";
const MAX_HISTORY_ROWS = 100;
@@ -372,42 +372,60 @@ async function initializeModule() {
const pf1Flags = message.flags?.pf1 ?? {};
const metadata = pf1Flags.metadata ?? {};
- // Extract damage types from metadata or message content
- const damageTypes = [];
-
- // Check for damage type data in metadata
+ // Legacy/simple metadata payloads may list damage types directly
+ const metadataTypes = [];
if (metadata.damageTypes) {
if (Array.isArray(metadata.damageTypes)) {
- damageTypes.push(...metadata.damageTypes);
- } else if (typeof metadata.damageTypes === 'string') {
- damageTypes.push(metadata.damageTypes);
+ metadataTypes.push(...metadata.damageTypes.filter(Boolean));
+ } else if (typeof metadata.damageTypes === "string") {
+ metadataTypes.push(metadata.damageTypes);
}
}
- // Try to parse damage types from message content (HTML)
- if (message.content && damageTypes.length === 0) {
- // Look for damage type patterns in HTML
- const contentMatch = message.content.match(/
]*>.*?(Untyped|Slashing|Piercing|Bludgeoning|Fire|Cold|Electricity|Acid|Sonic|Force|Negative|Positive|Water)[^<]*<\/li>/gi);
- if (contentMatch) {
- contentMatch.forEach(match => {
- const type = match.replace(/<[^>]*>/g, '').trim();
- if (type && !damageTypes.includes(type)) {
- damageTypes.push(type);
- }
- });
+ // Damage parts from PF1 metadata rolls (preferred)
+ const damageParts = [];
+ const attacks = metadata?.rolls?.attacks ?? [];
+ for (const attack of attacks) {
+ const damages = attack?.damage ?? [];
+ for (const dmg of damages) {
+ const part = normalizeDamagePart(dmg);
+ if (part) damageParts.push(part);
}
}
+ // Fallback: PF1 forwards instances in options (contains per-type totals)
+ if (!damageParts.length && Array.isArray(options.instances)) {
+ for (const inst of options.instances) {
+ const part = normalizeDamageInstance(inst);
+ if (part) damageParts.push(part);
+ }
+ }
+
+ // Aggregate types for quick reference
+ const allTypes = Array.from(
+ new Set(
+ [
+ ...damageParts.flatMap((part) => part.allTypes ?? []),
+ ...metadataTypes,
+ ]
+ )
+ ).filter(Boolean);
+
// Extract critical hit flag
const isCritical = options?.isCritical ?? false;
- const critMultiplier = options?.critMult ?? 1;
+ const critMultiplier = options?.critMult ?? (metadata.config?.critMult ?? 1);
// Extract nonlethal flag
const asNonlethal = options?.asNonlethal ?? false;
- // Try to extract damage breakdown from rolls
+ // Build a human-readable breakdown from the parts
let breakdown = null;
- if (message.rolls && message.rolls.length > 0) {
+ if (damageParts.length) {
+ breakdown = damageParts.map((part) => formatDamagePart(part)).filter(Boolean).join("; ");
+ } else if (metadataTypes.length) {
+ breakdown = metadataTypes.join(", ");
+ } else if (message.rolls && message.rolls.length > 0) {
+ // Legacy fallback: use the first roll formula
try {
const roll = message.rolls[0];
breakdown = roll.formula ?? roll.toString?.() ?? null;
@@ -429,11 +447,12 @@ async function initializeModule() {
}
return {
- types: damageTypes.length > 0 ? damageTypes : null,
+ types: allTypes.length > 0 ? allTypes : null,
isCritical,
critMultiplier,
asNonlethal,
breakdown,
+ parts: damageParts,
};
}
@@ -1027,6 +1046,7 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
let source = "Manual";
let sourceDetails = "";
let damageBreakdown = "";
+ let matchedMessage = null;
// For HP damage, search recent messages for matching damage application
if (statId === "hp" && diffValue < 0) {
@@ -1034,7 +1054,6 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
const now = Date.now();
// Look for matching message in recent queue (within 4 seconds)
- let matchedMessage = null;
for (let i = ledgerState.recentMessages.length - 1; i >= 0; i--) {
const msg = ledgerState.recentMessages[i];
const timeSinceMessage = now - msg.timestamp;
@@ -1052,7 +1071,10 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
if (matchedMessage) {
source = matchedMessage.source;
- damageBreakdown = `${damageAmount} HP`;
+ damageBreakdown =
+ formatDamageBreakdown(matchedMessage.damageDetails, damageAmount) ||
+ matchedMessage.damageDetails?.breakdown ||
+ `${damageAmount} damage`;
console.log(`[GowlersTracking] Using matched damage source:`, source);
}
}
@@ -1091,9 +1113,13 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
source: source,
encounterId: encounterId,
damageBreakdown: damageBreakdown,
+ damageDetails: damageBreakdown ? matchedMessage?.damageDetails ?? null : null,
};
console.log(`[GowlersTracking] History entry created for ${statId}:`, entry);
+ if (statId === "hp" && diffValue < 0) {
+ console.log("[GowlersTracking] Stored damage breakdown:", damageBreakdown || "(none)");
+ }
const existing = (await actor.getFlag(FLAG_SCOPE, config.flag)) ?? [];
existing.unshift(entry);
@@ -1152,6 +1178,114 @@ function formatCurrency(obj = {}, signed = false) {
.join(" ");
}
+function normalizeDamagePart(roll) {
+ if (!roll || typeof roll !== "object") return null;
+
+ const typeObj = roll.options?.damageType ?? {};
+ const values = Array.isArray(typeObj.values) ? typeObj.values.filter(Boolean) : [];
+ const customTypes = typeof typeObj.custom === "string"
+ ? typeObj.custom.split(/[,/]+/).map((t) => t.trim()).filter(Boolean)
+ : [];
+ const materials = [];
+ if (Array.isArray(typeObj.materials)) materials.push(...typeObj.materials.filter(Boolean));
+ if (typeObj.material) materials.push(typeObj.material);
+
+ const rollType = roll.options?.type ?? null;
+ const total = Number.isFinite(roll.total) ? roll.total : null;
+ const formula = roll.formula ?? null;
+
+ const isNonlethal = values.some(isNonlethalType) ||
+ customTypes.some(isNonlethalType) ||
+ rollType === "nonlethal" ||
+ roll.options?.asNonlethal;
+
+ const baseTypes = values.filter((t) => !isNonlethalType(t));
+ const allTypes = [...baseTypes, ...customTypes, ...materials];
+ if (isNonlethal) allTypes.push("nonlethal");
+
+ return {
+ total,
+ formula,
+ types: baseTypes,
+ customTypes,
+ materials,
+ isNonlethal,
+ rollType,
+ allTypes,
+ };
+}
+
+function normalizeDamageInstance(instance) {
+ if (!instance || typeof instance !== "object") return null;
+
+ const totals = [instance.total, instance.value, instance.amount].filter((v) => Number.isFinite(v));
+ const total = totals.length ? totals[0] : null;
+ const formula = instance.formula ?? null;
+
+ const rawTypes = [];
+ if (Array.isArray(instance.types)) rawTypes.push(...instance.types.filter(Boolean));
+ if (instance.type) rawTypes.push(instance.type);
+
+ const customTypes = typeof instance.custom === "string"
+ ? instance.custom.split(/[,/]+/).map((t) => t.trim()).filter(Boolean)
+ : [];
+
+ const materials = [];
+ if (Array.isArray(instance.materials)) materials.push(...instance.materials.filter(Boolean));
+ if (instance.material) materials.push(instance.material);
+
+ const rollType = instance.typeLabel ?? null;
+ const isNonlethal = !!instance.nonlethal || rawTypes.some(isNonlethalType) || customTypes.some(isNonlethalType);
+ const baseTypes = rawTypes.filter((t) => !isNonlethalType(t));
+ const allTypes = [...baseTypes, ...customTypes, ...materials];
+ if (isNonlethal) allTypes.push("nonlethal");
+
+ return {
+ total,
+ formula,
+ types: baseTypes,
+ customTypes,
+ materials,
+ isNonlethal,
+ rollType,
+ allTypes,
+ };
+}
+
+function formatDamagePart(part) {
+ if (!part) return "";
+ const amount = Number.isFinite(part.total) ? `${part.total}` : (part.formula ?? "?");
+ const baseType = part.types?.length ? part.types.join("/") : "untyped";
+
+ const extras = [];
+ if (part.customTypes?.length) extras.push(part.customTypes.join("/"));
+ if (part.materials?.length) extras.push(part.materials.join("/"));
+ if (part.rollType && part.rollType !== "normal") extras.push(part.rollType);
+
+ let label = baseType;
+ if (extras.length) label += ` (${extras.join(", ")})`;
+ if (part.isNonlethal && !/nonlethal/i.test(label)) label += " (nonlethal)";
+
+ return `${amount} ${label}`.trim();
+}
+
+function formatDamageBreakdown(details, damageAmount) {
+ if (!details) return Number.isFinite(damageAmount) ? `${damageAmount} damage` : "";
+
+ if (Array.isArray(details.parts) && details.parts.length) {
+ const partsText = details.parts.map((part) => formatDamagePart(part)).filter(Boolean);
+ if (partsText.length) return partsText.join("; ");
+ }
+
+ if (details.breakdown) return details.breakdown;
+ if (Number.isFinite(damageAmount)) return `${damageAmount} damage`;
+ return "";
+}
+
+function isNonlethalType(type) {
+ return String(type ?? "").toLowerCase() === "nonlethal";
+}
+
function checkboxValue(value, fallback = false) {
if (value === undefined || value === null) return fallback;
if (Array.isArray(value)) value = value[value.length - 1];