const fs = require('fs'); const path = require('path'); const dbApi = require('../db.js'); const { parseMarkdownFile } = require('../lib/markdownTableParser.js'); const TABLES_DIR = path.join(__dirname, '..', 'tables'); const NPC_CORE_COLUMNS = [ 'first name', 'last name', 'brief description', 'wants and needs', 'secret or obstacle', 'also carrying', ]; const NPC_ATTRIBUTE_PATTERNS = [ [/\bpronoun/i, 'npc_pronouns'], [/\bage\b/i, 'npc_age'], [/\bintelligence\b/i, 'npc_intelligence'], [/^hair$/i, 'npc_hair'], [/^build$/i, 'npc_build'], [/^race$/i, 'npc_race'], ]; const LAYER1_LINKS = [ [/weather turns/i, 'Layer 2C: Weather Turns (d20)'], [/encounter a person/i, 'Layer 2A: Person Encountered (d20)'], [/encounter a group/i, 'Layer 2B: Group Encountered (d20)'], [/find something/i, 'Layer 2D: Find Something (d100)'], [/natural hazard/i, 'Layer 2E: Natural Hazard (d20)'], [/signs of recent violence/i, 'Layer 2F: Signs of Recent Violence (d20)'], [/pursuit or being followed/i, 'Layer 2G: Pursuit \/ Being Followed (d20)'], [/physical hardship/i, 'Layer 2H: Physical Hardship (d20)'], [/something uncanny/i, 'Layer 2I: Something Uncanny (d20)'], ]; function isNpcCoreTable(columns) { const norm = columns.map((c) => c.toLowerCase().trim()); return NPC_CORE_COLUMNS.every((req) => norm.includes(req)); } function detectNpcRole(headingText, columns, insideNpcSection) { if (isNpcCoreTable(columns)) return 'npc_core'; if (!insideNpcSection) return null; const trimmed = headingText.trim(); for (const [pattern, role] of NPC_ATTRIBUTE_PATTERNS) { if (pattern.test(trimmed)) return role; } return null; } const stats = { files: 0, nodes: 0, tables: 0, rows: 0, links: 0 }; function insertNode(node, sourceFile, parentId, sortOrder, insideNpcSection) { const treeNodeId = dbApi.tables.insertTreeNode({ source_file: sourceFile, parent_id: parentId, heading_text: node.headingText, heading_level: node.level, sort_order: sortOrder, }); stats.nodes++; const childInsideNpc = insideNpcSection || /random npc generator/i.test(node.headingText || ''); if (node.table) { const npcRole = detectNpcRole(node.headingText, node.table.columns, childInsideNpc); const tableId = dbApi.tables.insertTable({ tree_node_id: treeNodeId, dice_notation: node.table.diceNotation, table_type: node.table.tableType, npc_role: npcRole, }); stats.tables++; dbApi.tables.insertColumns(tableId, node.table.columns); const rowIds = dbApi.tables.insertRows(tableId, node.table.rows); stats.rows += rowIds.length; } node.children.forEach((child, idx) => insertNode(child, sourceFile, treeNodeId, idx, childInsideNpc)); } function importFile(filePath) { const sourceFile = path.basename(filePath); const roots = parseMarkdownFile(filePath); roots.forEach((node, idx) => insertNode(node, sourceFile, null, idx, false)); stats.files++; console.log(`Imported ${sourceFile}`); } function resolveLayer1Links() { const sourceFile = 'norse_encounter_tables.md'; const layer1Table = dbApi.tables.findTableByHeading(sourceFile, 'Layer 1: What Happens? (d20)'); if (!layer1Table) { console.warn('Could not find Layer 1 table for link resolution'); return; } const rows = dbApi.tables.getRowsForTable(layer1Table.id); for (const row of rows) { const text = row.cells[0] || ''; const match = LAYER1_LINKS.find(([pattern]) => pattern.test(text)); if (!match) continue; const [, targetHeading] = match; const targetTable = dbApi.tables.findTableByHeading(sourceFile, targetHeading); if (!targetTable) { console.warn(`Layer 1 link target not found: "${targetHeading}" (row: "${text}")`); continue; } dbApi.tables.insertLink({ table_id: layer1Table.id, row_id: row.id, target_table_id: targetTable.id }); stats.links++; } } const CULTURE_TABLE_LINKS = [ [/primitive/i, 'Primitive Occupation Table (d100)'], [/nomad/i, 'Nomad Occupation Table (d100)'], [/barbarian/i, 'Barbarian Occupation Table (d100)'], [/civilized/i, 'Civilized Occupation Table (d100)'], ]; const OCCUPATION_CRAFTER_LINKS = [ ['Barbarian Occupation Table (d100)', 'Barbarian Craft Table (d100)'], ['Civilized Occupation Table (d100)', 'Urban Crafts Table (d100)'], ]; function resolveCharacterTableLinks() { const sourceFile = 'rq3_character_tables.md'; // Culture Table → Occupation Tables const cultureTable = dbApi.tables.findTableByHeading(sourceFile, 'Culture Table (d8)'); if (cultureTable) { const rows = dbApi.tables.getRowsForTable(cultureTable.id); for (const row of rows) { const text = row.cells[0] || ''; const match = CULTURE_TABLE_LINKS.find(([pattern]) => pattern.test(text)); if (!match) continue; const [, targetHeading] = match; const target = dbApi.tables.findTableByHeading(sourceFile, targetHeading); if (!target) { console.warn(`Culture link target not found: "${targetHeading}"`); continue; } dbApi.tables.insertLink({ table_id: cultureTable.id, row_id: row.id, target_table_id: target.id }); stats.links++; } } else { console.warn('Could not find Culture Table for link resolution'); } // Occupation Tables → Craft Sub-tables (Crafter rows) for (const [occHeading, craftHeading] of OCCUPATION_CRAFTER_LINKS) { const occTable = dbApi.tables.findTableByHeading(sourceFile, occHeading); const craftTable = dbApi.tables.findTableByHeading(sourceFile, craftHeading); if (!occTable || !craftTable) continue; const rows = dbApi.tables.getRowsForTable(occTable.id); for (const row of rows) { if (/^crafter$/i.test((row.cells[0] || '').trim())) { dbApi.tables.insertLink({ table_id: occTable.id, row_id: row.id, target_table_id: craftTable.id }); stats.links++; } } } } function main() { dbApi.tables.clearImported(); const files = fs.readdirSync(TABLES_DIR).filter((f) => f.endsWith('.md')); for (const file of files) { importFile(path.join(TABLES_DIR, file)); } resolveLayer1Links(); resolveCharacterTableLinks(); console.log('---'); console.log(`Files: ${stats.files}`); console.log(`Tree nodes: ${stats.nodes}`); console.log(`Tables: ${stats.tables}`); console.log(`Rows: ${stats.rows}`); console.log(`Layer links: ${stats.links}`); } main();