diff --git a/routes/api.js b/routes/api.js index 489e7d9..c8ba396 100644 --- a/routes/api.js +++ b/routes/api.js @@ -1,109 +1 @@ -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; +aW1wb3J0IHsgUm91dGVyIH0gZnJvbSAnZXhwcmVzcyc7CmltcG9ydCBkYiBmcm9tICcuLi9kYi9pbmRleC5qcyc7Cgpjb25zdCByb3V0ZXIgPSBSb3V0ZXIoKTsKCi8vIFJhcml0eSBzcGF3biB3ZWlnaHRzIChjb21tb24gLT4gbGVnZW5kYXJ5KS4gTWlycm9ycyB0aGUgcHJvamVjdCdzCi8vIGNvbW1vbig3KS9yYXJlKDMpL2xlZ2VuZGFyeSgxKSB3ZWlnaHRpbmcsIG1hcHBlZCBvbnRvIHRoZSA0IHN0YXIgdGllcnMuCmNvbnN0IFJBUklUWV9XRUlHSFRTID0geyAxOiA3LCAyOiA0LCAzOiAzLCA0OiAxIH07CgpmdW5jdGlvbiByb3dUb0dob3N0KGcpIHsKICByZXR1cm4gewogICAgaWQ6IGcuaWQsCiAgICBuYW1lOiBnLm5hbWUsCiAgICBkaXNwbGF5TmFtZTogZy5kaXNwbGF5X25hbWUgfHwgZy5uYW1lLAogICAgdHlwZTogZy50eXBlLAogICAgcmFyaXR5OiBnLnJhcml0eSwKICAgIHNwZWVkOiBnLnNwZWVkLAogICAgcmFuZ2U6IGcucmFuZ2UsCiAgICBjaGFyZ2VTaG90OiBnLmNoYXJnZV9zaG90LAogICAgaGVhbHRoOiBnLmhlYWx0aCwKICAgIGRhbWFnZTogZy5kYW1hZ2UsCiAgICBhYmlsaXR5OiBnLmFiaWxpdHksCiAgICBpc0Jvc3M6ICEhZy5pc19ib3NzLAogICAgc2V0TnVtYmVyOiBnLnNldF9udW1iZXIsCiAgICBzZXROYW1lOiBnLnNldF9uYW1lLAogICAgaW1hZ2U6IGcuaW1hZ2VfcGF0aCA/IGAvdXBsb2Fkcy8ke2cuaW1hZ2VfcGF0aH1gIDogbnVsbCwKICAgIHdlYm06IGcud2VibV9wYXRoID8gYC91cGxvYWRzLyR7Zy53ZWJtX3BhdGh9YCA6IG51bGwsCiAgICB3ZWJwOiBnLndlYnBfcGF0aCA/IGAvdXBsb2Fkcy8ke2cud2VicF9wYXRofWAgOiBudWxsLAogIH07Cn0KCmZ1bmN0aW9uIHdlaWdodGVkUGljayhnaG9zdHMpIHsKICBjb25zdCB3ZWlnaHRlZCA9IFtdOwogIGZvciAoY29uc3QgZyBvZiBnaG9zdHMpIHsKICAgIGNvbnN0IHcgPSBSQVJJVFlfV0VJR0hUU1tnLnJhcml0eV0gPz8gMTsKICAgIGZvciAobGV0IGkgPSAwOyBpIDwgdzsgaSsrKSB3ZWlnaHRlZC5wdXNoKGcpOwogIH0KICBpZiAoIXdlaWdodGVkLmxlbmd0aCkgcmV0dXJuIG51bGw7CiAgcmV0dXJuIHdlaWdodGVkW01hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIHdlaWdodGVkLmxlbmd0aCldOwp9CgovLyBHRVQgL2FwaS9zY2FuLzpjb2RlICDigJQgbm8gYXV0aC4gUmV0dXJucyB0aGUgc2V0J3MgZ2hvc3Qgcm9zdGVyICsgYm9zcy4Kcm91dGVyLmdldCgnL3NjYW4vOmNvZGUnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBzZXQgPSBkYgogICAgLnByZXBhcmUoJ1NFTEVDVCAqIEZST00gc2V0cyBXSEVSRSBjb2RlID0gPyBBTkQgZW5hYmxlZCA9IDEnKQogICAgLmdldChyZXEucGFyYW1zLmNvZGUpOwogIGlmICghc2V0KSByZXR1cm4gcmVzLnN0YXR1cyg0MDQpLmpzb24oeyBlcnJvcjogJ3Vua25vd24gb3IgZGlzYWJsZWQgc2V0IGNvZGUnIH0pOwoKICBjb25zdCByb3N0ZXIgPSBkYgogICAgLnByZXBhcmUoCiAgICAgIGBTRUxFQ1QgZy4qIEZST00gZ2hvc3RzIGcKICAgICAgIEpPSU4gc2V0X2dob3N0cyBzZyBPTiBzZy5naG9zdF9pZCA9IGcuaWQKICAgICAgIFdIRVJFIHNnLnNldF9pZCA9ID8gQU5EIGcuZW5hYmxlZCA9IDEKICAgICAgIE9SREVSIEJZIGcuaXNfYm9zcyBERVNDLCBnLnJhcml0eSBERVNDLCBnLm5hbWVgCiAgICApCiAgICAuYWxsKHNldC5pZCk7CgogIGNvbnN0IGJvc3MgPSBzZXQuYm9zc19naG9zdF9pZAogICAgPyBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIGdob3N0cyBXSEVSRSBpZCA9ID8nKS5nZXQoc2V0LmJvc3NfZ2hvc3RfaWQpCiAgICA6IG51bGw7CgogIHJlcy5qc29uKHsKICAgIHNldDogewogICAgICBjb2RlOiBzZXQuY29kZSwKICAgICAgc2V0TnVtYmVyOiBzZXQuc2V0X251bWJlciwKICAgICAgc2V0TmFtZTogc2V0LnNldF9uYW1lLAogICAgfSwKICAgIGJvc3M6IGJvc3MgPyByb3dUb0dob3N0KGJvc3MpIDogbnVsbCwKICAgIHJvc3Rlcjogcm9zdGVyLm1hcChyb3dUb0dob3N0KSwKICB9KTsKfSk7CgovLyBHRVQgL2FwaS9mcmVlaHVudCAg4oCUIG5vIGF1dGguIFNwYXducyBOIHdlaWdodGVkIHJhbmRvbSBlbmFibGVkIG5vbi1ib3NzIGdob3N0cwovLyBmb3IgZnJlZS1odW50IG1vZGUgKHByb2NlZHVyYWwgd2lzcHMgY2xpZW50LXNpZGUgaWYgbm8gaW1hZ2UpLgpyb3V0ZXIuZ2V0KCcvZnJlZWh1bnQnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBuID0gTWF0aC5taW4ocGFyc2VJbnQocmVxLnF1ZXJ5Lm4sIDEwKSB8fCAzLCAxMCk7CiAgY29uc3QgdHlwZSA9IHJlcS5xdWVyeS50eXBlOyAvLyBvcHRpb25hbCByZWR8eWVsbG93fGJsdWUgZmlsdGVyCiAgbGV0IHEgPSAnU0VMRUNUICogRlJPTSBnaG9zdHMgV0hFUkUgZW5hYmxlZCA9IDEgQU5EIGlzX2Jvc3MgPSAwJzsKICBjb25zdCBwYXJhbXMgPSBbXTsKICBpZiAodHlwZSAmJiBbJ3JlZCcsICd5ZWxsb3cnLCAnYmx1ZSddLmluY2x1ZGVzKHR5cGUpKSB7CiAgICBxICs9ICcgQU5EIHR5cGUgPSA/JzsKICAgIHBhcmFtcy5wdXNoKHR5cGUpOwogIH0KICBjb25zdCBwb29sID0gZGIucHJlcGFyZShxKS5hbGwoLi4ucGFyYW1zKTsKICBjb25zdCBzcGF3bnMgPSBbXTsKICBmb3IgKGxldCBpID0gMDsgaSA8IG4gJiYgcG9vbC5sZW5ndGg7IGkrKykgewogICAgY29uc3QgcGljayA9IHdlaWdodGVkUGljayhwb29sKTsKICAgIGlmIChwaWNrKSBzcGF3bnMucHVzaChyb3dUb0dob3N0KHBpY2spKTsKICB9CiAgcmVzLmpzb24oeyBzcGF3bnMgfSk7Cn0pOwoKLy8gR0VUIC9hcGkvZ2hvc3RzICDigJQgbm8gYXV0aC4gUHVibGljIHJvc3RlciBicm93c2VyIChlbmFibGVkIG9ubHkpLgpyb3V0ZXIuZ2V0KCcvZ2hvc3RzJywgKHJlcSwgcmVzKSA9PiB7CiAgY29uc3QgeyB0eXBlLCByYXJpdHksIGJvc3MgfSA9IHJlcS5xdWVyeTsKICBsZXQgcSA9ICdTRUxFQ1QgKiBGUk9NIGdob3N0cyBXSEVSRSBlbmFibGVkID0gMSc7CiAgY29uc3QgcGFyYW1zID0gW107CiAgaWYgKHR5cGUpIHsgcSArPSAnIEFORCB0eXBlID0gPyc7IHBhcmFtcy5wdXNoKHR5cGUpOyB9CiAgaWYgKHJhcml0eSkgeyBxICs9ICcgQU5EIHJhcml0eSA9ID8nOyBwYXJhbXMucHVzaChwYXJzZUludChyYXJpdHksIDEwKSk7IH0KICBpZiAoYm9zcyA9PT0gJzEnKSBxICs9ICcgQU5EIGlzX2Jvc3MgPSAxJzsKICBxICs9ICcgT1JERVIgQlkgdHlwZSwgcmFyaXR5LCBuYW1lJzsKICByZXMuanNvbih7IGdob3N0czogZGIucHJlcGFyZShxKS5hbGwoLi4ucGFyYW1zKS5tYXAocm93VG9HaG9zdCkgfSk7Cn0pOwoKLy8gR0VUIC9hcGkvYWJpbGl0aWVzIOKAlCBubyBhdXRoLiBSZWZlcmVuY2UgZGF0YS4Kcm91dGVyLmdldCgnL2FiaWxpdGllcycsIChyZXEsIHJlcykgPT4gewogIGNvbnN0IHJvd3MgPSBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIGFiaWxpdGllcyBPUkRFUiBCWSBraW5kLCBuYW1lJykuYWxsKCk7CiAgcmVzLmpzb24oeyBhYmlsaXRpZXM6IHJvd3MgfSk7Cn0pOwoKZXhwb3J0IGRlZmF1bHQgcm91dGVyOwo= \ No newline at end of file