Emit webm and webp URLs in public ghost objects
rowToGhost now returns webm/webp alongside image, so the client renderer can select the VP9+alpha video billboard (with WebP/GIF fallback).
This commit is contained in:
+1
-109
@@ -1,109 +1 @@
|
|||||||
import { Router } from 'express';
|
aW1wb3J0IHsgUm91dGVyIH0gZnJvbSAnZXhwcmVzcyc7CmltcG9ydCBkYiBmcm9tICcuLi9kYi9pbmRleC5qcyc7Cgpjb25zdCByb3V0ZXIgPSBSb3V0ZXIoKTsKCi8vIFJhcml0eSBzcGF3biB3ZWlnaHRzIChjb21tb24gLT4gbGVnZW5kYXJ5KS4gTWlycm9ycyB0aGUgcHJvamVjdCdzCi8vIGNvbW1vbig3KS9yYXJlKDMpL2xlZ2VuZGFyeSgxKSB3ZWlnaHRpbmcsIG1hcHBlZCBvbnRvIHRoZSA0IHN0YXIgdGllcnMuCmNvbnN0IFJBUklUWV9XRUlHSFRTID0geyAxOiA3LCAyOiA0LCAzOiAzLCA0OiAxIH07CgpmdW5jdGlvbiByb3dUb0dob3N0KGcpIHsKICByZXR1cm4gewogICAgaWQ6IGcuaWQsCiAgICBuYW1lOiBnLm5hbWUsCiAgICBkaXNwbGF5TmFtZTogZy5kaXNwbGF5X25hbWUgfHwgZy5uYW1lLAogICAgdHlwZTogZy50eXBlLAogICAgcmFyaXR5OiBnLnJhcml0eSwKICAgIHNwZWVkOiBnLnNwZWVkLAogICAgcmFuZ2U6IGcucmFuZ2UsCiAgICBjaGFyZ2VTaG90OiBnLmNoYXJnZV9zaG90LAogICAgaGVhbHRoOiBnLmhlYWx0aCwKICAgIGRhbWFnZTogZy5kYW1hZ2UsCiAgICBhYmlsaXR5OiBnLmFiaWxpdHksCiAgICBpc0Jvc3M6ICEhZy5pc19ib3NzLAogICAgc2V0TnVtYmVyOiBnLnNldF9udW1iZXIsCiAgICBzZXROYW1lOiBnLnNldF9uYW1lLAogICAgaW1hZ2U6IGcuaW1hZ2VfcGF0aCA/IGAvdXBsb2Fkcy8ke2cuaW1hZ2VfcGF0aH1gIDogbnVsbCwKICAgIHdlYm06IGcud2VibV9wYXRoID8gYC91cGxvYWRzLyR7Zy53ZWJtX3BhdGh9YCA6IG51bGwsCiAgICB3ZWJwOiBnLndlYnBfcGF0aCA/IGAvdXBsb2Fkcy8ke2cud2VicF9wYXRofWAgOiBudWxsLAogIH07Cn0KCmZ1bmN0aW9uIHdlaWdodGVkUGljayhnaG9zdHMpIHsKICBjb25zdCB3ZWlnaHRlZCA9IFtdOwogIGZvciAoY29uc3QgZyBvZiBnaG9zdHMpIHsKICAgIGNvbnN0IHcgPSBSQVJJVFlfV0VJR0hUU1tnLnJhcml0eV0gPz8gMTsKICAgIGZvciAobGV0IGkgPSAwOyBpIDwgdzsgaSsrKSB3ZWlnaHRlZC5wdXNoKGcpOwogIH0KICBpZiAoIXdlaWdodGVkLmxlbmd0aCkgcmV0dXJuIG51bGw7CiAgcmV0dXJuIHdlaWdodGVkW01hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIHdlaWdodGVkLmxlbmd0aCldOwp9CgovLyBHRVQgL2FwaS9zY2FuLzpjb2RlICDigJQgbm8gYXV0aC4gUmV0dXJucyB0aGUgc2V0J3MgZ2hvc3Qgcm9zdGVyICsgYm9zcy4Kcm91dGVyLmdldCgnL3NjYW4vOmNvZGUnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBzZXQgPSBkYgogICAgLnByZXBhcmUoJ1NFTEVDVCAqIEZST00gc2V0cyBXSEVSRSBjb2RlID0gPyBBTkQgZW5hYmxlZCA9IDEnKQogICAgLmdldChyZXEucGFyYW1zLmNvZGUpOwogIGlmICghc2V0KSByZXR1cm4gcmVzLnN0YXR1cyg0MDQpLmpzb24oeyBlcnJvcjogJ3Vua25vd24gb3IgZGlzYWJsZWQgc2V0IGNvZGUnIH0pOwoKICBjb25zdCByb3N0ZXIgPSBkYgogICAgLnByZXBhcmUoCiAgICAgIGBTRUxFQ1QgZy4qIEZST00gZ2hvc3RzIGcKICAgICAgIEpPSU4gc2V0X2dob3N0cyBzZyBPTiBzZy5naG9zdF9pZCA9IGcuaWQKICAgICAgIFdIRVJFIHNnLnNldF9pZCA9ID8gQU5EIGcuZW5hYmxlZCA9IDEKICAgICAgIE9SREVSIEJZIGcuaXNfYm9zcyBERVNDLCBnLnJhcml0eSBERVNDLCBnLm5hbWVgCiAgICApCiAgICAuYWxsKHNldC5pZCk7CgogIGNvbnN0IGJvc3MgPSBzZXQuYm9zc19naG9zdF9pZAogICAgPyBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIGdob3N0cyBXSEVSRSBpZCA9ID8nKS5nZXQoc2V0LmJvc3NfZ2hvc3RfaWQpCiAgICA6IG51bGw7CgogIHJlcy5qc29uKHsKICAgIHNldDogewogICAgICBjb2RlOiBzZXQuY29kZSwKICAgICAgc2V0TnVtYmVyOiBzZXQuc2V0X251bWJlciwKICAgICAgc2V0TmFtZTogc2V0LnNldF9uYW1lLAogICAgfSwKICAgIGJvc3M6IGJvc3MgPyByb3dUb0dob3N0KGJvc3MpIDogbnVsbCwKICAgIHJvc3Rlcjogcm9zdGVyLm1hcChyb3dUb0dob3N0KSwKICB9KTsKfSk7CgovLyBHRVQgL2FwaS9mcmVlaHVudCAg4oCUIG5vIGF1dGguIFNwYXducyBOIHdlaWdodGVkIHJhbmRvbSBlbmFibGVkIG5vbi1ib3NzIGdob3N0cwovLyBmb3IgZnJlZS1odW50IG1vZGUgKHByb2NlZHVyYWwgd2lzcHMgY2xpZW50LXNpZGUgaWYgbm8gaW1hZ2UpLgpyb3V0ZXIuZ2V0KCcvZnJlZWh1bnQnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBuID0gTWF0aC5taW4ocGFyc2VJbnQocmVxLnF1ZXJ5Lm4sIDEwKSB8fCAzLCAxMCk7CiAgY29uc3QgdHlwZSA9IHJlcS5xdWVyeS50eXBlOyAvLyBvcHRpb25hbCByZWR8eWVsbG93fGJsdWUgZmlsdGVyCiAgbGV0IHEgPSAnU0VMRUNUICogRlJPTSBnaG9zdHMgV0hFUkUgZW5hYmxlZCA9IDEgQU5EIGlzX2Jvc3MgPSAwJzsKICBjb25zdCBwYXJhbXMgPSBbXTsKICBpZiAodHlwZSAmJiBbJ3JlZCcsICd5ZWxsb3cnLCAnYmx1ZSddLmluY2x1ZGVzKHR5cGUpKSB7CiAgICBxICs9ICcgQU5EIHR5cGUgPSA/JzsKICAgIHBhcmFtcy5wdXNoKHR5cGUpOwogIH0KICBjb25zdCBwb29sID0gZGIucHJlcGFyZShxKS5hbGwoLi4ucGFyYW1zKTsKICBjb25zdCBzcGF3bnMgPSBbXTsKICBmb3IgKGxldCBpID0gMDsgaSA8IG4gJiYgcG9vbC5sZW5ndGg7IGkrKykgewogICAgY29uc3QgcGljayA9IHdlaWdodGVkUGljayhwb29sKTsKICAgIGlmIChwaWNrKSBzcGF3bnMucHVzaChyb3dUb0dob3N0KHBpY2spKTsKICB9CiAgcmVzLmpzb24oeyBzcGF3bnMgfSk7Cn0pOwoKLy8gR0VUIC9hcGkvZ2hvc3RzICDigJQgbm8gYXV0aC4gUHVibGljIHJvc3RlciBicm93c2VyIChlbmFibGVkIG9ubHkpLgpyb3V0ZXIuZ2V0KCcvZ2hvc3RzJywgKHJlcSwgcmVzKSA9PiB7CiAgY29uc3QgeyB0eXBlLCByYXJpdHksIGJvc3MgfSA9IHJlcS5xdWVyeTsKICBsZXQgcSA9ICdTRUxFQ1QgKiBGUk9NIGdob3N0cyBXSEVSRSBlbmFibGVkID0gMSc7CiAgY29uc3QgcGFyYW1zID0gW107CiAgaWYgKHR5cGUpIHsgcSArPSAnIEFORCB0eXBlID0gPyc7IHBhcmFtcy5wdXNoKHR5cGUpOyB9CiAgaWYgKHJhcml0eSkgeyBxICs9ICcgQU5EIHJhcml0eSA9ID8nOyBwYXJhbXMucHVzaChwYXJzZUludChyYXJpdHksIDEwKSk7IH0KICBpZiAoYm9zcyA9PT0gJzEnKSBxICs9ICcgQU5EIGlzX2Jvc3MgPSAxJzsKICBxICs9ICcgT1JERVIgQlkgdHlwZSwgcmFyaXR5LCBuYW1lJzsKICByZXMuanNvbih7IGdob3N0czogZGIucHJlcGFyZShxKS5hbGwoLi4ucGFyYW1zKS5tYXAocm93VG9HaG9zdCkgfSk7Cn0pOwoKLy8gR0VUIC9hcGkvYWJpbGl0aWVzIOKAlCBubyBhdXRoLiBSZWZlcmVuY2UgZGF0YS4Kcm91dGVyLmdldCgnL2FiaWxpdGllcycsIChyZXEsIHJlcykgPT4gewogIGNvbnN0IHJvd3MgPSBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIGFiaWxpdGllcyBPUkRFUiBCWSBraW5kLCBuYW1lJykuYWxsKCk7CiAgcmVzLmpzb24oeyBhYmlsaXRpZXM6IHJvd3MgfSk7Cn0pOwoKZXhwb3J0IGRlZmF1bHQgcm91dGVyOwo=
|
||||||
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;
|
|
||||||
Reference in New Issue
Block a user