Initial commit
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { resolve, basename } from 'node:path';
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { z } from 'zod';
|
||||
import type { ParsedDoc, ParserContext, ParserFn, SourceBookId } from './types';
|
||||
import { slugify } from './helpers';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
/** Looser zod schema — mirrors `SpellData` defaults; build pipeline converts to system payload. */
|
||||
const SpellSystemSchema = z.object({
|
||||
castingMode: z.enum(['standard', 'sacred', 'witch', 'blood']),
|
||||
school: z.string(),
|
||||
deity: z.string().default(''),
|
||||
sourceBook: z.string(),
|
||||
manaCost: z.number().int().min(0),
|
||||
bloodCost: z.number().int().min(0).default(0),
|
||||
complexityLevel: z.number().int().min(0).default(0),
|
||||
isSuperspell: z.boolean().default(false),
|
||||
requiresGroupCast: z.boolean().default(false),
|
||||
minCasters: z.number().int().min(1).default(1),
|
||||
nonCombatOnly: z.boolean().default(false),
|
||||
damageBase: z.string().default(''),
|
||||
damageType: z.string().default(''),
|
||||
ignoresArmor: z.boolean().default(false),
|
||||
statusEffects: z.array(z.string()).default([]),
|
||||
saveAttribute: z.string().default(''),
|
||||
saveSkill: z.string().default(''),
|
||||
triggers: z.array(z.object({ event: z.string(), effect: z.string() })).default([]),
|
||||
components: z.object({
|
||||
verbal: z.boolean().default(false),
|
||||
somatic: z.boolean().default(false),
|
||||
material: z.string().default(''),
|
||||
symbols: z.array(z.string()).default([]),
|
||||
}),
|
||||
requirements: z.object({
|
||||
race: z.array(z.string()).default([]),
|
||||
talent: z.array(z.string()).default([]),
|
||||
discipline: z.array(z.string()).default([]),
|
||||
}),
|
||||
difficulty: z.object({
|
||||
threshold: z.number().int().min(1).max(6),
|
||||
successes: z.number().int().min(1),
|
||||
}),
|
||||
range: z.object({
|
||||
kind: z.string(),
|
||||
value: z.number().optional(),
|
||||
unit: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
}),
|
||||
duration: z.string().default(''),
|
||||
castingTime: z.string().default(''),
|
||||
target: z.string().default(''),
|
||||
areaOfEffect: z
|
||||
.object({ shape: z.string(), x: z.number().optional(), y: z.number().optional(), unit: z.string().optional() })
|
||||
.nullable()
|
||||
.default(null),
|
||||
overcastOptions: z
|
||||
.array(z.object({ text: z.string(), repeatable: z.boolean().default(true), cost: z.number().int().default(0) }))
|
||||
.default([]),
|
||||
description: z.string().default(''),
|
||||
});
|
||||
|
||||
function getAllFiles(dirPath: string, arrayOfFiles: string[] = []) {
|
||||
const files = readdirSync(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
const fullPath = resolve(dirPath, file);
|
||||
if (statSync(fullPath).isDirectory()) {
|
||||
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
|
||||
} else if (file.endsWith('.md')) {
|
||||
arrayOfFiles.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return arrayOfFiles;
|
||||
}
|
||||
|
||||
export const parseSpells: ParserFn = async (ctx: ParserContext): Promise<ParsedDoc[]> => {
|
||||
const docs: ParsedDoc[] = [];
|
||||
const spellsDir = resolve(ctx.repoRoot, 'ObsidianNotes', 'spells');
|
||||
|
||||
let files: string[] = [];
|
||||
try {
|
||||
files = getAllFiles(spellsDir);
|
||||
} catch (e) {
|
||||
console.warn('[spell-parser] No ObsidianNotes/spells directory found or unreadable.');
|
||||
return docs;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const text = readFileSync(file, 'utf8');
|
||||
const match = text.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n([\s\S]*)$/);
|
||||
if (!match) continue;
|
||||
|
||||
const [_, frontmatterRaw, bodyRaw] = match;
|
||||
let frontmatter: any;
|
||||
try {
|
||||
frontmatter = yaml.load(frontmatterRaw);
|
||||
} catch (e) {
|
||||
if (ctx.strict) throw new Error(`[spell-parser] Invalid YAML in ${file}: ${e}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload = { ...frontmatter, description: bodyRaw.trim() };
|
||||
|
||||
const nameMatch = payload.description.match(/^#\s+(.+)$/m);
|
||||
const spellName = nameMatch ? nameMatch[1].trim() : basename(file, '.md');
|
||||
payload.description = payload.description.replace(/^#\s+.+$/m, '').trim();
|
||||
|
||||
const parsed = SpellSystemSchema.safeParse(payload);
|
||||
if (!parsed.success) {
|
||||
if (ctx.strict) throw new Error(`[spell-parser] Validation failed for "${file}": ${parsed.error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const baseId = basename(file, '.md');
|
||||
|
||||
let pack = 'spells-academic';
|
||||
const relativePath = file.substring(spellsDir.length).replace(/\\/g, '/');
|
||||
if (relativePath.includes('/general/')) pack = 'spells-general';
|
||||
else if (relativePath.includes('/sacred/')) pack = 'spells-sacred';
|
||||
else if (relativePath.includes('/eldritch/')) pack = 'spells-eldritch';
|
||||
else if (relativePath.includes('/crimson/')) pack = 'spells-crimson';
|
||||
else if (relativePath.includes('/blood/')) pack = 'spells-blood';
|
||||
|
||||
docs.push({
|
||||
id: baseId,
|
||||
name: spellName,
|
||||
documentType: 'Item',
|
||||
subType: 'spell',
|
||||
pack,
|
||||
source: { book: parsed.data.sourceBook, chapter: 'Spells', line: 1 },
|
||||
system: parsed.data,
|
||||
description: payload.description,
|
||||
});
|
||||
}
|
||||
|
||||
return docs;
|
||||
};
|
||||
Reference in New Issue
Block a user