134 lines
4.7 KiB
JavaScript
134 lines
4.7 KiB
JavaScript
#!/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();
|