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>
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
/**
|
|
* The mechanics kernel: structured, enforceable game rules derived on-demand
|
|
* from the already-cached compendium JSON. No new assets are loaded — these
|
|
* wrap the existing lazy loaders in src/lib/compendium, so the PWA cache config
|
|
* is unchanged. Derived mechanics are memoized per process.
|
|
*/
|
|
import { loadSpells, loadArmor5e, loadMonsters } from '@/lib/compendium';
|
|
import { normalizeSpell5e } from './normalize/spell';
|
|
import { normalizeArmor5e } from './normalize/armor';
|
|
import { normalizeMonsterDefenses } from './normalize/monsterDefenses';
|
|
import type { ArmorMechanics, DamageDefenses, SpellMechanics } from './types';
|
|
|
|
export * from './types';
|
|
export type { EnforceResult } from './result';
|
|
export { normalizeSpell5e, normalizeSpellPf2e } from './normalize/spell';
|
|
export { normalizeArmor5e } from './normalize/armor';
|
|
export { normalizeMonsterDefenses } from './normalize/monsterDefenses';
|
|
export { castSpell, dropConcentration } from './cast';
|
|
export { spendResource, regainResource } from './resources';
|
|
export { rollDeathSave, type DeathSaveOutcome } from './deathSaves';
|
|
|
|
let spellMechCache: Map<string, SpellMechanics> | null = null;
|
|
let armorMechCache: Map<string, ArmorMechanics> | null = null;
|
|
|
|
/** Mechanics for a single 5e spell by slug (undefined if not in the SRD set). */
|
|
export async function spellMechanics5e(slug: string): Promise<SpellMechanics | undefined> {
|
|
if (!spellMechCache) {
|
|
const raw = await loadSpells();
|
|
spellMechCache = new Map();
|
|
for (const s of raw) {
|
|
const m = normalizeSpell5e(s);
|
|
if (m) spellMechCache.set(m.slug, m);
|
|
}
|
|
}
|
|
return spellMechCache.get(slug);
|
|
}
|
|
|
|
/** Mechanics for a single 5e armor by name (case-insensitive). */
|
|
export async function armorMechanics5e(name: string): Promise<ArmorMechanics | undefined> {
|
|
if (!armorMechCache) {
|
|
const raw = await loadArmor5e();
|
|
armorMechCache = new Map();
|
|
for (const a of raw) {
|
|
const m = normalizeArmor5e(a);
|
|
if (m) armorMechCache.set(m.name.toLowerCase(), m);
|
|
}
|
|
}
|
|
return armorMechCache.get(name.toLowerCase());
|
|
}
|
|
|
|
/** All 5e armor mechanics (for the "equip" picker). */
|
|
export async function allArmorMechanics5e(): Promise<ArmorMechanics[]> {
|
|
await armorMechanics5e('');
|
|
return [...(armorMechCache?.values() ?? [])];
|
|
}
|
|
|
|
/** Damage/condition defenses for a 5e monster by slug. */
|
|
export async function monsterDefenses5e(slug: string): Promise<DamageDefenses | undefined> {
|
|
const raw = await loadMonsters();
|
|
const m = raw.find((x) => x.slug === slug);
|
|
return m ? normalizeMonsterDefenses(m) : undefined;
|
|
}
|