From 056f715b194ae04b58306233d20b17bd5f354a1e Mon Sep 17 00:00:00 2001 From: Guy Friley Date: Thu, 4 Jun 2026 23:01:54 -0400 Subject: [PATCH] fix: FVTT HP sync writes Max HP modifiers to tempmax, not max (#1158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dnd5e system stores Max HP modifiers (Aid, Heroes' Feast, Specter life-drain, etc.) in `attributes.hp.tempmax`, while `attributes.hp.max` is the actor's base maximum. Foundry derives the effective displayed max as `max + tempmax`. Before this change, updateHP wrote DDB's `total` directly into `.max` on every sync. That permanently overwrote the actor's base max whenever DDB reported a temporarily-buffed total, and erased the distinction between a real level-up and a 1-hour Aid bump. Refactor the dnd5e payload to compute `tempmax = total - actorMax` and write that to `.tempmax`, leaving `.max` alone. Per actor (not once before the loop), since different tokens of the same name could resolve to actors with different base max values. When `actorMax` is 0 (uninitialized actor — never opened in Foundry), fall back to the previous behavior of writing `.max = total` so the first sync still gives the actor a usable max. The SWS (Star Wars Saga) health path is unchanged — that system has no tempmax equivalent. AI assistance: data-model analysis (dnd5e attributes.hp shape) and the buildDnd5eUpdate refactor drafted with Claude (Opus 4.7). I cannot run Foundry locally to verify behavior in a live session; the logic was checked against the dnd5e public actor data model and matches what the issue reporter described as the expected target field. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/fvtt/page-script.js | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/fvtt/page-script.js b/src/fvtt/page-script.js index 5a5f152ad..a7d5ccaac 100644 --- a/src/fvtt/page-script.js +++ b/src/fvtt/page-script.js @@ -477,11 +477,30 @@ function updateHP(name, current, total, temp) { const tokens = canvas.tokens.placeables.filter((t) => docIsOwner(t) && t.name.toLowerCase().trim() == name); const prefix = fvtt_isNewer(fvttVersion, "10") ? "system": "data"; - const dnd5e_data = { - [`${prefix}.attributes.hp.value`]: current, - [`${prefix}.attributes.hp.temp`]: temp, - [`${prefix}.attributes.hp.max`]: total - } + + // dnd5e stores Max HP modifiers (Aid, Heroes' Feast, Specter's life-drain, etc.) + // in `attributes.hp.tempmax`, NOT in `attributes.hp.max`. Foundry derives the + // effective max as `max + tempmax`. Previously we overwrote `.max` with DDB's + // total, permanently changing the actor's base max every sync and losing the + // distinction between a real level-up and a temporary buff (issue #1158). + // + // Surface the modifier as `tempmax = total - existing_max` and leave `.max` + // alone. When the actor's max is 0 (uninitialized — never opened in Foundry), + // fall back to writing `.max = total` so the first sync still gives the actor + // a usable value. + const buildDnd5eUpdate = (actorMax) => { + const update = { + [`${prefix}.attributes.hp.value`]: current, + [`${prefix}.attributes.hp.temp`]: temp, + }; + if (actorMax > 0) { + update[`${prefix}.attributes.hp.tempmax`] = total - actorMax; + } else { + update[`${prefix}.attributes.hp.max`] = total; + } + return update; + }; + const sws_data = { [`${prefix}.health.value`]: current + temp, [`${prefix}.health.max`]: total @@ -491,7 +510,8 @@ function updateHP(name, current, total, temp) { const actor = actors.find((a) => docIsOwner(a) && a.name.toLowerCase().trim() == name); const systemData = fvtt_isNewer(fvttVersion, "10") ? actor?.system : actor?.data?.data; if (actor && fvtt_getProperty(systemData, "attributes.hp") !== undefined) { - actor.update(dnd5e_data); + const actorMax = Number(fvtt_getProperty(systemData, "attributes.hp.max")) || 0; + actor.update(buildDnd5eUpdate(actorMax)); } else if (actor && fvtt_getProperty(systemData, "health") !== undefined) { actor.update(sws_data); } @@ -500,7 +520,8 @@ function updateHP(name, current, total, temp) { for (let token of tokens) { const systemData = fvtt_isNewer(fvttVersion, "10") ? token.actor?.system : token.actor?.data?.data; if (token.actor && fvtt_getProperty(systemData, "attributes.hp") !== undefined) { - token.actor.update(dnd5e_data); + const actorMax = Number(fvtt_getProperty(systemData, "attributes.hp.max")) || 0; + token.actor.update(buildDnd5eUpdate(actorMax)); } else if (token.actor && fvtt_getProperty(systemData, "health") !== undefined) { token.actor.update(sws_data); }