Files
hbm-foundry-system/scripts/parsers/gear-parser.ts
T

193 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { resolve, join, extname, basename } from 'node:path';
import type { ParsedDoc, ParserContext, ParserFn } from './types';
import { slugify, parseList } from './helpers';
export const parseGear: ParserFn = async (ctx: ParserContext): Promise<ParsedDoc[]> => {
const docs: ParsedDoc[] = [];
const itemsDir = resolve(ctx.repoRoot, 'ObsidianNotes', 'items');
if (!existsSync(itemsDir)) {
console.warn(`[gear-parser] Directory not found: ${itemsDir}`);
return docs;
}
const subdirs = [
{ name: 'weapons', pack: 'items-weapons', category: 'weapon' },
{ name: 'armor', pack: 'items-armor', category: 'armor' },
{ name: 'gear', pack: 'items-gear', category: 'equipment' },
];
for (const dir of subdirs) {
const fullDirPath = resolve(itemsDir, dir.name);
if (!existsSync(fullDirPath)) continue;
const files = readdirSync(fullDirPath).filter((f) => extname(f) === '.md');
for (const file of files) {
const filePath = join(fullDirPath, file);
const raw = readFileSync(filePath, 'utf8');
const { frontmatter, content } = extractMarkdown(raw);
const name = basename(file, '.md').replace(/_/g, ' ');
const doc = buildGearDoc(name, frontmatter, content, dir.category, dir.pack);
docs.push(doc);
}
}
return docs;
};
type WoundKind = 'slashing' | 'piercing' | 'blunt';
/**
* Vault `wound` frontmatter → woundKinds. Accepts one value, a comma list ("piercing, slashing"),
* or a YAML list (flattened to a comma list by extractMarkdown). Unknown values are dropped,
* absent stays empty – never guessed.
*/
export function parseWoundKinds(raw: string | undefined): WoundKind[] {
const map: Record<string, WoundKind> = {
slashing: 'slashing', sieczne: 'slashing', sieczna: 'slashing',
piercing: 'piercing', kłute: 'piercing', kłuta: 'piercing', kluta: 'piercing', kłujące: 'piercing',
blunt: 'blunt', obuchowe: 'blunt', obuchowa: 'blunt',
};
const out: WoundKind[] = [];
for (const part of (raw ?? '').split(',')) {
const kind = map[part.trim().toLowerCase()];
if (kind && !out.includes(kind)) out.push(kind);
}
return out;
}
function extractMarkdown(raw: string): { frontmatter: Record<string, string>; content: string } {
const fm: Record<string, string> = {};
let content = raw;
if (raw.startsWith('---')) {
const endIdx = raw.indexOf('---', 3);
if (endIdx > -1) {
const fmText = raw.slice(3, endIdx).trim();
content = raw.slice(endIdx + 3).trim();
const lines = fmText.split(/\r?\n/);
let listKey = '';
for (const line of lines) {
// "key:" followed by " - item" lines → comma-joined value.
const item = line.match(/^\s+-\s+(.*)$/);
if (item && listKey) {
fm[listKey] = fm[listKey] ? `${fm[listKey]}, ${item[1].trim()}` : item[1].trim();
continue;
}
const colonIdx = line.indexOf(':');
listKey = colonIdx > -1 && !line.slice(colonIdx + 1).trim() ? line.slice(0, colonIdx).trim() : '';
if (colonIdx > -1) {
const key = line.slice(0, colonIdx).trim();
let val = line.slice(colonIdx + 1).trim();
if (val.startsWith("'") && val.endsWith("'")) val = val.slice(1, -1);
if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
fm[key] = val;
}
}
}
}
return { frontmatter: fm, content };
}
function buildGearDoc(name: string, fm: Record<string, string>, content: string, category: string, pack: string): ParsedDoc {
// Common properties
const priceStr = fm.price || fm.cena || '0';
const priceMatch = priceStr.match(/\d+/);
const value = priceMatch ? parseInt(priceMatch[0], 10) : 0;
const description = content
.split('\n')
.filter(l => !l.startsWith('# ') && !l.startsWith('* **Cena:**') && !l.startsWith('* **Obrażenia:**') && !l.startsWith('* **Cechy:**') && !l.startsWith('* **Pancerz:**'))
.join('\n')
.trim();
// HTML format description by wrapping paragraphs if needed
const htmlDescription = description ? `<p>${description.replace(/\n\n/g, '</p><p>')}</p>` : '';
const system: any = {
category,
rarity: 'common',
quantity: 1,
weight: 0,
value,
equipped: false,
damageReductionBonus: 0,
description: htmlDescription,
weapon: {
damage: '',
damageType: 'physical',
woundKinds: [],
properties: [],
},
armor: {
damageReduction: 0,
condition: 1,
conditionMax: 1,
armorType: 'light',
stealthDisadvantage: false,
strengthRequirement: 0,
}
};
if (category === 'weapon') {
system.weapon.damage = fm.damage || fm.obrazenia || '';
system.weapon.woundKinds = parseWoundKinds(fm.wound || fm.rana);
if (fm.traits || fm.cechy) {
system.weapon.properties = parseList(fm.traits || fm.cechy);
}
} else if (category === 'armor') {
const isShield = name.toLowerCase().includes('tarcza') || name.toLowerCase().includes('shield');
const drStr = fm.armorClass || fm.pancerz || fm.armor_points || (isShield ? '1' : '0');
const drMatch = drStr.match(/\d+/);
const dr = drMatch ? parseInt(drMatch[0], 10) : 0;
system.armor.damageReduction = dr;
const conditionStr = fm.wytrzymalosc || fm.condition || fm.durability;
let cond = dr;
if (conditionStr) {
const condMatch = conditionStr.match(/\d+/);
if (condMatch) {
cond = parseInt(condMatch[0], 10);
}
}
if (cond < 1) cond = 1;
system.armor.condition = cond;
system.armor.conditionMax = cond;
if (isShield) {
system.armor.armorType = 'shield';
} else if (dr === 3) {
system.armor.armorType = 'heavy';
} else if (dr === 2) {
system.armor.armorType = 'medium';
} else {
system.armor.armorType = 'light';
}
const traitsStr = fm.traits || fm.cechy || '';
if (traitsStr.toLowerCase().includes('hałaśliwy') || traitsStr.toLowerCase().includes('noisy')) {
system.armor.stealthDisadvantage = true;
}
const reqStr = fm.requirements || fm.wymagania || '';
const reqMatch = reqStr.match(/(?:ciało|body)\s*>=\s*(\d+)/i);
if (reqMatch) {
system.armor.strengthRequirement = parseInt(reqMatch[1], 10);
}
}
return {
id: slugify(`gear-${name}`),
name,
documentType: 'Item',
subType: 'gear',
pack,
source: { book: 'core-rules' },
system,
};
}