feat: Complete character system, animated login, WebSocket sync
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>
This commit is contained in:
263
server/prisma/seed-feats.ts
Normal file
263
server/prisma/seed-feats.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user