# PF1e Damage Roll Detection & Structure Guide ## Overview Comprehensive guide to understanding how damage rolls are created, stored, and detected in the PF1e system for Foundry VTT v11.315. --- ## 1. Damage Roll Classes ### DamageRoll Class **Location**: `module/dice/damage-roll.mjs` Extends `RollPF`. Damage rolls store damage-specific metadata: ```javascript export class DamageRoll extends RollPF { static TYPES = { NORMAL: "normal", CRITICAL: "crit", NON_CRITICAL: "nonCrit", }; constructor(formula, data, options = {}) { super(formula, data, options); this.options.damageType ??= { values: ["untyped"], custom: "" }; } get damageType() { return this.options.damageType; // {values: ["fire"], custom: ""} } get type() { return this.options.type; // "normal", "crit", or "nonCrit" } get isCritical() { return this.type === this.constructor.TYPES.CRITICAL; } } ``` **Key Properties**: - `options.damageType` - Object with damage types: `{values: string[], custom: string}` - `options.type` - Critical status: "normal", "crit", or "nonCrit" - Extends `RollPF` which extends Foundry's `Roll` class --- ## 2. Chat Attack Structure ### ChatAttack Class **Location**: `module/action-use/chat-attack.mjs` Manages all rolls for a single attack action: ```javascript export class ChatAttack { action; // ItemAction reference hasDamage = false; damage = new AttackDamage(); // Normal damage rolls damageRows = []; // Formatted rows for display nonlethal = false; critDamage = new AttackDamage(); // Critical multiplier damage } ``` ### AttackDamage Class Container for damage rolls: ```javascript class AttackDamage { flavor = ""; // "Damage", "Healing", "Nonlethal" total = 0; // Total damage sum rolls = []; // Array of DamageRoll instances get isActive() { return this.rolls.length > 0; } get half() { return Math.floor(this.total / 2); } } ``` --- ## 3. Chat Message Metadata Storage ### Flag Path **Key Location**: `chatMessage.flags.pf1.metadata.rolls` All PF1-specific data stored in the message flags hierarchy: ```javascript { actor: "", item: "", action: "", template: "", targets: ["", ...], combat: "", rolls: { attacks: [ { // Attack roll (d20) attack: {class: "D20RollPF", formula: "1d20 + 5", total: 18, ...}, // Normal damage rolls damage: [ { class: "DamageRoll", formula: "1d8 + 3[Strength]", type: "normal", options: { damageType: {values: ["slashing"], custom: ""}, type: "normal" }, total: 7, ... } ], // Critical confirmation roll critConfirm: null, // Or D20RollPF if confirmed // Critical damage (only if crit confirmed) critDamage: [ { class: "DamageRoll", formula: "1d8 + 3[Strength]", type: "crit", total: 6, ... } ], // Ammo tracking (ranged only) ammo: {id: "", quantity: 1, misfire: false} } ] }, // Save DC for spells save: {dc: 16, type: "reflex"}, // Spell-specific spell: {cl: 5, sl: 2}, // Config data config: {critMult: 2} } ``` --- ## 4. Damage Roll Creation Flow ``` ActionUse.perform() ↓ generateChatAttacks() ↓ addAttacks() OR addDamage() ↓ ChatAttack.addDamage({flavor, extraParts, critical, conditionalParts}) ↓ action.rollDamage() → returns DamageRoll[] ↓ Stored in ChatAttack.damage.rolls[] or ChatAttack.critDamage.rolls[] ↓ generateChatMetadata() → converts to JSON for storage ↓ ChatMessage.flags.pf1.metadata.rolls.attacks[index] ``` --- ## 5. Detecting Damage Rolls ### From ChatMessage ```javascript // Access metadata const metadata = message.flags?.pf1?.metadata; // Check if damage exists const hasAttacks = metadata?.rolls?.attacks?.length > 0; if (hasAttacks) { const attack = metadata.rolls.attacks[0]; // Normal damage rolls (array) const normalDamage = attack.damage; // Each item is serialized DamageRoll // Critical damage rolls (array, if critical hit) const critDamage = attack.critDamage; // Attack roll (d20) const attackRoll = attack.attack; } // Reconstruct Roll objects from JSON const damageRollData = metadata.rolls.attacks[0].damage[0]; const damageRoll = Roll.fromData(damageRollData); ``` ### SimpleDetection Pattern ```javascript // Check if message has PF1 attack card with damage function hasDamageRolls(message) { return !!message.flags?.pf1?.metadata?.rolls?.attacks?.some( atk => atk.damage?.length > 0 ); } // Get total damage function getTotalDamage(message) { const metadata = message.flags?.pf1?.metadata; if (!metadata?.rolls?.attacks?.length) return 0; return metadata.rolls.attacks.reduce((total, attack) => { const normal = attack.damage?.reduce((sum, r) => sum + r.total, 0) ?? 0; const crit = attack.critDamage?.reduce((sum, r) => sum + r.total, 0) ?? 0; return total + normal + crit; }, 0); } ``` --- ## 6. Damage Roll Properties ### Stored in Metadata (JSON) ```javascript // Each damage roll in metadata contains: { class: "DamageRoll", formula: "1d8 + 3[Strength]", type: "normal", // or "crit" or "nonCrit" result: [{class: "DiceTerm", ...}, ...], total: 7, _formula: "1d8 + 3[Strength]", _evaluated: true, // Options (may need reconstruction from Roll) options: { damageType: { values: ["slashing"], custom: "" }, type: "normal" } } ``` ### On Reconstructed DamageRoll ```javascript const rollData = message.flags.pf1.metadata.rolls.attacks[0].damage[0]; const roll = Roll.fromData(rollData); // Direct access console.log(roll.formula); // "1d8 + 3[Strength]" console.log(roll.total); // 7 // If DamageRoll instance if (roll instanceof pf1.dice.DamageRoll) { console.log(roll.damageType); // {values: ["slashing"], custom: ""} console.log(roll.type); // "normal" console.log(roll.isCritical); // false } ``` --- ## 7. Critical vs Non-Critical Detection ```javascript function analyzeDamageBreakdown(message) { const metadata = message.flags?.pf1?.metadata; const result = {normalTotal: 0, critTotal: 0, attacks: []}; metadata?.rolls?.attacks?.forEach((attack, idx) => { const attackData = { index: idx, normalDamage: [], criticalDamage: [] }; // Normal damage if (attack.damage?.length) { attack.damage.forEach(roll => { attackData.normalDamage.push({ formula: roll.formula, total: roll.total, type: roll.type, // "normal" damageType: roll.options?.damageType?.values }); result.normalTotal += roll.total; }); } // Critical damage if (attack.critDamage?.length) { attack.critDamage.forEach(roll => { attackData.criticalDamage.push({ formula: roll.formula, total: roll.total, type: roll.type, // "crit" damageType: roll.options?.damageType?.values }); result.critTotal += roll.total; }); } result.attacks.push(attackData); }); return result; } ``` --- ## 8. Damage Type Detection ```javascript function getDamageTypes(message) { const metadata = message.flags?.pf1?.metadata; const types = new Set(); metadata?.rolls?.attacks?.forEach(attack => { attack.damage?.forEach(roll => { roll.options?.damageType?.values?.forEach(type => types.add(type)); }); attack.critDamage?.forEach(roll => { roll.options?.damageType?.values?.forEach(type => types.add(type)); }); }); return Array.from(types); } ``` --- ## 9. Healing vs Damage ### Healing Identification ```javascript // Method 1: Simple heal rolls function isHealingRoll(message) { return message.flags?.pf1?.subject?.health === "healing"; } // Method 2: Full attack card (need to check template data, not in metadata) // Healing is indicated in ChatAttack.isHealing flag during creation ``` ### Nonlethal Damage ```javascript // Nonlethal flag stored during attack execution // Indicates damage reduced to minimum (1) function isNonlethal(message) { // Would need access to ChatAttack instance during creation // Metadata doesn't preserve this flag post-creation // Check formula comments or flavor text instead return message.content?.includes("Nonlethal"); } ``` --- ## 10. ChatMessagePF Helpers **Location**: `module/documents/chat-message.mjs` ```javascript export class ChatMessagePF extends ChatMessage { // Get source item of attack get itemSource() { const itemId = this.flags?.pf1?.metadata?.item; if (itemId) { const actor = this.constructor.getSpeakerActor(this.speaker); return actor?.items.get(itemId); } return null; } // Get source action get actionSource() { const actionId = this.flags?.pf1?.metadata?.action; return actionId ? this.itemSource?.actions.get(actionId) : null; } // Get targeted tokens get targets() { const targetIds = this.flags?.pf1?.metadata?.targets ?? []; return targetIds.map(uuid => fromUuidSync(uuid)?.object).filter(t => !!t); } // Reconstruct rolls from JSON get systemRolls() { return this._initRollObject(this.flags?.pf1?.metadata?.rolls ?? {}); } } ``` --- ## 11. Key Files Reference | File | Purpose | |------|---------| | `module/dice/damage-roll.mjs` | DamageRoll class with damage-specific properties | | `module/action-use/chat-attack.mjs` | ChatAttack / AttackDamage classes | | `module/action-use/action-use.mjs` | Main flow and metadata generation (line 1300+) | | `module/documents/chat-message.mjs` | ChatMessagePF with helpers | | `module/utils/chat.mjs` | Chat utilities for rendering | --- ## 12. Important Patterns ### 1. Damage rolls ALWAYS stored in arrays - `attack.damage[]` - multiple rolls possible - `attack.critDamage[]` - critical hits also array ### 2. Type property distinguishes roll category - Roll `type: "normal"` = standard damage - Roll `type: "crit"` = critical damage - Roll `type: "nonCrit"` = non-critical special case ### 3. Damage types in `options` object - Access via `roll.options?.damageType?.values` array - Multiple types possible: `["fire", "slashing"]` ### 4. Critical damage includes normal damage - When calculating total crit damage: add normal + critical totals - `totalDamage = normal.reduce() + crit.reduce()` ### 5. Nonlethal applies minimum damage rule - If total < 1, becomes 1 and flagged nonlethal - Happens during roll calculation, not stored in metadata