diff --git a/lang/en.json b/lang/en.json index 53642ae..bfcb748 100644 --- a/lang/en.json +++ b/lang/en.json @@ -105,7 +105,6 @@ "spell": { "label": "Spell", "cast": "Cast spell", - "circle": "Circle", "school": "School", "discipline": "Discipline", "difficulty": "Spell Threshold", @@ -200,14 +199,14 @@ "eros": "Eros" }, "sourceBook": { - "core-rules": "PG", - "magic-book": "KM", - "arcanum-sanguinis": "AS", - "crimson-cult": "CS", - "abyss-curse": "KO", - "humanity-guide": "PLM", - "bestiary": "B", - "gold-steel-magic": "ZSM" + "podrecznik-gry": "Podręcznik Gry", + "ksiega-magii": "Księga Magii", + "arcanum-sanguinis": "Arcanum Sanguinis", + "crimson-cult": "Szkarłatny Kult", + "klatwa-otchlani": "Klątwa Otchłani", + "przewodnik": "Przewodnik", + "bestiariusz": "Bestiariusz", + "zlote-stal-magia": "Złoto i Stal: Magia" }, "aoeShape": { "point": "Point", diff --git a/lang/pl.json b/lang/pl.json index 641ca1d..c8d2f3a 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -105,7 +105,6 @@ "spell": { "label": "Zaklęcie", "cast": "Rzuć zaklęcie", - "circle": "Krąg", "school": "Szkoła", "discipline": "Dziedzina", "difficulty": "Próg zaklęcia", @@ -200,14 +199,14 @@ "eros": "Eros" }, "sourceBook": { - "core-rules": "PG", - "magic-book": "KM", - "arcanum-sanguinis": "AS", - "crimson-cult": "CS", - "abyss-curse": "KO", - "humanity-guide": "PLM", - "bestiary": "B", - "gold-steel-magic": "ZSM" + "podrecznik-gry": "Podręcznik Gry", + "ksiega-magii": "Księga Magii", + "arcanum-sanguinis": "Arcanum Sanguinis", + "crimson-cult": "Szkarłatny Kult", + "klatwa-otchlani": "Klątwa Otchłani", + "przewodnik": "Przewodnik", + "bestiariusz": "Bestiariusz", + "zlote-stal-magia": "Złoto i Stal: Magia" }, "aoeShape": { "point": "Punkt", diff --git a/scripts/extract-spells-to-md.ts b/scripts/extract-spells-to-md.ts index b3ba000..90f3cb5 100644 --- a/scripts/extract-spells-to-md.ts +++ b/scripts/extract-spells-to-md.ts @@ -1,3 +1,4 @@ +// DEPRECATED: one-off extractor that writes the old nested spell frontmatter. The vault now uses the flat format read by scripts/parsers/spell-parser.ts - do not run this over ObsidianNotes/spells. import { resolve, dirname } from 'node:path'; import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; diff --git a/scripts/parsers/spell-parser.ts b/scripts/parsers/spell-parser.ts index 5a22728..8a2f9a6 100644 --- a/scripts/parsers/spell-parser.ts +++ b/scripts/parsers/spell-parser.ts @@ -5,59 +5,109 @@ 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({ +/** + * Flat, human-editable vault frontmatter for a spell. Nothing is nested; + * optional keys can simply be left out. `toSystem()` expands it into the + * (partly nested) Foundry `SpellData` shape. + */ +const SpellNoteSchema = 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), + threshold: z.number().int().min(1).max(6), + successes: z.number().int().min(1), 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(''), + verbal: z.boolean().default(false), + somatic: z.boolean().default(false), + material: z.coerce.string().default(''), + symbols: z.array(z.string()).default([]), + requiresRace: z.array(z.string()).default([]), + requiresTalent: z.array(z.string()).default([]), + requiresDiscipline: z.array(z.string()).default([]), + castingTime: z.coerce.string().default('Instant'), + range: z.coerce.string().default(''), + targets: z.coerce.string().default(''), + duration: z.coerce.string().default(''), + damageBase: z.coerce.string().default(''), + damageType: z.string().default('magical'), 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(''), + overcast: z.array(z.string()).default([]), }); +type SpellNote = z.infer; + +type Aoe = { shape: string; x: number; y: number; unit: string }; + +/** Derive the structured area of effect from the free-text `targets` line. */ +export function deriveAreaOfEffect(targets: string): Aoe { + const t = targets.replace(/\s+/g, ' '); + let m = t.match(/kwadrat o boku (\d+)\s*m/i); + if (m) return { shape: 'square', x: Number(m[1]), y: Number(m[1]), unit: 'm' }; + m = t.match(/prostokąt (\d+)\s*m\s*[x×]\s*(\d+)\s*m/i); + if (m) return { shape: 'rectangle', x: Number(m[1]), y: Number(m[2]), unit: 'm' }; + m = t.match(/(?:sfera|kula) o średnicy (\d+)\s*m/i); + if (m) return { shape: 'sphere', x: Number(m[1]) / 2, y: 0, unit: 'm' }; + m = t.match(/(?:sfera|kula|kopuła) o promieniu (\d+)\s*m/i); + if (m) return { shape: 'sphere', x: Number(m[1]), y: 0, unit: 'm' }; + if (/^linia/i.test(t)) return { shape: 'line', x: 0, y: 0, unit: 'm' }; + return { shape: 'point', x: 0, y: 0, unit: 'm' }; +} + +/** Casting time in whole rounds / minutes, for the sheet's numeric fields. */ +export function deriveCastingTime(text: string): { rounds: number; minutes: number } { + let m = text.match(/(\d+)\s*godzin/i); + if (m) return { rounds: 0, minutes: Number(m[1]) * 60 }; + m = text.match(/(\d+)\s*minut/i); + if (m) return { rounds: 0, minutes: Number(m[1]) }; + m = text.match(/(\d+)\s*rund/i); + if (m) return { rounds: Number(m[1]), minutes: 0 }; + return { rounds: 0, minutes: 0 }; +} + +function toSystem(n: SpellNote): Record { + const time = deriveCastingTime(n.castingTime); + return { + school: n.school, + deity: n.deity, + sourceBook: n.sourceBook, + castingMode: n.castingMode, + manaCost: n.manaCost, + bloodCost: n.bloodCost, + difficulty: { threshold: n.threshold, successes: n.successes }, + components: { verbal: n.verbal, somatic: n.somatic, material: n.material, symbols: n.symbols }, + castingTime: n.castingTime, + castingTimeRounds: time.rounds, + castingTimeMinutes: time.minutes, + range: n.range, + targets: n.targets, + duration: n.duration, + areaOfEffect: deriveAreaOfEffect(n.targets), + damageBase: n.damageBase, + damageType: n.damageType || 'magical', + ignoresArmor: n.ignoresArmor, + statusEffects: n.statusEffects, + saveAttribute: n.saveAttribute, + saveSkill: n.saveSkill, + requirements: { race: n.requiresRace, talent: n.requiresTalent, discipline: n.requiresDiscipline }, + isSuperspell: n.isSuperspell, + requiresGroupCast: n.requiresGroupCast, + minCasters: n.minCasters, + nonCombatOnly: n.nonCombatOnly, + overcastOptions: n.overcast.map((description) => ({ description, manaPerStep: 1 })), + triggers: n.triggers, + complexityLevel: n.complexityLevel, + }; +} function getAllFiles(dirPath: string, arrayOfFiles: string[] = []) { const files = readdirSync(dirPath); @@ -100,13 +150,12 @@ export const parseSpells: ParserFn = async (ctx: ParserContext): Promise ({ school, label: game.i18n.localize(`HBM.spellSchool.${school}`), - items: items.sort((a: any, b: any) => (a.system?.circle ?? 0) - (b.system?.circle ?? 0)), + items: items.sort((a: any, b: any) => a.name.localeCompare(b.name, 'pl')), })) .sort((a, b) => a.label.localeCompare(b.label, 'pl')); diff --git a/src/sheets/npc-sheet.ts b/src/sheets/npc-sheet.ts index a3a26f9..1bf7cc2 100644 --- a/src/sheets/npc-sheet.ts +++ b/src/sheets/npc-sheet.ts @@ -49,7 +49,7 @@ export class NpcSheet extends HandlebarsApplicationMixin(ActorSheetV2) { const ctx = (await super._prepareContext(options)) as Record; const actor = (this as unknown as { actor: { system: any; items: any[] } }).actor; const spells = actor.items.filter((it: any) => it.type === 'spell') - .sort((a: any, b: any) => (a.system?.circle ?? 0) - (b.system?.circle ?? 0) || a.name.localeCompare(b.name, 'pl')); + .sort((a: any, b: any) => a.name.localeCompare(b.name, 'pl')); const actorEffects = [...((actor as any).effects ?? [])].map((ef: any) => ({ id: ef.id, name: ef.name, diff --git a/templates/actor/character.hbs b/templates/actor/character.hbs index 4dd9577..bb266f8 100644 --- a/templates/actor/character.hbs +++ b/templates/actor/character.hbs @@ -275,14 +275,13 @@ title="Wymagana rasa">🔒{{/if}} - K{{spell.system.circle}} - · {{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}} + {{localize (concat 'HBM.spell.castingMode.' spell.system.castingMode)}} · {{localize 'HBM.resources.mana'}}: {{spell.system.manaCost}} {{#if (eq spell.system.castingMode 'sacred')}}{{#if spell.system.deity}} · {{spell.system.deity}}{{/if}}{{/if}} {{#if (eq spell.system.castingMode 'blood')}} · krew: {{spell.system.bloodCost}}{{/if}} {{#if spell.system.sourceBook}} · {{spell.system.sourceBook}}{{/if}} + class="source-pip">{{localize (concat 'HBM.sourceBook.' spell.system.sourceBook)}}{{/if}}