diff --git a/routes/api.js b/routes/api.js new file mode 100644 index 0000000..489e7d9 --- /dev/null +++ b/routes/api.js @@ -0,0 +1,109 @@ +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;