2651ac03b7
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>
49 lines
1.6 KiB
TypeScript
49 lines
1.6 KiB
TypeScript
import type { Character } from '@/lib/schemas/character';
|
|
|
|
export interface DeathSaveOutcome {
|
|
patch: Partial<Character>;
|
|
/** 'pending' = still rolling; 'stable' = 3 successes; 'dead' = 3 failures; 'revived' = nat 20 */
|
|
outcome: 'pending' | 'stable' | 'dead' | 'revived';
|
|
log: string;
|
|
}
|
|
|
|
/**
|
|
* Apply one 5e death saving throw given a raw d20 face. Pure:
|
|
* - nat 20 → regain 1 HP, clear the death-save clock (back in the fight)
|
|
* - nat 1 → two failures
|
|
* - ≥10 → one success (3 → stable)
|
|
* - <10 → one failure (3 → dead)
|
|
*/
|
|
export function rollDeathSave(c: Character, d20: number): DeathSaveOutcome {
|
|
const ds = c.defenses.deathSaves;
|
|
|
|
if (d20 === 20) {
|
|
return {
|
|
patch: {
|
|
hp: { ...c.hp, current: 1 },
|
|
defenses: { ...c.defenses, deathSaves: { successes: 0, failures: 0 } },
|
|
},
|
|
outcome: 'revived',
|
|
log: `${c.name} rolls a natural 20 and regains 1 HP!`,
|
|
};
|
|
}
|
|
|
|
let successes = ds.successes;
|
|
let failures = ds.failures;
|
|
if (d20 === 1) failures = Math.min(3, failures + 2);
|
|
else if (d20 >= 10) successes = Math.min(3, successes + 1);
|
|
else failures = Math.min(3, failures + 1);
|
|
|
|
const patch: Partial<Character> = { defenses: { ...c.defenses, deathSaves: { successes, failures } } };
|
|
|
|
if (failures >= 3) return { patch, outcome: 'dead', log: `${c.name} fails a third death save and dies.` };
|
|
if (successes >= 3) return { patch, outcome: 'stable', log: `${c.name} stabilizes.` };
|
|
return {
|
|
patch,
|
|
outcome: 'pending',
|
|
log: d20 >= 10
|
|
? `${c.name} succeeds a death save (${successes}/3).`
|
|
: `${c.name} fails a death save (${failures}/3).`,
|
|
};
|
|
}
|