Character System: - Inventory system with 5,482 equipment items - Feats tab with categories and details - Actions tab with 99 PF2e actions - Item detail modal with equipment info - Feat detail modal with descriptions - Edit character modal with image cropping Auth & UI: - Animated login screen with splash → form transition - Letter-by-letter "DIMENSION 47" animation - Starfield background with floating orbs - Logo tap glow effect - "Remember me" functionality (localStorage/sessionStorage) Real-time Sync: - WebSocket gateway for character updates - Live sync for HP, conditions, inventory, equipment status, money, level Database: - Added credits field to characters - Added custom fields for items - Added feat fields and relations - Included full database backup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
264 lines
8.0 KiB
TypeScript
264 lines
8.0 KiB
TypeScript
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<string, FeatLevelData>();
|
|
|
|
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();
|
|
});
|