Files
newbury-nights/routes/api.js
T
2026-06-17 11:44:43 +10:00

110 lines
3.3 KiB
JavaScript

import { Router } from 'express';
import db from '../db/index.js';
const router = Router();
// Rarity spawn weights (common -> legendary). Mirrors the project's
// common(7)/rare(3)/legendary(1) weighting, mapped onto the 4 star tiers.
const RARITY_WEIGHTS = { 1: 7, 2: 4, 3: 3, 4: 1 };
function rowToGhost(g) {
return {
id: g.id,
name: g.name,
displayName: g.display_name || g.name,
type: g.type,
rarity: g.rarity,
speed: g.speed,
range: g.range,
chargeShot: g.charge_shot,
health: g.health,
damage: g.damage,
ability: g.ability,
isBoss: !!g.is_boss,
setNumber: g.set_number,
setName: g.set_name,
image: g.image_path ? `/uploads/${g.image_path}` : null,
};
}
function weightedPick(ghosts) {
const weighted = [];
for (const g of ghosts) {
const w = RARITY_WEIGHTS[g.rarity] ?? 1;
for (let i = 0; i < w; i++) weighted.push(g);
}
if (!weighted.length) return null;
return weighted[Math.floor(Math.random() * weighted.length)];
}
// GET /api/scan/:code — no auth. Returns the set's ghost roster + boss.
router.get('/scan/:code', (req, res) => {
const set = db
.prepare('SELECT * FROM sets WHERE code = ? AND enabled = 1')
.get(req.params.code);
if (!set) return res.status(404).json({ error: 'unknown or disabled set code' });
const roster = db
.prepare(
`SELECT g.* FROM ghosts g
JOIN set_ghosts sg ON sg.ghost_id = g.id
WHERE sg.set_id = ? AND g.enabled = 1
ORDER BY g.is_boss DESC, g.rarity DESC, g.name`
)
.all(set.id);
const boss = set.boss_ghost_id
? db.prepare('SELECT * FROM ghosts WHERE id = ?').get(set.boss_ghost_id)
: null;
res.json({
set: {
code: set.code,
setNumber: set.set_number,
setName: set.set_name,
},
boss: boss ? rowToGhost(boss) : null,
roster: roster.map(rowToGhost),
});
});
// GET /api/freehunt — no auth. Spawns N weighted random enabled non-boss ghosts
// for free-hunt mode (procedural wisps client-side if no image).
router.get('/freehunt', (req, res) => {
const n = Math.min(parseInt(req.query.n, 10) || 3, 10);
const type = req.query.type; // optional red|yellow|blue filter
let q = 'SELECT * FROM ghosts WHERE enabled = 1 AND is_boss = 0';
const params = [];
if (type && ['red', 'yellow', 'blue'].includes(type)) {
q += ' AND type = ?';
params.push(type);
}
const pool = db.prepare(q).all(...params);
const spawns = [];
for (let i = 0; i < n && pool.length; i++) {
const pick = weightedPick(pool);
if (pick) spawns.push(rowToGhost(pick));
}
res.json({ spawns });
});
// GET /api/ghosts — no auth. Public roster browser (enabled only).
router.get('/ghosts', (req, res) => {
const { type, rarity, boss } = req.query;
let q = 'SELECT * FROM ghosts WHERE enabled = 1';
const params = [];
if (type) { q += ' AND type = ?'; params.push(type); }
if (rarity) { q += ' AND rarity = ?'; params.push(parseInt(rarity, 10)); }
if (boss === '1') q += ' AND is_boss = 1';
q += ' ORDER BY type, rarity, name';
res.json({ ghosts: db.prepare(q).all(...params).map(rowToGhost) });
});
// GET /api/abilities — no auth. Reference data.
router.get('/abilities', (req, res) => {
const rows = db.prepare('SELECT * FROM abilities ORDER BY kind, name').all();
res.json({ abilities: rows });
});
export default router;