452 lines
11 KiB
Markdown
452 lines
11 KiB
Markdown
# 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: "<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
|
|
|
|
```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
|