Skip to content

feat: living vitals — current vs. max, P.P.E. spend on cast, damage tracking#40

Merged
StreamDemon merged 2 commits into
mainfrom
feat/living-vitals
Jul 5, 2026
Merged

feat: living vitals — current vs. max, P.P.E. spend on cast, damage tracking#40
StreamDemon merged 2 commits into
mainfrom
feat/living-vitals

Conversation

@StreamDemon

@StreamDemon StreamDemon commented Jul 5, 2026

Copy link
Copy Markdown
Owner

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)

  • characterSchema gains optional current: { 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.
  • deriveSheet rejects illegal states (a current without its rolled maximum, above it, or H.P. below the -(P.E.) coma/death floor). Since the backend validates every write through deriveSheet, illegal states can never be stored.
  • Sheet StatValue now carries current, defaulting to the rolled maximum — the wizard''s pre-roll range display is untouched.
  • New pure 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 }) — clears current back to "full".
  • rollVitals now clears current — new maximums invalidate old live state.
  • Every mutation still round-trips the whole document through the rules layer before patching.

Web (Ley Terminal)

  • Vital bars fill by current / rolled and read current / 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.
  • Casting spends: click → mutation → > CAST :: ENERGY BOLT — 5 P.P.E. [119/124] in the telemetry log (magic tone). Refusals log in blood tone.
  • Unaffordable spell rows go dead-steel (disabled, 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.
  • Telemetry rail gains a damage input + > Damage and > Full Restore — damage logs > DAMAGE :: 25 — S.D.C. 0 · H.P. 11.
  • All three new async actions guard the navigation-state bug class: in-flight results are dropped if the dossier switches characters.

Verification

  • 26 new tests (184 total): rules (applyDamage boundaries, current invariants + 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.
  • Browser-verified end-to-end on the live local deployment: cast spent 5 P.P.E. (124→119) with the spec''d log line; 11 more casts drained to 9 and the three 10-P.P.E. spells went dead-steel while cheaper ones stayed castable; 25 damage took S.D.C. 17→0 and the overflow left H.P. at 11/19; state survived a page reload; Full Restore refilled every pool; an unrolled character showed ranges and refused casts. Zero console errors throughout.

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

    • Rules (@riftforge/rules): characterSchema adds current {hitPoints, sdc, ppe}; deriveSheet rejects illegal current (above max or H.P. below coma floor) and exposes current; new pure applyDamage (S.D.C. first, H.P. clamped at floor).
    • Backend (Convex): castSpell({ id, spellId }) derives cost server-side and rejects unlearned/unrolled/insufficient casts; applyDamage({ id, amount }); heal({ id, ... }) clamps to max; restoreVitals({ id }); rollVitals clears stale current; all writes validate through rules.
    • Web (Ley Terminal): Vital bars show current / max and 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.
    • Tests: Coverage for applyDamage, current invariants, casting (ok/refused), healing, full restore, and reroll clearing.
  • Bug Fixes

    • Web: Blocked spell rows now use aria-disabled with a guarded click so they stay focusable and the title reason is accessible.
    • Web: Damage input uses strict Number parsing; non‑integers (e.g., “3.5”, “3abc”) are refused with a telemetry message.

Written for commit d1cb502. Summary will update on new commits.

Review in cubic

… 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 10 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread apps/web/src/components/sheet-view.tsx Outdated
Comment thread apps/web/src/pages/character-sheet.tsx Outdated
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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.

Living vitals: current vs. max — P.P.E. spend on cast, damage tracking, recovery

1 participant