damage detailed
This commit is contained in:
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user