Files
Dimension-47/server/prisma/seed-feats.ts
Alexander Zielonka 55419d3896 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>
2026-01-19 15:36:29 +01:00

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();
});