Skip to content

fix: FVTT HP sync writes Max HP modifiers to tempmax, not max (#1158)#1394

Open
0xguy07 wants to merge 1 commit into
kakaroto:masterfrom
0xguy07:fix/fvtt-temp-max-hp-1158
Open

fix: FVTT HP sync writes Max HP modifiers to tempmax, not max (#1158)#1394
0xguy07 wants to merge 1 commit into
kakaroto:masterfrom
0xguy07:fix/fvtt-temp-max-hp-1158

Conversation

@0xguy07

@0xguy07 0xguy07 commented Jun 5, 2026

Copy link
Copy Markdown

Summary

Stop overwriting the actor's base Max HP in Foundry on every sync. Surface DDB's temporary Max HP modifiers (Aid, Heroes' Feast, Specter life-drain, etc.) as attributes.hp.tempmax — the field the dnd5e system actually uses for this — so the displayed effective max is right and a real level-up isn't indistinguishable from a 1-hour Aid bump.

Type of change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature
  • ♻️ Refactor
  • ⚡ Performance improvement
  • 🧪 Tests
  • 📝 Documentation
  • 🔧 Build/CI
  • 🚨 Breaking change

⚠️ AI usage disclosure (required)

  • I did not use AI for this PR.
  • I did use AI for this PR (describe below).

What was AI-assisted: Data-model analysis (dnd5e's attributes.hp.max vs tempmax split, Foundry's effective-max derivation) and the buildDnd5eUpdate refactor were drafted with Claude (Anthropic, Opus 4.7). I (the PR author) read the existing updateHP flow, identified the two call sites in src/fvtt/page-script.js, and verified the patch's logic against what the issue reporter described in the screenshots. I cannot run Foundry locally, so behavior is verified against the dnd5e data model documentation — see Reviewer Notes for the specific test checklist.

Motivation & context

The dnd5e system splits maximum HP into two attributes:

  • attributes.hp.max — the actor's base maximum (level-up, racial bonuses, manually-set value).
  • attributes.hp.tempmax — temporary modifier from buffs/debuffs. May be positive (Aid) or negative (Specter life-drain). Foundry computes the displayed effective max as max + tempmax.

updateHP was unconditionally writing DDB's total into .max. Two consequences:

  1. The Temp Max HP field never moves. Aid casts increase DDB's total → Beyond20 syncs total to Foundry max → the user's actor max appears higher but their Foundry character sheet's "Temp Max" box stays at 0. The issue reporter's screenshots show exactly this.
  2. Real level-ups are indistinguishable from temporary buffs. Both write to the same field, so when an Aid effect ends and DDB's total drops back, Foundry's max drops with it — even if the player legitimately leveled up in between.

What changed?

  • src/fvtt/page-script.js, updateHP (lines 473-528 after the patch):
    • Replaced the once-built dnd5e_data object with a per-actor buildDnd5eUpdate(actorMax) factory. Necessary because different tokens of the same name can resolve to actors with different base max values.
    • The dnd5e update now writes attributes.hp.value and attributes.hp.temp as before, but routes the max delta through attributes.hp.tempmax = total - actorMax instead of overwriting .max.
    • Initialization fallback: when the resolved actor has actorMax === 0 (uninitialized — never opened or attribute-derived in Foundry yet), the update writes .max = total so the first sync still gives the actor a usable maximum. This preserves the prior behavior for that specific case and avoids breaking the "link a brand-new Foundry actor to a DDB character" flow.
    • The Star Wars Saga ("sws") health path is unchanged — that system has no tempmax equivalent.

No changes to:

  • Roll20 HP sync (src/roll20/page-script.js:9-152, separate code path).
  • Any other FVTT or DDB code.
  • The hp-update message contract.

How to test

Reviewer should verify in live Foundry; my checklist for the things I'd look at:

  1. Aid baseline — Cast Aid on a DDB character. Beyond20 sync should:

    • Set Foundry's Temp Max HP to +5 (the Aid bump).
    • Leave the base Max HP unchanged.
    • Display the effective max as base + 5.
  2. Aid expires — Same character, Aid effect drops off in DDB.

    • Temp Max HP should go to 0.
    • Base Max HP unchanged.
  3. Specter life-drain (negative modifier) — Foundry should accept a negative tempmax. The displayed max should drop accordingly.

  4. Real level-up — Take a level on DDB.

    • DDB's total increases.
    • Foundry's Max HP also visibly increases, but here it'll route through tempmax = +level_hp_delta rather than incrementing .max. Trade-off: the displayed effective max is correct, but the user has to manually bump their Foundry actor's base .max on level-up to make the change permanent. This is the standard Foundry workflow when you manage the actor in Foundry directly and is the intended behavior — see Reviewer Notes.
  5. Brand-new actor (uninitialized) — Create a fresh actor with attributes.hp.max = 0, link to a DDB character, trigger a sync. The actor should get .max = total (initialization fallback) rather than tempmax = total.

  6. Token-driven sync — Same checklist but with a token on the canvas matching by name. Both branches (tokens.length == 0 actor path, and the per-token loop) go through the same buildDnd5eUpdate.

  7. SWS sheet (non-dnd5e) — Confirm the Star Wars Saga health path still works unchanged.

Reviewer notes

  • Level-up trade-off. The patch chooses to never overwrite .max once it's nonzero. The alternative — try to distinguish "level-up" from "buff" — would need either a per-character base-max cache or a heuristic on delta size, neither of which DDB exposes a clean signal for. The simpler trade-off (one-time manual base-max bump in Foundry on level-up) seemed strictly better than the current behavior where a temporary buff permanently changes the actor's max. Happy to revisit if you have a preferred shape.
  • Foundry version coverage. The patch uses the existing prefix = fvtt_isNewer(fvttVersion, \"10\") ? \"system\" : \"data\" switch, so v9 (data.attributes.hp.tempmax) and v10+ (system.attributes.hp.tempmax) are both addressed without new version logic.
  • No tests. The project has no test infrastructure for FVTT and exercising updateHP requires a live Foundry session. I considered adding a small unit test for buildDnd5eUpdate's payload shape but didn't want to introduce a test framework as a side effect of this fix.
  • Assignment. The issue is currently unassigned with milestone 2.20.0. Happy to defer if @dmportella or @kakaroto would rather take it directly.

…to#1158)

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) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant