Files
newbury-nights/scripts/seed.js
T

131 lines
4.9 KiB
JavaScript

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