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, webm: g.webm_path ? `/uploads/${g.webm_path}` : null, webp: g.webp_path ? `/uploads/${g.webp_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 // Free hunt only spawns ghosts with an uploaded video (webm); the webp/image // fallbacks are derived from it, so these always render on every platform. let q = 'SELECT * FROM ghosts WHERE enabled = 1 AND is_boss = 0 AND webm_path IS NOT NULL'; 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;