damage detailed

This commit is contained in:
centron\schwoerer
2025-11-21 11:22:14 +01:00
parent ea2070598f
commit 8083003252

View File

@@ -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(/<li[^>]*>.*?(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];