damage detailed
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
const MODULE_ID = "gowlers-tracking-ledger";
|
const MODULE_ID = "gowlers-tracking-ledger";
|
||||||
const MODULE_VERSION = "0.1.21";
|
const MODULE_VERSION = "0.1.23";
|
||||||
const TRACK_SETTING = "actorSettings";
|
const TRACK_SETTING = "actorSettings";
|
||||||
const FLAG_SCOPE = "world";
|
const FLAG_SCOPE = "world";
|
||||||
const MAX_HISTORY_ROWS = 100;
|
const MAX_HISTORY_ROWS = 100;
|
||||||
@@ -372,42 +372,60 @@ async function initializeModule() {
|
|||||||
const pf1Flags = message.flags?.pf1 ?? {};
|
const pf1Flags = message.flags?.pf1 ?? {};
|
||||||
const metadata = pf1Flags.metadata ?? {};
|
const metadata = pf1Flags.metadata ?? {};
|
||||||
|
|
||||||
// Extract damage types from metadata or message content
|
// Legacy/simple metadata payloads may list damage types directly
|
||||||
const damageTypes = [];
|
const metadataTypes = [];
|
||||||
|
|
||||||
// Check for damage type data in metadata
|
|
||||||
if (metadata.damageTypes) {
|
if (metadata.damageTypes) {
|
||||||
if (Array.isArray(metadata.damageTypes)) {
|
if (Array.isArray(metadata.damageTypes)) {
|
||||||
damageTypes.push(...metadata.damageTypes);
|
metadataTypes.push(...metadata.damageTypes.filter(Boolean));
|
||||||
} else if (typeof metadata.damageTypes === 'string') {
|
} else if (typeof metadata.damageTypes === "string") {
|
||||||
damageTypes.push(metadata.damageTypes);
|
metadataTypes.push(metadata.damageTypes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse damage types from message content (HTML)
|
// Damage parts from PF1 metadata rolls (preferred)
|
||||||
if (message.content && damageTypes.length === 0) {
|
const damageParts = [];
|
||||||
// Look for damage type patterns in HTML
|
const attacks = metadata?.rolls?.attacks ?? [];
|
||||||
const contentMatch = message.content.match(/<li[^>]*>.*?(Untyped|Slashing|Piercing|Bludgeoning|Fire|Cold|Electricity|Acid|Sonic|Force|Negative|Positive|Water)[^<]*<\/li>/gi);
|
for (const attack of attacks) {
|
||||||
if (contentMatch) {
|
const damages = attack?.damage ?? [];
|
||||||
contentMatch.forEach(match => {
|
for (const dmg of damages) {
|
||||||
const type = match.replace(/<[^>]*>/g, '').trim();
|
const part = normalizeDamagePart(dmg);
|
||||||
if (type && !damageTypes.includes(type)) {
|
if (part) damageParts.push(part);
|
||||||
damageTypes.push(type);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Extract critical hit flag
|
||||||
const isCritical = options?.isCritical ?? false;
|
const isCritical = options?.isCritical ?? false;
|
||||||
const critMultiplier = options?.critMult ?? 1;
|
const critMultiplier = options?.critMult ?? (metadata.config?.critMult ?? 1);
|
||||||
|
|
||||||
// Extract nonlethal flag
|
// Extract nonlethal flag
|
||||||
const asNonlethal = options?.asNonlethal ?? false;
|
const asNonlethal = options?.asNonlethal ?? false;
|
||||||
|
|
||||||
// Try to extract damage breakdown from rolls
|
// Build a human-readable breakdown from the parts
|
||||||
let breakdown = null;
|
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 {
|
try {
|
||||||
const roll = message.rolls[0];
|
const roll = message.rolls[0];
|
||||||
breakdown = roll.formula ?? roll.toString?.() ?? null;
|
breakdown = roll.formula ?? roll.toString?.() ?? null;
|
||||||
@@ -429,11 +447,12 @@ async function initializeModule() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
types: damageTypes.length > 0 ? damageTypes : null,
|
types: allTypes.length > 0 ? allTypes : null,
|
||||||
isCritical,
|
isCritical,
|
||||||
critMultiplier,
|
critMultiplier,
|
||||||
asNonlethal,
|
asNonlethal,
|
||||||
breakdown,
|
breakdown,
|
||||||
|
parts: damageParts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1027,6 +1046,7 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
|||||||
let source = "Manual";
|
let source = "Manual";
|
||||||
let sourceDetails = "";
|
let sourceDetails = "";
|
||||||
let damageBreakdown = "";
|
let damageBreakdown = "";
|
||||||
|
let matchedMessage = null;
|
||||||
|
|
||||||
// For HP damage, search recent messages for matching damage application
|
// For HP damage, search recent messages for matching damage application
|
||||||
if (statId === "hp" && diffValue < 0) {
|
if (statId === "hp" && diffValue < 0) {
|
||||||
@@ -1034,7 +1054,6 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
// Look for matching message in recent queue (within 4 seconds)
|
// Look for matching message in recent queue (within 4 seconds)
|
||||||
let matchedMessage = null;
|
|
||||||
for (let i = ledgerState.recentMessages.length - 1; i >= 0; i--) {
|
for (let i = ledgerState.recentMessages.length - 1; i >= 0; i--) {
|
||||||
const msg = ledgerState.recentMessages[i];
|
const msg = ledgerState.recentMessages[i];
|
||||||
const timeSinceMessage = now - msg.timestamp;
|
const timeSinceMessage = now - msg.timestamp;
|
||||||
@@ -1052,7 +1071,10 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
|||||||
|
|
||||||
if (matchedMessage) {
|
if (matchedMessage) {
|
||||||
source = matchedMessage.source;
|
source = matchedMessage.source;
|
||||||
damageBreakdown = `${damageAmount} HP`;
|
damageBreakdown =
|
||||||
|
formatDamageBreakdown(matchedMessage.damageDetails, damageAmount) ||
|
||||||
|
matchedMessage.damageDetails?.breakdown ||
|
||||||
|
`${damageAmount} damage`;
|
||||||
console.log(`[GowlersTracking] Using matched damage source:`, source);
|
console.log(`[GowlersTracking] Using matched damage source:`, source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1091,9 +1113,13 @@ async function recordHistoryEntry(actor, statId, previous, nextValue, userId, op
|
|||||||
source: source,
|
source: source,
|
||||||
encounterId: encounterId,
|
encounterId: encounterId,
|
||||||
damageBreakdown: damageBreakdown,
|
damageBreakdown: damageBreakdown,
|
||||||
|
damageDetails: damageBreakdown ? matchedMessage?.damageDetails ?? null : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`[GowlersTracking] History entry created for ${statId}:`, entry);
|
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)) ?? [];
|
const existing = (await actor.getFlag(FLAG_SCOPE, config.flag)) ?? [];
|
||||||
existing.unshift(entry);
|
existing.unshift(entry);
|
||||||
@@ -1152,6 +1178,114 @@ function formatCurrency(obj = {}, signed = false) {
|
|||||||
.join(" ");
|
.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) {
|
function checkboxValue(value, fallback = false) {
|
||||||
if (value === undefined || value === null) return fallback;
|
if (value === undefined || value === null) return fallback;
|
||||||
if (Array.isArray(value)) value = value[value.length - 1];
|
if (Array.isArray(value)) value = value[value.length - 1];
|
||||||
|
|||||||
Reference in New Issue
Block a user