Files
FoundryVTT/.serena/memories/pf1e_damage_roll_guide.md
centron\schwoerer 4f0b3af6e1 zwischenstand
2025-11-18 14:32:01 +01:00

11 KiB

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:

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:

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:

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:

{
  actor: "<actor-uuid>",
  item: "<item-id>",
  action: "<action-id>",
  template: "<template-id>",
  targets: ["<target-uuid>", ...],
  combat: "<combat-id>",

  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: "<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

// 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

// 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)

// 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

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

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

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

// 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

// 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

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