feat: living vitals — current vs. max, P.P.E. spend on cast, damage tracking#40
Merged
Conversation
… recovery plumbing
Rules: characterSchema gains optional `current` {hitPoints, sdc, ppe};
deriveSheet rejects illegal states (current without a rolled maximum,
above it, or H.P. below the -(P.E.) coma floor) so they can never be
stored, and exposes `current` on each StatValue defaulting to the
rolled maximum. New pure applyDamage: S.D.C. absorbs before H.P.
(RUE p.347), H.P. clamps at the coma/death floor (p.287).
Backend: castSpell derives the cost server-side from the spell id and
rejects unknown/unlearned/unaffordable/unrolled casts; applyDamage
routes through the rules helper; heal clamps recovery at the maximums;
restoreVitals clears `current` (absent = full); rollVitals now clears
stale live state when new maximums land. Every write still round-trips
through deriveSheet.
Web: vital bars fill by current/rolled and read `current / max`
(DESIGN.md), flashing on live-value changes; casting calls the
mutation and logs `> CAST :: NAME — 5 P.P.E. [79/84]`; unaffordable
(or pre-roll) spell rows go dead-steel with the reason in the title;
telemetry rail gains Damage input + Full Restore. All three async
actions drop in-flight results when the dossier switches characters.
26 new tests (184 total). Browser-verified end-to-end: cast spends,
S.D.C.-before-H.P. damage, dead-steel gating as the pool drains,
persistence across reload, full restore, unrolled-character refusal.
There was a problem hiding this comment.
2 issues found across 10 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Cubic round: (1) blocked spell rows now use aria-disabled + a guarded onClick instead of the native disabled attribute, so they stay focusable and the refusal reason (title) reaches keyboard and screen-reader users; dead-steel styling moves to aria-disabled variants. (2) The damage input parses with Number, not parseInt — "3.5" and "3abc" are refused with a telemetry line instead of being silently truncated to 3.
There was a problem hiding this comment.
0 issues found across 2 files (changes from recent commits).
Requires human review: Adds live vitals (S.D.C., H.P., P.P.E.) with current-vs-max tracking, spend, and damage across rules, backend, and UI. High-impact core logic change requires human review.
Re-trigger cubic
This was referenced Jul 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #38 (plumbing + UI; page-stamped recovery rates remain as the issue''s noted follow-up).
What
The P.P.E. bar was live but nothing ever spent — no current-vs-max state existed anywhere. This adds persistent live vitals across all three layers:
Rules (
@riftforge/rules)characterSchemagains optionalcurrent: { hitPoints?, sdc?, ppe? }— what''s left of each rolled maximum; absent fields mean "at maximum". H.P. may be negative down to the coma band.deriveSheetrejects illegal states (acurrentwithout its rolled maximum, above it, or H.P. below the -(P.E.) coma/death floor). Since the backend validates every write throughderiveSheet, illegal states can never be stored.StatValuenow carriescurrent, defaulting to the rolled maximum — the wizard''s pre-roll range display is untouched.applyDamage(pool, damage, floor): S.D.C. absorbs before H.P. ("all the S.D.C. of a living thing must be reduced to zero before the Hit Points can be affected" — RUE p.347), overflow comes off H.P., clamped at the coma/death floor (p.287). Rejects negative/fractional damage.Backend (Convex)
castSpell({ id, spellId })— the server derives the cost from the spell id (the client never names a price), and rejects: spells the character hasn''t learned, casts before vitals are rolled, and casts it can''t afford (nothing is spent on refusal).applyDamage({ id, amount })— routes through the rules helper.heal({ id, hitPoints?, sdc?, ppe? })— additive recovery, clamped at the rolled maximums (API surface for recovery rates/VTT; no UI yet by design).restoreVitals({ id })— clearscurrentback to "full".rollVitalsnow clearscurrent— new maximums invalidate old live state.Web (Ley Terminal)
current / rolledand readcurrent / max(as DESIGN.md already prescribed); the rolled range moves to the tooltip. The strike-flash now fires on any live-value change — cast, damage, restore, reroll.> CAST :: ENERGY BOLT — 5 P.P.E. [119/124]in the telemetry log (magic tone). Refusals log in blood tone.title="Insufficient P.P.E."); before vitals are rolled every cast row is dead-steel with"Roll vitals to cast". Gating is reactive as the pool drains. The wizard''s read-only preview is unaffected.> Damageand> Full Restore— damage logs> DAMAGE :: 25 — S.D.C. 0 · H.P. 11.Verification
applyDamageboundaries,currentinvariants + sheet exposure), backend (spend/insufficient/unlearned/unrolled casts, S.D.C.-before-H.P. overflow + floor clamp, heal clamping, restore, reroll clearing, illegal-state rejection at create).vp check+ all per-package checks clean.Out of scope (per issue)
M.D.C. armor, temporary P.P.E. batteries, the death/coma state machine, and page-stamped recovery rates (P.P.E. per hour of rest, ley-line draw — needs rulebook extraction; lands on #38''s follow-up).
Summary by cubic
Adds live vitals with current vs. max across rules, backend, and UI, enabling P.P.E. spend on cast and damage that depletes S.D.C. before Hit Points. Addresses #38; recovery rate math remains a follow-up, plus an accessibility fix and stricter damage input parsing.
New Features
@riftforge/rules):characterSchemaaddscurrent{hitPoints, sdc, ppe};deriveSheetrejects illegalcurrent(above max or H.P. below coma floor) and exposescurrent; new pureapplyDamage(S.D.C. first, H.P. clamped at floor).castSpell({ id, spellId })derives cost server-side and rejects unlearned/unrolled/insufficient casts;applyDamage({ id, amount });heal({ id, ... })clamps to max;restoreVitals({ id });rollVitalsclears stalecurrent; all writes validate through rules.current / maxand flash on live changes; casting spends and disables unaffordable or pre‑roll spells with reasons; telemetry adds “> Damage” and “> Full Restore”; drops late mutation results on character switch.applyDamage,currentinvariants, casting (ok/refused), healing, full restore, and reroll clearing.Bug Fixes
aria-disabledwith a guarded click so they stay focusable and thetitlereason is accessible.Numberparsing; non‑integers (e.g., “3.5”, “3abc”) are refused with a telemetry message.Written for commit d1cb502. Summary will update on new commits.