V2 Phase 2: spell + resource enforcement (sheet → combat → live)

The Slice-0 vertical that proves the enforcement architecture end-to-end.

- kernel: castSpell (slot consumption + concentration set/replace, upcasting,
  warlock pact slots), spendResource/regainResource, rollDeathSave — pure
  functions returning Partial<Character> patches. 13 unit tests.
- applyRest now ends concentration on any rest (+ test).
- character sheet SpellcastingSection: per-spell Cast button, concentration
  banner with Drop, concentration markers; ResourcesSection −/+ routed through
  the kernel.
- player MyCharacterPanel: castable spell list + concentration banner, so a
  seated player casts and it broadcasts via the existing playerPatch flow.
- live sync: partialCharacterDiffSchema carries concentration; GM applies it;
  playerCharacterSchema mirrors slots/concentration/resources (PC-only) and the
  player-facing AC now respects equippedArmor. Wire round-trip test added.

Verified live: casting decrements the right slot, switching concentration logs
"Concentration on X ends", banner + Drop render. Build + 258 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 01:10:07 +02:00
parent 026927b5f5
commit 2651ac03b7
16 changed files with 377 additions and 5 deletions
+8
View File
@@ -98,4 +98,12 @@ describe('applyRest', () => {
expect(patch.defenses!.exhaustion).toBe(1);
expect(patch.defenses!.deathSaves).toEqual({ successes: 0, failures: 0 });
});
it('any rest ends ongoing concentration', () => {
const c = { ...makeCharacter(), concentration: { spellId: 's', spellName: 'Bless' } };
const short = dnd5e.restOptions.find((o) => o.id === 'short')!;
expect(applyRest(c, short).concentration).toBeNull();
// no concentration → no concentration key in the patch
expect('concentration' in applyRest(makeCharacter(), short)).toBe(false);
});
});