test
This commit is contained in:
265
.serena/memories/pf1_damage_application_system.md
Normal file
265
.serena/memories/pf1_damage_application_system.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
# PF1 System Damage Application Analysis
|
||||||
|
|
||||||
|
## 1. WHERE `applyDamage` IS DEFINED
|
||||||
|
|
||||||
|
### Primary Definition
|
||||||
|
- **File**: `c:\DEV\Foundry2\Foundry_VTT\src\foundryvtt-pathfinder1-v10.8\module\documents\actor\actor-pf.mjs`
|
||||||
|
- **Line**: 3780 (instance method) and 3809 (static method)
|
||||||
|
|
||||||
|
### Instance Method
|
||||||
|
```javascript
|
||||||
|
// Line 3780
|
||||||
|
async applyDamage(value, options = {}) {
|
||||||
|
return this.constructor.applyDamage(
|
||||||
|
value,
|
||||||
|
foundry.utils.mergeObject(options, {
|
||||||
|
targets: [this],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Static Method
|
||||||
|
```javascript
|
||||||
|
// Line 3809
|
||||||
|
static async applyDamage(
|
||||||
|
value = 0,
|
||||||
|
{
|
||||||
|
forceDialog = false,
|
||||||
|
reductionDefault = "",
|
||||||
|
asNonlethal = false,
|
||||||
|
targets = null,
|
||||||
|
critMult = 0,
|
||||||
|
dualHeal = false,
|
||||||
|
asWounds = false,
|
||||||
|
instances = [],
|
||||||
|
event,
|
||||||
|
element,
|
||||||
|
message = null,
|
||||||
|
} = {}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. METHOD PARAMETERS & OPTIONS STRUCTURE
|
||||||
|
|
||||||
|
### Method Signature
|
||||||
|
```javascript
|
||||||
|
static async applyDamage(value = 0, options = {})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
- **value** (number): The damage amount to apply
|
||||||
|
- Positive = damage
|
||||||
|
- Negative = healing
|
||||||
|
- Returns warning if 0 or not finite
|
||||||
|
|
||||||
|
### Options Object Structure
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
forceDialog: false, // Forces dialog even if Shift not pressed
|
||||||
|
reductionDefault: "", // Default damage reduction value
|
||||||
|
asNonlethal: false, // Marks damage as nonlethal
|
||||||
|
targets: null, // Array of Token|Actor to apply damage to
|
||||||
|
critMult: 0, // Critical multiplier (for Wounds & Vigor variant)
|
||||||
|
dualHeal: false, // If healing, also heals nonlethal damage
|
||||||
|
asWounds: false, // Apply to wounds directly (Wounds & Vigor variant)
|
||||||
|
instances: [], // Individual damage instances (not currently processed)
|
||||||
|
event, // Triggering event object
|
||||||
|
element, // Triggering HTML element
|
||||||
|
message: null, // ChatMessage reference (for modules)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Value
|
||||||
|
```javascript
|
||||||
|
Promise<false|Actor[]> // False if cancelled, array of updated actors if successful
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. HOW DAMAGE IS APPLIED FROM CHAT CARDS
|
||||||
|
|
||||||
|
### Chat Card Button Flow
|
||||||
|
|
||||||
|
**Template Files**:
|
||||||
|
- `simple-damage.hbs` (line 19-20): Simple damage/healing cards
|
||||||
|
- `attack-roll.hbs`: Attack roll cards with damage buttons
|
||||||
|
|
||||||
|
**Button HTML**:
|
||||||
|
```handlebars
|
||||||
|
<button type="button" data-action="applyDamage" data-value="{{value.total}}">Apply</button>
|
||||||
|
<button type="button" data-action="applyDamage" data-value="{{value.half}}">Apply Half</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or inline actions:
|
||||||
|
```handlebars
|
||||||
|
<a class="inline-action" data-action="applyDamage" data-type="{{type}}" data-value="...">Apply</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Click Handler Chain
|
||||||
|
|
||||||
|
1. **Item Chat Handler** (`item-pf.mjs`, line 1637):
|
||||||
|
```javascript
|
||||||
|
html.on("click", ".card-buttons button, .inline-action", this._onChatCardButton.bind(this));
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **_onChatCardButton** (`item-pf.mjs`, line 1643):
|
||||||
|
- Extracts button data and message
|
||||||
|
- Checks permissions
|
||||||
|
- Routes to `_onChatCardAction` if action is "applyDamage"
|
||||||
|
|
||||||
|
3. **_onChatCardAction** (`item-pf.mjs`, line 1688):
|
||||||
|
- Extracts damage value from `button.dataset.value`
|
||||||
|
- Gets attack data from `message.systemRolls.attacks`
|
||||||
|
- Builds damage instances array
|
||||||
|
- **Calls**: `pf1.documents.actor.ActorPF.applyDamage(value, options)`
|
||||||
|
|
||||||
|
### Extracted Data at Application Point
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// From item-pf.mjs line 1723-1731
|
||||||
|
pf1.documents.actor.ActorPF.applyDamage(value, {
|
||||||
|
asNonlethal, // from button.dataset.tags
|
||||||
|
event, // click event
|
||||||
|
element: button, // button element
|
||||||
|
message, // ChatMessage object
|
||||||
|
isCritical, // attack type === "critical"
|
||||||
|
critMult: isCritical ? metadata.config.critMult ?? 0 : 0,
|
||||||
|
instances, // damage roll instances
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative Entry Point: Text Enrichers
|
||||||
|
|
||||||
|
**File**: `text-enrichers.mjs`, line 480
|
||||||
|
```javascript
|
||||||
|
actor.applyDamage(value, { ...options, event, element: target });
|
||||||
|
```
|
||||||
|
- Used for inline damage/heal links in chat
|
||||||
|
- Creates damage instances from formula evaluation
|
||||||
|
|
||||||
|
## 4. CHAT MESSAGE FLAGS STRUCTURE
|
||||||
|
|
||||||
|
### Message Flags
|
||||||
|
```javascript
|
||||||
|
message.flags.pf1.metadata = {
|
||||||
|
action: itemActionId, // Line 36: ItemAction UUID
|
||||||
|
item: itemId, // Line 46: ItemPF ID
|
||||||
|
template: templateId, // Line 67: MeasuredTemplate ID
|
||||||
|
targets: [uuid, uuid, ...], // Line 78: Target actor/token UUIDs
|
||||||
|
rolls: { // Line 97: Rolls data
|
||||||
|
attacks: [
|
||||||
|
{
|
||||||
|
damage: [ { total: 10, damageType: "slashing" } ],
|
||||||
|
critDamage: [ { total: 20, damageType: "slashing" } ],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
critMult: 2, // Critical multiplier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message.flags.pf1.subject = {
|
||||||
|
health: "damage" | "healing" // Line 152: Type of application
|
||||||
|
}
|
||||||
|
|
||||||
|
message.flags.pf1.ammoRecovery = {
|
||||||
|
[attackIndex]: {
|
||||||
|
[ammoId]: { recovered: count }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relevant Message Properties
|
||||||
|
```javascript
|
||||||
|
message.systemRolls // Chat message rolls (init'd from flags.pf1.metadata.rolls)
|
||||||
|
message.targets // Property getter that extracts targets from flags (line 77)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. ALTERNATIVE DAMAGE APPLICATION PATHS
|
||||||
|
|
||||||
|
### Instance Method Wrapper
|
||||||
|
**File**: `actor-pf.mjs`, line 3780
|
||||||
|
```javascript
|
||||||
|
async applyDamage(value, options = {}) {
|
||||||
|
return this.constructor.applyDamage(value, { targets: [this], ...options });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Used to apply damage to specific actor instance
|
||||||
|
- Automatically sets targets to self
|
||||||
|
- All options passed through
|
||||||
|
|
||||||
|
### Direct Actor Update Alternative
|
||||||
|
Not a separate method, but damage internally calls:
|
||||||
|
```javascript
|
||||||
|
a.update({
|
||||||
|
"system.attributes.hp.value": newHP,
|
||||||
|
"system.attributes.hp.nonlethal": newNL,
|
||||||
|
"system.attributes.hp.temp": newTemp,
|
||||||
|
// Or for Wounds & Vigor:
|
||||||
|
"system.attributes.vigor.value": newHP,
|
||||||
|
"system.attributes.wounds.value": newWounds,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### No Hook Integration
|
||||||
|
- **Hooks searched**: None found in applyDamage
|
||||||
|
- **Pre-hooks**: None (could wrap to intercept)
|
||||||
|
- **Post-hooks**: None (could call custom hook after update)
|
||||||
|
|
||||||
|
## 6. KEY IMPLEMENTATION DETAILS
|
||||||
|
|
||||||
|
### Health System Support
|
||||||
|
- **Normal HP Mode**: Uses `system.attributes.hp`
|
||||||
|
- **Wounds & Vigor Mode**: Uses `system.attributes.vigor` + `system.attributes.wounds`
|
||||||
|
- Determined by: `healthConfig.variants[actorType].useWoundsAndVigor`
|
||||||
|
|
||||||
|
### Damage Flow (Normal Mode)
|
||||||
|
1. Nonlethal damage applied first
|
||||||
|
2. Temp HP reduced before actual HP
|
||||||
|
3. Remaining damage reduces actual HP
|
||||||
|
4. Cannot go below 0
|
||||||
|
|
||||||
|
### Damage Flow (Wounds & Vigor Mode)
|
||||||
|
1. Vigor (temp health) reduced first
|
||||||
|
2. Wounds (permanent damage) calculated
|
||||||
|
3. Critical multiplier applied if applicable
|
||||||
|
4. Wounds clamped to [0, max]
|
||||||
|
|
||||||
|
### Dialog Display
|
||||||
|
- Only shown if `forceDialog` is true OR Shift key pressed
|
||||||
|
- Allows user to:
|
||||||
|
- Select target tokens
|
||||||
|
- Enter custom damage reduction
|
||||||
|
- Choose damage multiplier (normal/half)
|
||||||
|
- Apply to specific targets
|
||||||
|
|
||||||
|
### Permissions Check
|
||||||
|
```javascript
|
||||||
|
if (!a.isOwner) {
|
||||||
|
ui.notifications.warn(`No permission for ${actor.name}`);
|
||||||
|
continue; // Skip this actor
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. WRAPPING POINTS FOR INTERCEPTION
|
||||||
|
|
||||||
|
### Recommended Hooks to Add
|
||||||
|
1. **Pre-damage hook**: Before `actor.update()` calls
|
||||||
|
```javascript
|
||||||
|
Hooks.call("pf1.preDamageApplied", actor, updateData, options)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Post-damage hook**: After all updates complete
|
||||||
|
```javascript
|
||||||
|
Hooks.call("pf1.damageApplied", updatedActors, options)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Intercept Location
|
||||||
|
- Line 3950 in actor-pf.mjs: `promises.push(a.update(updateData))`
|
||||||
|
- Could wrap this to modify updateData or add custom logging
|
||||||
|
|
||||||
|
### For Gowler's Tracking Ledger
|
||||||
|
- **Best approach**: Add post-damage hook
|
||||||
|
- Call `actor.getFlag("pf1", "gowlers-tracking")` to get ledger
|
||||||
|
- Log damage with timestamp and source
|
||||||
|
- Store in actor flags or external system
|
||||||
108
Manual_dmgtracking.md
Normal file
108
Manual_dmgtracking.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# PF1 Tracking Ledger: Damage Source Logging
|
||||||
|
|
||||||
|
This document describes how the tracking-ledger module captures detailed HP change metadata (damage source, damage type, encounter ID, etc.) so the ledger can show where every hit point delta came from.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The module instruments every HP update by:
|
||||||
|
|
||||||
|
1. Wrapping the PF1 system's `ActorPF.applyDamage` method to intercept the combat context (chat card, damage/healing roll, target list, action name).
|
||||||
|
2. Buffering the captured metadata (source label, type, encounter) in a temporary cache keyed by actor ID.
|
||||||
|
3. Listening to the `updateActor` hook for the specific actor, detecting HP deltas, and consuming the buffered metadata while persisting it into the world flag (`flags.world.pf1HpHistory`).
|
||||||
|
|
||||||
|
This preserves full provenance of each HP change without requiring upstream system modifications.
|
||||||
|
|
||||||
|
## Hooking `ActorPF.applyDamage`
|
||||||
|
|
||||||
|
```js
|
||||||
|
const ActorPF = pf1?.documents?.actor?.ActorPF;
|
||||||
|
const original = ActorPF.applyDamage;
|
||||||
|
|
||||||
|
ActorPF.applyDamage = async function wrappedApplyDamage(value, options = {}) {
|
||||||
|
try {
|
||||||
|
noteDamageSource(value, options);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Tracking Ledger | Failed to record damage source", err);
|
||||||
|
}
|
||||||
|
return original.call(this, value, options);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* Run once, guarded to avoid double wrapping.
|
||||||
|
* `value` is the delta applied to each target (negative for damage, positive for healing); `options` carries metadata from PF1's roll flow.
|
||||||
|
|
||||||
|
## Recording the source details
|
||||||
|
|
||||||
|
```js
|
||||||
|
function noteDamageSource(value, options) {
|
||||||
|
const actors = resolveActorTargets(options?.targets);
|
||||||
|
if (!actors.length) return;
|
||||||
|
|
||||||
|
const label = buildSourceLabel(value, options);
|
||||||
|
if (!label) return;
|
||||||
|
|
||||||
|
for (const actor of actors) {
|
||||||
|
trackingState.sources.set(actor.id, { label, ts: Date.now() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* `resolveActorTargets` converts the PF1 target collection (tokens or actors) into concrete `Actor` instances.
|
||||||
|
* `buildSourceLabel` inspects `options.message.flags.pf1` (identified action info, metadata, flavor text) to construct a human-readable source such as `"Goblin -> Scimitar Slash"` or `"Manual healing"`.
|
||||||
|
* The resulting label is cached in `trackingState.sources` until the corresponding HP delta is observed on that actor.
|
||||||
|
|
||||||
|
### Source label construction
|
||||||
|
|
||||||
|
Order of precedence when building the label:
|
||||||
|
|
||||||
|
1. `options.message.flags.pf1.identifiedInfo` (action name, item name).
|
||||||
|
2. Chat card metadata (`flags.pf1.metadata.actor` / `.item`) to resolve the actor+item pair responsible.
|
||||||
|
3. Chat card flavor text (`message.flavor`).
|
||||||
|
4. Fallbacks: `"Healing"` or `"Damage"` depending on delta sign.
|
||||||
|
|
||||||
|
This ensures we always have some context, even for custom macros.
|
||||||
|
|
||||||
|
## Consuming the buffered metadata
|
||||||
|
|
||||||
|
Inside the `updateActor` hook, every time `system.attributes.hp.value` changes, we:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const diff = newValue - previous;
|
||||||
|
const label = consumeDamageSource(actor.id) ?? inferManualSource(diff);
|
||||||
|
const entry = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
hp: newValue,
|
||||||
|
diff: diff >= 0 ? `+${diff}` : `${diff}`,
|
||||||
|
user: game.users.get(userId)?.name ?? "System",
|
||||||
|
source: label ?? "",
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
* `consumeDamageSource` pulls (and deletes) the cached metadata for that actor if present.
|
||||||
|
* `inferManualSource` supplies `"Manual healing"` or `"Manual damage"` when the HP change didn't originate from a wrapped `applyDamage` call (e.g., direct sheet edits).
|
||||||
|
* The entry is stored in `flags.world.pf1HpHistory` with a max history size of 50 rows.
|
||||||
|
|
||||||
|
## Encounter tagging
|
||||||
|
|
||||||
|
Additionally, every recorded entrée is tagged with the active encounter:
|
||||||
|
|
||||||
|
```js
|
||||||
|
entry.encounterId = game.combats?.active?.id ?? null;
|
||||||
|
```
|
||||||
|
|
||||||
|
`updateEncounterSummary` maintains `flags.world.pf1EncounterHistory`, aggregating:
|
||||||
|
|
||||||
|
* Encounter ID
|
||||||
|
* Start/end timestamps
|
||||||
|
* Participants (actor names captured per HP/XP change)
|
||||||
|
* XP gained per actor (sum of positive XP deltas)
|
||||||
|
|
||||||
|
The history dialog exposes this as a dedicated "Encounters" tab that updates in real time as HP/XP changes occur within the same combat.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
* **Damage source detection**: Done by wrapping `ActorPF.applyDamage` to capture chat metadata.
|
||||||
|
* **Storage**: Cached per-actor, consumed when HP changes fire in `updateActor`, saved to `flags.world.pf1HpHistory`.
|
||||||
|
* **Encounter tracking**: Each HP/XP change is tagged with `game.combats.active.id` and aggregated into `flags.world.pf1EncounterHistory`.
|
||||||
|
|
||||||
|
This instrumentation gives the ledger full visibility into who dealt which damage (and why), while remaining self-contained in the module.
|
||||||
Reference in New Issue
Block a user