V2 Phase 1: mechanics kernel + compendium→sheet integration
Lay the foundation for full rules enforcement: a new pure, testable mechanics kernel (src/lib/mechanics/) that derives structured, enforceable game data from the loosely-typed compendium, plus the first user-facing payoff (accurate armor AC + spell mechanics on the sheet). - normalizers: spell (5e + pf2e), armor, monster defenses → typed SpellMechanics / ArmorMechanics / DamageDefenses, with memoized derivers wrapping the existing lazy compendium loaders (no new assets). 11 unit tests. - character schema: spell entries snapshot concentration/save/damage at compendium-link time; new top-level `concentration` slot and structured `equippedArmor` (additive, defaulted). Dexie v14 migration backfills. - AC model: baseArmorClass now consults equippedArmor (heavy = flat, medium = Dex-capped) and falls back to the old 10+Dex formula when unarmored. Threaded through every AC call site; armor picker added to the sheet (5e). - compendium "Add spell to character" snapshots real mechanics via the kernel. Backward-compatible: un-enriched spells/characters behave exactly as before. Build + 244 unit tests green; armor→AC verified live in preview. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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 { normalizeSpell5e, normalizeSpellPf2e } from './normalize/spell';
|
||||
export { normalizeArmor5e } from './normalize/armor';
|
||||
export { normalizeMonsterDefenses } from './normalize/monsterDefenses';
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user