diff --git a/.codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md b/.codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md new file mode 100644 index 0000000..d3b347a --- /dev/null +++ b/.codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md @@ -0,0 +1,482 @@ +# Full M.D.C. Combat Exchange Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deliver issue #51 by extending the persisted combat exchange through the complete S.D.C./M.D.C. routing matrix, including deterministic fatal overflow and a terminal dead-dossier state, while preserving legacy exchange history. + +**Architecture:** Extend the page-stamped rules boundary and existing pure combat resolver rather than creating a second M.D.C. path. Every newly resolved hit emits a version-2 evidence route containing native damage, conversion, protection/body deltas, final-blast absorption, and life-state outcome. Convex remains the sole dice and persistence authority and applies the route atomically. SolidJS exposes legal M.D. weapons, explicit units/history, and a readable terminal state within the existing Ley Terminal design. + +**Tech Stack:** TypeScript, Zod, JSON rules content, Convex 1.42, SolidJS 1.9, Tailwind CSS 4, Vite+ (`vp`), Vite+ Test, pnpm 11. + +## Global Constraints + +- Work directly in `D:\Projects\riftforge` on `feat/mdc-combat-exchange`; do not create a worktree. +- Treat `.codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md` as the approved contract and rendered RUE pp. 287, 288, and 354-359 as rules authority. +- Keep optional pp. 358-359 injury/survival work in issue #54 and the generic effects pipeline in issue #53; neither belongs in #51. +- Use the existing persisted exchange, not a parallel M.D.C. resolver. Keep `characters.applyDamage` S.D.C.-only; hostile M.D. writes occur only through exchanges. +- Multiply critical damage in the native tier before conversion. `totalDamage` remains the native completed weapon total. +- Apply A.R. to S.D.C. armor for both tiers. M.D.C. armor has no A.R. The final M.D.C. absorbs the full destroying blast with no spill. +- A depleted M.D.C. shell stops S.D.C. strike totals 1-7, admits 8+, and does not stop M.D. +- Raw H.P. below `-P.E.` is fatal; exactly `-P.E.` is coma. Fatal persistence is S.D.C. `0`, H.P. at the floor, `current.lifeState = "dead"`. +- Rules remain pure/deterministic. Convex owns dice and state. Clients never choose damage tier, route, conversion, or after-values. +- Preserve exact legacy route validation/history without migration. Old/racing pending exchanges stale safely. +- Preserve route-epoch and exchange-ID ownership guards because parameterized routes do not remount. +- Use stable `ConvexError` codes for expected combat refusals; the web must not parse English messages. +- Use Vite+; in `packages/backend` use `pnpm exec`, never `npx`. +- Work red -> green -> refactor. Run package gates before root gates. Checkpoint-commit every task. +- No AI attribution. Never commit to `main`, merge, or manually close #51; the human maintainer merges. +- Live-browser acceptance is mandatory for this user-visible change. + +### Frontend direction + +- Continue the compact Ley Terminal command rail; add no rounded card system, decorative art, or cyan M.D.C. language. +- Use amber for stopped/armor absorption, blood red for body/fatal harm, and green for defended/safely settled results. +- Label every amount `S.D.C.` or `M.D.`; color is never the only signal. +- Keep a dead dossier, equipment, inventory management, narrative, cancellation cleanup, and history readable while disabling gameplay actions with an explicit reason. + +--- + +## File Map + +### Rules + +- `packages/rules/src/schema/combat-exchange.ts` and `src/content/combat/combat-exchange.json`: page-stamped M.D.C. constants and stable errors. +- `packages/rules/src/schema/character.ts`: optional persisted terminal marker only. +- `packages/rules/src/engine/combat.ts`: fatal-aware body damage plus legacy pool wrapper. +- `packages/rules/src/engine/character.ts`: derived `alive | coma | dead` and terminal invariants. +- `packages/rules/src/engine/combat-exchange.ts`: legal M.D. profiles, M.D.C. readiness, v2 tokens, complete routing matrix. +- `packages/rules/src/index.ts`: additive exports. +- `packages/rules/tests/{combat,character,combat-exchange}.test.ts`: pure boundaries and matrix. + +### Backend + +- `packages/backend/convex/schema.ts`: optional dead marker. +- `packages/backend/convex/character_state.ts`: shared living guard. +- `packages/backend/convex/characters.ts`: fatal manual S.D.C. damage and terminal guards. +- `packages/backend/convex/combat_values.ts`: exact union of legacy and v2 routes. +- `packages/backend/convex/combat.ts`: target readiness, legal M.D. declarations, atomic route writes. +- `packages/backend/convex/_generated/{api,dataModel}.d.ts`: regenerate only if codegen changes them. +- `packages/backend/tests/{characters,combat}.test.ts`: mutation, compatibility, race, atomicity tests. + +### Web and delivery + +- `apps/web/src/lib/combat-exchange.ts`: version-aware presentation and disabled reasons. +- `apps/web/src/components/combat-exchange-panel.tsx`: M.D. selection/results and terminal mode. +- `apps/web/src/components/sheet-view.tsx`: derived life-state presentation and gameplay disabling. +- `apps/web/src/pages/character-sheet.tsx`: terminal command rail and navigation ownership. +- `apps/web/tests/combat-exchange.test.ts`: selection, labels, routes, tones, ownership. +- Create `apps/web/tests/character-sheet.test.ts`: terminal dossier behavior. +- `README.md`, approved design, and this plan: final aligned evidence. + +--- + +### Task 1: Page-Stamped M.D.C. Rules Boundary + +**Files:** Modify `packages/rules/src/schema/combat-exchange.ts`, `packages/rules/src/content/combat/combat-exchange.json`, `packages/rules/src/index.ts`, and `packages/rules/tests/combat-exchange.test.ts`. + +**Contract:** + +```ts +pages: { + megaDamageIntro: 288; + megaDamageCombat: 355; +} +rules: { + sdcPerMd: 100; + minimumSdcToDamageMdc: 100; + depletedMdcArmorBypassStrike: 8; + finalMdcAbsorbsDestroyingBlast: true; +} +``` + +- [x] Add failing tests asserting all six exact values and negative schema tests for altered literals. +- [x] Run `vp test packages/rules/tests/combat-exchange.test.ts`; observe missing-field failure. +- [x] Add `z.literal(...)` fields under the existing `pages`/`rules` objects and matching JSON values; retain all existing constants/exports. +- [x] Run `vp test packages/rules/tests/combat-exchange.test.ts`, `vp run @riftforge/rules#check`, and `vp run @riftforge/rules#test`; expect PASS. +- [x] Commit: + +```text +git add -- packages/rules/src/schema/combat-exchange.ts packages/rules/src/content/combat/combat-exchange.json packages/rules/src/index.ts packages/rules/tests/combat-exchange.test.ts +git commit -m "feat(rules): stamp mega-damage exchange rules" +``` + +--- + +### Task 2: Fatal-Aware Body Damage and Derived Life State + +**Files:** Modify `packages/rules/src/schema/character.ts`, `packages/rules/src/engine/{combat,character}.ts`, `packages/rules/src/index.ts`, and `packages/rules/tests/{combat,character}.test.ts`. + +**Contract:** + +```ts +export type LifeState = "alive" | "coma" | "dead"; +export interface BodyDamageResult { + before: VitalsPool; + after: VitalsPool; + rawHitPoints: number; + lifeState: LifeState; +} +export function applyBodyDamage( + pool: VitalsPool, + damage: number, + comaDeathFloor: number, +): BodyDamageResult; +``` + +Persist only `lifeState: z.literal("dead").optional()`; add `lifeState` to `CharacterSheet.vitals`. + +- [x] Add a table for `{sdc:0, hitPoints:1}`, floor `-10`: damage `1 -> coma/0`, `11 -> coma/-10`, `12 -> dead/-10` with raw `-11`. Prove S.D.C. drains first and existing `applyDamage` still returns the clamped pool. +- [x] Add character tests: positive H.P. => alive; H.P. `0` through floor => coma; dead marker valid only with rolled vitals, S.D.C. `0`, H.P. exactly floor; contradictory/unrolled dead state rejected; legacy marker-absent documents valid. +- [x] Run `vp test packages/rules/tests/combat.test.ts packages/rules/tests/character.test.ts`; observe failure. +- [x] Implement: + +```ts +const sdcDamage = Math.min(pool.sdc, damage); +const rawHitPoints = pool.hitPoints - (damage - sdcDamage); +const lifeState = rawHitPoints < comaDeathFloor ? "dead" : rawHitPoints <= 0 ? "coma" : "alive"; +const after = { + sdc: pool.sdc - sdcDamage, + hitPoints: Math.max(comaDeathFloor, rawHitPoints), +}; +``` + +Keep integer/nonnegative validation. Make `applyDamage(...)` return `applyBodyDamage(...).after` for compatibility. + +- [x] In `deriveSheet`, validate terminal invariants after pools/floor are known and derive unpersisted alive/coma from current H.P. +- [x] Run focused tests and rules check/test; expect PASS. +- [x] Commit: + +```text +git add -- packages/rules/src/schema/character.ts packages/rules/src/engine/combat.ts packages/rules/src/engine/character.ts packages/rules/src/index.ts packages/rules/tests/combat.test.ts packages/rules/tests/character.test.ts +git commit -m "feat(rules): derive terminal life state" +``` + +--- + +### Task 3: Legal M.D. Profiles, M.D.C. Readiness, and v2 Tokens + +**Files:** Modify `packages/rules/src/schema/combat-exchange.ts`, `packages/rules/src/engine/combat-exchange.ts`, and `packages/rules/tests/combat-exchange.test.ts`. + +**Contract:** Supported `AttackProfile.damageType` is `"sdc" | "md"`. Preserve M.D.C. protection even at current `0`; worn unrolled armor derives `mdcArmor` with absent max/current. Add `combatantDead` and `armorNotReady` errors, but retain legacy unsupported error values for persisted data. + +- [x] Replace M.D. refusal tests with legal energy-pistol/rifle profiles, printed formulas, unchanged strike/critical data, and `damageType: "md"`. +- [x] Test full/partial/depleted M.D.C. armor stays `mdcArmor`; unrolled armor exposes absent pools; depleted S.D.C. armor retains current no-protection behavior. +- [x] Test attacker token changes with damage tier/selected weapon. Defender token changes with life state, tier, M.D.C. readiness/max/current. Narrative, P.P.E., unrelated inventory remain excluded. +- [x] Run focused rules test; observe current refusal/collapse failure. +- [x] Return the validated item tier directly in supported profiles. Preserve all worn M.D.C. armor in `deriveProtection`. +- [x] Prefix explicit ordered token tuples `attacker-v2`/`defender-v2`; include the new relevant fields without serializing the whole sheet. +- [x] Run focused test and rules check/test; expect PASS. +- [x] Commit: + +```text +git add -- packages/rules/src/schema/combat-exchange.ts packages/rules/src/engine/combat-exchange.ts packages/rules/tests/combat-exchange.test.ts +git commit -m "feat(rules): authorize mega-damage attacks" +``` + +--- + +### Task 4: Version-2 Tiered Routing and Resolution + +**Files:** Modify `packages/rules/src/engine/combat-exchange.ts`, `packages/rules/src/index.ts`, and `packages/rules/tests/combat-exchange.test.ts`. + +**Contract:** + +```ts +export type DamageAmount = { type: "sdc" | "md"; value: number }; +export type ProtectionDamageSnapshot = { + kind: "sdcArmor" | "mdcArmor"; + itemId: string; + name: string; + before: number; + after: number; +}; +export type BodyDamageSnapshot = { before: VitalsPool; after: VitalsPool }; +``` + +`TieredDamageRoute` is an exact union: + +- `{routingVersion:2, kind:"stopped", reason:"intactMdcImpervious"|"depletedMdcShell", nativeDamage, armor, body}` +- `{routingVersion:2, kind:"armor", nativeDamage, convertedDamage?, armor, body, finalBlastAbsorbed}` +- `{routingVersion:2, kind:"body", nativeDamage, convertedDamage?, armor?, body, lifeState:{before:"alive"|"coma", after:"alive"|"coma"}}` +- `{routingVersion:2, kind:"fatal", nativeDamage, convertedDamage?, armor?, body, lifeState:{before:"alive"|"coma", after:"dead"}}` + +All new hits use this union. Keep legacy `SdcDamageRoute` exported only for persisted validation/presentation. + +- [x] Add intact-M.D.C. conversion tests for `99/100/199/200` plus printed `450/496 -> 4 M.D.C.`. +- [x] Add final-point M.D.C. no-spill and `finalBlastAbsorbed` tests. +- [x] Add depleted-shell tests for S.D.C. strike `7/8` and native M.D. bypass. +- [x] Add S.D.C.-armor tests for S.D. and M.D. attacks at/below/above A.R. +- [x] Add no-armor S.D./M.D. body tests for S.D.C.-before-H.P., exact floor, and fatal overflow. +- [x] Add a critical M.D. resolver test proving `totalDamage = native roll * multiplier` before `convertedDamage = totalDamage * 100`. +- [x] Assert every new hit has v2, native/converted evidence, exact armor/body snapshots, and no unsupported-M.D.C. route. +- [x] Run focused test; observe S.D.C.-only failures. +- [x] Implement exact helpers: + +```ts +const sdcToMd = (value: number): DamageAmount => ({ + type: "md", + value: Math.floor(value / combatExchangeRules.rules.sdcPerMd), +}); +const mdToSdc = (value: number): DamageAmount => ({ + type: "sdc", + value: value * combatExchangeRules.rules.sdcPerMd, +}); +``` + +- [x] Implement routing in this order: S.D.C. armor A.R.; intact M.D.C.; depleted M.D.C. shell; no protection; fatal-aware body. `damageArmor` clamps armor; `finalBlastAbsorbed` is true when the armor hit ends at `0`; never spill armor damage. +- [x] Resolve using `{type: input.attack.damageType, value: totalDamage}` after critical multiplication and pass the actual tier to strike evaluation. +- [x] Run focused tests and rules check/test; expect PASS. +- [x] Commit: + +```text +git add -- packages/rules/src/engine/combat-exchange.ts packages/rules/src/index.ts packages/rules/tests/combat-exchange.test.ts +git commit -m "feat(rules): resolve tiered combat damage" +``` + +--- + +### Task 5: Backward-Compatible Convex Values and Schema + +**Files:** Modify `packages/backend/convex/schema.ts`, `packages/backend/convex/combat_values.ts`, generated types if changed, and `packages/backend/tests/combat.test.ts`. + +**Contract:** Keep the legacy route validator exact, add separately discriminated v2 validators matching Task 4, and use `route: v.union(legacySdcDamageRouteValidator, tieredDamageRouteValidator)`. Add `lifeState: v.optional(v.literal("dead"))` to character current state. + +- [x] Add fixtures proving legacy armor/body routes and v2 stopped/armor/fatal routes all insert/read unchanged. Add negative fixtures for missing v2 native damage, fatal ending in coma, and extra legacy fields. +- [x] Run `vp test packages/backend/tests/combat.test.ts`; observe validator failures. +- [x] Add exact `DamageAmount`, protection/body snapshot, and four v2 route validators with `v.literal(2)`. Optional fields are optional only on branches allowed by the rules type. +- [x] Preserve legacy unsupported error codes; add `combatantDead` and `armorNotReady`. +- [x] Add the dead marker to schema. From `packages/backend`, run `pnpm exec convex codegen`; do not hand-edit or commit generated files unless they change. +- [x] Run backend focused test and backend check/test; expect PASS. +- [x] Commit: + +```text +git add -- packages/backend/convex/schema.ts packages/backend/convex/combat_values.ts packages/backend/convex/_generated/dataModel.d.ts packages/backend/convex/_generated/api.d.ts packages/backend/tests/combat.test.ts +git commit -m "feat(backend): validate tiered combat routes" +``` + +--- + +### Task 6: Terminal Character Writes and Fatal Manual S.D.C. Damage + +**Files:** Modify `packages/backend/convex/character_state.ts`, `packages/backend/convex/characters.ts`, and `packages/backend/tests/characters.test.ts`. + +**Contract:** + +```ts +export function requireLiving(character: Character, action: string): void; +// Throws: Life signs terminated — dead characters cannot ${action}. +``` + +Manual damage returns `lifeState: "alive" | "coma" | "dead"` but accepts no tier argument. + +- [x] Add manual damage tests at P.E. 10/current `{sdc:0,hp:1}`: 11 damage stores coma at -10 without marker; 12 stores dead at -10 with marker and raw overflow is not persisted. +- [x] Add dead-character rejections without state change for full update, roll/restore vitals, damage/healing, rest/meditation, treatment, ley draw, and casting (including cross-character healing). +- [x] Prove narrative edits and inventory add/remove/equip remain available and preserve the marker. +- [x] Run `vp test packages/backend/tests/characters.test.ts`; observe current floor-only/unguarded failures. +- [x] Implement `requireLiving` and call it after authoritative load but before dice or gameplay writes. Guard both caster and healing target. Guard existing document before full replacement. +- [x] Replace pool-only damage with: + +```ts +const result = applyBodyDamage(damagePools(sheet), args.amount, sheet.vitals.comaDeathFloor); +const current = { + ...character.current, + sdc: result.after.sdc, + hitPoints: result.after.hitPoints, + ...(result.lifeState === "dead" ? { lifeState: "dead" as const } : {}), +}; +``` + +Patch once; return before/after/amount/lifeState. Keep the mutation S.D.C.-only. + +- [x] Run focused test and backend check/test; expect PASS. +- [x] Commit: + +```text +git add -- packages/backend/convex/character_state.ts packages/backend/convex/characters.ts packages/backend/tests/characters.test.ts +git commit -m "feat(backend): enforce terminal life state" +``` + +--- + +### Task 7: M.D.C.-Ready Target Discovery and Declaration + +**Files:** Modify `packages/backend/convex/combat.ts` and `packages/backend/tests/combat.test.ts`. + +**Contract:** Target summaries add `lifeState`, full `ProtectionState`, and optional `disabledReason: "defenderNotReady" | "armorNotReady" | "combatantDead"`. + +- [x] Test discovery for intact/depleted rolled M.D.C. armor (enabled), unrolled worn M.D.C. (`armorNotReady`), dead target (`combatantDead` precedence), unrolled body (`defenderNotReady`), and unchanged S.D.C. target. +- [x] Test legal energy-pistol/rifle declarations store `damageType:"md"` and roll strike exactly once. Dead attacker/defender and unready armor must insert nothing and roll nothing. +- [x] Run backend combat test; observe current unsupported refusals. +- [x] Reject dead sheets with `combatFailure("combatantDead", ...)`. Reject only M.D.C. protection with absent max/current using `combatFailure("armorNotReady", ...)`; do not reject legal M.D. or depleted armor. +- [x] Persist the real attack profile/tier. Re-read both sheets, verify expected item identity/state, compute v2 tokens, roll once, and insert once. Keep current indexes/limits. +- [x] Run focused test and backend check/test; expect PASS. +- [x] Commit: + +```text +git add -- packages/backend/convex/combat.ts packages/backend/tests/combat.test.ts +git commit -m "feat(backend): declare mega-damage attacks" +``` + +--- + +### Task 8: Atomic Tiered Resolution, Death, and Stale Races + +**Files:** Modify `packages/backend/convex/combat.ts` and `packages/backend/tests/combat.test.ts`. + +- [x] Add atomic intact-M.D.C. tests: S.D.C. 99 stopped, 100 ablates 1, and a destroying M.D. blast patches armor only. +- [x] Add atomic depleted-shell tests: S.D.C. strike 7 stopped, strike 8 hits body, and M.D. converts into body. +- [x] Add atomic S.D.C.-armor tests for M.D. at/below and above A.R. +- [x] Add exact-floor coma and one-point-overflow fatal marker tests. Assert native `totalDamage` and full route evidence. +- [x] Add stale races for death, armor readiness, and selected-weapon change after declaration. +- [x] Add idempotency/compatibility tests: cleanup cancellation remains legal, duplicate response never double-applies, and legacy v1 tokens stale safely. +- [x] Run backend combat test; observe S.D.C.-only write failures. +- [x] Call `resolveCombatExchange` with stored attack tier, completed server rolls, rederived protection/body, and derived floor. Convex does no conversion. +- [x] Apply one route patch: stopped => none; armor => armor only; body => S.D.C./H.P.; fatal => S.D.C./H.P./dead together. Patch exchange resolution in the same Convex mutation. Legacy routes are never newly generated. +- [x] Check v2 tokens before defense/damage rolls. Any race, including newly dead, settles through stale logic with no character patch; stable dead errors are for new declarations. +- [x] Run `vp test packages/backend/tests/combat.test.ts packages/backend/tests/characters.test.ts` and backend check/test; expect PASS. +- [x] Commit: + +```text +git add -- packages/backend/convex/combat.ts packages/backend/tests/combat.test.ts +git commit -m "feat(backend): persist tiered combat outcomes" +``` + +--- + +### Task 9: Unit-Aware Web Presentation and Legal M.D. Selection + +**Files:** Modify `apps/web/src/lib/combat-exchange.ts`, `apps/web/src/components/combat-exchange-panel.tsx`, and `apps/web/tests/combat-exchange.test.ts`. + +- [x] Replace M.D. disabled tests: legal catalog M.D. weapons are enabled and labeled with formula plus `M.D.`; invalid modes retain their reason. +- [x] Pin server-reason copy exactly: `defenderNotReady -> Roll this target's H.P. and S.D.C. first.`, `armorNotReady -> Roll this target's worn armor M.D.C. first.`, `combatantDead -> Life signs terminated; this target cannot enter combat.` +- [x] Pin exact legacy formatting unchanged and v2 summaries for stopped S.D.C., `496 S.D.C. -> 4 M.D.C.`, native M.D. ablation, M.D.-to-body conversion, final blast, depleted shell, and fatal termination. +- [x] Pin tones: stopped/armor `warn`, body/fatal `bad`, defended `good`, cancelled `dim`. +- [x] Run `vp test apps/web/tests/combat-exchange.test.ts`; observe M.D. refusal/S.D.C.-only formatter failures. +- [x] Remove unsupported-M.D. copy, map only server stable disabled reasons, and add: + +```ts +function isTieredRoute(route: ExchangeRoute): route is TieredDamageRoute { + return "routingVersion" in route && route.routingVersion === 2; +} +``` + +Keep legacy formatter branch. Build v2 text from persisted native/converted amounts, reason, before/after values, final-blast flag, and life state; never reconstruct evidence from `totalDamage`. + +- [x] Update panel result labels using existing primitives and rail structure; add no new card system. +- [x] Run focused test and web check/test; expect PASS. +- [x] Commit: + +```text +git add -- apps/web/src/lib/combat-exchange.ts apps/web/src/components/combat-exchange-panel.tsx apps/web/tests/combat-exchange.test.ts +git commit -m "feat(web): present tiered combat outcomes" +``` + +--- + +### Task 10: Terminal Dossier UI Without Hiding History + +**Files:** Modify `apps/web/src/components/sheet-view.tsx`, `apps/web/src/components/combat-exchange-panel.tsx`, `apps/web/src/pages/character-sheet.tsx`, `apps/web/tests/combat-exchange.test.ts`; create `apps/web/tests/character-sheet.test.ts`. + +**Contract:** Add `gameplayDisabledReason?: string` to `SheetView` props. Use `Life signs terminated; gameplay actions are unavailable.` consistently. + +- [x] Add a dead `SheetView` test: terminal label/reason render; identity/vitals/equipment remain; save/skill/weapon/spell/combat rows are disabled with `aria-disabled`/`title`; inventory remains usable. +- [x] Add a dead page/rail test: narrative, telemetry, history, navigation, and cancellation remain; declaration/response/damage/restore/recovery controls are unavailable. +- [x] Add an alive-sheet/page regression proving existing controls remain wired. +- [x] Add route-change regression: dead A -> living B during in-flight request resets drafts, increments epoch, ignores A result, enables B actions. +- [x] Run focused web tests; observe missing terminal behavior. +- [x] Pass terminal reason into save/skill/combat/weapon/spell rows while retaining the actions object for inventory. Terminal reason takes precedence over spell affordability. +- [x] In the page, derive `sheet()?.vitals.lifeState === "dead"`. Replace command-rail gameplay controls with a danger terminal alert; keep telemetry. In combat panel, keep outgoing cancellation/recent history but suppress declaration/response controls and show the alert. +- [x] Extend the existing ID-change reset/epoch logic; do not key/remount the page or replace ownership tokens with booleans. +- [x] Run focused tests and web check/test; expect PASS. +- [x] Commit: + +```text +git add -- apps/web/src/components/sheet-view.tsx apps/web/src/components/combat-exchange-panel.tsx apps/web/src/pages/character-sheet.tsx apps/web/tests/character-sheet.test.ts apps/web/tests/combat-exchange.test.ts +git commit -m "feat(web): render terminal character dossiers" +``` + +--- + +### Task 11: Cross-Package Hardening and Automated Verification + +**Files:** Modify only files already in scope when a reproduced failure requires it; update this plan's checked steps/evidence. + +- [x] Run focused boundary suite: + +```text +vp test packages/rules/tests/combat.test.ts packages/rules/tests/character.test.ts packages/rules/tests/combat-exchange.test.ts packages/backend/tests/characters.test.ts packages/backend/tests/combat.test.ts apps/web/tests/combat-exchange.test.ts apps/web/tests/character-sheet.test.ts apps/web/tests/convex.test.ts +``` + +- [x] Run all package gates: + +```text +vp run @riftforge/rules#check +vp run @riftforge/rules#test +vp run @riftforge/backend#check +vp run @riftforge/backend#test +vp run @riftforge/web#check +vp run @riftforge/web#test +``` + +- [x] Run `vp check`, `vp test`, and `git diff --check`; expect PASS. +- [x] Audit the bug class: + +```text +rg -n "unsupportedMdWeapon|unsupportedMdcProtection|Full M\.D\.C\. combat is follow-up work|damageType: \"sdc\"" packages apps README.md +rg -n "applyDamage\(|applyBodyDamage\(|lifeState|routingVersion" packages apps +rg -n "TODO|TBD|FIXME" packages/rules/src packages/backend/convex apps/web/src .codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md +``` + +Legacy validator/enum hits are expected; active refusals, hardcoded S.D.C. resolution, unguarded writes, and incomplete implementation markers are not. + +- [x] Compare rules type, Convex validator, and formatter branch-by-branch. Confirm client non-authority, one atomic character patch, exact-floor distinction, and no final-blast spill. +- [x] Use `superpowers:requesting-code-review`. Reproduce findings, add regression tests for real bugs, fix root causes, rerun affected gates. +- [x] If hardening changes code, commit `fix(combat): harden mega-damage invariants`; do not create an empty commit. + +--- + +### Task 12: Live Convex and Browser Acceptance + +**Files:** None expected; for a live defect modify the narrow source/test responsible. + +- [x] Confirm port 3210 has no stale owner. Start `pnpm exec convex dev` in `packages/backend` and `vp dev` in `apps/web`. +- [x] Seed two reproducible dossiers with `pnpm exec convex run characters:create`: an alive attacker with S.D./M.D. weapons and an alive defender with worn rolled M.D.C. armor. Record returned IDs. +- [x] At `http://localhost:5173`, verify every legal-catalog live route: legal M.D. selection, sub-100 stop, native ablation, final-blast no spill, depleted-shell `7/8`, fatal terminal marker, and legacy/v2 history together. Verify the unavailable 100+ S.D.C.-vs-M.D.C. and M.D.-vs-S.D.C.-armor rows through exact rules/backend tests; the production catalog has no legal browser path for either row. +- [x] On the dead dossier, verify explicit inaccessible gameplay actions while identity, pools, equipment, inventory, narrative, cleanup cancellation, and history remain. Navigate to the living ID without reload; drafts reset/actions re-enable. +- [x] Check desktop and narrow viewport, keyboard/accessibility text, layout clipping, and console errors/warnings. +- [x] For defects: add failing automated test, fix, rerun package gates, repeat scenario. +- [x] Stop web, Convex CLI, and orphaned backend; confirm ports 5173/3210 are closed. + +--- + +### Task 13: Documentation, Issue Evidence, and Draft PR + +**Files:** Modify `README.md`, approved design spec, and this plan. + +- [x] Replace README's stale “full M.D.C. interaction remains future work” boundary with completed conversion/armor/shell/final-blast/death behavior; defer optional survival to #54. +- [x] Append exact date/time, package/root command outputs and fresh counts, live IDs/scenarios, viewports, and console result to the design; check all completed plan boxes. Never reuse older counts. +- [x] Re-run `vp check`, `vp test`, `git diff --check`, and `git status --short --branch`. +- [x] Commit: + +```text +git add -- README.md .codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md .codex/superpowers/plans/2026-07-21-mdc-combat-exchange.md +git commit -m "docs: record mega-damage combat delivery" +``` + +- [x] Push `feat/mdc-combat-exchange` and open draft PR [#55](https://github.com/StreamDemon/RiftForge/pull/55) to `main` summarizing architecture, compatibility, fatal boundary, automation, and live evidence. Link `Closes #51`, `Follow-up #54`, and `Roadmap #53`; no AI attribution; never merge. +- [x] Comment on #51 with verified time-scoped evidence/PR link: [issue comment 5037268203](https://github.com/StreamDemon/RiftForge/issues/51#issuecomment-5037268203). Do not close it manually. +- [x] Use CodeRabbit as the temporary review gate while Cubic is quota-blocked until 2026-08-01. CodeRabbit approved the implementation, then its full `3067c99..4f5f0bb` re-review found one valid trivial nit: missing source-contract files could pass negative assertions vacuously. Fix the whole duplicated-helper class TDD, rerun affected and root gates, and stop ready for human merge. + +--- + +## Final Acceptance Checklist + +- [x] Rendered/page-stamped authority covers every mechanic. +- [x] `99/100/199/200`, printed `450/496`, critical-before-conversion, final-blast, depleted-shell `7/8`, and M.D.-vs-S.D.C.-A.R. are pinned. +- [x] Exact `-P.E.` is coma; below is dead; all gameplay mutations reject dead while inventory/narrative preserve the marker. +- [x] Legacy routes read without migration; pending legacy/racing exchanges stale and never double-apply. +- [x] New history exposes native tier, conversion, reason, and before/after evidence. +- [x] Dead dossiers remain readable/accessibly terminal; route navigation owns async results. +- [x] Rules/backend/web package gates and root gates pass with fresh evidence. +- [x] Live desktop/narrow acceptance passes with clean console. +- [x] README, #51, #54, #53, design, plan, and ready PR agree; human retains merge authority. diff --git a/.codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md b/.codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md new file mode 100644 index 0000000..da082b9 --- /dev/null +++ b/.codex/superpowers/specs/2026-07-21-mdc-combat-exchange-design.md @@ -0,0 +1,691 @@ +# Full M.D.C. Combat Exchange Design + +**Date:** 2026-07-21 +**Status:** PR #55 ready; CodeRabbit nit resolved; human merge pending +**Branch:** `feat/mdc-combat-exchange` +**Primary issue:** [#51 — Full M.D.C. combat](https://github.com/StreamDemon/RiftForge/issues/51) + +## Goal + +Extend the persisted hostile-combat vertical slice from Issue #44 through the +complete core S.D.C./M.D.C. damage-tier boundary without inventing optional +injury rules or introducing a generic effect interpreter. + +This slice will: + +- support owned S.D.C. and M.D. weapon attacks through one exchange protocol; +- apply the printed S.D.C.-to-M.D.C. conversion and imperviousness rules; +- resolve S.D.C. and M.D. attacks against S.D.C. and M.D.C. body armor; +- preserve the final-blast absorption rule for destroyed armor; +- preserve the limited S.D.C. protection of depleted M.D.C. body armor; +- resolve M.D. against an unprotected mortal and persist deterministic death; +- keep all dice, conversion, routing, and writes authoritative on the server; +- keep existing Issue #44 exchange history readable without a data rewrite; and +- present the complete route in the live SolidJS combat rail. + +The existing exchange remains the authority boundary. Issue #51 does not add +M.D. input to the sheet's manual damage control. + +## Delivered outcome + +The local implementation now delivers the complete core boundary described here: +page-stamped tier constants, critical-before-conversion routing, intact and +depleted M.D.C. armor behavior, S.D.C. armor A.R. for either attack tier, +final-blast absorption without spill, versioned immutable evidence, atomic Convex +writes, and persisted terminal death. It extends the existing exchange and keeps +legacy history readable. The optional RUE pp.358-359 near-fatal injury/survival +procedure remains in issue #54, and the generic effects pipeline remains roadmap +issue #53. + +## Approved decisions + +1. **One exchange protocol.** Extend the existing typed exchange model. Do not + build a parallel M.D.C. ledger or resolver. +2. **Core outcomes only.** M.D. against an unprotected mortal follows the core + damage and death rules. The optional near-fatal injury procedure on RUE + pp.358-359 is deferred to Issue #54. +3. **Exchange-only M.D. writes.** M.D. can enter persistent character state only + through the server-rolled exchange. The manual damage control remains S.D.C. +4. **No generic effects layer.** Issue #51 remains feature-specific. A future + page-stamped effect pipeline is tracked in Issue #53 and has explicit entry + criteria. +5. **Backward-compatible history.** Existing resolved, cancelled, stale, and + pending Issue #44 records remain accepted. Legacy pending records may safely + stale when their page-stamped rules token no longer matches. +6. **Terminal death is explicit.** Fatal overflow cannot be represented as a + survivable character clamped to the coma floor. A persisted terminal marker + distinguishes death from a character exactly at the legal negative H.P. + floor. +7. **No client-authored tier.** Weapon content determines S.D.C. versus M.D.; the + UI never submits a damage tier or conversion. + +## Rendered rules evidence + +The scanned local Rifts Ultimate Edition has no usable text layer. The following +pages were rendered from PDF indexes `printed page + 2` at 2.2x and inspected +visually on 2026-07-21. + +| Printed page | Rule used in this design | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 287 | Artificial S.D.C. armor routes every completed strike by A.R.: at or below A.R. the armor absorbs the attack; above A.R. the attack reaches the wearer. Depleted S.D.C. armor affords no future protection. | +| 288 | One M.D. equals 100 S.D.C. M.D.C. armor is impervious to S.D.C./Hit Point damage below 100. S.D.C. totals of 100 or more can harm M.D.C.; divide by 100 and round down. M.D.C. armor has no A.R. The last M.D.C. absorbs the complete destroying blast. | +| 354-355 | Zero or negative H.P. is the coma band down to `-P.E.` inclusive. Damage beyond that floor is death with no hope of recovery. The final M.D.C. of armor absorbs the complete destroying blast; subsequent M.D. attacks reach the unprotected wearer. | +| 355 | At zero M.D.C., body armor is scrap but still stops S.D.C. strike totals 1-7; totals of 8 or higher reach the body inside. Ordinary S.D.C. weapons do not harm M.D.C. beings except for printed vulnerabilities. | +| 356-357 | Context only: M.D.C. technology, equipment scarcity, and GM guidance. No new deterministic damage constant is introduced from these pages. | +| 358 | Core text describes M.D. against an S.D.C. body as normally lethal. The near-fatal survival procedure begins as an optional guideline requiring GM agreement and medical intervention. | +| 359 | The optional survival path introduces hit locations, called-shot eligibility, limb/internal injury, immediate care, surgery, and trauma. Those mechanics are deliberately outside Issue #51 and tracked in Issue #54. | + +Existing Issue #44 sources remain authoritative for declaration, defense, +critical multiplication, ranged combat, S.D.C. armor A.R., and S.D.C.-before-H.P. +routing. + +## Scope + +### In scope + +- Owned S.D.C. knife, axe, handgun, and submachine-gun attacks already supported + by Issue #44. +- Owned M.D. energy-pistol and energy-rifle attacks from the current catalog. +- Existing melee/ranged declaration context, defense authorization, critical + rules, and server-owned dice. +- S.D.C. attacks against intact and depleted M.D.C. body armor. +- M.D. attacks against S.D.C. armor, intact M.D.C. armor, depleted M.D.C. + armor, and an unprotected S.D.C./Hit Point body. +- Final-blast armor absorption without spill into the wearer. +- Explicit stopped, armor, body, and fatal route evidence. +- Persisted terminal death state and derived alive/coma/dead presentation. +- Consistent fatal-threshold handling for the existing manual S.D.C. damage + mutation, without adding an M.D. input mode. +- Backward-compatible Convex validators and history formatting. +- Live two-dossier combat acceptance and parameter-route ownership checks. + +### Out of scope + +- Optional near-fatal M.D. survival, hit locations, limb loss, medical checks, + surgery, trauma, and bionic reconstruction (Issue #54). +- A generic spell/psionic/combat effect pipeline (Issue #53). +- General called shots, aimed locations, bursts, payload tracking, thrown weapon + modes, initiative, rounds, or action-budget enforcement. +- M.D.C. creatures, supernatural bodies, dragons, vehicles, robot vehicles, + power-armor operation, force fields, and printed vulnerability exceptions. +- Explosions, blast radii, impact damage, knockdown, cover geometry, and VTT + positioning. +- Authentication, ownership, GM permissions, resurrection, or character deletion + policy. +- Manual M.D. damage entry. + +## Architecture + +The existing authority flow remains intact: + +```text +page-stamped tier constants + pure exchange resolver + | + v +Convex exchange ledger + atomic character write + | + v +SolidJS declaration / response / history rail +``` + +Issue #51 expands the types and route calculation inside each boundary. It does +not add a fourth orchestration layer. + +### Rules package + +The pure rules layer owns: + +- damage-tier classification from weapon content; +- page-stamped S.D.C./M.D.C. conversion constants; +- protection classification, including depleted M.D.C. armor identity; +- critical multiplication before tier conversion; +- stopped, armor, body, and fatal route calculation; +- life-state derivation and terminal-state invariants; and +- stable combat-state tokens over every route-relevant input. + +Random rolls, elapsed time, completed strike/defense rolls, and current pools are +inputs. The rules layer never reads Convex or mutates a character. + +### Backend + +Convex continues to own exchange identity, synchronization, random dice, +immutable history, current-state rederivation, and the exactly-once character +write. It persists the rules layer's authorized route rather than reproducing +tier math. + +### Web + +SolidJS surfaces server-derived weapon/target choices, declaration context, +authorized defenses, and immutable route evidence. It never converts S.D.C. to +M.D.C., decides death, or calculates armor damage. + +## Page-stamped combat content + +The combat exchange content and Zod schema gain explicit printed references and +constants for: + +- M.D.C. introduction: p.288; +- M.D.C. combat: p.355; +- S.D.C. per M.D.: `100`; +- minimum S.D.C. total that can harm intact M.D.C.: `100`; +- depleted M.D.C. body-armor bypass strike total: `8`; and +- final-blast absorption: enabled for the last positive armor point. + +The schema pins literal printed values and rejects contradictory shapes at import. +Tests assert the complete content object so page or constant drift fails loudly. + +## Pure rules model + +### Attack profiles + +`AttackProfile` expands its supported branch from `damageType: "sdc"` to +`damageType: "sdc" | "md"`. + +| Weapon category | Attack kind | Tier | +| ----------------------------- | ----------- | ------ | +| `knife`, `axe` | melee | S.D.C. | +| `handgun`, `submachineGun` | ranged | S.D.C. | +| `energyPistol`, `energyRifle` | ranged | M.D. | + +Existing strike bonuses, ranged minimums, defense options, critical thresholds, +and firearm restrictions apply unchanged. M.D. weapons do not receive invented +bonuses or special defense behavior. + +The attack snapshot and attacker state token include the damage tier and all +page-stamped conversion constants. Any weapon/content change invalidates a pending +exchange before additional dice or writes. + +### Protection classification + +Protection remains derived from the worn physical item: + +- `none`; +- `sdcArmor` with A.R., maximum, and current S.D.C.; or +- `mdcArmor` with maximum and current M.D.C. + +Unlike Issue #44, a worn M.D.C. suit at zero remains `mdcArmor`. Printed p.355 +gives depleted body armor limited S.D.C. protection, so treating it as `none` +would lose a real mechanic and its identity. + +A dice-capacity M.D.C. suit whose per-instance maximum has not been rolled is +classified as unready. It can be displayed, but no attack may be declared against +it until its maximum/current protection is known. + +### Resolution order + +1. Validate the attack snapshot and combat context. +2. Validate the completed strike roll and declaration minimum. +3. Resolve an authorized defense with the existing `resolveStrike` primitive. +4. Determine critical state and multiplier. +5. Validate and total the native weapon damage roll. +6. Apply the critical multiplier in the weapon's native tier. +7. Classify current protection, including a depleted M.D.C. shell. +8. Convert tiers only when the route requires conversion. +9. Apply the route without spill from a destroying armor hit. +10. Return immutable before/after evidence and terminal outcome. + +Conversion never happens before the critical multiplier. This makes the value +being converted the actual completed damage total. + +### Routing matrix + +| Attack | Protection | Pure result | +| ------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| S.D.C. | None | Apply to personal S.D.C., then H.P.; classify survivable versus fatal overflow. | +| S.D.C. | S.D.C. armor | Existing strike-vs-A.R. route: at/below A.R. ablates armor; above A.R. reaches body. Destroying armor absorbs the full hit. | +| S.D.C. | Intact M.D.C. armor | Under 100 is stopped with no pool change. At 100+, apply `floor(total / 100)` M.D. to armor. Destroying armor absorbs the full hit. | +| S.D.C. | Depleted M.D.C. armor | Completed strike totals 1-7 are stopped. Totals 8+ reach personal S.D.C./H.P. as S.D.C. | +| M.D. | None | Convert completed M.D. to `M.D. * 100` S.D.C./H.P. damage and classify fatal overflow. | +| M.D. | S.D.C. armor | Apply the existing A.R. comparison. At/below A.R., convert to S.D.C. and ablate armor with no destroying-hit spill. Above A.R., convert the full hit to personal S.D.C./H.P. damage and classify fatal overflow without changing armor. | +| M.D. | Intact M.D.C. armor | Apply native M.D. to armor. The destroying hit does not spill into the body. | +| M.D. | Depleted M.D.C. armor | Convert the full completed M.D. total to S.D.C./H.P. damage and classify fatal overflow. | + +An intact M.D.C. hit by 99 S.D.C. yields a persisted stopped route. A hit by 450 +or 496 S.D.C. applies 4 M.D., matching the printed round-down examples. A hit by +21 M.D. against 3 remaining M.D.C. changes armor from 3 to 0 and leaves body +pools untouched. A subsequent M.D. hit reaches the wearer. + +### Versioned route evidence + +Existing Issue #44 routes remain valid in their original shape. New routes carry +a required routing version so the validator and UI can distinguish legacy and +tier-aware evidence without guessing from optional fields. + +A tier-aware route records: + +- native completed damage value and tier; +- converted damage value and tier when conversion occurred; +- route kind: `stopped`, `armor`, `body`, or `fatal`; +- stable stopped reason, when applicable; +- armor identity and before/after pool, when applicable; +- body S.D.C./H.P. before and after, when applicable; +- whether final-blast absorption prevented spill; and +- life state before and after for fatal routes. + +`totalDamage` remains the completed native weapon total. Converted/application +values live in route evidence so old S.D.C. records do not change meaning. + +### Life state and fatal overflow + +Character storage gains `current.lifeState?: "dead"`. Absence means the +character is not terminally dead and remains backward-compatible with every +existing document. + +The derived sheet exposes: + +- `alive` when current H.P. is above zero; +- `coma` when current H.P. is zero through `-P.E.` inclusive and no terminal + marker exists; and +- `dead` when the terminal marker exists. + +Body damage calculates the raw H.P. result before enforcing the storable numeric +floor: + +- raw H.P. at or above `-P.E.` is survivable and stores the actual value; +- raw H.P. below `-P.E.` is fatal, stores H.P. at `-P.E.`, stores personal S.D.C. + at zero, and sets `lifeState: "dead"`. + +The terminal marker is valid only when rolled vitals exist, current personal +S.D.C. is zero, and current H.P. equals the derived `-P.E.` floor. These +contradictions are rejected by `deriveSheet`, so no mutation can store a dead +character with healthy pools. + +Death is terminal in current product scope. Existing damage, healing, treatment, +rest, inventory, and combat mutations must preserve the marker or reject the +operation. Only a separately designed resurrection mechanic may clear it. + +The existing manual damage mutation remains S.D.C.-only but uses the same raw +overflow classifier. This fixes the existing case where arbitrarily large S.D.C. +damage could clamp to the coma floor and remain survivable. + +## Persisted exchange model + +The existing discriminated exchange variants remain: + +- `pendingDefense`; +- `resolved`; +- `cancelled`; and +- `stale`. + +The base attack snapshot expands `damageType` to the tier union. The resolved +result validator accepts both the legacy Issue #44 route and the new versioned +tier-aware route. + +No historical rewrite is required: + +- old characters lack `current.lifeState` and derive as nonterminal; +- old attack snapshots already contain `damageType: "sdc"`; +- old resolved routes remain accepted by their legacy validator branch; and +- old pending exchanges whose state tokens no longer match resolve safely to + `stale`, never through legacy rules. + +## Combat-state tokens + +Attacker tokens continue to fingerprint the selected weapon and derived attack. +They additionally cover damage tier and the page-stamped M.D.C. constants. + +Defender tokens continue to fingerprint rolled/current body pools and complete +worn-protection identity. They additionally cover: + +- derived life state; +- the terminal marker; +- depleted M.D.C. armor identity and zero pool; and +- armor readiness for a dice-capacity suit. + +Narrative, P.P.E., and unrelated inventory entries remain excluded so irrelevant +edits do not stale combat. + +## Backend operations + +### Target query + +The bounded target query continues to exclude self and report readiness. It now +also reports: + +- derived life state; +- protection tier and current/max pool; +- whether a dice-capacity M.D.C. suit still needs its roll; and +- a stable disabled reason for dead or protection-unready targets. + +M.D.C. protection is no longer itself a disabled reason. + +### Declare attack + +Declaration continues to: + +1. load and rederive attacker and defender; +2. reject self, missing, unready, or dead combatants; +3. derive the selected owned weapon profile; +4. derive current protection and require any per-suit armor roll; +5. parse explicit combat context; +6. roll the strike on the server; +7. persist an immediate miss or a pending defense; and +8. return only stable exchange/result data. + +M.D. weapon modes are no longer refused. No damage die is rolled for an immediate +miss. + +### Respond to attack + +Response retains the exactly-once transaction: + +1. load a pending exchange; +2. rederive both combatants; +3. compare weapon, attack, context, defense options, state tokens, life state, and + current protection; +4. parse and authorize the selected response; +5. roll defense when required; +6. roll native damage only for a hit; +7. resolve the tier-aware route; +8. atomically patch armor/body/life state when changed; and +9. replace pending history with the immutable resolved result. + +Any changed route-relevant state finalizes the exchange as stale before defense or +damage dice and before character writes. + +### Existing character mutations + +Every mutation that can change resources or combat readiness is audited against +the terminal-state invariant. Dead characters cannot: + +- take additional manual damage; +- heal or receive battle-injury treatment; +- rest or restore resources; +- declare, receive, or respond to a live combat action as an active combatant. + +Read queries and immutable combat history remain available. A pre-existing +pending exchange involving a newly dead combatant may still be cancelled as +ledger cleanup; any attempted resolution finalizes it as stale before dice or +character writes. Inventory management does not clear or bypass the terminal +marker. + +## Web experience + +### Weapon and target selection + +Energy pistols and rifles become selectable and keep explicit `M.D.` units in +their labels. Tier is informational and cannot be edited. + +M.D.C.-armored targets become legal. A target with an unrolled dice-capacity suit +is disabled with a precise instruction to roll the suit's M.D.C. first. Dead +characters are disabled with terminal-state text. + +### Declaration and response + +The existing GM-context and defense forms remain unchanged. M.D. attacks use the +same ranged awareness, dodge, automatic-dodge, modifier, and response authority as +their weapon category requires. + +No new conversion, armor, or fatality controls appear. These are outputs. + +### Result presentation + +The recent-history formatter renders explicit route evidence, including examples +such as: + +```text +96 S.D.C. -> M.D.C. ARMOR IMPERVIOUS - NO EFFECT +250 S.D.C. -> 2 M.D. :: ARMOR 70 -> 68 +21 M.D. :: ARMOR 3 -> 0 :: FINAL BLAST ABSORBED +1 M.D. -> 100 S.D.C. :: UNPROTECTED BODY :: FATAL +DEPLETED SHELL STOPPED STRIKE 7 +DEPLETED SHELL BYPASSED :: BODY S.D.C. 12 -> 4 +``` + +Legacy Issue #44 results continue through their existing formatter branch. + +Fatal outcomes use the existing blood-red signal and explicit text such as +`LIFE SIGNS TERMINATED`. M.D.C. is technological, not magical, so it never uses +ley cyan. Stopped/no-effect armor results use the machine's amber voice; defended +results retain confirmed green. + +### Terminal dossier state + +The dossier remains navigable and readable after death. Vitals show the terminal +state without relying on color alone. Damage, healing, treatment, rest, and combat +controls are unavailable with a concise explanation. Historical exchanges remain +visible. + +### Navigation and asynchronous ownership + +The Issue #44 ownership model remains mandatory: + +- parameterized routes do not remount; +- route ID changes reset weapon/target/context drafts, incoming/outgoing/recent + feeds, notices, telemetry, and row-local state; +- in-flight declaration, response, and cancellation results carry route ID plus + monotonic epoch; and +- results are ignored when ownership no longer matches. + +## Error handling + +Stable structured failures cover: + +- dead attacker or defender; +- missing per-suit M.D.C. roll; +- changed weapon or attack tier; +- changed armor identity, pool, or life state; +- illegal context or defense; +- missing combatant or exchange; and +- non-pending exchange operations. + +The following are successful persisted outcomes, not errors: + +- S.D.C. below the intact-M.D.C. threshold; +- depleted shell stopping a low S.D.C. strike; +- final-blast absorption; +- a survivable coma result; and +- a deterministic fatal result. + +## Migration and compatibility + +Convex schema deployment adds optional character state and expanded union +branches. No data rewrite or backfill runs. + +Compatibility requirements: + +- existing characters derive without a terminal marker; +- existing resolved S.D.C. history remains queryable and renderable; +- new code accepts old cancelled and stale variants; +- legacy pending exchanges can be cancelled or safely become stale; and +- current indexes remain sufficient because exchange identity/status ownership + does not change. + +## Testing strategy + +### Rules package + +Pin printed constants and page stamps, then test the complete matrix: + +- 99, 100, 199, and 200 S.D.C. against intact M.D.C.; +- the printed 450 and 496 S.D.C. round-down examples; +- native M.D. against intact and nearly depleted M.D.C.; +- final-blast absorption for native and converted damage; +- depleted-shell completed strike totals 7 and 8; +- M.D. at/below and above S.D.C. armor A.R., including no destroying-hit spill; +- M.D. against an unprotected body; +- exact `-P.E.` survival versus one point beyond fatal overflow; +- critical multiplication before conversion; +- malformed or mismatched native/converted route evidence; +- life-state invariants and terminal mutation inputs; +- old S.D.C. route compatibility; and +- state-token sensitivity for tier, rules, depleted armor, and death. + +### Backend + +Verify: + +- target readiness for fixed, rolled, unrolled, intact, depleted, and dead state; +- M.D. attack declaration through the existing exchange API; +- no defense/damage dice or writes for invalid declarations; +- authoritative native damage rolls and conversions; +- atomic armor/body/death updates; +- final-blast no-spill; +- immutable stopped and fatal history; +- stale-state rejection before rolls/writes; +- two concurrent responses produce one winner and one character write; +- all existing recovery/resource mutations reject dead characters; and +- legacy exchange documents remain readable. + +### Web + +Verify: + +- M.D. weapons are selectable and accurately labelled; +- M.D.C. protection is targetable when ready; +- unrolled armor and dead targets explain why they are disabled; +- every tiered route format includes units, conversion, and before/after evidence; +- fatal state disables every relevant control without color-only meaning; +- old S.D.C. history formatting is unchanged; +- combat colors remain semantic with no non-magic cyan; +- async ownership rejects late results after route changes; and +- history disclosure/accessibility contracts remain intact. + +## Live-browser acceptance + +Use at least two local dossiers and server-owned dice to verify: + +1. M.D. against intact M.D.C. armor. +2. A final M.D.C. point absorbing an oversized blast with no body change. +3. A subsequent M.D. hit reaching the now-unprotected wearer. +4. S.D.C. below 100 producing an immutable no-effect route. +5. S.D.C. at/above 100 converting and ablating M.D.C. +6. Depleted-shell S.D.C. strike totals on both sides of 8. +7. M.D. against an unprotected character producing deterministic death. +8. The dead dossier remaining readable while damage, recovery, rest, and combat + controls remain unavailable. +9. Old S.D.C. history beside new tier-aware results. +10. A-to-B route changes with pending/completed exchanges and no stale UI state. +11. Keyboard and screen-reader labels, narrow-rail overflow, and clean consoles. + +## Validation gates + +Before publication, run with fresh, time-scoped evidence: + +```text +vp run @riftforge/rules#check +vp run @riftforge/rules#test +vp run @riftforge/backend#check +vp run @riftforge/backend#test +vp run @riftforge/web#check +vp run @riftforge/web#test +vp check +vp test +git diff --check +``` + +User-visible changes also require the live-browser acceptance above. Test counts +must be quoted as observations from the final branch revision, never as timeless +project totals. + +## Delivery evidence + +This delivery record was assembled on **2026-07-22 (Asia/Singapore)**. The live +acceptance itself ran on **2026-07-21 (Asia/Singapore)** against source commit +`f67236e2badbb539dcd3b866ed5fb9ace1e915b6`; the automated gates below were rerun +fresh on the final documentation revision before the local documentation commit. + +### Fresh automated gates + +Captured **2026-07-22 01:44:28 +08:00 (Asia/Singapore)** with package task +caching explicitly disabled. Counts are observations from this branch revision, +not timeless project totals. + +| Command | Fresh result | +| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `vp run --no-cache @riftforge/rules#check` | exit 0; 60/60 files formatted; 46/46 files free of warnings, lint errors, and type errors | +| `vp run --no-cache @riftforge/rules#test` | exit 0; 17/17 files; 358/358 tests | +| `vp run --no-cache @riftforge/backend#check` | exit 0; 13/13 files formatted; 15/15 files free of warnings, lint errors, and type errors | +| `vp run --no-cache @riftforge/backend#test` | exit 0; 3/3 files; 128/128 tests | +| `vp run --no-cache @riftforge/web#check` | exit 0; 37/37 files formatted; 33/33 files free of warnings, lint errors, and type errors | +| `vp run --no-cache @riftforge/web#test` | exit 0; 3/3 files; 52/52 tests | +| `vp check` | exit 0; 130/130 files formatted; 95/95 files free of warnings, lint errors, and type errors | +| `vp test` | exit 0; 23/23 files; 538/538 tests | +| `git diff --check` | exit 0; no output | +| `git status --short --branch` | `feat/mdc-combat-exchange`; only README, this design, and the implementation plan modified before the documentation commit | + +### Review publication + +- Draft PR [#55](https://github.com/StreamDemon/RiftForge/pull/55) opened against + `main` on **2026-07-22 01:51:21 +08:00 (Asia/Singapore)** from published head + `1ce2130adadc06bafe473236f3db0e52fc844265`. +- Issue #51 received the verified delivery evidence and draft-PR link at + **2026-07-22 01:51:57 +08:00 (Asia/Singapore)** in + [issue comment 5037268203](https://github.com/StreamDemon/RiftForge/issues/51#issuecomment-5037268203). +- Cubic exhausted its monthly review allowance and is unavailable until + 2026-08-01. The maintainer designated CodeRabbit as the active reviewer until + Cubic returns and marked PR #55 ready for review. +- CodeRabbit reviewed the complete `3067c99..4e0afc5` change set and submitted + `APPROVED` at **2026-07-22 03:04:16 +08:00 (Asia/Singapore)** with zero inline + threads. The human maintainer retains sole merge authority. +- A full re-review through `4f5f0bb` completed at + **2026-07-22 03:27:47 +08:00 (Asia/Singapore)** with one trivial nit: the two + source-contract test helpers returned an empty string for a missing file, + allowing negative assertions to pass vacuously. Both duplicated helpers now + throw with the missing relative path. RED failed 2/48 as expected; GREEN passed + 48/48, the web package passed 54/54, and root validation passed 540/540 with + 130/130 files formatted and 95/95 files lint/type clean. + +### Live Convex and browser acceptance + +- Attacker dossier: `j5765xjcskpg5jwrj32qx3br9d8azgwe`. +- Defender dossier: `j57b5e6myeyxxhffcqwy09kg8n8ayxw9`, with 39 rolled + M.D.C. staged at 3 current M.D.C. for the ablation sequence. +- A legal Wilk's `1D6 M.D.` profile dealt 1 M.D., reducing armor from 3 to 2. +- An NG-P7 10 M.D. hit reduced armor from 2 to 0, rendered + `FINAL BLAST ABSORBED`, and left body pools at 120 S.D.C./18 H.P. +- With the same depleted shell still worn, exact completed strike 7 was stopped; + exact strike 8 bypassed and reduced body S.D.C. from 120 to 115. +- A 6 S.D.C. hit against intact 39 M.D.C. persisted an impervious/no-change + result. A later 5 M.D. hit converted to 500 S.D.C., stored body pools at + 0/-14, and persisted `DEAD`. +- Six version-2 resolved rows rendered beside legacy/null resolved history. + Cancellation cleanup remained available and recorded `CANCELLED` after death. +- The dead dossier kept identity, vitals, equipment, inventory, narrative, and + history readable while gameplay actions exposed the terminal reason. +- Direct `/characters/:id` navigation from the dead defender to the living + attacker stayed in the same document: the window marker, + `performance.timeOrigin`, and sole Navigation Timing entry were unchanged. + Drafts reset, living controls re-enabled, and the dead target remained disabled. +- Desktop acceptance ran at 1440px. Narrow acceptance ran at 390x844; browser + chrome yielded a 375 CSS-pixel document, and document/body scroll width equaled + client width with no overflowing elements. +- `SHOW20` exposed `aria-controls`; keyboard activation updated `aria-expanded` + and rendered 20 rows. Fatal/body, stopped/armor, defended, and dim history + states retained explicit text in addition to color. +- A fresh Chrome session reported zero console errors and zero warnings. +- Web, Convex CLI, the orphan-prone local backend, and temporary logs were stopped + or removed; ports 5173 and 3210 were closed at handoff. + +The legal production catalog has no S.D.C. armor and no S.D.C. attack profile +capable of reaching 100+ completed damage. Consequently, the 100+ S.D.C.-against- +M.D.C. conversion rows and M.D.-against-S.D.C.-armor A.R. rows were verified by +exact pure-rules and atomic-backend tests, not by the browser UI. No temporary or +invented rules content was introduced to manufacture those scenarios. + +## Tracker and documentation delivery + +- Keep Issue #51 aligned with the implemented core scope and rendered evidence. +- Keep optional near-fatal survival in Issue #54. +- Keep the generic page-stamped effect pipeline in Issue #53. +- Update README/current-status wording once M.D.C. exchanges are live. +- Record explicit exclusions and final validation evidence in the PR and Issue + #51. +- Follow branch -> PR -> active automated review -> human merge. CodeRabbit is + the temporary review gate while Cubic is quota-blocked. Never merge the PR from + the agent workflow. + +## Success criteria + +Issue #51 is complete when: + +- printed M.D.C. constants are page-stamped and load-validated; +- one pure resolver covers the approved S.D.C./M.D.C. routing matrix; +- existing history remains valid without a rewrite; +- Convex persists server-authorized armor/body/death outcomes atomically and + exactly once; +- terminal death cannot be bypassed by existing mutations; +- the SolidJS rail presents all new route evidence and terminal state; +- package, root, diff, and live-browser gates pass; and +- the branch is published as a PR, the active automated reviewer approves it, + and the human maintainer retains merge authority. diff --git a/README.md b/README.md index ad3fa5f..57a76de 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,16 @@ apps/web/ The engine is isomorphic: the Convex backend derives sheets at query time, and the client imports the same package for ephemeral rolls. The runtime never parses the -source book. Combat APIs now support persisted, server-rolled S.D.C. weapon -exchanges with engine-authorized defense choices, immutable history, and atomic -body S.D.C.-then-Hit-Point routing. Artificial S.D.C. armor A.R. and ablation -routing are pure-tested and ready for page-stamped production content. Natural -A.R. is intentionally absent per RUE p.339; full M.D.C. interaction remains -follow-up work. Spell APIs expose load-validated structured damage, derivation, -and injected-RNG rolling. +source book. Combat APIs support persisted, server-rolled S.D.C. and M.D. weapon +exchanges through one engine-authorized protocol. The delivered core covers +critical-before-conversion damage, S.D.C./M.D.C. cross-tier routing, S.D.C. armor +A.R. and ablation, intact and depleted M.D.C. armor, final-blast absorption with +no spill, immutable route evidence, atomic body damage, and explicit terminal +death. Legacy S.D.C. exchange history remains readable without migration. +Natural A.R. is intentionally absent per RUE p.339. The optional near-fatal M.D. +injury and survival procedure remains deferred to issue #54, and a generic +page-stamped effects pipeline remains roadmap work in issue #53. Spell APIs expose +load-validated structured damage, derivation, and injected-RNG rolling. ### Current status diff --git a/apps/web/index.html b/apps/web/index.html index ac8e5a6..3b2d74e 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -4,6 +4,7 @@ RiftForge + ; sheet: CharacterSheet; + gameplayDisabledReason?: string; onTelemetry: (text: string, tone?: TelemetryTone) => void; } @@ -331,6 +333,7 @@ export function CombatExchangePanel(props: CombatExchangePanelProps): JSX.Elemen const [notice, setNotice] = createSignal(); const [historyExpanded, setHistoryExpanded] = createSignal(false); const [flashingIds, setFlashingIds] = createSignal>(new Set()); + const gameplayEnabled = () => props.gameplayDisabledReason === undefined; const choices = createMemo(() => combatWeaponChoices(props.sheet)); const selectedIndex = createMemo(() => { const raw = weaponIndex(); @@ -352,6 +355,7 @@ export function CombatExchangePanel(props: CombatExchangePanelProps): JSX.Elemen const attack = selectedSupportedAttack(); const modifier = modifierValue(); return ( + gameplayEnabled() && !busy() && target !== undefined && combatTargetDisabledReason(target) === undefined && @@ -521,147 +525,162 @@ export function CombatExchangePanel(props: CombatExchangePanelProps): JSX.Elemen return ( COMBAT EXCHANGE -
-
-
+
{(alignment) => ( @@ -387,7 +397,7 @@ export function SheetView(props: { sheet: CharacterSheet; actions?: SheetActions // to show — hide the block unless at least one row has a value. when={APPEARANCE_ROWS.some(([field]) => s().narrative?.appearance?.[field])} > -
+
s().narrative?.appearance?.[field])}> {([field, label]) => (
@@ -401,6 +411,15 @@ export function SheetView(props: { sheet: CharacterSheet; actions?: SheetActions
+ + {(reason) => ( + + LIFE SIGNS TERMINATED + {reason()} + + )} + +
ATTRIBUTES @@ -458,6 +477,8 @@ export function SheetView(props: { sheet: CharacterSheet; actions?: SheetActions onRoll={ props.actions && (() => props.actions!.rollCombat("strike", s().combat.strike)) } + disabled={props.gameplayDisabledReason !== undefined} + title={props.gameplayDisabledReason ?? "Roll"} /> props.actions!.rollCombat("parry", s().combat.parry)) } + disabled={props.gameplayDisabledReason !== undefined} + title={props.gameplayDisabledReason ?? "Roll"} /> props.actions!.rollCombat("dodge", s().combat.dodge)) } + disabled={props.gameplayDisabledReason !== undefined} + title={props.gameplayDisabledReason ?? "Roll"} />
@@ -499,6 +524,8 @@ export function SheetView(props: { sheet: CharacterSheet; actions?: SheetActions ? () => props.actions!.rollSave(name, save) : undefined } + disabled={props.gameplayDisabledReason !== undefined} + title={props.gameplayDisabledReason ?? "Roll"} /> )}
@@ -532,6 +559,8 @@ export function SheetView(props: { sheet: CharacterSheet; actions?: SheetActions } onRoll={props.actions && (() => props.actions!.rollSkill(skill))} + disabled={props.gameplayDisabledReason !== undefined} + title={props.gameplayDisabledReason ?? "Roll"} /> )} @@ -552,13 +581,14 @@ export function SheetView(props: { sheet: CharacterSheet; actions?: SheetActions // pay for (or can't measure, pre-roll) goes dead-steel. const ppeLeft = () => s().ppe?.current; const blocked = () => - props.actions === undefined + props.gameplayDisabledReason ?? + (props.actions === undefined ? undefined : ppeLeft() === undefined ? "Roll vitals to cast" : spell.ppe > ppeLeft()! ? "Insufficient P.P.E." - : undefined; + : undefined); return ( )} diff --git a/apps/web/src/layouts/app.tsx b/apps/web/src/layouts/app.tsx index dde160d..a45248d 100644 --- a/apps/web/src/layouts/app.tsx +++ b/apps/web/src/layouts/app.tsx @@ -24,8 +24,8 @@ export function AppLayout(props: RouteSectionProps) { -
-