import 'dotenv/config'; import * as fs from 'fs'; import * as path from 'path'; import { PrismaClient } from '../src/generated/prisma/client.js'; import { PrismaPg } from '@prisma/adapter-pg'; const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); const prisma = new PrismaClient({ adapter }); interface RawFeat { name: string; trait: string; summary: string; actions: string; damage: string; trigger: string; url: string; } interface FeatLevelData { name: string; level: string; prerequisite: string; } // Known classes in Pathfinder 2e const CLASSES = [ 'Alchemist', 'Barbarian', 'Bard', 'Champion', 'Cleric', 'Druid', 'Fighter', 'Gunslinger', 'Inventor', 'Investigator', 'Kineticist', 'Magus', 'Monk', 'Oracle', 'Psychic', 'Ranger', 'Rogue', 'Sorcerer', 'Summoner', 'Swashbuckler', 'Thaumaturge', 'Witch', 'Wizard', // Archetype dedication markers 'Archetype', ]; // Known ancestries in Pathfinder 2e const ANCESTRIES = [ 'Android', 'Anadi', 'Aasimar', 'Aphorite', 'Automaton', 'Azarketi', 'Beastkin', 'Catfolk', 'Changeling', 'Conrasu', 'Dhampir', 'Duskwalker', 'Dwarf', 'Elf', 'Fetchling', 'Fleshwarp', 'Ganzi', 'Ghoran', 'Gnoll', 'Gnome', 'Goblin', 'Goloma', 'Grippli', 'Halfling', 'Hobgoblin', 'Human', 'Ifrit', 'Kashrishi', 'Kitsune', 'Kobold', 'Leshy', 'Lizardfolk', 'Nagaji', 'Orc', 'Oread', 'Poppet', 'Ratfolk', 'Reflection', 'Shisk', 'Shoony', 'Skeleton', 'Sprite', 'Strix', 'Suli', 'Sylph', 'Tanuki', 'Tengu', 'Tiefling', 'Undine', 'Vanara', 'Vishkanya', 'Wayang', // Heritage markers 'Half-Elf', 'Half-Orc', 'Versatile Heritage', ]; // Rarity levels const RARITIES = ['Common', 'Uncommon', 'Rare', 'Unique']; // Feat type markers const GENERAL_MARKERS = ['General']; const SKILL_MARKERS = ['Skill']; // Clean up prerequisites text function cleanPrerequisites(prereq: string): string | null { if (!prereq || prereq.trim() === '') return null; let cleaned = prereq.trim(); // Remove leading semicolons or commas cleaned = cleaned.replace(/^[;,\s]+/, ''); // Normalize some common patterns cleaned = cleaned.replace(/\s+/g, ' '); // normalize whitespace return cleaned || null; } function parseFeat(raw: RawFeat, levelData?: FeatLevelData) { const traits = raw.trait.split(',').map(t => t.trim()).filter(Boolean); let featType: string | null = null; let className: string | null = null; let ancestryName: string | null = null; let archetypeName: string | null = null; let skillName: string | null = null; let rarity: string = 'Common'; const actualTraits: string[] = []; for (const trait of traits) { // Check for rarity if (RARITIES.includes(trait)) { rarity = trait; continue; } // Check for class if (CLASSES.includes(trait)) { if (trait === 'Archetype') { featType = 'Archetype'; } else { className = trait; featType = 'Class'; } continue; } // Check for ancestry if (ANCESTRIES.includes(trait)) { ancestryName = trait; featType = 'Ancestry'; continue; } // Check for general/skill markers if (GENERAL_MARKERS.includes(trait)) { if (!featType) featType = 'General'; continue; } if (SKILL_MARKERS.includes(trait)) { featType = 'Skill'; continue; } // Check for lineage/heritage if (trait === 'Lineage' || trait === 'Heritage') { featType = 'Heritage'; continue; } // Everything else is an actual trait actualTraits.push(trait); } // Parse actions let actions: string | null = null; if (raw.actions) { const actionsLower = raw.actions.toLowerCase(); if (actionsLower.includes('free')) { actions = 'free'; } else if (actionsLower.includes('reaction')) { actions = 'reaction'; } else if (actionsLower.includes('1') || actionsLower === 'a') { actions = '1'; } else if (actionsLower.includes('2') || actionsLower === 'aa') { actions = '2'; } else if (actionsLower.includes('3') || actionsLower === 'aaa') { actions = '3'; } } // Get level and prerequisites from levelData if available const level = levelData ? parseInt(levelData.level, 10) : null; const prerequisites = levelData ? cleanPrerequisites(levelData.prerequisite) : null; return { name: raw.name, traits: actualTraits, summary: raw.summary || null, description: raw.trigger ? `Trigger: ${raw.trigger}` : null, actions, url: raw.url || null, featType, rarity, className, ancestryName, archetypeName, skillName, level, prerequisites, }; } async function seedFeats() { console.log('Starting feats seed...'); // Read feats JSON const featsPath = path.join(__dirname, 'data', 'feats.json'); const rawFeats: RawFeat[] = JSON.parse(fs.readFileSync(featsPath, 'utf-8')); console.log(`Found ${rawFeats.length} feats to import`); // Read feat levels JSON const levelsPath = path.join(__dirname, 'data', 'featlevels.json'); let levelDataMap = new Map(); if (fs.existsSync(levelsPath)) { const levelData: FeatLevelData[] = JSON.parse(fs.readFileSync(levelsPath, 'utf-8')); console.log(`Found ${levelData.length} feat level entries`); // Create lookup map by name (case-insensitive) levelData.forEach(ld => { levelDataMap.set(ld.name.toLowerCase(), ld); }); } else { console.log('Warning: featlevels.json not found, skipping level data'); } // Clear existing feats console.log('Clearing existing feats...'); await prisma.feat.deleteMany(); // Parse and prepare feats with level data let matchedCount = 0; const feats = rawFeats.map(raw => { const levelData = levelDataMap.get(raw.name.toLowerCase()); if (levelData) matchedCount++; return parseFeat(raw, levelData); }); console.log(`Matched ${matchedCount}/${rawFeats.length} feats with level data`); // Insert in batches to avoid memory issues const BATCH_SIZE = 500; let inserted = 0; for (let i = 0; i < feats.length; i += BATCH_SIZE) { const batch = feats.slice(i, i + BATCH_SIZE); // Use createMany with skipDuplicates for efficiency try { await prisma.feat.createMany({ data: batch, skipDuplicates: true, }); inserted += batch.length; console.log(`Inserted ${inserted}/${feats.length} feats...`); } catch (error) { // If batch fails, try one by one to identify problematic entries console.log(`Batch failed, inserting one by one...`); for (const feat of batch) { try { await prisma.feat.create({ data: feat }); inserted++; } catch (e) { console.warn(`Failed to insert feat: ${feat.name}`, e); } } } } // Count results const totalFeats = await prisma.feat.count(); const classFeatCount = await prisma.feat.count({ where: { featType: 'Class' } }); const ancestryFeatCount = await prisma.feat.count({ where: { featType: 'Ancestry' } }); const generalFeatCount = await prisma.feat.count({ where: { featType: 'General' } }); const skillFeatCount = await prisma.feat.count({ where: { featType: 'Skill' } }); const archetypeFeatCount = await prisma.feat.count({ where: { featType: 'Archetype' } }); const withLevel = await prisma.feat.count({ where: { level: { not: null } } }); const withPrereqs = await prisma.feat.count({ where: { prerequisites: { not: null } } }); console.log('\n=== Feats Import Complete ==='); console.log(`Total feats: ${totalFeats}`); console.log(`Class feats: ${classFeatCount}`); console.log(`Ancestry feats: ${ancestryFeatCount}`); console.log(`General feats: ${generalFeatCount}`); console.log(`Skill feats: ${skillFeatCount}`); console.log(`Archetype feats: ${archetypeFeatCount}`); console.log(`Feats with level: ${withLevel}`); console.log(`Feats with prerequisites: ${withPrereqs}`); } seedFeats() .catch((e) => { console.error('Error seeding feats:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });