import 'dotenv/config'; import bcrypt from 'bcryptjs'; import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import db from '../db/index.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const dataDir = join(__dirname, '..', 'data'); const ghosts = JSON.parse(readFileSync(join(dataDir, 'ghosts.json'), 'utf8')); const abilities = JSON.parse(readFileSync(join(dataDir, 'abilities.json'), 'utf8')); const sets = JSON.parse(readFileSync(join(dataDir, 'sets.json'), 'utf8')); // Some source ghosts have "-" (null) health/damage. Derive a sensible value from // the average of same type+rarity peers so AR combat always has valid numbers. function deriveStats(list) { const buckets = {}; for (const g of list) { if (g.health == null || g.damage == null) continue; const k = `${g.type}-${g.rarity}`; (buckets[k] ??= { h: [], d: [] }); buckets[k].h.push(g.health); buckets[k].d.push(g.damage); } const avg = (a) => (a.length ? Math.round(a.reduce((x, y) => x + y, 0) / a.length) : null); const fallback = { h: 338, d: 192 }; // global-ish midpoint if a whole bucket is empty for (const g of list) { const k = `${g.type}-${g.rarity}`; const b = buckets[k]; if (g.health == null) g.health = (b && avg(b.h)) ?? fallback.h; if (g.damage == null) g.damage = (b && avg(b.d)) ?? fallback.d; } return list; } deriveStats(ghosts); const insertAbility = db.prepare(` INSERT INTO abilities (name, kind, charges, cooldown, effect) VALUES (@name, @kind, @charges, @cooldown, @effect) ON CONFLICT(name) DO UPDATE SET kind=excluded.kind, charges=excluded.charges, cooldown=excluded.cooldown, effect=excluded.effect `); const insertGhost = db.prepare(` INSERT INTO ghosts (name, display_name, type, rarity, speed, range, charge_shot, health, damage, ability, is_boss, set_number, set_name, enabled) VALUES (@name, @name, @type, @rarity, @speed, @range, @chargeShot, @health, @damage, @ability, @isBoss, @setNumber, @setName, 1) `); const seed = db.transaction(() => { // Only seed ghosts/abilities/sets if the ghosts table is empty, // so re-running seed never clobbers admin edits. const count = db.prepare('SELECT COUNT(*) AS n FROM ghosts').get().n; if (count === 0) { for (const a of abilities) { insertAbility.run({ name: a.name, kind: a.kind ?? 'common', charges: a.charges ?? null, cooldown: a.cooldown ?? null, effect: a.effect ?? null, }); } const idByName = {}; for (const g of ghosts) { const info = insertGhost.run({ ...g, isBoss: g.isBoss ? 1 : 0 }); idByName[g.name] = info.lastInsertRowid; } // Build a set per canonical boss-linked LEGO reference. // Roster = the set's boss + a curated sampling of type-matched ghosts, // so each scan returns a playable mix across rarities. const insertSet = db.prepare(` INSERT INTO sets (code, set_number, set_name, boss_ghost_id, enabled) VALUES (@code, @setNumber, @setName, @bossId, 1) `); const linkGhost = db.prepare( 'INSERT OR IGNORE INTO set_ghosts (set_id, ghost_id) VALUES (?, ?)' ); for (const s of sets) { const bossId = s.boss ? idByName[s.boss] : null; const bossGhost = ghosts.find((g) => g.name === s.boss); const code = `NN-${s.setNumber}`; const info = insertSet.run({ code, setNumber: s.setNumber, setName: s.setName, bossId, }); const setId = info.lastInsertRowid; // roster: boss + up to 6 non-boss ghosts whose type matches the boss type const roster = new Set(); if (bossId) roster.add(bossId); const pool = ghosts.filter( (g) => !g.isBoss && (!bossGhost || g.type === bossGhost.type) ); // spread across rarities 1..4 for (let r = 1; r <= 4; r++) { const pick = pool.filter((g) => g.rarity === r).slice(0, 2); for (const g of pick) roster.add(idByName[g.name]); } for (const gid of roster) linkGhost.run(setId, gid); } } // Always ensure an admin user exists (bootstrap). const adminUser = process.env.ADMIN_USER || 'admin'; const adminPass = process.env.ADMIN_PASS || 'changeme'; const existing = db.prepare("SELECT COUNT(*) AS n FROM users WHERE role='admin'").get().n; if (existing === 0) { const hash = bcrypt.hashSync(adminPass, 10); db.prepare( 'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)' ).run(adminUser, hash, 'admin'); console.log(`Created admin user "${adminUser}". CHANGE THE PASSWORD after first login.`); } }); seed(); const stats = { ghosts: db.prepare('SELECT COUNT(*) AS n FROM ghosts').get().n, abilities: db.prepare('SELECT COUNT(*) AS n FROM abilities').get().n, sets: db.prepare('SELECT COUNT(*) AS n FROM sets').get().n, rosterLinks: db.prepare('SELECT COUNT(*) AS n FROM set_ghosts').get().n, }; console.log('Seed complete:', stats);