Phase 2: wire MPMB spell data in (XGtE/TCE + more sources)
The app loaded SRD-only spells; the parsed MPMB spell file was shallow (names only) and
unused. Now:
- parse_mpmb.py extracts the rich spell fields (level, school, casting time, range,
components, duration, save, ritual, classes) and writes to src/data/srd.
- New compendium/mpmb.ts normalizes MPMB spells to the Open5e Spell shape (school/time
expansion, source-code → label) — pure, unit-tested.
- loadSpells merges 204 non-SRD MPMB spells (95 XGtE, 21 TCE, + Eberron/Theros/Strixhaven/
Fizban's/etc.) into the 321 SRD spells (525 total), deduped by name.
- Spell type gains a field; the compendium spell list shows it ('Cantrip · XGtE')
and adds a Source filter.
Verified in-app (Toll the Dead → 'Cantrip · XGtE'). 333 tests green, build OK.
Only mpmb-spells.json regenerated; feats/others untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import type { Spell } from './types';
|
||||
|
||||
/** A parsed MPMB spell entry (scripts/parse_mpmb.py output). */
|
||||
export interface MpmbSpell {
|
||||
name: string;
|
||||
source?: { source: string; page: number | null }[];
|
||||
level?: number;
|
||||
school?: string;
|
||||
time?: string;
|
||||
range?: string;
|
||||
components?: string;
|
||||
duration?: string;
|
||||
save?: string;
|
||||
ritual?: boolean;
|
||||
classes?: string[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// MPMB sourcebook codes → readable labels. Unknown codes pass through unchanged.
|
||||
const SOURCE_LABEL: Record<string, string> = {
|
||||
P: 'PHB', X: 'XGtE', T: 'TCE', V: 'VGtM', M: 'MToF', MM: 'MM',
|
||||
E: 'Eberron', 'E:RLW': 'Eberron', WGtE: 'Wayfinder', MOT: 'Theros',
|
||||
G: 'GGtR', S: 'SCAG', SCC: 'Strixhaven', FToD: "Fizban's", BoMT: 'BoMT',
|
||||
AcqInc: 'Acq. Inc.', RotF: 'Rime', 'S:AiS': 'Spelljammer', 'P:AitM': 'Planescape',
|
||||
LLoK: 'Lost Lab', W: 'Witchlight', DMG: 'DMG',
|
||||
};
|
||||
|
||||
const SCHOOL: Record<string, string> = {
|
||||
abjur: 'Abjuration', conj: 'Conjuration', div: 'Divination', ench: 'Enchantment',
|
||||
evoc: 'Evocation', illus: 'Illusion', necro: 'Necromancy', trans: 'Transmutation',
|
||||
};
|
||||
|
||||
const TIME: Record<string, string> = {
|
||||
'1 a': '1 action', '1 bns': '1 bonus action', '1 rea': '1 reaction',
|
||||
'1 min': '1 minute', '10 min': '10 minutes', '1 h': '1 hour', '8 h': '8 hours', '24 h': '24 hours',
|
||||
};
|
||||
|
||||
/** The primary (canonical, first-listed) source code of a parsed MPMB entry. */
|
||||
export function mpmbPrimarySource(raw: { source?: { source: string }[] }): string {
|
||||
return raw.source?.[0]?.source ?? '';
|
||||
}
|
||||
|
||||
/** Readable label for an MPMB source code (e.g. 'X' -> 'XGtE'). */
|
||||
export function sourceLabel(code: string): string {
|
||||
return SOURCE_LABEL[code] ?? code;
|
||||
}
|
||||
|
||||
function slugify(name: string): string {
|
||||
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
/** Normalize a parsed MPMB spell into the app's Open5e-shaped Spell. */
|
||||
export function normalizeMpmbSpell(raw: MpmbSpell): Spell | null {
|
||||
if (!raw.name) return null;
|
||||
const level = typeof raw.level === 'number' ? raw.level : 0;
|
||||
const conc = /conc/i.test(raw.duration ?? '');
|
||||
const school = raw.school ? SCHOOL[raw.school.toLowerCase()] ?? raw.school : '';
|
||||
return {
|
||||
slug: slugify(raw.name),
|
||||
name: raw.name,
|
||||
desc: raw.description ?? '',
|
||||
level_int: level,
|
||||
level: level === 0 ? 'Cantrip' : `Level ${level}`,
|
||||
school,
|
||||
casting_time: raw.time ? TIME[raw.time] ?? raw.time : '',
|
||||
range: raw.range ?? '',
|
||||
duration: raw.duration ?? '',
|
||||
concentration: conc ? 'yes' : 'no',
|
||||
requires_concentration: conc,
|
||||
ritual: raw.ritual ? 'yes' : 'no',
|
||||
can_be_cast_as_ritual: !!raw.ritual,
|
||||
components: raw.components ?? '',
|
||||
dnd_class: (raw.classes ?? []).map((c) => c.charAt(0).toUpperCase() + c.slice(1)).join(', '),
|
||||
source: sourceLabel(mpmbPrimarySource(raw)),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user