feat: implement core Foundry VTT system framework, including actor/item data models, spellcasting logic, dice mechanics, and automation scripts
This commit is contained in:
@@ -223,7 +223,7 @@ for (const pack of byPack.keys()) {
|
||||
console.log(` ✓ compiled ${pack}`);
|
||||
}
|
||||
|
||||
console.log(`[build-packs] done — ${byPack.size} packs in packs/`);
|
||||
console.log(`[build-packs] done - ${byPack.size} packs in packs/`);
|
||||
|
||||
/** Convert a ParsedDoc into a Foundry document JSON object suitable for compilePack. */
|
||||
function toFoundryDoc(doc: ParsedDoc, folderId: string | null = null): Record<string, unknown> {
|
||||
|
||||
@@ -127,6 +127,7 @@ const nameTranslations: Record<string, string> = {
|
||||
"necromancy": "Necromancy",
|
||||
"blood": "Blood Magic",
|
||||
"wildwitch": "Wild Witch Magic",
|
||||
"take-breather": "Take a Breather",
|
||||
};
|
||||
|
||||
const folderNameTranslations: Record<string, string> = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* UUID reference linter — scans parsed docs (packs-src/*) for any
|
||||
* UUID reference linter - scans parsed docs (packs-src/*) for any
|
||||
* `@UUID[...]` references that don't resolve within the generated set.
|
||||
*
|
||||
* Run after build-packs.ts: `bun scripts/lint-uuid-refs.ts`
|
||||
@@ -37,14 +37,14 @@ function walk(dir: string): void {
|
||||
walk(packsSrcDir);
|
||||
|
||||
const broken = refs.filter((r) => {
|
||||
// UUID format: Compendium.system.pack.Item.<id> — only validate Item refs.
|
||||
// UUID format: Compendium.system.pack.Item.<id> - only validate Item refs.
|
||||
const m = r.uuid.match(/^Compendium\.[^.]+\.[^.]+\.Item\.(.+)$/);
|
||||
if (!m) return false;
|
||||
return !allIds.has(m[1]);
|
||||
});
|
||||
|
||||
if (broken.length === 0) {
|
||||
console.log(`[lint-uuid-refs] OK — ${refs.length} refs scanned, none broken`);
|
||||
console.log(`[lint-uuid-refs] OK - ${refs.length} refs scanned, none broken`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// HbM Apply Condition — pick a status effect and toggle it on targeted/selected tokens.
|
||||
// HbM Apply Condition - pick a status effect and toggle it on targeted/selected tokens.
|
||||
const targets = game.user.targets.size > 0 ? Array.from(game.user.targets) : canvas.tokens.controlled;
|
||||
if (targets.length === 0) return ui.notifications.warn('Zaznacz lub naceluj token(y).');
|
||||
const effects = CONFIG.statusEffects.filter((e) => e.id?.startsWith('hbm.'));
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// HbM Attribute Check — pick attribute on selected token's actor, roll d6 pool.
|
||||
// HbM Attribute Check - pick attribute on selected token's actor, roll d6 pool.
|
||||
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||
const attrs = ['body', 'mind', 'soul', 'magic'];
|
||||
const labels = { body: 'Ciało', mind: 'Umysł', soul: 'Dusza', magic: 'Magia' };
|
||||
const opts = attrs.map((a) => `<option value="${a}">${labels[a]} (${actor.system.attributes[a]?.value ?? 0})</option>`).join('');
|
||||
const result = await Dialog.prompt({
|
||||
title: `Test atrybutu — ${actor.name}`,
|
||||
title: `Test atrybutu - ${actor.name}`,
|
||||
content: `
|
||||
<form>
|
||||
<div class="form-group">
|
||||
@@ -27,4 +27,4 @@ const result = await Dialog.prompt({
|
||||
if (!result) return;
|
||||
const pool = actor.system.attributes[result.attr]?.value ?? 1;
|
||||
const roll = await new Roll(`${pool}d6cs>=${result.threshold}`).evaluate();
|
||||
await roll.toMessage({ flavor: `${actor.name} — test ${labels[result.attr]} (TS ${result.threshold})`, speaker: ChatMessage.getSpeaker({ actor }) });
|
||||
await roll.toMessage({ flavor: `${actor.name} - test ${labels[result.attr]} (TS ${result.threshold})`, speaker: ChatMessage.getSpeaker({ actor }) });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// HbM Group Cast Helper — sum mana of controlled token actors (potential co-casters).
|
||||
// HbM Group Cast Helper - sum mana of controlled token actors (potential co-casters).
|
||||
const tokens = canvas.tokens.controlled;
|
||||
if (tokens.length < 2) return ui.notifications.warn('Zaznacz co najmniej 2 tokeny współrzucających.');
|
||||
const lines = [];
|
||||
@@ -11,7 +11,7 @@ for (const t of tokens) {
|
||||
const magic = a.system.attributes?.magic?.value ?? 0;
|
||||
totalMana += mana;
|
||||
totalPool += magic;
|
||||
lines.push(`<li><strong>${a.name}</strong> — Mana ${mana}, Magia ${magic}</li>`);
|
||||
lines.push(`<li><strong>${a.name}</strong> - Mana ${mana}, Magia ${magic}</li>`);
|
||||
}
|
||||
const html = `
|
||||
<div class="hbm-group-cast">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// HbM Long Rest — invoke game.hbm.rest(actor, 'long') on selected token's actor.
|
||||
// HbM Long Rest - invoke game.hbm.rest(actor, 'long') on selected token's actor.
|
||||
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest — system niezainicjowany.');
|
||||
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest - system niezainicjowany.');
|
||||
const result = await game.hbm.rest(actor, 'long');
|
||||
ui.notifications.info(`${actor.name}: długi odpoczynek — pełna regeneracja zasobów.`);
|
||||
ui.notifications.info(`${actor.name}: długi odpoczynek - pełna regeneracja zasobów.`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// HbM Pool Roll — prompts for pool size and threshold, then rolls a d6 pool.
|
||||
// HbM Pool Roll - prompts for pool size and threshold, then rolls a d6 pool.
|
||||
const result = await Dialog.prompt({
|
||||
title: 'Rzut puli d6',
|
||||
content: `
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// HbM Refill Zeal — bumps zeal by 1 on selected token's actor (debug helper).
|
||||
// HbM Refill Zeal - bumps zeal by 1 on selected token's actor (debug helper).
|
||||
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||
const cur = actor.system.attributes?.zeal?.value ?? 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// HbM Short Rest — invoke game.hbm.rest(actor, 'short') on selected token's actor.
|
||||
// HbM Short Rest - invoke game.hbm.rest(actor, 'short') on selected token's actor.
|
||||
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest — system niezainicjowany.');
|
||||
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest - system niezainicjowany.');
|
||||
const result = await game.hbm.rest(actor, 'short');
|
||||
ui.notifications.info(`${actor.name}: krótki odpoczynek — przywrócono ${result?.healed ?? 0} PW.`);
|
||||
ui.notifications.info(`${actor.name}: krótki odpoczynek - przywrócono ${result?.hpRestored ?? 0} PW.`);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// HbM Take a Breather - invoke game.hbm.rest(actor, 'breather') on selected token's actor.
|
||||
const actor = canvas.tokens.controlled[0]?.actor ?? game.user.character;
|
||||
if (!actor) return ui.notifications.warn('Wybierz token postaci.');
|
||||
if (!game.hbm?.rest) return ui.notifications.error('Brak game.hbm.rest - system niezainicjowany.');
|
||||
const result = await game.hbm.rest(actor, 'breather');
|
||||
ui.notifications.info(`${actor.name}: chwila wytchnienia - przywrócono ${result?.hpRestored ?? 0} PW i ${result?.manaRestored ?? 0} Many.`);
|
||||
@@ -180,5 +180,6 @@
|
||||
"echo-zmierzchu": "twilight-echo",
|
||||
"requiem": "requiem",
|
||||
"przywolanie-istoty-z-otchlani": "summon-eldritch-entity",
|
||||
"nic-przeznaczenia": "thread-of-destiny"
|
||||
}
|
||||
"nic-przeznaczenia": "thread-of-destiny",
|
||||
"naturalny-lot": "natural-flight"
|
||||
}
|
||||
@@ -3,20 +3,20 @@
|
||||
"schools": {
|
||||
"magia ogólna": "general",
|
||||
"magia żywiołów": "elements",
|
||||
"magia żywiołów — powietrze": "elementsAir",
|
||||
"magia żywiołów - powietrze": "elementsAir",
|
||||
"magia żywiołów (powietrze)": "elementsAir",
|
||||
"magia powietrza": "elementsAir",
|
||||
"magia żywiołów — woda": "elementsWater",
|
||||
"magia żywiołów - woda": "elementsWater",
|
||||
"magia żywiołów (woda)": "elementsWater",
|
||||
"magia wody": "elementsWater",
|
||||
"magia żywiołów — ogień": "elementsFire",
|
||||
"magia żywiołów - ogień": "elementsFire",
|
||||
"magia żywiołów (ogień)": "elementsFire",
|
||||
"magia ognia": "elementsFire",
|
||||
"magia żywiołów — ziemia": "elementsEarth",
|
||||
"magia żywiołów - ziemia": "elementsEarth",
|
||||
"magia żywiołów (ziemia)": "elementsEarth",
|
||||
"magia ziemi": "elementsEarth",
|
||||
"magia sakralna": "sacred",
|
||||
"magia sakralna — egzorcyzmy": "sacredExorcism",
|
||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||
"egzorcyzmy": "sacredExorcism",
|
||||
"wiedźmia magia": "witch",
|
||||
@@ -35,8 +35,8 @@
|
||||
"nekromancja": "necromancy",
|
||||
"magia krwi": "blood",
|
||||
"magia szkarłatu": "crimson",
|
||||
"magia otchłani — magia aspektów": "eldritchAspects",
|
||||
"magia otchłani — pierwotna magia": "eldritchPrimal",
|
||||
"magia otchłani - magia aspektów": "eldritchAspects",
|
||||
"magia otchłani - pierwotna magia": "eldritchPrimal",
|
||||
"dzika wiedźmia magia": "wildWitch"
|
||||
},
|
||||
"disciplines": {
|
||||
@@ -46,13 +46,13 @@
|
||||
"warzenie eliksirów": "alchemyBrewing",
|
||||
"alchemia - warzenie eliksirów": "alchemyBrewing",
|
||||
"botanika": "botany",
|
||||
"magia żywiołów — powietrze": "elementsAir",
|
||||
"magia żywiołów - powietrze": "elementsAir",
|
||||
"magia żywiołów (powietrze)": "elementsAir",
|
||||
"magia żywiołów — woda": "elementsWater",
|
||||
"magia żywiołów - woda": "elementsWater",
|
||||
"magia żywiołów (woda)": "elementsWater",
|
||||
"magia żywiołów — ogień": "elementsFire",
|
||||
"magia żywiołów - ogień": "elementsFire",
|
||||
"magia żywiołów (ogień)": "elementsFire",
|
||||
"magia żywiołów — ziemia": "elementsEarth",
|
||||
"magia żywiołów - ziemia": "elementsEarth",
|
||||
"magia żywiołów (ziemia)": "elementsEarth",
|
||||
"rzemiosło artefaktów": "artifice",
|
||||
"golemancja": "golemancy",
|
||||
@@ -61,7 +61,7 @@
|
||||
"źródło mocy": "manaSourceMage",
|
||||
"magia iluzji": "illusion",
|
||||
"magia sakralna": "sacred",
|
||||
"magia sakralna — egzorcyzmy": "sacredExorcism",
|
||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||
"magia sakralna - egzorcyzmy": "sacredExorcism",
|
||||
"egzorcyzmy": "sacredExorcism",
|
||||
"wiedźmia magia": "witch",
|
||||
@@ -126,4 +126,4 @@
|
||||
"zdolności magiczne": "magicalAbilities",
|
||||
"oddanie": "devotion"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
export interface BookBlock {
|
||||
/** The line directly preceding the bullet block — usually the item name. */
|
||||
/** The line directly preceding the bullet block - usually the item name. */
|
||||
name: string;
|
||||
/** Bullet keys/values, preserved in order. */
|
||||
fields: Array<{ key: string; value: string }>;
|
||||
@@ -45,7 +45,7 @@ const SEPARATOR = /^_{3,}$/;
|
||||
const BULLET = /^\*\s+([^:]+):\s*(.+)$/;
|
||||
// Match any markdown heading level (1-6 `#` chars).
|
||||
const HEADING = /^#{1,6}\s+(.+)$/;
|
||||
// Chapter headings are the two highest heading levels we encounter — heuristic:
|
||||
// Chapter headings are the two highest heading levels we encounter - heuristic:
|
||||
// treat ## headings as "chapter" and ### headings as "section".
|
||||
const CHAPTER_HEADING = /^#{1,2}\s+(.+)$/;
|
||||
const SECTION_HEADING = /^#{3,4}\s+(.+)$/;
|
||||
@@ -65,7 +65,7 @@ function skipFrontmatter(lines: string[]): number {
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
if (FRONTMATTER_FENCE.test(lines[i].trim())) return i + 1;
|
||||
}
|
||||
return 0; // malformed — start from 0
|
||||
return 0; // malformed - start from 0
|
||||
}
|
||||
|
||||
export interface WalkOptions {
|
||||
@@ -92,7 +92,7 @@ export function walkBook(path: string, opts: WalkOptions = {}): BookBlock[] {
|
||||
for (let i = startIdx; i < lines.length; i++) {
|
||||
const rawLine = lines[i].trimEnd();
|
||||
|
||||
// Skip Obsidian TOC bullets — they look like `- [[#Section|Label]]`.
|
||||
// Skip Obsidian TOC bullets - they look like `- [[#Section|Label]]`.
|
||||
if (OBSIDIAN_TOC_BULLET.test(rawLine.trim())) continue;
|
||||
|
||||
// Chapter headings: ## or # level (the two highest we honour).
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* with name, source book, and an empty description (filled in later).
|
||||
*
|
||||
* Many disciplines also have a "passive ability" mentioned right after the
|
||||
* discipline header in Podręcznik Gry — we capture the next non-empty line
|
||||
* discipline header in Podręcznik Gry - we capture the next non-empty line
|
||||
* if it looks like a short ability description.
|
||||
*/
|
||||
|
||||
@@ -34,13 +34,13 @@ const DISCIPLINE_SEEDS: DisciplineSeed[] = [
|
||||
{ id: 'manaSourceMage', polishName: 'Źródło Mocy', pack: 'disciplines', book: 'podrecznik-gry' },
|
||||
{ id: 'illusion', polishName: 'Magia Iluzji', pack: 'disciplines', book: 'ksiega-magii' },
|
||||
{ id: 'sacred', polishName: 'Magia Sakralna', pack: 'disciplines', book: 'ksiega-magii' },
|
||||
{ id: 'sacredExorcism', polishName: 'Magia Sakralna — Egzorcyzmy', pack: 'disciplines', book: 'ksiega-magii' },
|
||||
{ id: 'sacredExorcism', polishName: 'Magia Sakralna - Egzorcyzmy', pack: 'disciplines', book: 'ksiega-magii' },
|
||||
{ id: 'witch', polishName: 'Wiedźmia Magia', pack: 'disciplines', book: 'ksiega-magii' },
|
||||
{ id: 'necromancy', polishName: 'Nekromancja', pack: 'disciplines', book: 'ksiega-magii' },
|
||||
{ id: 'blood', polishName: 'Magia Krwi', pack: 'disciplines-forbidden', book: 'arcanum-sanguinis' },
|
||||
{ id: 'crimson', polishName: 'Magia Szkarłatu', pack: 'disciplines-forbidden', book: 'crimson-cult' },
|
||||
{ id: 'abyssAspects', polishName: 'Magia Otchłani — Magia Aspektów', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
||||
{ id: 'abyssPrimal', polishName: 'Magia Otchłani — Pierwotna Magia', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
||||
{ id: 'abyssAspects', polishName: 'Magia Otchłani - Magia Aspektów', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
||||
{ id: 'abyssPrimal', polishName: 'Magia Otchłani - Pierwotna Magia', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
||||
{ id: 'wildWitch', polishName: 'Dzika Wiedźmia Magia', pack: 'disciplines-forbidden', book: 'klatwa-otchlani' },
|
||||
];
|
||||
|
||||
@@ -81,7 +81,7 @@ function lookupDescription(repoRoot: string, polishName: string): string {
|
||||
resolve(repoRoot, 'ObsidianNotes/rules/02. Klątwa Otchłani.md'),
|
||||
resolve(repoRoot, 'ObsidianNotes/rules/00. Podręcznik Gry.md'),
|
||||
];
|
||||
const target = normalizeKey(polishName.split('—')[0]);
|
||||
const target = normalizeKey(polishName.split('-')[0]);
|
||||
|
||||
for (const file of splitSources) {
|
||||
let text = '';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Macro parser. Reads hand-coded macro source files from `scripts/macros/*.js`
|
||||
* and emits ParsedDoc[] for the build pipeline. Macros are not parsed from
|
||||
* books — they're a curated set of GM/player utilities.
|
||||
* books - they're a curated set of GM/player utilities.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
@@ -23,6 +23,7 @@ const MACROS: MacroDef[] = [
|
||||
{ id: 'pool-roll', name: 'Rzut Puli d6', file: 'pool-roll.js', img: 'icons/svg/d20.svg' },
|
||||
{ id: 'attribute-check', name: 'Test Atrybutu', file: 'attribute-check.js', img: 'icons/svg/dice-target.svg' },
|
||||
{ id: 'apply-condition', name: 'Nałóż Przypadłość', file: 'apply-condition.js', img: 'icons/svg/aura.svg' },
|
||||
{ id: 'take-breather', name: 'Chwila Wytchnienia', file: 'take-breather.js', img: 'icons/svg/clockwork.svg' },
|
||||
{ id: 'short-rest', name: 'Krótki Odpoczynek', file: 'short-rest.js', img: 'icons/svg/regen.svg' },
|
||||
{ id: 'long-rest', name: 'Długi Odpoczynek', file: 'long-rest.js', img: 'icons/svg/sun.svg' },
|
||||
{ id: 'group-cast-helper', name: 'Pomocnik Rzucania Grupowego', file: 'group-cast-helper.js', img: 'icons/svg/upgrade.svg' },
|
||||
|
||||
@@ -5,7 +5,7 @@ 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. */
|
||||
/** 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(),
|
||||
@@ -101,7 +101,7 @@ export const parseSpells: ParserFn = async (ctx: ParserContext): Promise<ParsedD
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -113,7 +113,7 @@ export const parseSpells: ParserFn = async (ctx: ParserContext): Promise<ParsedD
|
||||
}
|
||||
|
||||
const baseId = basename(file, '.md');
|
||||
|
||||
|
||||
let pack = 'spells-academic';
|
||||
const relativePath = file.substring(spellsDir.length).replace(/\\/g, '/');
|
||||
if (relativePath.includes('/general/')) pack = 'spells-general';
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* - Podręcznik Gry, "Rozdział IV - Talenty"
|
||||
* - Bestiariusz, "Rozdział III - Talenty" (NPC-only talents)
|
||||
*
|
||||
* The walker can't help us here — we segment by `________________`
|
||||
* The walker can't help us here - we segment by `________________`
|
||||
* separators within the talent chapter and look for a "Wymagania:" line.
|
||||
*/
|
||||
|
||||
@@ -73,7 +73,7 @@ const TALENT_SOURCES: TalentSource[] = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Talent chapters are noisy — split into segments by separator lines, then
|
||||
* Talent chapters are noisy - split into segments by separator lines, then
|
||||
* within each segment find consecutive talent blocks. A talent block is:
|
||||
* - first non-empty line = name (short, no trailing punctuation, no colon)
|
||||
* - optional `Wymagania: ...` line
|
||||
@@ -109,7 +109,7 @@ export const parseTalents: ParserFn = async (ctx: ParserContext): Promise<Parsed
|
||||
if (i >= slice.length) break;
|
||||
|
||||
const rawLine = slice[i].trim();
|
||||
// Stop heuristic — bail out of obvious chapter changes.
|
||||
// Stop heuristic - bail out of obvious chapter changes.
|
||||
if (/^Rozdział\b/.test(rawLine) || /^Aneks\b/.test(rawLine) || /^#{1,6}\s+(?:Rozdział|Aneks)\b/i.test(rawLine)) break;
|
||||
|
||||
let isName = false;
|
||||
@@ -127,7 +127,7 @@ export const parseTalents: ParserFn = async (ctx: ParserContext): Promise<Parsed
|
||||
}
|
||||
|
||||
if (!isName) {
|
||||
// Can't read this — skip the line and continue.
|
||||
// Can't read this - skip the line and continue.
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
@@ -205,9 +205,104 @@ export const parseTalents: ParserFn = async (ctx: ParserContext): Promise<Parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Parse discipline-specific talents from Wybór Dziedziny Magii
|
||||
docs.push(...(await parseDisciplineTalents(ctx)));
|
||||
|
||||
return docs;
|
||||
};
|
||||
|
||||
async function parseDisciplineTalents(ctx: ParserContext): Promise<ParsedDoc[]> {
|
||||
const docs: ParsedDoc[] = [];
|
||||
const path = resolve(ctx.repoRoot, 'ObsidianNotes/rules/00. Podr\u0119cznik Gry.md');
|
||||
let lines: string[] = [];
|
||||
try {
|
||||
lines = readFileSync(path, 'utf8').split(/\r?\n/);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const startIdx = lines.findIndex((l) => /^\s*Wyb[o\u00f3\u0143A3\ufffd]r Dziedziny Magii\s*$/i.test(l));
|
||||
if (startIdx < 0) return [];
|
||||
const endIdx = lines.findIndex((l, i) => i > startIdx && /^\s*Umiej[e\u0119\u0118\ufffd]tno[s\u015b\u015a\ufffd]ci i Talenty\s*$/i.test(l));
|
||||
const slice = lines.slice(startIdx + 1, endIdx > 0 ? endIdx : lines.length);
|
||||
|
||||
const talentNames = [
|
||||
'Podejrzliwo\u015b\u0107 Wobec Zmian',
|
||||
'Nadzwyczajna Odporno\u015b\u0107',
|
||||
'Magiczna Flora',
|
||||
'Mi\u0142osierdzie',
|
||||
'B\u0142ogos\u0142awieni Egzorcy\u015bci',
|
||||
'Jedno\u015b\u0107 z \u017bywio\u0142em',
|
||||
'Tajemna Technologia',
|
||||
'Pierwotna Energia',
|
||||
'Niezwyk\u0142a Intuicja',
|
||||
'Arytmetyczna Koncentracja'
|
||||
];
|
||||
|
||||
const cleanNames = talentNames.map(n => slugify(n));
|
||||
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
const rawLine = slice[i].trim();
|
||||
if (!rawLine) continue;
|
||||
|
||||
const slug = slugify(rawLine);
|
||||
const talentIndex = cleanNames.indexOf(slug);
|
||||
|
||||
if (talentIndex !== -1) {
|
||||
const name = talentNames[talentIndex];
|
||||
const descLines: string[] = [];
|
||||
let j = i + 1;
|
||||
while (j < slice.length) {
|
||||
const nextLine = slice[j].trim();
|
||||
if (SEPARATOR.test(nextLine) || /^Rozdzia/i.test(nextLine) || /^___/.test(nextLine)) {
|
||||
break;
|
||||
}
|
||||
const nextSlug = slugify(nextLine);
|
||||
if (cleanNames.includes(nextSlug)) {
|
||||
break;
|
||||
}
|
||||
// Stop if we hit a main section like Alchemia, Botanika, Magia Sakralna etc.
|
||||
const disciplineHeaders = [
|
||||
'Alchemia', 'Transmutacja', 'Warzenie Eliksirów', 'Botanika',
|
||||
'Magia Sakralna', 'Egzorcyzmy', 'Magia Żywiołów', 'Rzemiosło Artefaktów',
|
||||
'Źródło Mocy', 'Magia Iluzji', 'Wiedźmia Magia'
|
||||
];
|
||||
if (disciplineHeaders.some(h => slugify(h) === nextSlug)) {
|
||||
break;
|
||||
}
|
||||
|
||||
descLines.push(slice[j]);
|
||||
j++;
|
||||
}
|
||||
const description = descLines.join('\n').trim();
|
||||
const baseSlug = slugify(name);
|
||||
const id = ctx.idOverrides[baseSlug] ?? baseSlug;
|
||||
|
||||
docs.push({
|
||||
id,
|
||||
name,
|
||||
documentType: 'Item',
|
||||
subType: 'talent',
|
||||
pack: 'talents',
|
||||
source: { book: 'podrecznik-gry', chapter: 'Wyb\u00f3r Dziedziny Magii', line: startIdx + i + 2 },
|
||||
system: {
|
||||
requirements: { race: '', attribute: '', skill: '', talent: '', title: '', discipline: '' },
|
||||
multiSelect: false,
|
||||
cost: '',
|
||||
damageReductionBonus: 0,
|
||||
description,
|
||||
effect: '',
|
||||
},
|
||||
description,
|
||||
});
|
||||
|
||||
i = j - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return docs;
|
||||
}
|
||||
|
||||
/** A talent name is short, doesn't end with `.`, no `:`, not a bullet, no digits-only. */
|
||||
function isPlausibleName(line: string): boolean {
|
||||
if (!line || line.length > 80) return false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Shared parser types — what each parser emits before being handed off
|
||||
* Shared parser types - what each parser emits before being handed off
|
||||
* to the pack builder.
|
||||
*
|
||||
* Every parsed document has a stable id (slug) and a sourceBook ref so
|
||||
@@ -21,7 +21,7 @@ export interface ParsedDoc {
|
||||
id: string;
|
||||
/** Display name (Polish, as in book). */
|
||||
name: string;
|
||||
/** Foundry document type — `Item` or `Actor`. */
|
||||
/** Foundry document type - `Item` or `Actor`. */
|
||||
documentType: 'Item' | 'Actor';
|
||||
/** Foundry sub-type for the data model (e.g. `spell`, `talent`). */
|
||||
subType: string;
|
||||
|
||||
Reference in New Issue
Block a user