From 0c3b5ec5a3b63ce9ee1f9a88fe8677f629311d42 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Tue, 23 Jun 2026 00:04:09 +1000 Subject: [PATCH] Add better-sqlite3 seed script for ghost-roster.db --- seed-ghosts.js | 133 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 seed-ghosts.js diff --git a/seed-ghosts.js b/seed-ghosts.js new file mode 100644 index 0000000..36e5b26 --- /dev/null +++ b/seed-ghosts.js @@ -0,0 +1,133 @@ +#!/usr/bin/env node +/** + * seed-ghosts.js — builds a standalone ghost-roster SQLite DB for Newbury Nights. + * + * Separate from the live app DB (Free Hunt pool). Read-reference for the hunt + * loop until you merge. Re-runnable: drops + rebuilds the roster tables only. + * + * Usage: + * node seed-ghosts.js # writes ./ghost-roster.db + * node seed-ghosts.js /path/to.db # custom output path + * + * Requires: better-sqlite3, and ghosts.json / abilities.json / locations.json + * sitting beside this script. + */ +const Database = require('better-sqlite3'); +const fs = require('fs'); +const path = require('path'); + +const here = __dirname; +const dbPath = process.argv[2] || path.join(here, 'ghost-roster.db'); + +const ghosts = JSON.parse(fs.readFileSync(path.join(here, 'ghosts.json'), 'utf8')); +const abilities = JSON.parse(fs.readFileSync(path.join(here, 'abilities.json'), 'utf8')); +const locations = JSON.parse(fs.readFileSync(path.join(here, 'locations.json'), 'utf8')); + +const db = new Database(dbPath); +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +// --- schema (roster tables only; namespaced to avoid clashing with app tables) --- +db.exec(` +DROP TABLE IF EXISTS roster_ghost_locations; +DROP TABLE IF EXISTS roster_ghosts; +DROP TABLE IF EXISTS roster_abilities; +DROP TABLE IF EXISTS roster_locations; + +CREATE TABLE roster_abilities ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL CHECK (type IN ('Common','Boss')), + charges INTEGER, + cooldown_s INTEGER, + effect TEXT +); + +CREATE TABLE roster_locations ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, -- plane | image-target | qr + backdrop TEXT, + qr_code TEXT +); + +CREATE TABLE roster_ghosts ( + id TEXT PRIMARY KEY, -- name slug, e.g. nagging-nathan + name TEXT NOT NULL, + color TEXT NOT NULL CHECK (color IN ('Red','Blue','Yellow')), + rarity TEXT NOT NULL CHECK (rarity IN ('Common','Rare','Epic','Legendary')), + rarity_tier INTEGER NOT NULL, -- 1-4 + speed_pips INTEGER, + range_pips INTEGER, + charge_pips INTEGER, + health_max INTEGER, + damage_max INTEGER, + ability_id TEXT REFERENCES roster_abilities(id), + face_sprite TEXT, + fullbody_sprite TEXT, + webm_path TEXT, + webp_path TEXT, + ar_health REAL, + haunt_value REAL, + is_boss INTEGER, + collection_order INTEGER +); + +CREATE TABLE roster_ghost_locations ( + ghost_id TEXT NOT NULL REFERENCES roster_ghosts(id) ON DELETE CASCADE, + location_id TEXT NOT NULL REFERENCES roster_locations(id) ON DELETE CASCADE, + PRIMARY KEY (ghost_id, location_id) +); + +CREATE INDEX idx_roster_ghosts_color ON roster_ghosts(color); +CREATE INDEX idx_roster_ghosts_rarity ON roster_ghosts(rarity_tier); +`); + +// --- inserts (transactional) --- +const insAbility = db.prepare(` + INSERT INTO roster_abilities (id,name,type,charges,cooldown_s,effect) + VALUES (@id,@name,@type,@charges,@cooldownSeconds,@effect)`); + +const insLocation = db.prepare(` + INSERT INTO roster_locations (id,name,type,backdrop,qr_code) + VALUES (@id,@name,@type,@backdrop,@qrCode)`); + +const insGhost = db.prepare(` + INSERT INTO roster_ghosts + (id,name,color,rarity,rarity_tier,speed_pips,range_pips,charge_pips, + health_max,damage_max,ability_id,face_sprite,fullbody_sprite, + webm_path,webp_path,ar_health,haunt_value,is_boss,collection_order) + VALUES + (@id,@name,@color,@rarity,@rarityTier,@speedPips,@rangePips,@chargePips, + @healthMax,@damageMax,@abilityId,@faceSprite,@fullBodySprite, + @webmPath,@webpPath,@arHealth,@hauntValue,@isBoss,@collectionOrder)`); + +const insGhostLoc = db.prepare(` + INSERT OR IGNORE INTO roster_ghost_locations (ghost_id,location_id) + VALUES (?,?)`); + +const seed = db.transaction(() => { + for (const a of abilities) insAbility.run(a); + for (const l of locations) insLocation.run({ backdrop:null, qrCode:null, ...l }); + let order = 0; + for (const g of ghosts) { + insGhost.run({ + isBoss: g.isBoss == null ? null : (g.isBoss ? 1 : 0), + collectionOrder: order++, + ...g + }); + for (const locId of (g.locations || [])) insGhostLoc.run(g.id, locId); + } +}); +seed(); + +const n = id => db.prepare(`SELECT COUNT(*) c FROM ${id}`).get().c; +console.log(`Seeded ${dbPath}`); +console.log(` abilities: ${n('roster_abilities')}`); +console.log(` locations: ${n('roster_locations')}`); +console.log(` ghosts: ${n('roster_ghosts')}`); +console.log(` by color:`, db.prepare( + `SELECT color, COUNT(*) c FROM roster_ghosts GROUP BY color`).all()); +console.log(` by rarity:`, db.prepare( + `SELECT rarity, COUNT(*) c FROM roster_ghosts GROUP BY rarity_tier ORDER BY rarity_tier`).all()); +db.close();