zwischenstand
This commit is contained in:
451
.serena/memories/pf1e_damage_roll_guide.md
Normal file
451
.serena/memories/pf1e_damage_roll_guide.md
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
# 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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,84 @@
|
|||||||
<form class="tracking-ledger-config">
|
<form class="tracking-ledger-config">
|
||||||
|
<section class="tracking-ledger-controls">
|
||||||
|
<label class="tracking-ledger-filter">
|
||||||
|
Filter actors:
|
||||||
|
<input type="text" data-filter-input placeholder="Type a name..." value="{{filter}}" autocomplete="off" autofocus>
|
||||||
|
</label>
|
||||||
|
<label class="tracking-ledger-page-size">
|
||||||
|
Rows per page:
|
||||||
|
<select data-page-size>
|
||||||
|
{{#each pageOptions}}
|
||||||
|
<option value="{{value}}" {{#if selected}}selected{{/if}}>{{value}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="tracking-ledger-pagination">
|
||||||
|
<button type="button" data-action="page" data-direction="prev" {{#unless hasPrev}}disabled{{/unless}}>« Prev</button>
|
||||||
|
{{#if totalActors}}
|
||||||
|
<span>Page {{displayPage}} / {{totalPages}}</span>
|
||||||
|
{{else}}
|
||||||
|
<span>Page 0 / 0</span>
|
||||||
|
{{/if}}
|
||||||
|
<button type="button" data-action="page" data-direction="next" {{#unless hasNext}}disabled{{/unless}}>Next »</button>
|
||||||
|
</div>
|
||||||
|
<div class="tracking-ledger-summary">
|
||||||
|
{{#if totalActors}}
|
||||||
|
Showing {{showingFrom}}-{{showingTo}} of {{totalActors}} actors
|
||||||
|
{{else}}
|
||||||
|
No actors found.
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<table class="tracking-ledger-table">
|
<table class="tracking-ledger-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Actor</th>
|
<th rowspan="2">Actor</th>
|
||||||
<th>HP</th>
|
<th colspan="2">HP</th>
|
||||||
<th>XP</th>
|
<th colspan="2">XP</th>
|
||||||
<th>Currency</th>
|
<th colspan="2">Currency</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Track</th>
|
||||||
|
<th>Chat</th>
|
||||||
|
<th>Track</th>
|
||||||
|
<th>Chat</th>
|
||||||
|
<th>Track</th>
|
||||||
|
<th>Chat</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#each actors}}
|
{{#if actors.length}}
|
||||||
<tr>
|
{{#each actors}}
|
||||||
<td>{{name}}</td>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" name="actors.{{id}}.hp" {{#if tracking.hp}}checked{{/if}}>
|
{{name}}
|
||||||
</td>
|
<input type="hidden" name="actors.{{id}}.__present" value="1">
|
||||||
<td>
|
</td>
|
||||||
<input type="checkbox" name="actors.{{id}}.xp" {{#if tracking.xp}}checked{{/if}}>
|
<td>
|
||||||
</td>
|
<input type="checkbox" name="actors.{{id}}.tracking.hp" {{#if tracking.hp}}checked{{/if}}>
|
||||||
<td>
|
</td>
|
||||||
<input type="checkbox" name="actors.{{id}}.currency" {{#if tracking.currency}}checked{{/if}}>
|
<td>
|
||||||
</td>
|
<input type="checkbox" name="actors.{{id}}.chat.hp" {{#if chat.hp}}checked{{/if}}>
|
||||||
</tr>
|
</td>
|
||||||
{{/each}}
|
<td>
|
||||||
|
<input type="checkbox" name="actors.{{id}}.tracking.xp" {{#if tracking.xp}}checked{{/if}}>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="actors.{{id}}.chat.xp" {{#if chat.xp}}checked{{/if}}>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="actors.{{id}}.tracking.currency" {{#if tracking.currency}}checked{{/if}}>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" name="actors.{{id}}.chat.currency" {{#if chat.currency}}checked{{/if}}>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="empty">No actors match the current filter.</td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<footer>
|
<footer>
|
||||||
|
|||||||
322
src/macros_new/macro_activate-damage-meter.js
Normal file
322
src/macros_new/macro_activate-damage-meter.js
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
(async () => {
|
||||||
|
/*
|
||||||
|
* Activate damage tracking meter for encounters - similar to WoW Damage Meter
|
||||||
|
* Tracks total damage dealt by each actor during active combat
|
||||||
|
* Stores data in actor flags for persistence during encounter
|
||||||
|
* Optimized for PF1e damage roll detection
|
||||||
|
*/
|
||||||
|
|
||||||
|
const FLAG_SCOPE = "world";
|
||||||
|
const FLAG_KEY = "pf1DamageHistory";
|
||||||
|
const MODULE_ID = "pf1-damage-meter";
|
||||||
|
const MAX_DAMAGE_RECORDS = 200;
|
||||||
|
|
||||||
|
// Initialize global state
|
||||||
|
game.pf1 ??= {};
|
||||||
|
game.pf1.damageMeter ??= {
|
||||||
|
state: {}, // actorId -> { totalDamage, damageLog: [] }
|
||||||
|
hooks: {}, // Store hook IDs for cleanup
|
||||||
|
currentCombat: null, // Current active combat ID
|
||||||
|
isActive: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const meterState = game.pf1.damageMeter;
|
||||||
|
const logKey = `pf1-damage-meter`;
|
||||||
|
|
||||||
|
// Check if already active - toggle off
|
||||||
|
if (meterState.hooks[logKey]) {
|
||||||
|
Hooks.off("createChatMessage", meterState.hooks[logKey].createMessage);
|
||||||
|
Hooks.off("combatStart", meterState.hooks[logKey].combatStart);
|
||||||
|
Hooks.off("combatEnd", meterState.hooks[logKey].combatEnd);
|
||||||
|
delete meterState.hooks[logKey];
|
||||||
|
meterState.isActive = false;
|
||||||
|
ui.notifications.info("Damage meter tracking disabled.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable damage meter
|
||||||
|
meterState.hooks[logKey] = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract damage from PF1e attack metadata
|
||||||
|
* Handles both normal and critical damage
|
||||||
|
*/
|
||||||
|
function extractPF1eDamage(chatMessage) {
|
||||||
|
try {
|
||||||
|
const metadata = chatMessage.flags?.pf1?.metadata?.rolls?.attacks;
|
||||||
|
if (!metadata || !Array.isArray(metadata)) return 0;
|
||||||
|
|
||||||
|
let totalDamage = 0;
|
||||||
|
|
||||||
|
// Sum all damage from all attacks in this message
|
||||||
|
metadata.forEach(attack => {
|
||||||
|
// Normal damage rolls
|
||||||
|
if (Array.isArray(attack.damage)) {
|
||||||
|
attack.damage.forEach(roll => {
|
||||||
|
totalDamage += roll.total || 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Critical damage rolls (in addition to normal)
|
||||||
|
if (Array.isArray(attack.critDamage)) {
|
||||||
|
attack.critDamage.forEach(roll => {
|
||||||
|
totalDamage += roll.total || 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return totalDamage;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[Damage Meter] Error extracting PF1e damage:", err);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook into chat message creation to capture damage rolls
|
||||||
|
*/
|
||||||
|
meterState.hooks[logKey].createMessage = Hooks.on(
|
||||||
|
"createChatMessage",
|
||||||
|
async (chatMessage, options, userId) => {
|
||||||
|
// Skip if no active combat
|
||||||
|
if (!game.combat?.active) return;
|
||||||
|
|
||||||
|
// Get the actor who made this roll
|
||||||
|
const actor = chatMessage.actor;
|
||||||
|
if (!actor) return;
|
||||||
|
|
||||||
|
// Try PF1e damage detection first
|
||||||
|
const damageTotal = extractPF1eDamage(chatMessage);
|
||||||
|
|
||||||
|
// Fallback: check for other roll metadata
|
||||||
|
if (damageTotal === 0 && chatMessage.rolls?.length > 0) {
|
||||||
|
const roll = chatMessage.rolls[0];
|
||||||
|
if (
|
||||||
|
roll.formula &&
|
||||||
|
/damage|harmful|d\d+/i.test(roll.formula) &&
|
||||||
|
roll.total > 0
|
||||||
|
) {
|
||||||
|
// Potential damage roll but not PF1e format
|
||||||
|
// Only track if it looks like damage (has explicit "damage" keyword)
|
||||||
|
if (/damage/i.test(roll.formula)) {
|
||||||
|
const potential = roll.total;
|
||||||
|
if (potential > 0) {
|
||||||
|
recordDamage(actor, potential, roll.formula);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (damageTotal > 0) {
|
||||||
|
recordDamage(actor, damageTotal, "PF1e Attack");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record damage for an actor
|
||||||
|
*/
|
||||||
|
async function recordDamage(actor, damageTotal, source = "Unknown") {
|
||||||
|
// Initialize actor damage state if needed
|
||||||
|
if (!meterState.state[actor.id]) {
|
||||||
|
meterState.state[actor.id] = {
|
||||||
|
actorName: actor.name,
|
||||||
|
totalDamage: 0,
|
||||||
|
damageLog: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const actorData = meterState.state[actor.id];
|
||||||
|
actorData.totalDamage += damageTotal;
|
||||||
|
|
||||||
|
// Add entry to damage log
|
||||||
|
actorData.damageLog.unshift({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
damage: damageTotal,
|
||||||
|
source: source,
|
||||||
|
roundNumber: game.combat.round
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep log size manageable
|
||||||
|
if (actorData.damageLog.length > MAX_DAMAGE_RECORDS) {
|
||||||
|
actorData.damageLog = actorData.damageLog.slice(0, MAX_DAMAGE_RECORDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist to actor flag
|
||||||
|
await persistDamageData(actor, actorData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset damage tracking when combat starts
|
||||||
|
*/
|
||||||
|
meterState.hooks[logKey].combatStart = Hooks.on("combatStart", (combat) => {
|
||||||
|
meterState.currentCombat = combat.id;
|
||||||
|
meterState.state = {}; // Reset for new encounter
|
||||||
|
ui.notifications.info("Damage meter tracking started for new encounter.");
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save damage stats when combat ends
|
||||||
|
*/
|
||||||
|
meterState.hooks[logKey].combatEnd = Hooks.on(
|
||||||
|
"combatEnd",
|
||||||
|
async (combat) => {
|
||||||
|
const damageReport = formatDamageReport(meterState.state);
|
||||||
|
|
||||||
|
// Persist final damage data to chat
|
||||||
|
if (Object.keys(meterState.state).length > 0) {
|
||||||
|
await ChatMessage.create({
|
||||||
|
content: damageReport,
|
||||||
|
flags: { [MODULE_ID]: { combatId: combat.id } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save damage stats to actors
|
||||||
|
for (const [actorId, data] of Object.entries(meterState.state)) {
|
||||||
|
const actor = game.actors.get(actorId);
|
||||||
|
if (actor) {
|
||||||
|
await persistDamageData(actor, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
meterState.state = {};
|
||||||
|
ui.notifications.info("Damage meter encounter ended. Stats saved to chat.");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
meterState.isActive = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist damage data to actor flags
|
||||||
|
*/
|
||||||
|
async function persistDamageData(actor, data) {
|
||||||
|
try {
|
||||||
|
const existing = (await actor.getFlag(FLAG_SCOPE, FLAG_KEY)) ?? [];
|
||||||
|
|
||||||
|
// Add new encounter entry
|
||||||
|
existing.unshift({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
totalDamage: data.totalDamage,
|
||||||
|
damageCount: data.damageLog.length,
|
||||||
|
combatId: game.combat?.id || "debug",
|
||||||
|
roundNumber: game.combat?.round || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep recent encounters
|
||||||
|
if (existing.length > 50) {
|
||||||
|
existing.splice(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
await actor.setFlag(FLAG_SCOPE, FLAG_KEY, existing);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Damage Meter | Failed to persist data:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format damage data for display
|
||||||
|
*/
|
||||||
|
function formatDamageReport(state) {
|
||||||
|
if (Object.keys(state).length === 0) {
|
||||||
|
return "<p>No damage data recorded.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by total damage (descending)
|
||||||
|
const sorted = Object.values(state).sort(
|
||||||
|
(a, b) => b.totalDamage - a.totalDamage
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalTeamDamage = sorted.reduce(
|
||||||
|
(sum, actor) => sum + actor.totalDamage,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<section style="padding: 10px; background: #222; border-radius: 5px; color: #fff; font-family: monospace;">
|
||||||
|
<h3 style="margin: 0 0 10px 0; color: #aaa; font-size: 14px;">⚔️ Damage Meter Report</h3>
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid #555;">
|
||||||
|
<th style="text-align: left; padding: 5px;">Actor</th>
|
||||||
|
<th style="text-align: right; padding: 5px;">Damage</th>
|
||||||
|
<th style="text-align: right; padding: 5px;">Hits</th>
|
||||||
|
<th style="text-align: right; padding: 5px;">Avg/Hit</th>
|
||||||
|
<th style="text-align: right; padding: 5px;">% Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
`;
|
||||||
|
|
||||||
|
sorted.forEach((actor, index) => {
|
||||||
|
const avgDamage =
|
||||||
|
actor.damageLog.length > 0
|
||||||
|
? Math.round(actor.totalDamage / actor.damageLog.length)
|
||||||
|
: 0;
|
||||||
|
const percentage =
|
||||||
|
totalTeamDamage > 0
|
||||||
|
? Math.round((actor.totalDamage / totalTeamDamage) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const rowColor = index % 2 === 0 ? "#2a2a2a" : "#1f1f1f";
|
||||||
|
const rankColor = index === 0 ? "#ffd700" : "#ccc"; // Gold for #1
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<tr style="background: ${rowColor}; border-bottom: 1px solid #444;">
|
||||||
|
<td style="padding: 5px; color: ${rankColor};">${
|
||||||
|
index + 1
|
||||||
|
}. ${actor.actorName}</td>
|
||||||
|
<td style="text-align: right; padding: 5px; color: #ff6b6b;">${
|
||||||
|
actor.totalDamage
|
||||||
|
}</td>
|
||||||
|
<td style="text-align: right; padding: 5px;">${
|
||||||
|
actor.damageLog.length
|
||||||
|
}</td>
|
||||||
|
<td style="text-align: right; padding: 5px;">${avgDamage}</td>
|
||||||
|
<td style="text-align: right; padding: 5px; color: #4ecdc4;">${percentage}%</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #555; text-align: right; color: #aaa; font-size: 11px;">
|
||||||
|
Total Team Damage: <span style="color: #ff6b6b; font-weight: bold;">${totalTeamDamage}</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show current damage meter in UI
|
||||||
|
*/
|
||||||
|
function showDamageMeter() {
|
||||||
|
const content = formatDamageReport(meterState.state);
|
||||||
|
|
||||||
|
new Dialog({
|
||||||
|
title: "Damage Meter - Current Encounter",
|
||||||
|
content: content,
|
||||||
|
buttons: {
|
||||||
|
close: {
|
||||||
|
icon: '<i class="fas fa-times"></i>',
|
||||||
|
label: "Close"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
default: "close",
|
||||||
|
width: 500
|
||||||
|
}).render(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide public access
|
||||||
|
game.pf1.showDamageMeter = showDamageMeter;
|
||||||
|
|
||||||
|
const statusMessage = meterState.isActive
|
||||||
|
? `Damage meter tracking ACTIVE. Use 'game.pf1.showDamageMeter()' to view current stats.`
|
||||||
|
: `Damage meter tracking DISABLED.`;
|
||||||
|
|
||||||
|
ui.notifications.info(statusMessage);
|
||||||
|
console.log("[Damage Meter]", statusMessage);
|
||||||
|
})();
|
||||||
79
src/macros_new/macro_reset-damage-meter.js
Normal file
79
src/macros_new/macro_reset-damage-meter.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
(async () => {
|
||||||
|
/*
|
||||||
|
* Reset damage meter - disables tracking and clears all damage data
|
||||||
|
* Removes all damage history flags from all actors
|
||||||
|
* Clears in-memory tracking state
|
||||||
|
*/
|
||||||
|
|
||||||
|
const FLAG_SCOPE = "world";
|
||||||
|
const FLAG_KEY = "pf1DamageHistory";
|
||||||
|
const logKey = `pf1-damage-meter`;
|
||||||
|
|
||||||
|
console.log("[Damage Meter Reset] Starting cleanup...");
|
||||||
|
|
||||||
|
// Get the damage meter state
|
||||||
|
const meterState = game.pf1?.damageMeter;
|
||||||
|
|
||||||
|
if (!meterState) {
|
||||||
|
ui.notifications.warn(
|
||||||
|
"Damage meter was not initialized. Nothing to reset."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable hooks
|
||||||
|
if (meterState.hooks[logKey]) {
|
||||||
|
try {
|
||||||
|
Hooks.off("createChatMessage", meterState.hooks[logKey].createMessage);
|
||||||
|
Hooks.off("combatStart", meterState.hooks[logKey].combatStart);
|
||||||
|
Hooks.off("combatEnd", meterState.hooks[logKey].combatEnd);
|
||||||
|
delete meterState.hooks[logKey];
|
||||||
|
console.log("[Damage Meter Reset] Hooks disabled");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[Damage Meter Reset] Error disabling hooks:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear in-memory state
|
||||||
|
meterState.state = {};
|
||||||
|
meterState.currentCombat = null;
|
||||||
|
meterState.isActive = false;
|
||||||
|
|
||||||
|
console.log("[Damage Meter Reset] In-memory state cleared");
|
||||||
|
|
||||||
|
// Clear damage history from all actors
|
||||||
|
let actorsCleared = 0;
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const actor of game.actors) {
|
||||||
|
try {
|
||||||
|
const hasFlag = await actor.getFlag(FLAG_SCOPE, FLAG_KEY);
|
||||||
|
if (hasFlag) {
|
||||||
|
await actor.unsetFlag(FLAG_SCOPE, FLAG_KEY);
|
||||||
|
actorsCleared++;
|
||||||
|
console.log(`[Damage Meter Reset] Cleared ${FLAG_KEY} from ${actor.name}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = `Failed to clear ${actor.name}: ${err.message}`;
|
||||||
|
console.error(`[Damage Meter Reset] ${errorMsg}`);
|
||||||
|
errors.push(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report results
|
||||||
|
let message = `✓ Damage meter reset complete!\n`;
|
||||||
|
message += `• Hooks disabled\n`;
|
||||||
|
message += `• In-memory state cleared\n`;
|
||||||
|
message += `• Damage history removed from ${actorsCleared} actor(s)`;
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
message += `\n\n⚠️ Errors occurred:\n`;
|
||||||
|
errors.forEach((err) => {
|
||||||
|
message += `• ${err}\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[Damage Meter Reset] Completed:", message);
|
||||||
|
|
||||||
|
ui.notifications.info(message);
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user