From 458c66a2c0895172bd9cb7e17c52e17bd304c833 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Fri, 19 Jun 2026 05:06:43 +0000 Subject: [PATCH] Fix: decode base64-corrupted source files (html/css/js + backend) --- db/index.js | 89 ++- lib/ghost-media.js | 112 +++- package-lock.json | 1496 ++++++++++++++++++++++++++++++++++++++++++++ public/js/game.js | 655 ++++++++++++++++++- routes/admin.js | 240 ++++++- routes/api.js | 112 +++- 6 files changed, 2699 insertions(+), 5 deletions(-) create mode 100644 package-lock.json diff --git a/db/index.js b/db/index.js index b46869b..ab8c226 100644 --- a/db/index.js +++ b/db/index.js @@ -1 +1,88 @@ -aW1wb3J0IERhdGFiYXNlIGZyb20gJ2JldHRlci1zcWxpdGUzJzsKaW1wb3J0IHsgbWtkaXJTeW5jIH0gZnJvbSAnbm9kZTpmcyc7CmltcG9ydCB7IGRpcm5hbWUsIGpvaW4gfSBmcm9tICdub2RlOnBhdGgnOwppbXBvcnQgeyBmaWxlVVJMVG9QYXRoIH0gZnJvbSAnbm9kZTp1cmwnOwoKY29uc3QgX19kaXJuYW1lID0gZGlybmFtZShmaWxlVVJMVG9QYXRoKGltcG9ydC5tZXRhLnVybCkpOwpjb25zdCBEQl9QQVRIID0gam9pbihfX2Rpcm5hbWUsICduZXdidXJ5LnNxbGl0ZScpOwoKbWtkaXJTeW5jKF9fZGlybmFtZSwgeyByZWN1cnNpdmU6IHRydWUgfSk7Cgpjb25zdCBkYiA9IG5ldyBEYXRhYmFzZShEQl9QQVRIKTsKZGIucHJhZ21hKCdqb3VybmFsX21vZGUgPSBXQUwnKTsKZGIucHJhZ21hKCdmb3JlaWduX2tleXMgPSBPTicpOwoKZGIuZXhlYyhgCkNSRUFURSBUQUJMRSBJRiBOT1QgRVhJU1RTIHVzZXJzICgKICBpZCAgICAgICAgICAgIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT0lOQ1JFTUVOVCwKICB1c2VybmFtZSAgICAgIFRFWFQgVU5JUVVFIE5PVCBOVUxMLAogIHBhc3N3b3JkX2hhc2ggVEVYVCBOT1QgTlVMTCwKICByb2xlICAgICAgICAgIFRFWFQgTk9UIE5VTEwgREVGQVVMVCAnYWRtaW4nLAogIGNyZWF0ZWRfYXQgICAgVEVYVCBOT1QgTlVMTCBERUZBVUxUIChkYXRldGltZSgnbm93JykpCik7CgpDUkVBVEUgVEFCTEUgSUYgTk9UIEVYSVNUUyBhYmlsaXRpZXMgKAogIGlkICAgICAgICBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9JTkNSRU1FTlQsCiAgbmFtZSAgICAgIFRFWFQgVU5JUVVFIE5PVCBOVUxMLAogIGtpbmQgICAgICBURVhUIE5PVCBOVUxMIERFRkFVTFQgJ2NvbW1vbicsICAgLS0gY29tbW9uIHwgYm9zcwogIGNoYXJnZXMgICBJTlRFR0VSLAogIGNvb2xkb3duICBURVhULAogIGVmZmVjdCAgICBURVhUCik7CgpDUkVBVEUgVEFCTEUgSUYgTk9UIEVYSVNUUyBnaG9zdHMgKAogIGlkICAgICAgICAgICBJTlRFR0VSIFBSSU1BUlkgS0VZIEFVVE9JTkNSRU1FTlQsCiAgbmFtZSAgICAgICAgIFRFWFQgTk9UIE5VTEwsCiAgZGlzcGxheV9uYW1lIFRFWFQsICAgICAgICAgICAgICAgICAgICAgICAgICAgLS0gc2hvd24gb24gdGhlIGxvY2stb24gbGFiZWw7IGZhbGxzIGJhY2sgdG8gbmFtZQogIHR5cGUgICAgICAgICBURVhUIE5PVCBOVUxMLCAgICAgICAgICAgICAgICAgIC0tIHJlZCB8IHllbGxvdyB8IGJsdWUKICByYXJpdHkgICAgICAgSU5URUdFUiBOT1QgTlVMTCwgICAgICAgICAgICAgICAtLSAxLi40IChzdGFycykKICBzcGVlZCAgICAgICAgSU5URUdFUiBOT1QgTlVMTCBERUZBVUxUIDAsCiAgcmFuZ2UgICAgICAgIElOVEVHRVIgTk9UIE5VTEwgREVGQVVMVCAwLAogIGNoYXJnZV9zaG90ICBJTlRFR0VSIE5PVCBOVUxMIERFRkFVTFQgMCwKICBoZWFsdGggICAgICAgSU5URUdFUiBOT1QgTlVMTCwKICBkYW1hZ2UgICAgICAgSU5URUdFUiBOT1QgTlVMTCwKICBhYmlsaXR5ICAgICAgVEVYVCwKICBpc19ib3NzICAgICAgSU5URUdFUiBOT1QgTlVMTCBERUZBVUxUIDAsCiAgc2V0X251bWJlciAgIFRFWFQsCiAgc2V0X25hbWUgICAgIFRFWFQsCiAgaW1hZ2VfcGF0aCAgIFRFWFQsICAgICAgICAgICAgICAgICAgICAgICAgICAgLS0gdXBsb2FkZWQgR0lGL1BORyBiaWxsYm9hcmQgKG51bGxhYmxlKQogIGVuYWJsZWQgICAgICBJTlRFR0VSIE5PVCBOVUxMIERFRkFVTFQgMSwKICBjcmVhdGVkX2F0ICAgVEVYVCBOT1QgTlVMTCBERUZBVUxUIChkYXRldGltZSgnbm93JykpCik7CkNSRUFURSBJTkRFWCBJRiBOT1QgRVhJU1RTIGlkeF9naG9zdHNfdHlwZSAgIE9OIGdob3N0cyh0eXBlKTsKQ1JFQVRFIElOREVYIElGIE5PVCBFWElTVFMgaWR4X2dob3N0c19yYXJpdHkgT04gZ2hvc3RzKHJhcml0eSk7CkNSRUFURSBJTkRFWCBJRiBOT1QgRVhJU1RTIGlkeF9naG9zdHNfYm9zcyAgIE9OIGdob3N0cyhpc19ib3NzKTsKCkNSRUFURSBUQUJMRSBJRiBOT1QgRVhJU1RTIHNldHMgKAogIGlkICAgICAgICAgIElOVEVHRVIgUFJJTUFSWSBLRVkgQVVUT0lOQ1JFTUVOVCwKICBjb2RlICAgICAgICBURVhUIFVOSVFVRSBOT1QgTlVMTCwgICAgICAgICAgICAtLSBRUiBwYXlsb2FkIC8gc2NhbiBjb2RlCiAgc2V0X251bWJlciAgVEVYVCwgICAgICAgICAgICAgICAgICAgICAgICAgICAgLS0gZS5nLiA3MDQxOSAocmVmZXJlbmNlIG9ubHkpCiAgc2V0X25hbWUgICAgVEVYVCBOT1QgTlVMTCwKICBib3NzX2dob3N0X2lkIElOVEVHRVIgUkVGRVJFTkNFUyBnaG9zdHMoaWQpIE9OIERFTEVURSBTRVQgTlVMTCwKICBlbmFibGVkICAgICBJTlRFR0VSIE5PVCBOVUxMIERFRkFVTFQgMSwKICBjcmVhdGVkX2F0ICBURVhUIE5PVCBOVUxMIERFRkFVTFQgKGRhdGV0aW1lKCdub3cnKSkKKTsKCkNSRUFURSBUQUJMRSBJRiBOT1QgRVhJU1RTIHNldF9naG9zdHMgKAogIHNldF9pZCAgIElOVEVHRVIgTk9UIE5VTEwgUkVGRVJFTkNFUyBzZXRzKGlkKSBPTiBERUxFVEUgQ0FTQ0FERSwKICBnaG9zdF9pZCBJTlRFR0VSIE5PVCBOVUxMIFJFRkVSRU5DRVMgZ2hvc3RzKGlkKSBPTiBERUxFVEUgQ0FTQ0FERSwKICBQUklNQVJZIEtFWSAoc2V0X2lkLCBnaG9zdF9pZCkKKTsKYCk7CgovKiAtLS0tIE1pZ3JhdGlvbnMgKGlkZW1wb3RlbnQpIC0tLS0KICogQWRkIHRyYW5zcGFyZW50LXZpZGVvIHNwcml0ZSBjb2x1bW5zIHRvIHRoZSBleGlzdGluZyBnaG9zdHMgdGFibGUgd2l0aG91dAogKiBkcm9wcGluZyBkYXRhLiB3ZWJtX3BhdGggaXMgdGhlIFZQOSthbHBoYSBiaWxsYm9hcmQ7IHdlYnBfcGF0aCBpcyB0aGUKICogYW5pbWF0ZWQtV2ViUCBmYWxsYmFjayB1c2VkIHdoZXJlIFZQOSBhbHBoYSBpc24ndCBzdXBwb3J0ZWQgKGUuZy4gaU9TKS4KICovCmZ1bmN0aW9uIGFkZENvbHVtbklmTWlzc2luZyh0YWJsZSwgY29sdW1uLCBkZWNsKSB7CiAgY29uc3QgY29scyA9IGRiLnByZXBhcmUoYFBSQUdNQSB0YWJsZV9pbmZvKCR7dGFibGV9KWApLmFsbCgpOwogIGlmICghY29scy5zb21lKChjKSA9PiBjLm5hbWUgPT09IGNvbHVtbikpIHsKICAgIGRiLmV4ZWMoYEFMVEVSIFRBQkxFICR7dGFibGV9IEFERCBDT0xVTU4gJHtjb2x1bW59ICR7ZGVjbH1gKTsKICB9Cn0KYWRkQ29sdW1uSWZNaXNzaW5nKCdnaG9zdHMnLCAnd2VibV9wYXRoJywgJ1RFWFQnKTsgLy8gVlA5K2FscGhhIGJpbGxib2FyZCAobnVsbGFibGUpCmFkZENvbHVtbklmTWlzc2luZygnZ2hvc3RzJywgJ3dlYnBfcGF0aCcsICdURVhUJyk7IC8vIGFuaW1hdGVkLVdlYlAgZmFsbGJhY2sgKG51bGxhYmxlKQoKZXhwb3J0IGRlZmF1bHQgZGI7CmV4cG9ydCB7IERCX1BBVEggfTsK \ No newline at end of file +import Database from 'better-sqlite3'; +import { mkdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DB_PATH = join(__dirname, 'newbury.sqlite'); + +mkdirSync(__dirname, { recursive: true }); + +const db = new Database(DB_PATH); +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +db.exec(` +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'admin', + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS abilities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + kind TEXT NOT NULL DEFAULT 'common', -- common | boss + charges INTEGER, + cooldown TEXT, + effect TEXT +); + +CREATE TABLE IF NOT EXISTS ghosts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + display_name TEXT, -- shown on the lock-on label; falls back to name + type TEXT NOT NULL, -- red | yellow | blue + rarity INTEGER NOT NULL, -- 1..4 (stars) + speed INTEGER NOT NULL DEFAULT 0, + range INTEGER NOT NULL DEFAULT 0, + charge_shot INTEGER NOT NULL DEFAULT 0, + health INTEGER NOT NULL, + damage INTEGER NOT NULL, + ability TEXT, + is_boss INTEGER NOT NULL DEFAULT 0, + set_number TEXT, + set_name TEXT, + image_path TEXT, -- uploaded GIF/PNG billboard (nullable) + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_ghosts_type ON ghosts(type); +CREATE INDEX IF NOT EXISTS idx_ghosts_rarity ON ghosts(rarity); +CREATE INDEX IF NOT EXISTS idx_ghosts_boss ON ghosts(is_boss); + +CREATE TABLE IF NOT EXISTS sets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + code TEXT UNIQUE NOT NULL, -- QR payload / scan code + set_number TEXT, -- e.g. 70419 (reference only) + set_name TEXT NOT NULL, + boss_ghost_id INTEGER REFERENCES ghosts(id) ON DELETE SET NULL, + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS set_ghosts ( + set_id INTEGER NOT NULL REFERENCES sets(id) ON DELETE CASCADE, + ghost_id INTEGER NOT NULL REFERENCES ghosts(id) ON DELETE CASCADE, + PRIMARY KEY (set_id, ghost_id) +); +`); + +/* ---- Migrations (idempotent) ---- + * Add transparent-video sprite columns to the existing ghosts table without + * dropping data. webm_path is the VP9+alpha billboard; webp_path is the + * animated-WebP fallback used where VP9 alpha isn't supported (e.g. iOS). + */ +function addColumnIfMissing(table, column, decl) { + const cols = db.prepare(`PRAGMA table_info(${table})`).all(); + if (!cols.some((c) => c.name === column)) { + db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${decl}`); + } +} +addColumnIfMissing('ghosts', 'webm_path', 'TEXT'); // VP9+alpha billboard (nullable) +addColumnIfMissing('ghosts', 'webp_path', 'TEXT'); // animated-WebP fallback (nullable) + +export default db; +export { DB_PATH }; diff --git a/lib/ghost-media.js b/lib/ghost-media.js index 12c609f..8b8bab8 100644 --- a/lib/ghost-media.js +++ b/lib/ghost-media.js @@ -1 +1,111 @@ -aW1wb3J0IHsgc3Bhd24gfSBmcm9tICdub2RlOmNoaWxkX3Byb2Nlc3MnOwppbXBvcnQgeyBleGlzdHNTeW5jLCBzdGF0U3luYyB9IGZyb20gJ25vZGU6ZnMnOwppbXBvcnQgeyBiYXNlbmFtZSwgZXh0bmFtZSwgam9pbiB9IGZyb20gJ25vZGU6cGF0aCc7CgovKgogKiBnaG9zdC1tZWRpYS5qcyDigJQgc2VydmVyLXNpZGUgZ2hvc3Qgc3ByaXRlIGNvbnZlcnNpb24uCiAqCiAqIFR1cm5zIGFuIHVwbG9hZGVkIGJsYWNrLWJhY2tncm91bmQgZ2hvc3QgTVA0IGludG8gYSB0cmFuc3BhcmVudCBhbmltYXRlZAogKiBXZWJNIChWUDkgKyBhbHBoYSkgcGx1cyBhbiBhbmltYXRlZCBXZWJQIGZhbGxiYWNrLCB1c2luZyBsdW1hIGtleWluZyBzbyB0aGUKICogYmxhY2sgYmVjb21lcyB0cmFuc3BhcmVudCBhbmQgdGhlIGdsb3cga2VlcHMgc29mdCBzZW1pLXRyYW5zcGFyZW50IGVkZ2VzLgogKgogKiBTaGVsbHMgb3V0IHRvIHRoZSBzeXN0ZW0gYGZmbXBlZ2AgYmluYXJ5IChubyBucG0gZGVwZW5kZW5jeSkuIElmIGZmbXBlZyBpcwogKiBub3QgaW5zdGFsbGVkLCBjb252ZXJzaW9uIGZhaWxzIGdyYWNlZnVsbHkgYW5kIHRoZSBjYWxsZXIga2VlcHMgdGhlIG9yaWdpbmFsLgogKgogKiBUaGlzIG1pcnJvcnMgdGhlIHN0YW5kYWxvbmUgZ2hvc3RpZnkuc2ggcGlwZWxpbmUgc28gbG9jYWwgYW5kIHNlcnZlciBvdXRwdXQKICogbWF0Y2guCiAqLwoKY29uc3QgRkZNUEVHID0gcHJvY2Vzcy5lbnYuRkZNUEVHX1BBVEggfHwgJ2ZmbXBlZyc7CgovLyBUdW5hYmxlcyAoZW52LW92ZXJyaWRhYmxlKSDigJQga2VwdCBpbiBzeW5jIHdpdGggZ2hvc3RpZnkuc2ggZGVmYXVsdHMuCmNvbnN0IFRIUkVTSCA9IHByb2Nlc3MuZW52LkdIT1NUX0tFWV9USFJFU0hPTEQgfHwgJzAuMDYnOwpjb25zdCBUT0wgPSBwcm9jZXNzLmVudi5HSE9TVF9LRVlfVE9MRVJBTkNFIHx8ICcwLjEwJzsKY29uc3QgU09GVCA9IHByb2Nlc3MuZW52LkdIT1NUX0tFWV9TT0ZUTkVTUyB8fCAnMC4xNSc7CmNvbnN0IEZQUyA9IHByb2Nlc3MuZW52LkdIT1NUX0ZQUyB8fCAnMTUnOwpjb25zdCBNQVhXID0gcHJvY2Vzcy5lbnYuR0hPU1RfTUFYVyB8fCAnNTEyJzsKY29uc3QgV0VCUF9RID0gcHJvY2Vzcy5lbnYuR0hPU1RfV0VCUF9RVUFMSVRZIHx8ICc4MCc7CmNvbnN0IFZQOV9DUkYgPSBwcm9jZXNzLmVudi5HSE9TVF9WUDlfQ1JGIHx8ICczMCc7CgovLyBUaGUgc2hhcmVkIGx1bWEta2V5ICsgZG93bnNjYWxlIGZpbHRlciBjaGFpbi4KZnVuY3Rpb24ga2V5Q2hhaW4oKSB7CiAgY29uc3Qga2V5ID0gYGZvcm1hdD1yZ2JhLGx1bWFrZXk9dGhyZXNob2xkPSR7VEhSRVNIfTp0b2xlcmFuY2U9JHtUT0x9OnNvZnRuZXNzPSR7U09GVH1gOwogIGNvbnN0IHNjYWxlID0gYHNjYWxlPSdtaW4oaXcsJHtNQVhXfSknOi0xOmZsYWdzPWxhbmN6b3NgOwogIHJldHVybiBgJHtrZXl9LGZwcz0ke0ZQU30sJHtzY2FsZX1gOwp9CgpmdW5jdGlvbiBydW4oYXJncykgewogIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7CiAgICBjb25zdCBwcm9jID0gc3Bhd24oRkZNUEVHLCBhcmdzLCB7IHN0ZGlvOiBbJ2lnbm9yZScsICdpZ25vcmUnLCAncGlwZSddIH0pOwogICAgbGV0IHN0ZGVyciA9ICcnOwogICAgcHJvYy5zdGRlcnIub24oJ2RhdGEnLCAoZCkgPT4geyBzdGRlcnIgKz0gZC50b1N0cmluZygpOyB9KTsKICAgIHByb2Mub24oJ2Vycm9yJywgcmVqZWN0KTsgLy8gZS5nLiBmZm1wZWcgbm90IGZvdW5kIChFTk9FTlQpCiAgICBwcm9jLm9uKCdjbG9zZScsIChjb2RlKSA9PiB7CiAgICAgIGlmIChjb2RlID09PSAwKSByZXNvbHZlKCk7CiAgICAgIGVsc2UgcmVqZWN0KG5ldyBFcnJvcihgZmZtcGVnIGV4aXRlZCAke2NvZGV9OiAke3N0ZGVyci5zbGljZSgtNTAwKX1gKSk7CiAgICB9KTsKICB9KTsKfQoKLyoqIFRydWUgaWYgYSB1c2FibGUgZmZtcGVnIGJpbmFyeSBpcyBvbiBQQVRILiAqLwpleHBvcnQgZnVuY3Rpb24gZmZtcGVnQXZhaWxhYmxlKCkgewogIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgY29uc3QgcHJvYyA9IHNwYXduKEZGTVBFRywgWyctdmVyc2lvbiddLCB7IHN0ZGlvOiAnaWdub3JlJyB9KTsKICAgIHByb2Mub24oJ2Vycm9yJywgKCkgPT4gcmVzb2x2ZShmYWxzZSkpOwogICAgcHJvYy5vbignY2xvc2UnLCAoY29kZSkgPT4gcmVzb2x2ZShjb2RlID09PSAwKSk7CiAgfSk7Cn0KCmZ1bmN0aW9uIG91dE5hbWUoc3JjRmlsZW5hbWUsIGV4dCkgewogIC8vIHdyYWl0aC0xNzE4Lm1wNCAtPiB3cmFpdGgtMTcxOC53ZWJtIC8gLndlYnAsIHdyaXR0ZW4gYWxvbmdzaWRlIHRoZSBzb3VyY2UuCiAgY29uc3Qgc3RlbSA9IGJhc2VuYW1lKHNyY0ZpbGVuYW1lLCBleHRuYW1lKHNyY0ZpbGVuYW1lKSk7CiAgcmV0dXJuIGAke3N0ZW19LiR7ZXh0fWA7Cn0KCi8qKgogKiBDb252ZXJ0IGEgc291cmNlIE1QNCAoYWxyZWFkeSBzYXZlZCBpbiB1cGxvYWREaXIpIGludG8gdHJhbnNwYXJlbnQgd2VibSt3ZWJwLgogKgogKiBAcGFyYW0ge3N0cmluZ30gdXBsb2FkRGlyICBhYnNvbHV0ZSB1cGxvYWRzIGRpcmVjdG9yeQogKiBAcGFyYW0ge3N0cmluZ30gc3JjRmlsZW5hbWUgdGhlIG11bHRlciBmaWxlbmFtZSBvZiB0aGUgdXBsb2FkZWQgbXA0CiAqIEByZXR1cm5zIHtQcm9taXNlPHsgd2VibTogc3RyaW5nfG51bGwsIHdlYnA6IHN0cmluZ3xudWxsIH0+fSBuZXcgZmlsZW5hbWVzCiAqICAgICAgICAgIChub3QgZnVsbCBwYXRocykuIEVpdGhlciBtYXkgYmUgbnVsbCBpZiB0aGF0IGVuY29kZSBmYWlsZWQuCiAqLwpleHBvcnQgYXN5bmMgZnVuY3Rpb24gY29udmVydEdob3N0TXA0KHVwbG9hZERpciwgc3JjRmlsZW5hbWUpIHsKICBjb25zdCBzcmNQYXRoID0gam9pbih1cGxvYWREaXIsIHNyY0ZpbGVuYW1lKTsKICBpZiAoIWV4aXN0c1N5bmMoc3JjUGF0aCkpIHRocm93IG5ldyBFcnJvcihgc291cmNlIG5vdCBmb3VuZDogJHtzcmNQYXRofWApOwoKICBjb25zdCB2ZiA9IGtleUNoYWluKCk7CiAgY29uc3QgcmVzdWx0ID0geyB3ZWJtOiBudWxsLCB3ZWJwOiBudWxsIH07CgogIC8vIC0tLSBXZWJNIChWUDkgKyBhbHBoYSk6IHRoZSBwcmltYXJ5LCBicm93c2VyLWRlY29kZWQgc3ByaXRlIC0tLQogIGNvbnN0IHdlYm1OYW1lID0gb3V0TmFtZShzcmNGaWxlbmFtZSwgJ3dlYm0nKTsKICB0cnkgewogICAgYXdhaXQgcnVuKFsKICAgICAgJy1oaWRlX2Jhbm5lcicsICctbG9nbGV2ZWwnLCAnZXJyb3InLCAnLXknLCAnLWknLCBzcmNQYXRoLAogICAgICAnLXZmJywgdmYsICctYW4nLAogICAgICAnLWM6dicsICdsaWJ2cHgtdnA5JywgJy1waXhfZm10JywgJ3l1dmE0MjBwJywKICAgICAgJy1iOnYnLCAnMCcsICctY3JmJywgVlA5X0NSRiwgJy1hdXRvLWFsdC1yZWYnLCAnMCcsCiAgICAgIGpvaW4odXBsb2FkRGlyLCB3ZWJtTmFtZSksCiAgICBdKTsKICAgIGlmIChzdGF0U3luYyhqb2luKHVwbG9hZERpciwgd2VibU5hbWUpKS5zaXplID4gMCkgcmVzdWx0LndlYm0gPSB3ZWJtTmFtZTsKICB9IGNhdGNoIChlKSB7CiAgICBjb25zb2xlLmVycm9yKCdbZ2hvc3QtbWVkaWFdIHdlYm0gZW5jb2RlIGZhaWxlZDonLCBlLm1lc3NhZ2UpOwogIH0KCiAgLy8gLS0tIFdlYlAgZmFsbGJhY2s6IGZvciBpT1MgU2FmYXJpIC8gbm8tVlA5LWFscGhhIGJyb3dzZXJzIC0tLQogIGNvbnN0IHdlYnBOYW1lID0gb3V0TmFtZShzcmNGaWxlbmFtZSwgJ3dlYnAnKTsKICB0cnkgewogICAgYXdhaXQgcnVuKFsKICAgICAgJy1oaWRlX2Jhbm5lcicsICctbG9nbGV2ZWwnLCAnZXJyb3InLCAnLXknLCAnLWknLCBzcmNQYXRoLAogICAgICAnLXZmJywgdmYsICctbG9vcCcsICcwJywgJy1hbicsCiAgICAgICctYzp2JywgJ2xpYndlYnBfYW5pbScsICctbG9zc2xlc3MnLCAnMCcsICctcTp2JywgV0VCUF9RLAogICAgICAnLXByZXNldCcsICdkcmF3aW5nJywgJy1jb21wcmVzc2lvbl9sZXZlbCcsICc2JywKICAgICAgam9pbih1cGxvYWREaXIsIHdlYnBOYW1lKSwKICAgIF0pOwogICAgaWYgKHN0YXRTeW5jKGpvaW4odXBsb2FkRGlyLCB3ZWJwTmFtZSkpLnNpemUgPiAwKSByZXN1bHQud2VicCA9IHdlYnBOYW1lOwogIH0gY2F0Y2ggKGUpIHsKICAgIGNvbnNvbGUuZXJyb3IoJ1tnaG9zdC1tZWRpYV0gd2VicCBlbmNvZGUgZmFpbGVkOicsIGUubWVzc2FnZSk7CiAgfQoKICByZXR1cm4gcmVzdWx0Owp9Cg== \ No newline at end of file +import { spawn } from 'node:child_process'; +import { existsSync, statSync } from 'node:fs'; +import { basename, extname, join } from 'node:path'; + +/* + * ghost-media.js — server-side ghost sprite conversion. + * + * Turns an uploaded black-background ghost MP4 into a transparent animated + * WebM (VP9 + alpha) plus an animated WebP fallback, using luma keying so the + * black becomes transparent and the glow keeps soft semi-transparent edges. + * + * Shells out to the system `ffmpeg` binary (no npm dependency). If ffmpeg is + * not installed, conversion fails gracefully and the caller keeps the original. + * + * This mirrors the standalone ghostify.sh pipeline so local and server output + * match. + */ + +const FFMPEG = process.env.FFMPEG_PATH || 'ffmpeg'; + +// Tunables (env-overridable) — kept in sync with ghostify.sh defaults. +const THRESH = process.env.GHOST_KEY_THRESHOLD || '0.06'; +const TOL = process.env.GHOST_KEY_TOLERANCE || '0.10'; +const SOFT = process.env.GHOST_KEY_SOFTNESS || '0.15'; +const FPS = process.env.GHOST_FPS || '15'; +const MAXW = process.env.GHOST_MAXW || '512'; +const WEBP_Q = process.env.GHOST_WEBP_QUALITY || '80'; +const VP9_CRF = process.env.GHOST_VP9_CRF || '30'; + +// The shared luma-key + downscale filter chain. +function keyChain() { + const key = `format=rgba,lumakey=threshold=${THRESH}:tolerance=${TOL}:softness=${SOFT}`; + const scale = `scale='min(iw,${MAXW})':-1:flags=lanczos`; + return `${key},fps=${FPS},${scale}`; +} + +function run(args) { + return new Promise((resolve, reject) => { + const proc = spawn(FFMPEG, args, { stdio: ['ignore', 'ignore', 'pipe'] }); + let stderr = ''; + proc.stderr.on('data', (d) => { stderr += d.toString(); }); + proc.on('error', reject); // e.g. ffmpeg not found (ENOENT) + proc.on('close', (code) => { + if (code === 0) resolve(); + else reject(new Error(`ffmpeg exited ${code}: ${stderr.slice(-500)}`)); + }); + }); +} + +/** True if a usable ffmpeg binary is on PATH. */ +export function ffmpegAvailable() { + return new Promise((resolve) => { + const proc = spawn(FFMPEG, ['-version'], { stdio: 'ignore' }); + proc.on('error', () => resolve(false)); + proc.on('close', (code) => resolve(code === 0)); + }); +} + +function outName(srcFilename, ext) { + // wraith-1718.mp4 -> wraith-1718.webm / .webp, written alongside the source. + const stem = basename(srcFilename, extname(srcFilename)); + return `${stem}.${ext}`; +} + +/** + * Convert a source MP4 (already saved in uploadDir) into transparent webm+webp. + * + * @param {string} uploadDir absolute uploads directory + * @param {string} srcFilename the multer filename of the uploaded mp4 + * @returns {Promise<{ webm: string|null, webp: string|null }>} new filenames + * (not full paths). Either may be null if that encode failed. + */ +export async function convertGhostMp4(uploadDir, srcFilename) { + const srcPath = join(uploadDir, srcFilename); + if (!existsSync(srcPath)) throw new Error(`source not found: ${srcPath}`); + + const vf = keyChain(); + const result = { webm: null, webp: null }; + + // --- WebM (VP9 + alpha): the primary, browser-decoded sprite --- + const webmName = outName(srcFilename, 'webm'); + try { + await run([ + '-hide_banner', '-loglevel', 'error', '-y', '-i', srcPath, + '-vf', vf, '-an', + '-c:v', 'libvpx-vp9', '-pix_fmt', 'yuva420p', + '-b:v', '0', '-crf', VP9_CRF, '-auto-alt-ref', '0', + join(uploadDir, webmName), + ]); + if (statSync(join(uploadDir, webmName)).size > 0) result.webm = webmName; + } catch (e) { + console.error('[ghost-media] webm encode failed:', e.message); + } + + // --- WebP fallback: for iOS Safari / no-VP9-alpha browsers --- + const webpName = outName(srcFilename, 'webp'); + try { + await run([ + '-hide_banner', '-loglevel', 'error', '-y', '-i', srcPath, + '-vf', vf, '-loop', '0', '-an', + '-c:v', 'libwebp_anim', '-lossless', '0', '-q:v', WEBP_Q, + '-preset', 'drawing', '-compression_level', '6', + join(uploadDir, webpName), + ]); + if (statSync(join(uploadDir, webpName)).size > 0) result.webp = webpName; + } catch (e) { + console.error('[ghost-media] webp encode failed:', e.message); + } + + return result; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3c4373e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1496 @@ +{ + "name": "newbury-nights", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "newbury-nights", + "version": "0.1.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.3.0", + "cookie-parser": "^1.4.6", + "dotenv": "^16.4.5", + "express": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/public/js/game.js b/public/js/game.js index a151bed..9080c6d 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -1 +1,654 @@ -aW1wb3J0ICogYXMgVEhSRUUgZnJvbSAndGhyZWUnOwoKLyogPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiAgIE5ld2J1cnkgTmlnaHRzIOKAlCBjbGllbnQgZ2FtZSBsb2dpYwogICAtIFNjcmVlbiByb3V0aW5nICh0aXRsZSAvIHNjYW4gLyBodW50IC8gcm9zdGVyKQogICAtIFFSIHNjYW4gdmlhIEJhcmNvZGVEZXRlY3RvciAobWFudWFsIGNvZGUgZmFsbGJhY2spCiAgIC0gQVIgaHVudDogY2FtZXJhIHBhc3N0aHJvdWdoICsgRGV2aWNlT3JpZW50YXRpb24gZ3lybyBsb29rLAogICAgIGNvbG9yLXdoZWVsIGdsb29tIGRldGVjdGlvbiwgYmxhc3RlciB3aXRoIG92ZXJoZWF0LAogICAgIHByb2NlZHVyYWwgd2lzcCBtZXNoZXMgKyBhbmltYXRlZC1HSUYgYmlsbGJvYXJkcy4KICAgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09ICovCgpjb25zdCAkID0gKHMsIHIgPSBkb2N1bWVudCkgPT4gci5xdWVyeVNlbGVjdG9yKHMpOwpjb25zdCAkJCA9IChzLCByID0gZG9jdW1lbnQpID0+IFsuLi5yLnF1ZXJ5U2VsZWN0b3JBbGwocyldOwoKY29uc3Qgc2NyZWVucyA9IHsKICB0aXRsZTogJCgnI3NjcmVlbi10aXRsZScpLAogIHNjYW46ICQoJyNzY3JlZW4tc2NhbicpLAogIGh1bnQ6ICQoJyNzY3JlZW4taHVudCcpLAogIHJvc3RlcjogJCgnI3NjcmVlbi1yb3N0ZXInKSwKICBhYm91dDogJCgnI3NjcmVlbi1hYm91dCcpLAp9OwpmdW5jdGlvbiBzaG93KG5hbWUpIHsKICBPYmplY3QudmFsdWVzKHNjcmVlbnMpLmZvckVhY2goKHMpID0+IHMuY2xhc3NMaXN0LmFkZCgnaGlkZGVuJykpOwogIHNjcmVlbnNbbmFtZV0uY2xhc3NMaXN0LnJlbW92ZSgnaGlkZGVuJyk7Cn0KCi8qIC0tLS0tLS0tLS0gVGl0bGUgbmF2IC0tLS0tLS0tLS0gKi8KJCgnI2J0bi1zY2FuLXNldCcpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gc3RhcnRTY2FuKCkpOwokKCcjYnRuLWZyZWVodW50JykuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiBzdGFydEh1bnQoeyBmcmVlSHVudDogdHJ1ZSB9KSk7CiQoJyNidG4tcm9zdGVyJykuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiBvcGVuUm9zdGVyKCkpOwokKCcjYnRuLWFib3V0JykuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiBzaG93KCdhYm91dCcpKTsKJCQoJ1tkYXRhLWJhY2tdJykuZm9yRWFjaCgoYikgPT4gYi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHJldHVybkhvbWUoKSkpOwoKZnVuY3Rpb24gcmV0dXJuSG9tZSgpIHsKICBzdG9wU2NhbigpOwogIGh1bnQuc3RvcCgpOwogIHNob3coJ3RpdGxlJyk7Cn0KCi8qID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQogICBTQ0FOCiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSAqLwpsZXQgc2NhblN0cmVhbSA9IG51bGw7CmxldCBzY2FuTG9vcCA9IG51bGw7CmxldCBkZXRlY3RvciA9IG51bGw7Cgphc3luYyBmdW5jdGlvbiBzdGFydFNjYW4oKSB7CiAgc2hvdygnc2NhbicpOwogICQoJyNzY2FuLWVycm9yJykuY2xhc3NMaXN0LmFkZCgnaGlkZGVuJyk7CiAgY29uc3Qgc3RhdHVzID0gJCgnI3NjYW4tc3RhdHVzJyk7CiAgY29uc3QgdmlkZW8gPSAkKCcjc2Nhbi12aWRlbycpOwoKICBpZiAoJ0JhcmNvZGVEZXRlY3RvcicgaW4gd2luZG93KSB7CiAgICB0cnkgewogICAgICBjb25zdCBmb3JtYXRzID0gYXdhaXQgd2luZG93LkJhcmNvZGVEZXRlY3Rvci5nZXRTdXBwb3J0ZWRGb3JtYXRzKCk7CiAgICAgIGlmIChmb3JtYXRzLmluY2x1ZGVzKCdxcl9jb2RlJykpIHsKICAgICAgICBkZXRlY3RvciA9IG5ldyB3aW5kb3cuQmFyY29kZURldGVjdG9yKHsgZm9ybWF0czogWydxcl9jb2RlJ10gfSk7CiAgICAgIH0KICAgIH0gY2F0Y2ggeyBkZXRlY3RvciA9IG51bGw7IH0KICB9CgogIHRyeSB7CiAgICBzY2FuU3RyZWFtID0gYXdhaXQgbmF2aWdhdG9yLm1lZGlhRGV2aWNlcy5nZXRVc2VyTWVkaWEoewogICAgICB2aWRlbzogeyBmYWNpbmdNb2RlOiAnZW52aXJvbm1lbnQnIH0sIGF1ZGlvOiBmYWxzZSwKICAgIH0pOwogICAgdmlkZW8uc3JjT2JqZWN0ID0gc2NhblN0cmVhbTsKICAgIGF3YWl0IHZpZGVvLnBsYXkoKTsKICAgIHN0YXR1cy50ZXh0Q29udGVudCA9IGRldGVjdG9yID8gJ0xvb2tpbmcgZm9yIGEgc2V0IGNvZGXigKYnIDogJ0NhbWVyYSBvbiDigJQgZW50ZXIgY29kZSBiZWxvdyc7CiAgICBpZiAoZGV0ZWN0b3IpIHRpY2tTY2FuKHZpZGVvKTsKICB9IGNhdGNoIChlKSB7CiAgICBzdGF0dXMudGV4dENvbnRlbnQgPSAnQ2FtZXJhIHVuYXZhaWxhYmxlIOKAlCBlbnRlciB0aGUgY29kZSBiZWxvdyc7CiAgfQp9Cgphc3luYyBmdW5jdGlvbiB0aWNrU2Nhbih2aWRlbykgewogIGlmICghZGV0ZWN0b3IgfHwgIXNjYW5TdHJlYW0pIHJldHVybjsKICB0cnkgewogICAgY29uc3QgY29kZXMgPSBhd2FpdCBkZXRlY3Rvci5kZXRlY3QodmlkZW8pOwogICAgaWYgKGNvZGVzLmxlbmd0aCkgewogICAgICBjb25zdCByYXcgPSBjb2Rlc1swXS5yYXdWYWx1ZT8udHJpbSgpOwogICAgICBpZiAocmF3KSB7IHJlc29sdmVDb2RlKHJhdyk7IHJldHVybjsgfQogICAgfQogIH0gY2F0Y2ggeyAvKiBrZWVwIHRyeWluZyAqLyB9CiAgc2Nhbkxvb3AgPSByZXF1ZXN0QW5pbWF0aW9uRnJhbWUoKCkgPT4gdGlja1NjYW4odmlkZW8pKTsKfQoKZnVuY3Rpb24gc3RvcFNjYW4oKSB7CiAgaWYgKHNjYW5Mb29wKSBjYW5jZWxBbmltYXRpb25GcmFtZShzY2FuTG9vcCksIChzY2FuTG9vcCA9IG51bGwpOwogIGlmIChzY2FuU3RyZWFtKSB7IHNjYW5TdHJlYW0uZ2V0VHJhY2tzKCkuZm9yRWFjaCgodCkgPT4gdC5zdG9wKCkpOyBzY2FuU3RyZWFtID0gbnVsbDsgfQp9CgokKCcjYnRuLW1hbnVhbC1nbycpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gewogIGNvbnN0IGNvZGUgPSAkKCcjbWFudWFsLWNvZGUnKS52YWx1ZS50cmltKCk7CiAgaWYgKGNvZGUpIHJlc29sdmVDb2RlKGNvZGUpOwp9KTsKJCgnI21hbnVhbC1jb2RlJykuYWRkRXZlbnRMaXN0ZW5lcigna2V5ZG93bicsIChlKSA9PiB7CiAgaWYgKGUua2V5ID09PSAnRW50ZXInKSAkKCcjYnRuLW1hbnVhbC1nbycpLmNsaWNrKCk7Cn0pOwoKYXN5bmMgZnVuY3Rpb24gcmVzb2x2ZUNvZGUoY29kZSkgewogIC8vIEFjY2VwdCBlaXRoZXIgYSBiYXJlIGNvZGUgb3IgYSBVUkwgd2hvc2UgbGFzdCBwYXRoL3F1ZXJ5IHNlZ21lbnQgaXMgdGhlIGNvZGUuCiAgY29uc3QgY2xlYW5lZCA9IGNvZGUuaW5jbHVkZXMoJy8nKSA/IGNvZGUuc3BsaXQoL1tcLz89XS8pLmZpbHRlcihCb29sZWFuKS5wb3AoKSA6IGNvZGU7CiAgdHJ5IHsKICAgIGNvbnN0IHJlcyA9IGF3YWl0IGZldGNoKGAvYXBpL3NjYW4vJHtlbmNvZGVVUklDb21wb25lbnQoY2xlYW5lZCl9YCk7CiAgICBpZiAoIXJlcy5vaykgdGhyb3cgbmV3IEVycm9yKCdVbmtub3duIHNldCBjb2RlJyk7CiAgICBjb25zdCBkYXRhID0gYXdhaXQgcmVzLmpzb24oKTsKICAgIHN0b3BTY2FuKCk7CiAgICBzdGFydEh1bnQoeyBzZXREYXRhOiBkYXRhIH0pOwogIH0gY2F0Y2ggKGUpIHsKICAgIGNvbnN0IGVyciA9ICQoJyNzY2FuLWVycm9yJyk7CiAgICBlcnIudGV4dENvbnRlbnQgPSBgQ291bGRuJ3QgZmluZCBhIHNldCBmb3IgIiR7Y2xlYW5lZH0iLiBDaGVjayB0aGUgY29kZSBhbmQgdHJ5IGFnYWluLmA7CiAgICBlcnIuY2xhc3NMaXN0LnJlbW92ZSgnaGlkZGVuJyk7CiAgfQp9CgovKiA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KICAgSFVOVCAoQVIpCiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSAqLwpjb25zdCBUWVBFX0NPTE9SUyA9IHsgcmVkOiAweGZmM2I1YywgeWVsbG93OiAweGZmYzIzYiwgYmx1ZTogMHgzYmI2ZmYgfTsKCi8vIERldGVjdCBWUDktd2l0aC1hbHBoYSBXZWJNIHN1cHBvcnQgb25jZS4gaU9TIFNhZmFyaSBoaXN0b3JpY2FsbHkgbGFja3MKLy8gcmVsaWFibGUgVlA5LWFscGhhLCBzbyB0aG9zZSBkZXZpY2VzIGZhbGwgYmFjayB0byB0aGUgR0lGL1dlYlAgPGltZz4gcGF0aC4KY29uc3QgU1VQUE9SVFNfV0VCTV9BTFBIQSA9ICgoKSA9PiB7CiAgdHJ5IHsKICAgIGNvbnN0IHYgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCd2aWRlbycpOwogICAgcmV0dXJuICEhdi5jYW5QbGF5VHlwZSAmJiB2LmNhblBsYXlUeXBlKCd2aWRlby93ZWJtOyBjb2RlY3M9InZwOSInKSAhPT0gJyc7CiAgfSBjYXRjaCB7CiAgICByZXR1cm4gZmFsc2U7CiAgfQp9KSgpOwoKY29uc3QgaHVudCA9IHsKICBydW5uaW5nOiBmYWxzZSwKICBzY2VuZTogbnVsbCwgY2FtZXJhOiBudWxsLCByZW5kZXJlcjogbnVsbCwgcmFmOiBudWxsLAogIHN0cmVhbTogbnVsbCwgdmlkZW86IG51bGwsCiAgZ2hvc3RzOiBbXSwgICAgICAgICAgLy8gYWN0aXZlIGdob3N0IG9iamVjdHMgaW4gdGhlIHNjZW5lCiAgdGFyZ2V0OiBudWxsLCAgICAgICAgLy8gY3VycmVudGx5IGxvY2tlZCBnaG9zdAogIGFybWVkVHlwZTogbnVsbCwgICAgIC8vIGNvbG9yLXdoZWVsIGFybWVkIHR5cGUKICBiYXR0ZXJ5OiAxMDAsCiAgZ2xvb206IDAsCiAgb3ZlcmhlYXQ6IDAsCiAgYmxhc3Rpbmc6IGZhbHNlLAogIG9yaWVudGF0aW9uOiB7IGFscGhhOiAwLCBiZXRhOiAwLCBnYW1tYTogMCwgYWN0aXZlOiBmYWxzZSB9LAogIHBvb2w6IFtdLCAgICAgICAgICAgIC8vIGdob3N0cyBhdmFpbGFibGUgdG8gc3Bhd24gKGZyb20gc2V0IHJvc3RlciBvciBmcmVlaHVudCkKICBib3NzOiBudWxsLAogIHJlbWFpbmluZzogMCwgICAgICAgIC8vIHNwb3RzIGxlZnQgdG8gY2xlYXIgKHNldCBtb2RlKQogIGZyZWVIdW50OiBmYWxzZSwKCiAgYXN5bmMgc3RhcnQoeyBzZXREYXRhLCBmcmVlSHVudCB9KSB7CiAgICBzaG93KCdodW50Jyk7CiAgICB0aGlzLnJ1bm5pbmcgPSB0cnVlOwogICAgdGhpcy5mcmVlSHVudCA9ICEhZnJlZUh1bnQ7CiAgICB0aGlzLmJhdHRlcnkgPSAxMDA7IHRoaXMuZ2xvb20gPSAwOyB0aGlzLm92ZXJoZWF0ID0gMDsgdGhpcy50YXJnZXQgPSBudWxsOyB0aGlzLmFybWVkVHlwZSA9IG51bGw7CiAgICB0aGlzLmdob3N0cyA9IFtdOwogICAgJCgnI3Jlc3VsdCcpLmNsYXNzTGlzdC5hZGQoJ2hpZGRlbicpOwogICAgJCgnI2xvY2tvbicpLmNsYXNzTGlzdC5hZGQoJ2hpZGRlbicpOwogICAgdGhpcy51cGRhdGVCYXR0ZXJ5KCk7IHRoaXMudXBkYXRlR2xvb20oKTsKCiAgICBpZiAoZnJlZUh1bnQpIHsKICAgICAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2goJy9hcGkvZnJlZWh1bnQ/bj02Jyk7CiAgICAgIHRoaXMucG9vbCA9IChhd2FpdCByZXMuanNvbigpKS5zcGF3bnM7CiAgICAgIHRoaXMuYm9zcyA9IG51bGw7CiAgICAgIHRoaXMucmVtYWluaW5nID0gSW5maW5pdHk7CiAgICB9IGVsc2UgewogICAgICB0aGlzLnBvb2wgPSBzZXREYXRhLnJvc3Rlci5maWx0ZXIoKGcpID0+ICFnLmlzQm9zcyk7CiAgICAgIHRoaXMuYm9zcyA9IHNldERhdGEuYm9zczsKICAgICAgdGhpcy5yZW1haW5pbmcgPSBNYXRoLm1pbig1LCB0aGlzLnBvb2wubGVuZ3RoKTsgLy8gc3BvdHMgdG8gY2xlYXIgYmVmb3JlIGJvc3MKICAgIH0KCiAgICBhd2FpdCB0aGlzLmluaXRDYW1lcmEoKTsKICAgIHRoaXMuaW5pdFNjZW5lKCk7CiAgICB0aGlzLmJpbmRDb250cm9scygpOwogICAgdGhpcy5tYXliZVByb21wdE1vdGlvbigpOwogICAgdGhpcy5zcGF3bk5leHQoKTsKICAgIHRoaXMubG9vcCgpOwogIH0sCgogIC8vIE9uIGlPUywgc3VyZmFjZSBhbiBleHBsaWNpdCAiRW5hYmxlIE1vdGlvbiIgYnV0dG9uLiBUYXBwaW5nIGl0IGlzIHRoZSBjbGVhbgogIC8vIHVzZXIgZ2VzdHVyZSBpT1MgbmVlZHMgdG8gc2hvdyBpdHMgTW90aW9uICYgT3JpZW50YXRpb24gQWNjZXNzIHByb21wdC4KICBtYXliZVByb21wdE1vdGlvbigpIHsKICAgIGNvbnN0IG92ZXJsYXkgPSAkKCcjbW90aW9uLWdhdGUnKTsKICAgIGlmICghdGhpcy5neXJvTmVlZHNQZXJtaXNzaW9uKCkpIHsgb3ZlcmxheS5jbGFzc0xpc3QuYWRkKCdoaWRkZW4nKTsgcmV0dXJuOyB9CiAgICBvdmVybGF5LmNsYXNzTGlzdC5yZW1vdmUoJ2hpZGRlbicpOwogICAgY29uc3QgYnRuID0gJCgnI21vdGlvbi1lbmFibGUnKTsKICAgIGNvbnN0IG9uVGFwID0gYXN5bmMgKCkgPT4gewogICAgICBjb25zdCBvayA9IGF3YWl0IHRoaXMucmVxdWVzdEd5cm8oKTsKICAgICAgb3ZlcmxheS5jbGFzc0xpc3QuYWRkKCdoaWRkZW4nKTsKICAgICAgaWYgKCFvaykgdGhpcy50b2FzdCgnTW90aW9uIGJsb2NrZWQg4oCUIGVuYWJsZSBpdCBpbiBTZXR0aW5ncyDigLogU2FmYXJpJywgMjIwMCk7CiAgICB9OwogICAgYnRuLm9uY2xpY2sgPSBvblRhcDsKICAgIHRoaXMuX21vdGlvbkVscyA9IHsgb3ZlcmxheSwgYnRuIH07CiAgfSwKCiAgYXN5bmMgaW5pdENhbWVyYSgpIHsKICAgIHRoaXMudmlkZW8gPSAkKCcjYXItdmlkZW8nKTsKICAgIHRyeSB7CiAgICAgIHRoaXMuc3RyZWFtID0gYXdhaXQgbmF2aWdhdG9yLm1lZGlhRGV2aWNlcy5nZXRVc2VyTWVkaWEoewogICAgICAgIHZpZGVvOiB7IGZhY2luZ01vZGU6ICdlbnZpcm9ubWVudCcgfSwgYXVkaW86IGZhbHNlLAogICAgICB9KTsKICAgICAgdGhpcy52aWRlby5zcmNPYmplY3QgPSB0aGlzLnN0cmVhbTsKICAgICAgYXdhaXQgdGhpcy52aWRlby5wbGF5KCk7CiAgICB9IGNhdGNoIHsgLyogbm8gY2FtZXJhIOKGkiBkYXJrIGJhY2tkcm9wLCBneXJvIHN0aWxsIHdvcmtzICovIH0KICB9LAoKICBpbml0U2NlbmUoKSB7CiAgICBjb25zdCBjYW52YXMgPSAkKCcjYXItY2FudmFzJyk7CiAgICB0aGlzLnJlbmRlcmVyID0gbmV3IFRIUkVFLldlYkdMUmVuZGVyZXIoeyBjYW52YXMsIGFscGhhOiB0cnVlLCBhbnRpYWxpYXM6IHRydWUgfSk7CiAgICB0aGlzLnJlbmRlcmVyLnNldFBpeGVsUmF0aW8oTWF0aC5taW4oZGV2aWNlUGl4ZWxSYXRpbywgMikpOwogICAgdGhpcy5yZW5kZXJlci5zZXRTaXplKGlubmVyV2lkdGgsIGlubmVySGVpZ2h0KTsKCiAgICB0aGlzLnNjZW5lID0gbmV3IFRIUkVFLlNjZW5lKCk7CiAgICB0aGlzLmNhbWVyYSA9IG5ldyBUSFJFRS5QZXJzcGVjdGl2ZUNhbWVyYSg3MCwgaW5uZXJXaWR0aCAvIGlubmVySGVpZ2h0LCAwLjEsIDEwMCk7CiAgICB0aGlzLmNhbWVyYS5wb3NpdGlvbi5zZXQoMCwgMCwgMCk7CgogICAgdGhpcy5zY2VuZS5hZGQobmV3IFRIUkVFLkFtYmllbnRMaWdodCgweGZmZmZmZiwgMC44KSk7CiAgICBjb25zdCBwID0gbmV3IFRIUkVFLlBvaW50TGlnaHQoMHg4OGFhZmYsIDEuMiwgNTApOwogICAgcC5wb3NpdGlvbi5zZXQoMCwgMiwgMik7CiAgICB0aGlzLnNjZW5lLmFkZChwKTsKCiAgICB0aGlzLl9vblJlc2l6ZSA9ICgpID0+IHsKICAgICAgdGhpcy5jYW1lcmEuYXNwZWN0ID0gaW5uZXJXaWR0aCAvIGlubmVySGVpZ2h0OwogICAgICB0aGlzLmNhbWVyYS51cGRhdGVQcm9qZWN0aW9uTWF0cml4KCk7CiAgICAgIHRoaXMucmVuZGVyZXIuc2V0U2l6ZShpbm5lcldpZHRoLCBpbm5lckhlaWdodCk7CiAgICB9OwogICAgYWRkRXZlbnRMaXN0ZW5lcigncmVzaXplJywgdGhpcy5fb25SZXNpemUpOwoKICAgIC8vIEd5cm8gbG9vay4gaU9TIFNhZmFyaSByZXF1aXJlcyBEZXZpY2VPcmllbnRhdGlvbkV2ZW50LnJlcXVlc3RQZXJtaXNzaW9uKCkKICAgIC8vIGZyb20gYW4gZXhwbGljaXQgdXNlciBnZXN0dXJlIChoYW5kbGVkIGJ5IHRoZSAiRW5hYmxlIE1vdGlvbiIgYnV0dG9uIGluCiAgICAvLyBzdGFydCgpKS4gT3RoZXIgYnJvd3NlcnMgY2FuIGF0dGFjaCB0aGUgbGlzdGVuZXIgaW1tZWRpYXRlbHkuCiAgICB0aGlzLl9vbk9yaWVudCA9IChlKSA9PiB7CiAgICAgIGlmIChlLmFscGhhID09IG51bGwpIHJldHVybjsKICAgICAgdGhpcy5vcmllbnRhdGlvbiA9IHsgYWxwaGE6IGUuYWxwaGEsIGJldGE6IGUuYmV0YSwgZ2FtbWE6IGUuZ2FtbWEsIGFjdGl2ZTogdHJ1ZSB9OwogICAgfTsKICAgIGlmICghdGhpcy5neXJvTmVlZHNQZXJtaXNzaW9uKCkpIHsKICAgICAgYWRkRXZlbnRMaXN0ZW5lcignZGV2aWNlb3JpZW50YXRpb24nLCB0aGlzLl9vbk9yaWVudCk7CiAgICAgIHRoaXMuX2d5cm9BdHRhY2hlZCA9IHRydWU7CiAgICB9CiAgfSwKCiAgZ3lyb05lZWRzUGVybWlzc2lvbigpIHsKICAgIGNvbnN0IERPRSA9IHdpbmRvdy5EZXZpY2VPcmllbnRhdGlvbkV2ZW50OwogICAgcmV0dXJuICEhKERPRSAmJiB0eXBlb2YgRE9FLnJlcXVlc3RQZXJtaXNzaW9uID09PSAnZnVuY3Rpb24nKTsKICB9LAoKICAvLyBDYWxsZWQgZnJvbSBhIGNsZWFuIHRhcCAodGhlIEVuYWJsZSBNb3Rpb24gb3ZlcmxheSkuIE9uIGlPUyB0aGlzIHRyaWdnZXJzCiAgLy8gdGhlIHN5c3RlbSAiTW90aW9uICYgT3JpZW50YXRpb24gQWNjZXNzIiBwcm9tcHQ7IG9uY2UgZ3JhbnRlZCB3ZSBhdHRhY2ggdGhlCiAgLy8gbGlzdGVuZXIuIFNhZmUgdG8gY2FsbCBtb3JlIHRoYW4gb25jZS4KICBhc3luYyByZXF1ZXN0R3lybygpIHsKICAgIGlmICh0aGlzLl9neXJvQXR0YWNoZWQpIHJldHVybiB0cnVlOwogICAgY29uc3QgRE9FID0gd2luZG93LkRldmljZU9yaWVudGF0aW9uRXZlbnQ7CiAgICB0cnkgewogICAgICBpZiAoRE9FICYmIHR5cGVvZiBET0UucmVxdWVzdFBlcm1pc3Npb24gPT09ICdmdW5jdGlvbicpIHsKICAgICAgICBjb25zdCByZXMgPSBhd2FpdCBET0UucmVxdWVzdFBlcm1pc3Npb24oKTsKICAgICAgICBpZiAocmVzICE9PSAnZ3JhbnRlZCcpIHJldHVybiBmYWxzZTsKICAgICAgfQogICAgICBhZGRFdmVudExpc3RlbmVyKCdkZXZpY2VvcmllbnRhdGlvbicsIHRoaXMuX29uT3JpZW50KTsKICAgICAgdGhpcy5fZ3lyb0F0dGFjaGVkID0gdHJ1ZTsKICAgICAgcmV0dXJuIHRydWU7CiAgICB9IGNhdGNoIHsKICAgICAgcmV0dXJuIGZhbHNlOwogICAgfQogIH0sCgogIGJpbmRDb250cm9scygpIHsKICAgIC8vIGNvbG9yIHdoZWVsCiAgICAkJCgnLndoZWVsLXNlZycpLmZvckVhY2goKHNlZykgPT4gewogICAgICBzZWcub25jbGljayA9ICgpID0+IHsKICAgICAgICBjb25zdCB0eXBlID0gc2VnLmRhdGFzZXQudHlwZTsKICAgICAgICB0aGlzLmFybWVkVHlwZSA9IHR5cGU7CiAgICAgICAgJCQoJy53aGVlbC1zZWcnKS5mb3JFYWNoKChzKSA9PiBzLmNsYXNzTGlzdC5yZW1vdmUoJ2FybWVkJykpOwogICAgICAgIHNlZy5jbGFzc0xpc3QuYWRkKCdhcm1lZCcpOwogICAgICAgICQoJyN3aGVlbC1jb3JlJykudGV4dENvbnRlbnQgPSB0eXBlLnRvVXBwZXJDYXNlKCk7CiAgICAgICAgdGhpcy5zY2FuR2xvb20odHlwZSk7CiAgICAgIH07CiAgICB9KTsKCiAgICBjb25zdCBibGFzdCA9ICQoJyNidG4tYmxhc3QnKTsKICAgIGNvbnN0IHN0YXJ0Qmxhc3QgPSAoZSkgPT4gewogICAgICBlLnByZXZlbnREZWZhdWx0KCk7CiAgICAgIHRoaXMuYmxhc3RpbmcgPSB0cnVlOwogICAgfTsKICAgIGNvbnN0IGVuZEJsYXN0ID0gKCkgPT4geyB0aGlzLmJsYXN0aW5nID0gZmFsc2U7IH07CiAgICBibGFzdC5hZGRFdmVudExpc3RlbmVyKCd0b3VjaHN0YXJ0Jywgc3RhcnRCbGFzdCwgeyBwYXNzaXZlOiBmYWxzZSB9KTsKICAgIGJsYXN0LmFkZEV2ZW50TGlzdGVuZXIoJ21vdXNlZG93bicsIHN0YXJ0Qmxhc3QpOwogICAgYWRkRXZlbnRMaXN0ZW5lcigndG91Y2hlbmQnLCBlbmRCbGFzdCk7CiAgICBhZGRFdmVudExpc3RlbmVyKCdtb3VzZXVwJywgZW5kQmxhc3QpOwogICAgdGhpcy5fYmxhc3RFbHMgPSB7IGJsYXN0LCBzdGFydEJsYXN0LCBlbmRCbGFzdCB9OwogIH0sCgogIC8qIHNwYXduIGEgd2lzcC9iaWxsYm9hcmQgZ2hvc3QgYXQgYSByYW5kb20gc3BvdCBhcm91bmQgdGhlIHBsYXllciAqLwogIHNwYXduTmV4dCgpIHsKICAgIGlmICghdGhpcy5wb29sLmxlbmd0aCkgcmV0dXJuOwogICAgY29uc3QgZGF0YSA9IHRoaXMucG9vbFtNYXRoLmZsb29yKE1hdGgucmFuZG9tKCkgKiB0aGlzLnBvb2wubGVuZ3RoKV07CiAgICB0aGlzLmFkZEdob3N0KGRhdGEpOwogIH0sCgogIGFkZEdob3N0KGRhdGEsIGlzQm9zcyA9IGZhbHNlKSB7CiAgICBjb25zdCBncm91cCA9IG5ldyBUSFJFRS5Hcm91cCgpOwogICAgY29uc3QgY29sb3IgPSBUWVBFX0NPTE9SU1tkYXRhLnR5cGVdID8/IDB4ZmZmZmZmOwoKICAgIGxldCBtZXNoOwogICAgbGV0IHRleHR1cmUgPSBudWxsOwogICAgaWYgKGRhdGEud2VibSAmJiBTVVBQT1JUU19XRUJNX0FMUEhBKSB7CiAgICAgIC8vIFdlYk0gKFZQOSthbHBoYSkgYmlsbGJvYXJkIHZpYSBWaWRlb1RleHR1cmUuIFRoZSBicm93c2VyIGRlY29kZXMgYW5kCiAgICAgIC8vIHVwZGF0ZXMgdGhlIHRleHR1cmUgaXRzZWxmIOKAlCBubyBwZXItZnJhbWUgbmVlZHNVcGRhdGUgcHVtcGluZyBuZWVkZWQuCiAgICAgIGNvbnN0IHZpZCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3ZpZGVvJyk7CiAgICAgIHZpZC5jcm9zc09yaWdpbiA9ICdhbm9ueW1vdXMnOwogICAgICB2aWQubXV0ZWQgPSB0cnVlOyAgICAgICAgLy8gcmVxdWlyZWQgZm9yIGF1dG9wbGF5CiAgICAgIHZpZC5sb29wID0gdHJ1ZTsKICAgICAgdmlkLnBsYXlzSW5saW5lID0gdHJ1ZTsgIC8vIGlPUzogc3RheSBpbmxpbmUsIGRvbid0IGZ1bGxzY3JlZW4KICAgICAgdmlkLmF1dG9wbGF5ID0gdHJ1ZTsKICAgICAgdmlkLnByZWxvYWQgPSAnYXV0byc7CiAgICAgIHZpZC5zcmMgPSBkYXRhLndlYm07CiAgICAgIHRleHR1cmUgPSBuZXcgVEhSRUUuVmlkZW9UZXh0dXJlKHZpZCk7CiAgICAgIHRleHR1cmUuY29sb3JTcGFjZSA9IFRIUkVFLlNSR0JDb2xvclNwYWNlOwogICAgICB0ZXh0dXJlLm1pbkZpbHRlciA9IFRIUkVFLkxpbmVhckZpbHRlcjsKICAgICAgdGV4dHVyZS5tYWdGaWx0ZXIgPSBUSFJFRS5MaW5lYXJGaWx0ZXI7CiAgICAgIHRleHR1cmUuZ2VuZXJhdGVNaXBtYXBzID0gZmFsc2U7CiAgICAgIGNvbnN0IG1hdCA9IG5ldyBUSFJFRS5NZXNoQmFzaWNNYXRlcmlhbCh7IG1hcDogdGV4dHVyZSwgdHJhbnNwYXJlbnQ6IHRydWUsIHNpZGU6IFRIUkVFLkRvdWJsZVNpZGUsIGRlcHRoV3JpdGU6IGZhbHNlIH0pOwogICAgICBtZXNoID0gbmV3IFRIUkVFLk1lc2gobmV3IFRIUkVFLlBsYW5lR2VvbWV0cnkoMS4yLCAxLjIpLCBtYXQpOwogICAgICAvLyBJZiB0aGUgdmlkZW8gZXJyb3JzIChlLmcuIGFscGhhIHVuc3VwcG9ydGVkIGRlc3BpdGUgY2FuUGxheVR5cGUpLCBzd2FwCiAgICAgIC8vIHRvIHRoZSBHSUYvaW1hZ2UgYmlsbGJvYXJkIGlmIHdlIGhhdmUgb25lLgogICAgICB2aWQub25lcnJvciA9ICgpID0+IHsKICAgICAgICBpZiAoZ3JvdXAudXNlckRhdGEudmlkZW9GZWxsQmFjaykgcmV0dXJuOwogICAgICAgIGdyb3VwLnVzZXJEYXRhLnZpZGVvRmVsbEJhY2sgPSB0cnVlOwogICAgICAgIGlmIChkYXRhLmltYWdlKSB7CiAgICAgICAgICBjb25zdCBpbWcgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdpbWcnKTsKICAgICAgICAgIGltZy5jcm9zc09yaWdpbiA9ICdhbm9ueW1vdXMnOwogICAgICAgICAgaW1nLnNyYyA9IGRhdGEuaW1hZ2U7CiAgICAgICAgICBjb25zdCB0MiA9IG5ldyBUSFJFRS5UZXh0dXJlKGltZyk7CiAgICAgICAgICBpbWcub25sb2FkID0gKCkgPT4geyB0Mi5uZWVkc1VwZGF0ZSA9IHRydWU7IH07CiAgICAgICAgICBtZXNoLm1hdGVyaWFsLm1hcCA9IHQyOwogICAgICAgICAgbWVzaC5tYXRlcmlhbC5uZWVkc1VwZGF0ZSA9IHRydWU7CiAgICAgICAgICBncm91cC51c2VyRGF0YS5naWZJbWcgPSBpbWc7CiAgICAgICAgICBncm91cC51c2VyRGF0YS5naWZUZXggPSB0MjsKICAgICAgICAgIGdyb3VwLnVzZXJEYXRhLnZpZEVsID0gbnVsbDsKICAgICAgICB9CiAgICAgIH07CiAgICAgIGNvbnN0IHByID0gdmlkLnBsYXkoKTsKICAgICAgaWYgKHByICYmIHByLmNhdGNoKSBwci5jYXRjaCgoKSA9PiB7fSk7CiAgICAgIGdyb3VwLnVzZXJEYXRhLnZpZEVsID0gdmlkOwogICAgfSBlbHNlIGlmIChkYXRhLmltYWdlKSB7CiAgICAgIC8vIGFuaW1hdGVkIEdJRiBiaWxsYm9hcmQg4oCUIHRleHR1cmUubmVlZHNVcGRhdGUgcHVtcGVkIGVhY2ggZnJhbWUKICAgICAgY29uc3QgaW1nID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnaW1nJyk7CiAgICAgIGltZy5jcm9zc09yaWdpbiA9ICdhbm9ueW1vdXMnOwogICAgICBpbWcuc3JjID0gZGF0YS5pbWFnZTsKICAgICAgdGV4dHVyZSA9IG5ldyBUSFJFRS5UZXh0dXJlKGltZyk7CiAgICAgIGltZy5vbmxvYWQgPSAoKSA9PiB7IHRleHR1cmUubmVlZHNVcGRhdGUgPSB0cnVlOyB9OwogICAgICBjb25zdCBtYXQgPSBuZXcgVEhSRUUuTWVzaEJhc2ljTWF0ZXJpYWwoeyBtYXA6IHRleHR1cmUsIHRyYW5zcGFyZW50OiB0cnVlLCBzaWRlOiBUSFJFRS5Eb3VibGVTaWRlIH0pOwogICAgICBtZXNoID0gbmV3IFRIUkVFLk1lc2gobmV3IFRIUkVFLlBsYW5lR2VvbWV0cnkoMS4yLCAxLjIpLCBtYXQpOwogICAgICBncm91cC51c2VyRGF0YS5naWZJbWcgPSBpbWc7CiAgICAgIGdyb3VwLnVzZXJEYXRhLmdpZlRleCA9IHRleHR1cmU7CiAgICB9IGVsc2UgewogICAgICAvLyBwcm9jZWR1cmFsIHdpc3A6IGdsb3dpbmcgc3BoZXJlICsgaGFsbwogICAgICBjb25zdCBnZW8gPSBuZXcgVEhSRUUuU3BoZXJlR2VvbWV0cnkoMC40NSwgMjQsIDI0KTsKICAgICAgY29uc3QgbWF0ID0gbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsKICAgICAgICBjb2xvciwgZW1pc3NpdmU6IGNvbG9yLCBlbWlzc2l2ZUludGVuc2l0eTogMS40LCByb3VnaG5lc3M6IDAuNCwKICAgICAgICB0cmFuc3BhcmVudDogdHJ1ZSwgb3BhY2l0eTogMC45MiwKICAgICAgfSk7CiAgICAgIG1lc2ggPSBuZXcgVEhSRUUuTWVzaChnZW8sIG1hdCk7CiAgICAgIGNvbnN0IGhhbG8gPSBuZXcgVEhSRUUuTWVzaCgKICAgICAgICBuZXcgVEhSRUUuU3BoZXJlR2VvbWV0cnkoMC42MiwgMjQsIDI0KSwKICAgICAgICBuZXcgVEhSRUUuTWVzaEJhc2ljTWF0ZXJpYWwoeyBjb2xvciwgdHJhbnNwYXJlbnQ6IHRydWUsIG9wYWNpdHk6IDAuMTYsIHNpZGU6IFRIUkVFLkJhY2tTaWRlIH0pCiAgICAgICk7CiAgICAgIGdyb3VwLmFkZChoYWxvKTsKICAgIH0KICAgIGdyb3VwLmFkZChtZXNoKTsKCiAgICAvLyBQbGFjZSBpbiBhIGZvcndhcmQtZmFjaW5nIGFyYyBzbyBnaG9zdHMgYXJlIGZpbmRhYmxlIHdpdGhvdXQgcGVyZmVjdAogICAgLy8gYWltaW5nOiB5YXcgd2l0aGluIMKxNjDCsCBvZiBzdHJhaWdodCBhaGVhZCwgbW9kZXN0IHBpdGNoLCBjbG9zZXIgaW4uCiAgICBjb25zdCB5YXcgPSAoTWF0aC5yYW5kb20oKSAtIDAuNSkgKiAoTWF0aC5QSSAqIDIgLyAzKTsKICAgIGNvbnN0IHBpdGNoID0gKE1hdGgucmFuZG9tKCkgLSAwLjUpICogMC41OwogICAgY29uc3QgZGlzdCA9IDMgKyBNYXRoLnJhbmRvbSgpICogMS41OwogICAgZ3JvdXAucG9zaXRpb24uc2V0KAogICAgICBNYXRoLnNpbih5YXcpICogZGlzdCwKICAgICAgTWF0aC5zaW4ocGl0Y2gpICogZGlzdCwKICAgICAgLU1hdGguY29zKHlhdykgKiBkaXN0CiAgICApOwoKICAgIGdyb3VwLnVzZXJEYXRhID0gewogICAgICAuLi5ncm91cC51c2VyRGF0YSwKICAgICAgZGF0YSwgaXNCb3NzLAogICAgICBocDogZGF0YS5oZWFsdGgsIG1heEhwOiBkYXRhLmhlYWx0aCwKICAgICAgYm9iUGhhc2U6IE1hdGgucmFuZG9tKCkgKiBNYXRoLlBJICogMiwKICAgICAgcmV2ZWFsZWQ6IHRydWUsIC8vIHZpc2libGUgb24gc3Bhd247IHRoZSB3aGVlbCBub3cgYWdncm9zL3RpbnRzIHJhdGhlciB0aGFuIHVuY2xvYWtzCiAgICB9OwogICAgdGhpcy5zY2VuZS5hZGQoZ3JvdXApOwogICAgdGhpcy5naG9zdHMucHVzaChncm91cCk7CiAgfSwKCiAgc2Nhbkdsb29tKHR5cGUpIHsKICAgIC8vIEdob3N0cyBhcmUgdmlzaWJsZSBvbiBzcGF3biBub3csIHNvIHRoZSB3aGVlbCBhY3RzIGFzIGEgImx1cmUiOiBzY2FubmluZyBhCiAgICAvLyBjb2xvdXIgcHVsbHMgdGhlIG5lYXJlc3QgbWF0Y2hpbmcgZ2hvc3QgdG93YXJkIHlvdXIgYWltIGFuZCBjaGFyZ2VzIGdsb29tLgogICAgLy8gTWlzbWF0Y2hlZCBjb2xvdXIgcmlza3MgYSBqdW1wc2NhcmUuCiAgICBjb25zdCBtYXRjaGVzID0gdGhpcy5naG9zdHMuZmlsdGVyKChnKSA9PiBnLnVzZXJEYXRhLmRhdGEudHlwZSA9PT0gdHlwZSk7CiAgICBpZiAobWF0Y2hlcy5sZW5ndGgpIHsKICAgICAgLy8gcHVsbCB0aGUgbmVhcmVzdCBtYXRjaCBpbnRvIGEgZm9yd2FyZCwgZWFzeS10by1haW0gcG9zaXRpb24KICAgICAgY29uc3QgZyA9IG1hdGNoZXNbMF07CiAgICAgIGNvbnN0IGRpc3QgPSAzOwogICAgICBnLnBvc2l0aW9uLnNldCgoTWF0aC5yYW5kb20oKSAtIDAuNSkgKiAxLjIsIChNYXRoLnJhbmRvbSgpIC0gMC4zKSAqIDAuOCwgLWRpc3QpOwogICAgICBnLnVzZXJEYXRhLmx1cmVkID0gdHJ1ZTsKICAgICAgdGhpcy5nbG9vbSArPSA1OyB0aGlzLnVwZGF0ZUdsb29tKCk7CiAgICAgIHRoaXMudG9hc3QoYCR7dHlwZS50b1VwcGVyQ2FzZSgpfSBnaG9zdCBsdXJlZCBpbmAsIDgwMCk7CiAgICB9IGVsc2UgewogICAgICAvLyBubyBnaG9zdCBvZiB0aGF0IGNvbG91ciBwcmVzZW50IOKAlCBzbWFsbCBjaGFuY2Ugb2YgYSBqdW1wc2NhcmUKICAgICAgaWYgKE1hdGgucmFuZG9tKCkgPCAwLjEyKSB0aGlzLmp1bXBzY2FyZSgpOwogICAgICBlbHNlIHsgdGhpcy5nbG9vbSArPSAyOyB0aGlzLnVwZGF0ZUdsb29tKCk7IHRoaXMudG9hc3QoJysyIGdsb29tJywgNjAwKTsgfQogICAgfQogIH0sCgogIGxvb3AoKSB7CiAgICBpZiAoIXRoaXMucnVubmluZykgcmV0dXJuOwogICAgdGhpcy5yYWYgPSByZXF1ZXN0QW5pbWF0aW9uRnJhbWUoKCkgPT4gdGhpcy5sb29wKCkpOwogICAgY29uc3QgdCA9IHBlcmZvcm1hbmNlLm5vdygpIC8gMTAwMDsKCiAgICAvLyBjYW1lcmEgeWF3L3BpdGNoIGZyb20gZ3lybwogICAgaWYgKHRoaXMub3JpZW50YXRpb24uYWN0aXZlKSB7CiAgICAgIGNvbnN0IHsgYWxwaGEsIGJldGEsIGdhbW1hIH0gPSB0aGlzLm9yaWVudGF0aW9uOwogICAgICAvLyBzaW1wbGUgbWFwcGluZzsgZ29vZCBlbm91Z2ggZm9yIGxvb2stYXJvdW5kIGFpbWluZwogICAgICB0aGlzLmNhbWVyYS5yb3RhdGlvbi5zZXQoCiAgICAgICAgVEhSRUUuTWF0aFV0aWxzLmRlZ1RvUmFkKChiZXRhID8/IDApIC0gOTApLAogICAgICAgIFRIUkVFLk1hdGhVdGlscy5kZWdUb1JhZChhbHBoYSA/PyAwKSwKICAgICAgICBUSFJFRS5NYXRoVXRpbHMuZGVnVG9SYWQoLShnYW1tYSA/PyAwKSksCiAgICAgICAgJ1lYWicKICAgICAgKTsKICAgIH0KCiAgICAvLyBhbmltYXRlIGdob3N0czogYm9iICsgZmFjZSBjYW1lcmEgKyBwdW1wIEdJRiB0ZXh0dXJlcwogICAgZm9yIChjb25zdCBnIG9mIHRoaXMuZ2hvc3RzKSB7CiAgICAgIGcucG9zaXRpb24ueSArPSBNYXRoLnNpbih0ICogMS41ICsgZy51c2VyRGF0YS5ib2JQaGFzZSkgKiAwLjAwMjU7CiAgICAgIGcubG9va0F0KHRoaXMuY2FtZXJhLnBvc2l0aW9uKTsKICAgICAgaWYgKGcudXNlckRhdGEuZ2lmVGV4ICYmIGcudXNlckRhdGEuZ2lmSW1nPy5jb21wbGV0ZSkgZy51c2VyRGF0YS5naWZUZXgubmVlZHNVcGRhdGUgPSB0cnVlOwogICAgfQoKICAgIC8vIHRhcmdldGluZzogZ2hvc3QgbmVhcmVzdCBzY3JlZW4tY2VudGVyIChhbmQgcmV2ZWFsZWQpIGJlY29tZXMgdGhlIGxvY2stb24KICAgIHRoaXMudXBkYXRlVGFyZ2V0KCk7CgogICAgLy8gYmxhc3RlcgogICAgaWYgKHRoaXMuYmxhc3RpbmcgJiYgdGhpcy5vdmVyaGVhdCA8IDEwMCAmJiB0aGlzLnRhcmdldCkgewogICAgICBjb25zdCBkbWcgPSAwLjk7IC8vIHBlci1mcmFtZSBjaGlwIGRhbWFnZQogICAgICB0aGlzLnRhcmdldC51c2VyRGF0YS5ocCAtPSBkbWc7CiAgICAgIHRoaXMub3ZlcmhlYXQgPSBNYXRoLm1pbigxMDAsIHRoaXMub3ZlcmhlYXQgKyAwLjgpOwogICAgICB0aGlzLnVwZGF0ZUdob3N0SHAoKTsKICAgICAgaWYgKHRoaXMudGFyZ2V0LnVzZXJEYXRhLmhwIDw9IDApIHRoaXMuY2FwdHVyZVRhcmdldCgpOwogICAgfSBlbHNlIGlmICghdGhpcy5ibGFzdGluZyAmJiB0aGlzLm92ZXJoZWF0ID4gMCkgewogICAgICB0aGlzLm92ZXJoZWF0ID0gTWF0aC5tYXgoMCwgdGhpcy5vdmVyaGVhdCAtIDEuMik7IC8vIGNvb2wgZG93bgogICAgfQogICAgaWYgKHRoaXMub3ZlcmhlYXQgPj0gMTAwKSB0aGlzLmJsYXN0aW5nID0gZmFsc2U7CiAgICB0aGlzLnVwZGF0ZU92ZXJoZWF0KCk7CgogICAgLy8gZ2hvc3RzIGF0dGFjazogZHJhaW4gYmF0dGVyeSBpZiBhIHJldmVhbGVkIGdob3N0IGlzIGluIGZyb250IG9mIHlvdQogICAgdGhpcy5tYXliZVRha2VEYW1hZ2UoKTsKCiAgICB0aGlzLnJlbmRlcmVyLnJlbmRlcih0aGlzLnNjZW5lLCB0aGlzLmNhbWVyYSk7CiAgfSwKCiAgdXBkYXRlVGFyZ2V0KCkgewogICAgY29uc3QgY2VudGVyID0gbmV3IFRIUkVFLlZlY3RvcjIoMCwgMCk7CiAgICBsZXQgYmVzdCA9IG51bGwsIGJlc3REaXN0ID0gMC41OyAvLyB3aXRoaW4gfmhhbGYtc2NyZWVuIG9mIGNlbnRlcgogICAgY29uc3QgdiA9IG5ldyBUSFJFRS5WZWN0b3IzKCk7CiAgICBmb3IgKGNvbnN0IGcgb2YgdGhpcy5naG9zdHMpIHsKICAgICAgaWYgKCFnLnVzZXJEYXRhLnJldmVhbGVkKSBjb250aW51ZTsKICAgICAgdi5jb3B5KGcucG9zaXRpb24pLnByb2plY3QodGhpcy5jYW1lcmEpOwogICAgICBpZiAodi56ID4gMSkgY29udGludWU7IC8vIGJlaGluZCBjYW1lcmEKICAgICAgY29uc3QgZCA9IE1hdGguaHlwb3Qodi54IC0gY2VudGVyLngsIHYueSAtIGNlbnRlci55KTsKICAgICAgaWYgKGQgPCBiZXN0RGlzdCkgeyBiZXN0RGlzdCA9IGQ7IGJlc3QgPSBnOyB9CiAgICB9CiAgICBpZiAoYmVzdCAhPT0gdGhpcy50YXJnZXQpIHsKICAgICAgdGhpcy50YXJnZXQgPSBiZXN0OwogICAgICB0aGlzLnJlbmRlckxvY2tvbigpOwogICAgfQogIH0sCgogIHJlbmRlckxvY2tvbigpIHsKICAgIGNvbnN0IGxvY2sgPSAkKCcjbG9ja29uJyk7CiAgICBpZiAoIXRoaXMudGFyZ2V0KSB7IGxvY2suY2xhc3NMaXN0LmFkZCgnaGlkZGVuJyk7IHJldHVybjsgfQogICAgY29uc3QgZCA9IHRoaXMudGFyZ2V0LnVzZXJEYXRhLmRhdGE7CiAgICBsb2NrLmNsYXNzTGlzdC5yZW1vdmUoJ2hpZGRlbicpOwogICAgLy8gSU1QT1JUQU5UOiBsYWJlbCByZWFkcyBkaXNwbGF5TmFtZSwgbm90IGEgaGFyZGNvZGVkIHRpbnQgbmFtZQogICAgJCgnI2xvY2tvbi1uYW1lJykudGV4dENvbnRlbnQgPSBkLmRpc3BsYXlOYW1lIHx8IGQubmFtZTsKICAgICQoJyNsb2Nrb24tbmFtZScpLmNsYXNzTmFtZSA9IGBsb2Nrb24tbmFtZSBkaXNwbGF5IHR5cGUtJHtkLnR5cGV9YDsKICAgICQoJyNsb2Nrb24tdHlwZScpLnRleHRDb250ZW50ID0gZC50eXBlOwogICAgJCgnI2xvY2tvbi1yYXJpdHknKS50ZXh0Q29udGVudCA9ICfimIUnLnJlcGVhdChkLnJhcml0eSk7CiAgICAkKCcjbG9ja29uLWFiaWxpdHknKS50ZXh0Q29udGVudCA9IGQuYWJpbGl0eSB8fCAn4oCUJzsKICAgIHRoaXMudXBkYXRlR2hvc3RIcCgpOwogIH0sCgogIHVwZGF0ZUdob3N0SHAoKSB7CiAgICBpZiAoIXRoaXMudGFyZ2V0KSByZXR1cm47CiAgICBjb25zdCBwY3QgPSBNYXRoLm1heCgwLCAodGhpcy50YXJnZXQudXNlckRhdGEuaHAgLyB0aGlzLnRhcmdldC51c2VyRGF0YS5tYXhIcCkgKiAxMDApOwogICAgJCgnI2dob3N0LWhwLWZpbGwnKS5zdHlsZS53aWR0aCA9IGAke3BjdH0lYDsKICB9LAoKICBjYXB0dXJlVGFyZ2V0KCkgewogICAgY29uc3QgZyA9IHRoaXMudGFyZ2V0OwogICAgY29uc3QgZCA9IGcudXNlckRhdGEuZGF0YTsKICAgIGlmIChnLnVzZXJEYXRhLnZpZEVsKSB7IHRyeSB7IGcudXNlckRhdGEudmlkRWwucGF1c2UoKTsgZy51c2VyRGF0YS52aWRFbC5zcmMgPSAnJzsgfSBjYXRjaCB7fSBnLnVzZXJEYXRhLnZpZEVsID0gbnVsbDsgfQogICAgdGhpcy5zY2VuZS5yZW1vdmUoZyk7CiAgICB0aGlzLmdob3N0cyA9IHRoaXMuZ2hvc3RzLmZpbHRlcigoeCkgPT4geCAhPT0gZyk7CiAgICB0aGlzLnRhcmdldCA9IG51bGw7CiAgICAkKCcjbG9ja29uJykuY2xhc3NMaXN0LmFkZCgnaGlkZGVuJyk7CiAgICB0aGlzLmdsb29tICs9IDEwICsgZC5yYXJpdHkgKiA1OwogICAgdGhpcy51cGRhdGVHbG9vbSgpOwogICAgdGhpcy50b2FzdChgQ2FwdHVyZWQgJHtkLmRpc3BsYXlOYW1lIHx8IGQubmFtZX0hYCwgOTAwKTsKCiAgICBpZiAoZy51c2VyRGF0YS5pc0Jvc3MpIHsgdGhpcy53aW4oZCk7IHJldHVybjsgfQoKICAgIGlmICghdGhpcy5mcmVlSHVudCkgewogICAgICB0aGlzLnJlbWFpbmluZyAtPSAxOwogICAgICBpZiAodGhpcy5yZW1haW5pbmcgPD0gMCAmJiB0aGlzLmJvc3MpIHsKICAgICAgICAvLyBzb3VsIGFydGlmYWN0IOKGkiBib3NzIGFwcGVhcnMKICAgICAgICB0aGlzLnRvYXN0KCdBIFNvdWwgQXJ0aWZhY3QgcHVsc2Vz4oCmJywgMTEwMCk7CiAgICAgICAgc2V0VGltZW91dCgoKSA9PiB0aGlzLmFkZEdob3N0KHRoaXMuYm9zcywgdHJ1ZSksIDExMDApOwogICAgICAgIHJldHVybjsKICAgICAgfQogICAgfQogICAgLy8gcmVzcGF3biBhbm90aGVyIGdob3N0IHRvIGtlZXAgdGhlIGh1bnQgZ29pbmcKICAgIHNldFRpbWVvdXQoKCkgPT4gdGhpcy5zcGF3bk5leHQoKSwgNjAwKTsKICB9LAoKICBtYXliZVRha2VEYW1hZ2UoKSB7CiAgICAvLyBhIHJldmVhbGVkIGdob3N0IHJvdWdobHkgY2VudGVyZWQgKyB3ZSdyZSBub3QgYmxhc3Rpbmcg4oaSIGl0IGdsb29tcyB1cwogICAgaWYgKHRoaXMuYmxhc3RpbmcpIHJldHVybjsKICAgIGlmICghdGhpcy50YXJnZXQpIHJldHVybjsKICAgIGlmIChNYXRoLnJhbmRvbSgpIDwgMC4wMDQpIHsKICAgICAgY29uc3QgZG1nID0gdGhpcy50YXJnZXQudXNlckRhdGEuZGF0YS5yYXJpdHkgKiAxLjU7IC8vIDTimIUgaHVydHMgbW9zdAogICAgICB0aGlzLmJhdHRlcnkgPSBNYXRoLm1heCgwLCB0aGlzLmJhdHRlcnkgLSBkbWcpOwogICAgICB0aGlzLnVwZGF0ZUJhdHRlcnkoKTsKICAgICAgdGhpcy5mbGFzaERhbWFnZSgpOwogICAgICBpZiAodGhpcy5iYXR0ZXJ5IDw9IDApIHRoaXMubG9zZSgpOwogICAgfQogIH0sCgogIC8qIC0tLS0gSFVEIHVwZGF0ZXMgLS0tLSAqLwogIHVwZGF0ZUJhdHRlcnkoKSB7CiAgICBjb25zdCBmaWxsID0gJCgnI2JhdHRlcnktZmlsbCcpOwogICAgZmlsbC5zdHlsZS53aWR0aCA9IGAke3RoaXMuYmF0dGVyeX0lYDsKICAgIGZpbGwuY2xhc3NMaXN0LnRvZ2dsZSgnbG93JywgdGhpcy5iYXR0ZXJ5IDw9IDMwKTsKICAgICQoJyNiYXR0ZXJ5LXBjdCcpLnRleHRDb250ZW50ID0gYCR7TWF0aC5yb3VuZCh0aGlzLmJhdHRlcnkpfSVgOwogIH0sCiAgdXBkYXRlR2xvb20oKSB7ICQoJyNnbG9vbS1jb3VudCcpLnRleHRDb250ZW50ID0gdGhpcy5nbG9vbTsgfSwKICB1cGRhdGVPdmVyaGVhdCgpIHsKICAgICQoJyNvdmVyaGVhdC1maWxsJykuc3R5bGUud2lkdGggPSBgJHt0aGlzLm92ZXJoZWF0fSVgOwogICAgJCgnI2J0bi1ibGFzdCcpLmNsYXNzTGlzdC50b2dnbGUoJ292ZXJoZWF0ZWQnLCB0aGlzLm92ZXJoZWF0ID49IDEwMCk7CiAgICAkKCcjYnRuLWJsYXN0JykudGV4dENvbnRlbnQgPSB0aGlzLm92ZXJoZWF0ID49IDEwMCA/ICdPVkVSSEVBVEVEJyA6ICdIT0xEIFRPIEJMQVNUJzsKICB9LAoKICB0b2FzdChtc2csIG1zID0gODAwLCBqdW1wID0gZmFsc2UpIHsKICAgIGNvbnN0IHQgPSAkKCcjdG9hc3QnKTsKICAgIHQudGV4dENvbnRlbnQgPSBtc2c7CiAgICB0LmNsYXNzTmFtZSA9IGB0b2FzdCR7anVtcCA/ICcganVtcHNjYXJlJyA6ICcnfWA7CiAgICBjbGVhclRpbWVvdXQodGhpcy5fdG9hc3RUKTsKICAgIHRoaXMuX3RvYXN0VCA9IHNldFRpbWVvdXQoKCkgPT4gdC5jbGFzc0xpc3QuYWRkKCdoaWRkZW4nKSwgbXMpOwogIH0sCiAganVtcHNjYXJlKCkgewogICAgdGhpcy50b2FzdCgnQk9PIScsIDcwMCwgdHJ1ZSk7CiAgICB0aGlzLmJhdHRlcnkgPSBNYXRoLm1heCgwLCB0aGlzLmJhdHRlcnkgLSA0KTsKICAgIHRoaXMudXBkYXRlQmF0dGVyeSgpOwogICAgaWYgKG5hdmlnYXRvci52aWJyYXRlKSBuYXZpZ2F0b3IudmlicmF0ZSgxMjApOwogIH0sCiAgZmxhc2hEYW1hZ2UoKSB7CiAgICBpZiAobmF2aWdhdG9yLnZpYnJhdGUpIG5hdmlnYXRvci52aWJyYXRlKDQwKTsKICAgIGRvY3VtZW50LmJvZHkuYW5pbWF0ZSgKICAgICAgW3sgYm94U2hhZG93OiAnaW5zZXQgMCAwIDAgOTk5OXB4IHJnYmEoMjU1LDU5LDkyLC4yNSknIH0sIHsgYm94U2hhZG93OiAnaW5zZXQgMCAwIDAgOTk5OXB4IHJnYmEoMjU1LDU5LDkyLDApJyB9XSwKICAgICAgeyBkdXJhdGlvbjogMjUwIH0KICAgICk7CiAgfSwKCiAgd2luKGJvc3MpIHsKICAgICQoJyNyZXN1bHQtdGl0bGUnKS50ZXh0Q29udGVudCA9ICdTZXQgQ2xlYXJlZCc7CiAgICAkKCcjcmVzdWx0LWJvZHknKS50ZXh0Q29udGVudCA9IGBZb3UgY2FwdHVyZWQgJHtib3NzLmRpc3BsYXlOYW1lIHx8IGJvc3MubmFtZX0gYW5kIGRyb3ZlIHRoZSBnbG9vbSBvdXQuIEdsb29tIGJhbmtlZDogJHt0aGlzLmdsb29tfS5gOwogICAgJCgnI3Jlc3VsdCcpLmNsYXNzTGlzdC5yZW1vdmUoJ2hpZGRlbicpOwogICAgdGhpcy5ydW5uaW5nID0gZmFsc2U7CiAgfSwKICBsb3NlKCkgewogICAgJCgnI3Jlc3VsdC10aXRsZScpLnRleHRDb250ZW50ID0gJ0JhdHRlcnkgRGVhZCc7CiAgICAkKCcjcmVzdWx0LWJvZHknKS50ZXh0Q29udGVudCA9ICdUaGUgZGV0ZWN0b3Igd2VudCBkYXJrIGFuZCB0aGUgZ2hvc3RzIHNsaXBwZWQgYXdheS4gUmVjaGFyZ2UgYW5kIHRyeSBhZ2Fpbi4nOwogICAgJCgnI3Jlc3VsdCcpLmNsYXNzTGlzdC5yZW1vdmUoJ2hpZGRlbicpOwogICAgdGhpcy5ydW5uaW5nID0gZmFsc2U7CiAgfSwKCiAgc3RvcCgpIHsKICAgIHRoaXMucnVubmluZyA9IGZhbHNlOwogICAgaWYgKHRoaXMucmFmKSBjYW5jZWxBbmltYXRpb25GcmFtZSh0aGlzLnJhZik7CiAgICBpZiAodGhpcy5zdHJlYW0pIHsgdGhpcy5zdHJlYW0uZ2V0VHJhY2tzKCkuZm9yRWFjaCgodCkgPT4gdC5zdG9wKCkpOyB0aGlzLnN0cmVhbSA9IG51bGw7IH0KICAgIGlmICh0aGlzLl9vblJlc2l6ZSkgcmVtb3ZlRXZlbnRMaXN0ZW5lcigncmVzaXplJywgdGhpcy5fb25SZXNpemUpOwogICAgaWYgKHRoaXMuX29uT3JpZW50ICYmIHRoaXMuX2d5cm9BdHRhY2hlZCkgewogICAgICByZW1vdmVFdmVudExpc3RlbmVyKCdkZXZpY2VvcmllbnRhdGlvbicsIHRoaXMuX29uT3JpZW50KTsKICAgICAgdGhpcy5fZ3lyb0F0dGFjaGVkID0gZmFsc2U7CiAgICB9CiAgICBpZiAodGhpcy5fbW90aW9uRWxzKSB7IHRoaXMuX21vdGlvbkVscy5vdmVybGF5LmNsYXNzTGlzdC5hZGQoJ2hpZGRlbicpOyB9CiAgICBpZiAodGhpcy5fYmxhc3RFbHMpIHsKICAgICAgcmVtb3ZlRXZlbnRMaXN0ZW5lcigndG91Y2hlbmQnLCB0aGlzLl9ibGFzdEVscy5lbmRCbGFzdCk7CiAgICAgIHJlbW92ZUV2ZW50TGlzdGVuZXIoJ21vdXNldXAnLCB0aGlzLl9ibGFzdEVscy5lbmRCbGFzdCk7CiAgICB9CiAgICBpZiAodGhpcy5yZW5kZXJlcikgeyB0aGlzLnJlbmRlcmVyLmRpc3Bvc2U/LigpOyB9CiAgICBmb3IgKGNvbnN0IGcgb2YgdGhpcy5naG9zdHMpIHsKICAgICAgaWYgKGcudXNlckRhdGEudmlkRWwpIHsgdHJ5IHsgZy51c2VyRGF0YS52aWRFbC5wYXVzZSgpOyBnLnVzZXJEYXRhLnZpZEVsLnNyYyA9ICcnOyB9IGNhdGNoIHt9IGcudXNlckRhdGEudmlkRWwgPSBudWxsOyB9CiAgICB9CiAgICB0aGlzLmdob3N0cyA9IFtdOyB0aGlzLnRhcmdldCA9IG51bGw7CiAgfSwKfTsKCmZ1bmN0aW9uIHN0YXJ0SHVudChvcHRzKSB7IGh1bnQuc3RhcnQob3B0cyk7IH0KCi8qID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQogICBST1NURVIgLyBHSE9TVCBJTkRFWAogICA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0gKi8KYXN5bmMgZnVuY3Rpb24gb3BlblJvc3RlcigpIHsKICBzaG93KCdyb3N0ZXInKTsKICBhd2FpdCBsb2FkUm9zdGVyKCk7Cn0KWycjZmlsdGVyLXR5cGUnLCAnI2ZpbHRlci1yYXJpdHknLCAnI2ZpbHRlci1ib3NzJ10uZm9yRWFjaCgoc2VsKSA9PgogICQoc2VsKS5hZGRFdmVudExpc3RlbmVyKCdjaGFuZ2UnLCBsb2FkUm9zdGVyKQopOwoKYXN5bmMgZnVuY3Rpb24gbG9hZFJvc3RlcigpIHsKICBjb25zdCB0eXBlID0gJCgnI2ZpbHRlci10eXBlJykudmFsdWU7CiAgY29uc3QgcmFyaXR5ID0gJCgnI2ZpbHRlci1yYXJpdHknKS52YWx1ZTsKICBjb25zdCBib3NzID0gJCgnI2ZpbHRlci1ib3NzJykuY2hlY2tlZCA/ICcxJyA6ICcnOwogIGNvbnN0IHBhcmFtcyA9IG5ldyBVUkxTZWFyY2hQYXJhbXMoKTsKICBpZiAodHlwZSkgcGFyYW1zLnNldCgndHlwZScsIHR5cGUpOwogIGlmIChyYXJpdHkpIHBhcmFtcy5zZXQoJ3Jhcml0eScsIHJhcml0eSk7CiAgaWYgKGJvc3MpIHBhcmFtcy5zZXQoJ2Jvc3MnLCBib3NzKTsKICBjb25zdCByZXMgPSBhd2FpdCBmZXRjaChgL2FwaS9naG9zdHM/JHtwYXJhbXN9YCk7CiAgY29uc3QgeyBnaG9zdHMgfSA9IGF3YWl0IHJlcy5qc29uKCk7CiAgY29uc3QgZ3JpZCA9ICQoJyNyb3N0ZXItZ3JpZCcpOwogIGdyaWQuaW5uZXJIVE1MID0gZ2hvc3RzLm1hcChnaG9zdENhcmQpLmpvaW4oJycpIHx8CiAgICAnPHAgY2xhc3M9Im11dGVkIj5ObyBnaG9zdHMgbWF0Y2ggdGhvc2UgZmlsdGVycy48L3A+JzsKfQoKZnVuY3Rpb24gZ2hvc3RDYXJkKGcpIHsKICBjb25zdCBjb2xvclZhciA9IGB2YXIoLS0ke2cudHlwZX0pYDsKICBjb25zdCB0aHVtYiA9IGcuaW1hZ2UKICAgID8gYDxpbWcgY2xhc3M9Imdob3N0LXRodW1iIiBzcmM9IiR7Zy5pbWFnZX0iIGFsdD0iJHtlc2NhcGVIdG1sKGcuZGlzcGxheU5hbWUpfSIgbG9hZGluZz0ibGF6eSI+YAogICAgOiAnJzsKICBjb25zdCBzZXRSZWYgPSBnLnNldE51bWJlciA/IGA8ZGl2IGNsYXNzPSJzZXQtcmVmIj5TZXQgJHtnLnNldE51bWJlcn0gwrcgJHtlc2NhcGVIdG1sKGcuc2V0TmFtZSB8fCAnJyl9PC9kaXY+YCA6ICcnOwogIHJldHVybiBgCiAgICA8ZGl2IGNsYXNzPSJnaG9zdC1jYXJkICR7Zy5pc0Jvc3MgPyAnYm9zcycgOiAnJ30iPgogICAgICA8ZGl2IGNsYXNzPSJhY2NlbnQiIHN0eWxlPSJiYWNrZ3JvdW5kOiR7Y29sb3JWYXJ9Ij48L2Rpdj4KICAgICAgJHt0aHVtYn0KICAgICAgPGRpdiBjbGFzcz0iZ25hbWUiPiR7ZXNjYXBlSHRtbChnLmRpc3BsYXlOYW1lKX0gJHtnLmlzQm9zcyA/ICc8c3BhbiBjbGFzcz0iYm9zcy1iYWRnZSI+Qk9TUzwvc3Bhbj4nIDogJyd9PC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9Im1ldGEiPjxzcGFuIGNsYXNzPSJkb3QgJHtnLnR5cGV9Ij48L3NwYW4+IDxzcGFuIGNsYXNzPSJzdGFycyI+JHsn4piFJy5yZXBlYXQoZy5yYXJpdHkpfTwvc3Bhbj48L2Rpdj4KICAgICAgPGRpdiBjbGFzcz0ic3RhdGxpbmUiPjxzcGFuPkhQICR7Zy5oZWFsdGh9PC9zcGFuPjxzcGFuPkRNRyAke2cuZGFtYWdlfTwvc3Bhbj48L2Rpdj4KICAgICAgPGRpdiBjbGFzcz0ic3RhdGxpbmUiPjxzcGFuPlNQRCAke2cuc3BlZWR9PC9zcGFuPjxzcGFuPlJORyAke2cucmFuZ2V9PC9zcGFuPjwvZGl2PgogICAgICAke2cuYWJpbGl0eSA/IGA8ZGl2IGNsYXNzPSJzZXQtcmVmIiBzdHlsZT0ibWFyZ2luLXRvcDo2cHgiPuKaoSAke2VzY2FwZUh0bWwoZy5hYmlsaXR5KX08L2Rpdj5gIDogJyd9CiAgICAgICR7c2V0UmVmfQogICAgPC9kaXY+YDsKfQoKZnVuY3Rpb24gZXNjYXBlSHRtbChzKSB7CiAgcmV0dXJuIFN0cmluZyhzID8/ICcnKS5yZXBsYWNlKC9bJjw+IiddL2csIChjKSA9PgogICAgKHsgJyYnOiAnJmFtcDsnLCAnPCc6ICcmbHQ7JywgJz4nOiAnJmd0OycsICciJzogJyZxdW90OycsICInIjogJyYjMzk7JyB9W2NdKSk7Cn0K \ No newline at end of file +import * as THREE from 'three'; + +/* ============================================================ + Newbury Nights — client game logic + - Screen routing (title / scan / hunt / roster) + - QR scan via BarcodeDetector (manual code fallback) + - AR hunt: camera passthrough + DeviceOrientation gyro look, + color-wheel gloom detection, blaster with overheat, + procedural wisp meshes + animated-GIF billboards. + ============================================================ */ + +const $ = (s, r = document) => r.querySelector(s); +const $$ = (s, r = document) => [...r.querySelectorAll(s)]; + +const screens = { + title: $('#screen-title'), + scan: $('#screen-scan'), + hunt: $('#screen-hunt'), + roster: $('#screen-roster'), + about: $('#screen-about'), +}; +function show(name) { + Object.values(screens).forEach((s) => s.classList.add('hidden')); + screens[name].classList.remove('hidden'); +} + +/* ---------- Title nav ---------- */ +$('#btn-scan-set').addEventListener('click', () => startScan()); +$('#btn-freehunt').addEventListener('click', () => startHunt({ freeHunt: true })); +$('#btn-roster').addEventListener('click', () => openRoster()); +$('#btn-about').addEventListener('click', () => show('about')); +$$('[data-back]').forEach((b) => b.addEventListener('click', () => returnHome())); + +function returnHome() { + stopScan(); + hunt.stop(); + show('title'); +} + +/* ============================================================ + SCAN + ============================================================ */ +let scanStream = null; +let scanLoop = null; +let detector = null; + +async function startScan() { + show('scan'); + $('#scan-error').classList.add('hidden'); + const status = $('#scan-status'); + const video = $('#scan-video'); + + if ('BarcodeDetector' in window) { + try { + const formats = await window.BarcodeDetector.getSupportedFormats(); + if (formats.includes('qr_code')) { + detector = new window.BarcodeDetector({ formats: ['qr_code'] }); + } + } catch { detector = null; } + } + + try { + scanStream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment' }, audio: false, + }); + video.srcObject = scanStream; + await video.play(); + status.textContent = detector ? 'Looking for a set code…' : 'Camera on — enter code below'; + if (detector) tickScan(video); + } catch (e) { + status.textContent = 'Camera unavailable — enter the code below'; + } +} + +async function tickScan(video) { + if (!detector || !scanStream) return; + try { + const codes = await detector.detect(video); + if (codes.length) { + const raw = codes[0].rawValue?.trim(); + if (raw) { resolveCode(raw); return; } + } + } catch { /* keep trying */ } + scanLoop = requestAnimationFrame(() => tickScan(video)); +} + +function stopScan() { + if (scanLoop) cancelAnimationFrame(scanLoop), (scanLoop = null); + if (scanStream) { scanStream.getTracks().forEach((t) => t.stop()); scanStream = null; } +} + +$('#btn-manual-go').addEventListener('click', () => { + const code = $('#manual-code').value.trim(); + if (code) resolveCode(code); +}); +$('#manual-code').addEventListener('keydown', (e) => { + if (e.key === 'Enter') $('#btn-manual-go').click(); +}); + +async function resolveCode(code) { + // Accept either a bare code or a URL whose last path/query segment is the code. + const cleaned = code.includes('/') ? code.split(/[\/?=]/).filter(Boolean).pop() : code; + try { + const res = await fetch(`/api/scan/${encodeURIComponent(cleaned)}`); + if (!res.ok) throw new Error('Unknown set code'); + const data = await res.json(); + stopScan(); + startHunt({ setData: data }); + } catch (e) { + const err = $('#scan-error'); + err.textContent = `Couldn't find a set for "${cleaned}". Check the code and try again.`; + err.classList.remove('hidden'); + } +} + +/* ============================================================ + HUNT (AR) + ============================================================ */ +const TYPE_COLORS = { red: 0xff3b5c, yellow: 0xffc23b, blue: 0x3bb6ff }; + +// Detect VP9-with-alpha WebM support once. iOS Safari historically lacks +// reliable VP9-alpha, so those devices fall back to the GIF/WebP path. +const SUPPORTS_WEBM_ALPHA = (() => { + try { + const v = document.createElement('video'); + return !!v.canPlayType && v.canPlayType('video/webm; codecs="vp9"') !== ''; + } catch { + return false; + } +})(); + +const hunt = { + running: false, + scene: null, camera: null, renderer: null, raf: null, + stream: null, video: null, + ghosts: [], // active ghost objects in the scene + target: null, // currently locked ghost + armedType: null, // color-wheel armed type + battery: 100, + gloom: 0, + overheat: 0, + blasting: false, + orientation: { alpha: 0, beta: 0, gamma: 0, active: false }, + pool: [], // ghosts available to spawn (from set roster or freehunt) + boss: null, + remaining: 0, // spots left to clear (set mode) + freeHunt: false, + + async start({ setData, freeHunt }) { + show('hunt'); + this.running = true; + this.freeHunt = !!freeHunt; + this.battery = 100; this.gloom = 0; this.overheat = 0; this.target = null; this.armedType = null; + this.ghosts = []; + $('#result').classList.add('hidden'); + $('#lockon').classList.add('hidden'); + this.updateBattery(); this.updateGloom(); + + if (freeHunt) { + const res = await fetch('/api/freehunt?n=6'); + this.pool = (await res.json()).spawns; + this.boss = null; + this.remaining = Infinity; + } else { + this.pool = setData.roster.filter((g) => !g.isBoss); + this.boss = setData.boss; + this.remaining = Math.min(5, this.pool.length); // spots to clear before boss + } + + await this.initCamera(); + this.initScene(); + this.bindControls(); + this.maybePromptMotion(); + this.spawnNext(); + this.loop(); + }, + + // On iOS, surface an explicit "Enable Motion" button. Tapping it is the clean + // user gesture iOS needs to show its Motion & Orientation Access prompt. + maybePromptMotion() { + const overlay = $('#motion-gate'); + if (!this.gyroNeedsPermission()) { overlay.classList.add('hidden'); return; } + overlay.classList.remove('hidden'); + const btn = $('#motion-enable'); + const onTap = async () => { + const ok = await this.requestGyro(); + overlay.classList.add('hidden'); + if (!ok) this.toast('Motion blocked — enable it in Settings › Safari', 2200); + }; + btn.onclick = onTap; + this._motionEls = { overlay, btn }; + }, + + async initCamera() { + this.video = $('#ar-video'); + try { + this.stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment' }, audio: false, + }); + this.video.srcObject = this.stream; + await this.video.play(); + } catch { /* no camera → dark backdrop, gyro still works */ } + }, + + initScene() { + const canvas = $('#ar-canvas'); + this.renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true }); + this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); + this.renderer.setSize(innerWidth, innerHeight); + + this.scene = new THREE.Scene(); + this.camera = new THREE.PerspectiveCamera(70, innerWidth / innerHeight, 0.1, 100); + this.camera.position.set(0, 0, 0); + + this.scene.add(new THREE.AmbientLight(0xffffff, 0.8)); + const p = new THREE.PointLight(0x88aaff, 1.2, 50); + p.position.set(0, 2, 2); + this.scene.add(p); + + this._onResize = () => { + this.camera.aspect = innerWidth / innerHeight; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(innerWidth, innerHeight); + }; + addEventListener('resize', this._onResize); + + // Gyro look. iOS Safari requires DeviceOrientationEvent.requestPermission() + // from an explicit user gesture (handled by the "Enable Motion" button in + // start()). Other browsers can attach the listener immediately. + this._onOrient = (e) => { + if (e.alpha == null) return; + this.orientation = { alpha: e.alpha, beta: e.beta, gamma: e.gamma, active: true }; + }; + if (!this.gyroNeedsPermission()) { + addEventListener('deviceorientation', this._onOrient); + this._gyroAttached = true; + } + }, + + gyroNeedsPermission() { + const DOE = window.DeviceOrientationEvent; + return !!(DOE && typeof DOE.requestPermission === 'function'); + }, + + // Called from a clean tap (the Enable Motion overlay). On iOS this triggers + // the system "Motion & Orientation Access" prompt; once granted we attach the + // listener. Safe to call more than once. + async requestGyro() { + if (this._gyroAttached) return true; + const DOE = window.DeviceOrientationEvent; + try { + if (DOE && typeof DOE.requestPermission === 'function') { + const res = await DOE.requestPermission(); + if (res !== 'granted') return false; + } + addEventListener('deviceorientation', this._onOrient); + this._gyroAttached = true; + return true; + } catch { + return false; + } + }, + + bindControls() { + // color wheel + $$('.wheel-seg').forEach((seg) => { + seg.onclick = () => { + const type = seg.dataset.type; + this.armedType = type; + $$('.wheel-seg').forEach((s) => s.classList.remove('armed')); + seg.classList.add('armed'); + $('#wheel-core').textContent = type.toUpperCase(); + this.scanGloom(type); + }; + }); + + const blast = $('#btn-blast'); + const startBlast = (e) => { + e.preventDefault(); + this.blasting = true; + }; + const endBlast = () => { this.blasting = false; }; + blast.addEventListener('touchstart', startBlast, { passive: false }); + blast.addEventListener('mousedown', startBlast); + addEventListener('touchend', endBlast); + addEventListener('mouseup', endBlast); + this._blastEls = { blast, startBlast, endBlast }; + }, + + /* spawn a wisp/billboard ghost at a random spot around the player */ + spawnNext() { + if (!this.pool.length) return; + const data = this.pool[Math.floor(Math.random() * this.pool.length)]; + this.addGhost(data); + }, + + addGhost(data, isBoss = false) { + const group = new THREE.Group(); + const color = TYPE_COLORS[data.type] ?? 0xffffff; + + let mesh; + let texture = null; + if (data.webm && SUPPORTS_WEBM_ALPHA) { + // WebM (VP9+alpha) billboard via VideoTexture. The browser decodes and + // updates the texture itself — no per-frame needsUpdate pumping needed. + const vid = document.createElement('video'); + vid.crossOrigin = 'anonymous'; + vid.muted = true; // required for autoplay + vid.loop = true; + vid.playsInline = true; // iOS: stay inline, don't fullscreen + vid.autoplay = true; + vid.preload = 'auto'; + vid.src = data.webm; + texture = new THREE.VideoTexture(vid); + texture.colorSpace = THREE.SRGBColorSpace; + texture.minFilter = THREE.LinearFilter; + texture.magFilter = THREE.LinearFilter; + texture.generateMipmaps = false; + const mat = new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide, depthWrite: false }); + mesh = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), mat); + // If the video errors (e.g. alpha unsupported despite canPlayType), swap + // to the GIF/image billboard if we have one. + vid.onerror = () => { + if (group.userData.videoFellBack) return; + group.userData.videoFellBack = true; + if (data.image) { + const img = document.createElement('img'); + img.crossOrigin = 'anonymous'; + img.src = data.image; + const t2 = new THREE.Texture(img); + img.onload = () => { t2.needsUpdate = true; }; + mesh.material.map = t2; + mesh.material.needsUpdate = true; + group.userData.gifImg = img; + group.userData.gifTex = t2; + group.userData.vidEl = null; + } + }; + const pr = vid.play(); + if (pr && pr.catch) pr.catch(() => {}); + group.userData.vidEl = vid; + } else if (data.image) { + // animated GIF billboard — texture.needsUpdate pumped each frame + const img = document.createElement('img'); + img.crossOrigin = 'anonymous'; + img.src = data.image; + texture = new THREE.Texture(img); + img.onload = () => { texture.needsUpdate = true; }; + const mat = new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide }); + mesh = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), mat); + group.userData.gifImg = img; + group.userData.gifTex = texture; + } else { + // procedural wisp: glowing sphere + halo + const geo = new THREE.SphereGeometry(0.45, 24, 24); + const mat = new THREE.MeshStandardMaterial({ + color, emissive: color, emissiveIntensity: 1.4, roughness: 0.4, + transparent: true, opacity: 0.92, + }); + mesh = new THREE.Mesh(geo, mat); + const halo = new THREE.Mesh( + new THREE.SphereGeometry(0.62, 24, 24), + new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.16, side: THREE.BackSide }) + ); + group.add(halo); + } + group.add(mesh); + + // Place in a forward-facing arc so ghosts are findable without perfect + // aiming: yaw within ±60° of straight ahead, modest pitch, closer in. + const yaw = (Math.random() - 0.5) * (Math.PI * 2 / 3); + const pitch = (Math.random() - 0.5) * 0.5; + const dist = 3 + Math.random() * 1.5; + group.position.set( + Math.sin(yaw) * dist, + Math.sin(pitch) * dist, + -Math.cos(yaw) * dist + ); + + group.userData = { + ...group.userData, + data, isBoss, + hp: data.health, maxHp: data.health, + bobPhase: Math.random() * Math.PI * 2, + revealed: true, // visible on spawn; the wheel now aggros/tints rather than uncloaks + }; + this.scene.add(group); + this.ghosts.push(group); + }, + + scanGloom(type) { + // Ghosts are visible on spawn now, so the wheel acts as a "lure": scanning a + // colour pulls the nearest matching ghost toward your aim and charges gloom. + // Mismatched colour risks a jumpscare. + const matches = this.ghosts.filter((g) => g.userData.data.type === type); + if (matches.length) { + // pull the nearest match into a forward, easy-to-aim position + const g = matches[0]; + const dist = 3; + g.position.set((Math.random() - 0.5) * 1.2, (Math.random() - 0.3) * 0.8, -dist); + g.userData.lured = true; + this.gloom += 5; this.updateGloom(); + this.toast(`${type.toUpperCase()} ghost lured in`, 800); + } else { + // no ghost of that colour present — small chance of a jumpscare + if (Math.random() < 0.12) this.jumpscare(); + else { this.gloom += 2; this.updateGloom(); this.toast('+2 gloom', 600); } + } + }, + + loop() { + if (!this.running) return; + this.raf = requestAnimationFrame(() => this.loop()); + const t = performance.now() / 1000; + + // camera yaw/pitch from gyro + if (this.orientation.active) { + const { alpha, beta, gamma } = this.orientation; + // simple mapping; good enough for look-around aiming + this.camera.rotation.set( + THREE.MathUtils.degToRad((beta ?? 0) - 90), + THREE.MathUtils.degToRad(alpha ?? 0), + THREE.MathUtils.degToRad(-(gamma ?? 0)), + 'YXZ' + ); + } + + // animate ghosts: bob + face camera + pump GIF textures + for (const g of this.ghosts) { + g.position.y += Math.sin(t * 1.5 + g.userData.bobPhase) * 0.0025; + g.lookAt(this.camera.position); + if (g.userData.gifTex && g.userData.gifImg?.complete) g.userData.gifTex.needsUpdate = true; + } + + // targeting: ghost nearest screen-center (and revealed) becomes the lock-on + this.updateTarget(); + + // blaster + if (this.blasting && this.overheat < 100 && this.target) { + const dmg = 0.9; // per-frame chip damage + this.target.userData.hp -= dmg; + this.overheat = Math.min(100, this.overheat + 0.8); + this.updateGhostHp(); + if (this.target.userData.hp <= 0) this.captureTarget(); + } else if (!this.blasting && this.overheat > 0) { + this.overheat = Math.max(0, this.overheat - 1.2); // cool down + } + if (this.overheat >= 100) this.blasting = false; + this.updateOverheat(); + + // ghosts attack: drain battery if a revealed ghost is in front of you + this.maybeTakeDamage(); + + this.renderer.render(this.scene, this.camera); + }, + + updateTarget() { + const center = new THREE.Vector2(0, 0); + let best = null, bestDist = 0.5; // within ~half-screen of center + const v = new THREE.Vector3(); + for (const g of this.ghosts) { + if (!g.userData.revealed) continue; + v.copy(g.position).project(this.camera); + if (v.z > 1) continue; // behind camera + const d = Math.hypot(v.x - center.x, v.y - center.y); + if (d < bestDist) { bestDist = d; best = g; } + } + if (best !== this.target) { + this.target = best; + this.renderLockon(); + } + }, + + renderLockon() { + const lock = $('#lockon'); + if (!this.target) { lock.classList.add('hidden'); return; } + const d = this.target.userData.data; + lock.classList.remove('hidden'); + // IMPORTANT: label reads displayName, not a hardcoded tint name + $('#lockon-name').textContent = d.displayName || d.name; + $('#lockon-name').className = `lockon-name display type-${d.type}`; + $('#lockon-type').textContent = d.type; + $('#lockon-rarity').textContent = '★'.repeat(d.rarity); + $('#lockon-ability').textContent = d.ability || '—'; + this.updateGhostHp(); + }, + + updateGhostHp() { + if (!this.target) return; + const pct = Math.max(0, (this.target.userData.hp / this.target.userData.maxHp) * 100); + $('#ghost-hp-fill').style.width = `${pct}%`; + }, + + captureTarget() { + const g = this.target; + const d = g.userData.data; + if (g.userData.vidEl) { try { g.userData.vidEl.pause(); g.userData.vidEl.src = ''; } catch {} g.userData.vidEl = null; } + this.scene.remove(g); + this.ghosts = this.ghosts.filter((x) => x !== g); + this.target = null; + $('#lockon').classList.add('hidden'); + this.gloom += 10 + d.rarity * 5; + this.updateGloom(); + this.toast(`Captured ${d.displayName || d.name}!`, 900); + + if (g.userData.isBoss) { this.win(d); return; } + + if (!this.freeHunt) { + this.remaining -= 1; + if (this.remaining <= 0 && this.boss) { + // soul artifact → boss appears + this.toast('A Soul Artifact pulses…', 1100); + setTimeout(() => this.addGhost(this.boss, true), 1100); + return; + } + } + // respawn another ghost to keep the hunt going + setTimeout(() => this.spawnNext(), 600); + }, + + maybeTakeDamage() { + // a revealed ghost roughly centered + we're not blasting → it glooms us + if (this.blasting) return; + if (!this.target) return; + if (Math.random() < 0.004) { + const dmg = this.target.userData.data.rarity * 1.5; // 4★ hurts most + this.battery = Math.max(0, this.battery - dmg); + this.updateBattery(); + this.flashDamage(); + if (this.battery <= 0) this.lose(); + } + }, + + /* ---- HUD updates ---- */ + updateBattery() { + const fill = $('#battery-fill'); + fill.style.width = `${this.battery}%`; + fill.classList.toggle('low', this.battery <= 30); + $('#battery-pct').textContent = `${Math.round(this.battery)}%`; + }, + updateGloom() { $('#gloom-count').textContent = this.gloom; }, + updateOverheat() { + $('#overheat-fill').style.width = `${this.overheat}%`; + $('#btn-blast').classList.toggle('overheated', this.overheat >= 100); + $('#btn-blast').textContent = this.overheat >= 100 ? 'OVERHEATED' : 'HOLD TO BLAST'; + }, + + toast(msg, ms = 800, jump = false) { + const t = $('#toast'); + t.textContent = msg; + t.className = `toast${jump ? ' jumpscare' : ''}`; + clearTimeout(this._toastT); + this._toastT = setTimeout(() => t.classList.add('hidden'), ms); + }, + jumpscare() { + this.toast('BOO!', 700, true); + this.battery = Math.max(0, this.battery - 4); + this.updateBattery(); + if (navigator.vibrate) navigator.vibrate(120); + }, + flashDamage() { + if (navigator.vibrate) navigator.vibrate(40); + document.body.animate( + [{ boxShadow: 'inset 0 0 0 9999px rgba(255,59,92,.25)' }, { boxShadow: 'inset 0 0 0 9999px rgba(255,59,92,0)' }], + { duration: 250 } + ); + }, + + win(boss) { + $('#result-title').textContent = 'Set Cleared'; + $('#result-body').textContent = `You captured ${boss.displayName || boss.name} and drove the gloom out. Gloom banked: ${this.gloom}.`; + $('#result').classList.remove('hidden'); + this.running = false; + }, + lose() { + $('#result-title').textContent = 'Battery Dead'; + $('#result-body').textContent = 'The detector went dark and the ghosts slipped away. Recharge and try again.'; + $('#result').classList.remove('hidden'); + this.running = false; + }, + + stop() { + this.running = false; + if (this.raf) cancelAnimationFrame(this.raf); + if (this.stream) { this.stream.getTracks().forEach((t) => t.stop()); this.stream = null; } + if (this._onResize) removeEventListener('resize', this._onResize); + if (this._onOrient && this._gyroAttached) { + removeEventListener('deviceorientation', this._onOrient); + this._gyroAttached = false; + } + if (this._motionEls) { this._motionEls.overlay.classList.add('hidden'); } + if (this._blastEls) { + removeEventListener('touchend', this._blastEls.endBlast); + removeEventListener('mouseup', this._blastEls.endBlast); + } + if (this.renderer) { this.renderer.dispose?.(); } + for (const g of this.ghosts) { + if (g.userData.vidEl) { try { g.userData.vidEl.pause(); g.userData.vidEl.src = ''; } catch {} g.userData.vidEl = null; } + } + this.ghosts = []; this.target = null; + }, +}; + +function startHunt(opts) { hunt.start(opts); } + +/* ============================================================ + ROSTER / GHOST INDEX + ============================================================ */ +async function openRoster() { + show('roster'); + await loadRoster(); +} +['#filter-type', '#filter-rarity', '#filter-boss'].forEach((sel) => + $(sel).addEventListener('change', loadRoster) +); + +async function loadRoster() { + const type = $('#filter-type').value; + const rarity = $('#filter-rarity').value; + const boss = $('#filter-boss').checked ? '1' : ''; + const params = new URLSearchParams(); + if (type) params.set('type', type); + if (rarity) params.set('rarity', rarity); + if (boss) params.set('boss', boss); + const res = await fetch(`/api/ghosts?${params}`); + const { ghosts } = await res.json(); + const grid = $('#roster-grid'); + grid.innerHTML = ghosts.map(ghostCard).join('') || + '

No ghosts match those filters.

'; +} + +function ghostCard(g) { + const colorVar = `var(--${g.type})`; + const thumb = g.image + ? `${escapeHtml(g.displayName)}` + : ''; + const setRef = g.setNumber ? `
Set ${g.setNumber} · ${escapeHtml(g.setName || '')}
` : ''; + return ` +
+
+ ${thumb} +
${escapeHtml(g.displayName)} ${g.isBoss ? 'BOSS' : ''}
+
${'★'.repeat(g.rarity)}
+
HP ${g.health}DMG ${g.damage}
+
SPD ${g.speed}RNG ${g.range}
+ ${g.ability ? `
⚡ ${escapeHtml(g.ability)}
` : ''} + ${setRef} +
`; +} + +function escapeHtml(s) { + return String(s ?? '').replace(/[&<>"']/g, (c) => + ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); +} diff --git a/routes/admin.js b/routes/admin.js index d3cbe1d..3ff8d50 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -1 +1,239 @@ -aW1wb3J0IHsgUm91dGVyIH0gZnJvbSAnZXhwcmVzcyc7CmltcG9ydCBtdWx0ZXIgZnJvbSAnbXVsdGVyJzsKaW1wb3J0IHsgcmFuZG9tQnl0ZXMgfSBmcm9tICdub2RlOmNyeXB0byc7CmltcG9ydCB7IG1rZGlyU3luYywgZXhpc3RzU3luYywgdW5saW5rU3luYyB9IGZyb20gJ25vZGU6ZnMnOwppbXBvcnQgeyBkaXJuYW1lLCBleHRuYW1lLCBqb2luIH0gZnJvbSAnbm9kZTpwYXRoJzsKaW1wb3J0IHsgZmlsZVVSTFRvUGF0aCB9IGZyb20gJ25vZGU6dXJsJzsKaW1wb3J0IGRiIGZyb20gJy4uL2RiL2luZGV4LmpzJzsKaW1wb3J0IHsgcmVxdWlyZUF1dGggfSBmcm9tICcuL2F1dGgtbWlkZGxld2FyZS5qcyc7CmltcG9ydCB7IGNvbnZlcnRHaG9zdE1wNCB9IGZyb20gJy4uL2xpYi9naG9zdC1tZWRpYS5qcyc7Cgpjb25zdCBfX2Rpcm5hbWUgPSBkaXJuYW1lKGZpbGVVUkxUb1BhdGgoaW1wb3J0Lm1ldGEudXJsKSk7CmNvbnN0IFVQTE9BRF9ESVIgPSBqb2luKF9fZGlybmFtZSwgJy4uJywgcHJvY2Vzcy5lbnYuVVBMT0FEX0RJUiB8fCAndXBsb2FkcycpOwpta2RpclN5bmMoVVBMT0FEX0RJUiwgeyByZWN1cnNpdmU6IHRydWUgfSk7Cgpjb25zdCByb3V0ZXIgPSBSb3V0ZXIoKTsKcm91dGVyLnVzZShyZXF1aXJlQXV0aCk7IC8vIGV2ZXJ5dGhpbmcgaGVyZSByZXF1aXJlcyBhIHZhbGlkIEpXVAoKY29uc3QgQUxMT1dFRCA9IG5ldyBTZXQoWycuZ2lmJywgJy5wbmcnLCAnLmpwZycsICcuanBlZycsICcud2VicCcsICcud2VibScsICcubXA0J10pOwpjb25zdCBWSURFT19FWFRTID0gbmV3IFNldChbJy5tcDQnLCAnLndlYm0nXSk7CmNvbnN0IHN0b3JhZ2UgPSBtdWx0ZXIuZGlza1N0b3JhZ2UoewogIGRlc3RpbmF0aW9uOiAoX3JlcSwgX2ZpbGUsIGNiKSA9PiBjYihudWxsLCBVUExPQURfRElSKSwKICBmaWxlbmFtZTogKF9yZXEsIGZpbGUsIGNiKSA9PiB7CiAgICBjb25zdCBleHQgPSBleHRuYW1lKGZpbGUub3JpZ2luYWxuYW1lKS50b0xvd2VyQ2FzZSgpOwogICAgY2IobnVsbCwgYCR7RGF0ZS5ub3coKX0tJHtyYW5kb21CeXRlcyg2KS50b1N0cmluZygnaGV4Jyl9JHtleHR9YCk7CiAgfSwKfSk7CmNvbnN0IHVwbG9hZCA9IG11bHRlcih7CiAgc3RvcmFnZSwKICBsaW1pdHM6IHsgZmlsZVNpemU6IDY0ICogMTAyNCAqIDEwMjQgfSwgLy8gNjRNQiDigJQgc291cmNlIE1QNHMgYXJlIGxhcmdlciB0aGFuIEdJRnMKICBmaWxlRmlsdGVyOiAoX3JlcSwgZmlsZSwgY2IpID0+IHsKICAgIGNvbnN0IGV4dCA9IGV4dG5hbWUoZmlsZS5vcmlnaW5hbG5hbWUpLnRvTG93ZXJDYXNlKCk7CiAgICBjYihBTExPV0VELmhhcyhleHQpID8gbnVsbCA6IG5ldyBFcnJvcigndW5zdXBwb3J0ZWQgZmlsZSB0eXBlJyksIEFMTE9XRUQuaGFzKGV4dCkpOwogIH0sCn0pOwoKLy8gUmVtb3ZlIGFuIHVwbG9hZGVkIGZpbGUgYnkgYmFyZSBmaWxlbmFtZSwgaWdub3JpbmcgZXJyb3JzLgpmdW5jdGlvbiByZW1vdmVVcGxvYWQoZmlsZW5hbWUpIHsKICBpZiAoIWZpbGVuYW1lKSByZXR1cm47CiAgY29uc3QgcCA9IGpvaW4oVVBMT0FEX0RJUiwgZmlsZW5hbWUpOwogIGlmIChleGlzdHNTeW5jKHApKSB0cnkgeyB1bmxpbmtTeW5jKHApOyB9IGNhdGNoIHsgLyogaWdub3JlICovIH0KfQoKY29uc3QgdG9JbnQgPSAodiwgZCA9IDApID0+IChOdW1iZXIuaXNGaW5pdGUoK3YpID8gcGFyc2VJbnQodiwgMTApIDogZCk7CgovKiAtLS0tLS0tLS0tLS0tLS0tIEdob3N0cyAtLS0tLS0tLS0tLS0tLS0tICovCgpyb3V0ZXIuZ2V0KCcvZ2hvc3RzJywgKHJlcSwgcmVzKSA9PiB7CiAgcmVzLmpzb24oeyBnaG9zdHM6IGRiLnByZXBhcmUoJ1NFTEVDVCAqIEZST00gZ2hvc3RzIE9SREVSIEJZIHR5cGUsIHJhcml0eSwgbmFtZScpLmFsbCgpIH0pOwp9KTsKCnJvdXRlci5wb3N0KCcvZ2hvc3RzJywgKHJlcSwgcmVzKSA9PiB7CiAgY29uc3QgYiA9IHJlcS5ib2R5IHx8IHt9OwogIGlmICghYi5uYW1lIHx8ICFiLnR5cGUgfHwgIWIucmFyaXR5KSB7CiAgICByZXR1cm4gcmVzLnN0YXR1cyg0MDApLmpzb24oeyBlcnJvcjogJ25hbWUsIHR5cGUsIHJhcml0eSByZXF1aXJlZCcgfSk7CiAgfQogIGNvbnN0IGluZm8gPSBkYgogICAgLnByZXBhcmUoCiAgICAgIGBJTlNFUlQgSU5UTyBnaG9zdHMKICAgICAgICAobmFtZSwgZGlzcGxheV9uYW1lLCB0eXBlLCByYXJpdHksIHNwZWVkLCByYW5nZSwgY2hhcmdlX3Nob3QsCiAgICAgICAgIGhlYWx0aCwgZGFtYWdlLCBhYmlsaXR5LCBpc19ib3NzLCBzZXRfbnVtYmVyLCBzZXRfbmFtZSwgZW5hYmxlZCkKICAgICAgIFZBTFVFUyAoPyw/LD8sPyw/LD8sPyw/LD8sPyw/LD8sPyw/KWAKICAgICkKICAgIC5ydW4oCiAgICAgIGIubmFtZSwgYi5kaXNwbGF5TmFtZSB8fCBiLm5hbWUsIGIudHlwZSwgdG9JbnQoYi5yYXJpdHksIDEpLAogICAgICB0b0ludChiLnNwZWVkKSwgdG9JbnQoYi5yYW5nZSksIHRvSW50KGIuY2hhcmdlU2hvdCksCiAgICAgIHRvSW50KGIuaGVhbHRoLCAzMDApLCB0b0ludChiLmRhbWFnZSwgMTUwKSwgYi5hYmlsaXR5IHx8IG51bGwsCiAgICAgIGIuaXNCb3NzID8gMSA6IDAsIGIuc2V0TnVtYmVyIHx8IG51bGwsIGIuc2V0TmFtZSB8fCBudWxsLAogICAgICBiLmVuYWJsZWQgPT09IGZhbHNlID8gMCA6IDEKICAgICk7CiAgcmVzLnN0YXR1cygyMDEpLmpzb24oeyBpZDogaW5mby5sYXN0SW5zZXJ0Um93aWQgfSk7Cn0pOwoKcm91dGVyLnBhdGNoKCcvZ2hvc3RzLzppZCcsIChyZXEsIHJlcykgPT4gewogIGNvbnN0IGdob3N0ID0gZGIucHJlcGFyZSgnU0VMRUNUICogRlJPTSBnaG9zdHMgV0hFUkUgaWQgPSA/JykuZ2V0KHJlcS5wYXJhbXMuaWQpOwogIGlmICghZ2hvc3QpIHJldHVybiByZXMuc3RhdHVzKDQwNCkuanNvbih7IGVycm9yOiAnbm90IGZvdW5kJyB9KTsKICBjb25zdCBiID0gcmVxLmJvZHkgfHwge307CiAgY29uc3QgbWFwID0gewogICAgbmFtZTogJ25hbWUnLCBkaXNwbGF5TmFtZTogJ2Rpc3BsYXlfbmFtZScsIHR5cGU6ICd0eXBlJywgcmFyaXR5OiAncmFyaXR5JywKICAgIHNwZWVkOiAnc3BlZWQnLCByYW5nZTogJ3JhbmdlJywgY2hhcmdlU2hvdDogJ2NoYXJnZV9zaG90JywKICAgIGhlYWx0aDogJ2hlYWx0aCcsIGRhbWFnZTogJ2RhbWFnZScsIGFiaWxpdHk6ICdhYmlsaXR5JywKICAgIGlzQm9zczogJ2lzX2Jvc3MnLCBzZXROdW1iZXI6ICdzZXRfbnVtYmVyJywgc2V0TmFtZTogJ3NldF9uYW1lJywgZW5hYmxlZDogJ2VuYWJsZWQnLAogIH07CiAgY29uc3Qgc2V0cyA9IFtdOwogIGNvbnN0IHZhbHMgPSBbXTsKICBmb3IgKGNvbnN0IFtrLCBjb2xdIG9mIE9iamVjdC5lbnRyaWVzKG1hcCkpIHsKICAgIGlmIChrIGluIGIpIHsKICAgICAgc2V0cy5wdXNoKGAke2NvbH0gPSA/YCk7CiAgICAgIGxldCB2ID0gYltrXTsKICAgICAgaWYgKGsgPT09ICdpc0Jvc3MnIHx8IGsgPT09ICdlbmFibGVkJykgdiA9IHYgPyAxIDogMDsKICAgICAgdmFscy5wdXNoKHYpOwogICAgfQogIH0KICBpZiAoIXNldHMubGVuZ3RoKSByZXR1cm4gcmVzLmpzb24oeyBvazogdHJ1ZSwgdW5jaGFuZ2VkOiB0cnVlIH0pOwogIHZhbHMucHVzaChyZXEucGFyYW1zLmlkKTsKICBkYi5wcmVwYXJlKGBVUERBVEUgZ2hvc3RzIFNFVCAke3NldHMuam9pbignLCAnKX0gV0hFUkUgaWQgPSA/YCkucnVuKC4uLnZhbHMpOwogIHJlcy5qc29uKHsgb2s6IHRydWUgfSk7Cn0pOwoKcm91dGVyLnBvc3QoJy9naG9zdHMvOmlkL2ltYWdlJywgdXBsb2FkLnNpbmdsZSgnaW1hZ2UnKSwgYXN5bmMgKHJlcSwgcmVzKSA9PiB7CiAgY29uc3QgZ2hvc3QgPSBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIGdob3N0cyBXSEVSRSBpZCA9ID8nKS5nZXQocmVxLnBhcmFtcy5pZCk7CiAgaWYgKCFnaG9zdCkgewogICAgaWYgKHJlcS5maWxlKSByZW1vdmVVcGxvYWQocmVxLmZpbGUuZmlsZW5hbWUpOwogICAgcmV0dXJuIHJlcy5zdGF0dXMoNDA0KS5qc29uKHsgZXJyb3I6ICdub3QgZm91bmQnIH0pOwogIH0KICBpZiAoIXJlcS5maWxlKSByZXR1cm4gcmVzLnN0YXR1cyg0MDApLmpzb24oeyBlcnJvcjogJ25vIGZpbGUnIH0pOwoKICBjb25zdCBleHQgPSBleHRuYW1lKHJlcS5maWxlLmZpbGVuYW1lKS50b0xvd2VyQ2FzZSgpOwoKICAvLyBDbGVhciBhbnkgcHJldmlvdXMgbWVkaWEgKGltYWdlICsgdmlkZW8gc3ByaXRlcykgYmVmb3JlIHJlY29yZGluZyB0aGUgbmV3IHNldC4KICBjb25zdCBjbGVhbnVwT2xkID0gKCkgPT4gewogICAgcmVtb3ZlVXBsb2FkKGdob3N0LmltYWdlX3BhdGgpOwogICAgcmVtb3ZlVXBsb2FkKGdob3N0LndlYm1fcGF0aCk7CiAgICByZW1vdmVVcGxvYWQoZ2hvc3Qud2VicF9wYXRoKTsKICB9OwoKICBpZiAoZXh0ID09PSAnLm1wNCcpIHsKICAgIC8vIENvbnZlcnQgdGhlIHNvdXJjZSBNUDQgdG8gYSB0cmFuc3BhcmVudCBXZWJNIChWUDkrYWxwaGEpIHBsdXMgYSBXZWJQCiAgICAvLyBmYWxsYmFjayB2aWEgbHVtYSBrZXlpbmcuIFRoZSBvcmlnaW5hbCBNUDQgaXMgcmVtb3ZlZCBhZnRlcndhcmRzLgogICAgbGV0IG91dDsKICAgIHRyeSB7CiAgICAgIG91dCA9IGF3YWl0IGNvbnZlcnRHaG9zdE1wNChVUExPQURfRElSLCByZXEuZmlsZS5maWxlbmFtZSk7CiAgICB9IGNhdGNoIChlKSB7CiAgICAgIHJlbW92ZVVwbG9hZChyZXEuZmlsZS5maWxlbmFtZSk7CiAgICAgIHJldHVybiByZXMuc3RhdHVzKDUwMCkuanNvbih7IGVycm9yOiAnY29udmVyc2lvbiBmYWlsZWQnLCBkZXRhaWw6IGUubWVzc2FnZSB9KTsKICAgIH0KICAgIHJlbW92ZVVwbG9hZChyZXEuZmlsZS5maWxlbmFtZSk7IC8vIGRpc2NhcmQgdGhlIHJhdyBtcDQKICAgIGlmICghb3V0LndlYm0gJiYgIW91dC53ZWJwKSB7CiAgICAgIHJldHVybiByZXMuc3RhdHVzKDUwMCkuanNvbih7IGVycm9yOiAnY29udmVyc2lvbiBwcm9kdWNlZCBubyBvdXRwdXQgKGlzIGZmbXBlZyBpbnN0YWxsZWQ/KScgfSk7CiAgICB9CiAgICBjbGVhbnVwT2xkKCk7CiAgICAvLyB3ZWJwIGRvdWJsZXMgYXMgdGhlIHN0aWxsL3RodW1ibmFpbCBpbWFnZSB3aGVyZSBwcmVzZW50LgogICAgZGIucHJlcGFyZSgnVVBEQVRFIGdob3N0cyBTRVQgd2VibV9wYXRoID0gPywgd2VicF9wYXRoID0gPywgaW1hZ2VfcGF0aCA9ID8gV0hFUkUgaWQgPSA/JykKICAgICAgLnJ1bihvdXQud2VibSwgb3V0LndlYnAsIG91dC53ZWJwLCBnaG9zdC5pZCk7CiAgICByZXR1cm4gcmVzLmpzb24oewogICAgICBvazogdHJ1ZSwKICAgICAgd2VibTogb3V0LndlYm0gPyBgL3VwbG9hZHMvJHtvdXQud2VibX1gIDogbnVsbCwKICAgICAgd2VicDogb3V0LndlYnAgPyBgL3VwbG9hZHMvJHtvdXQud2VicH1gIDogbnVsbCwKICAgICAgaW1hZ2U6IG91dC53ZWJwID8gYC91cGxvYWRzLyR7b3V0LndlYnB9YCA6IG51bGwsCiAgICB9KTsKICB9CgogIGlmIChleHQgPT09ICcud2VibScpIHsKICAgIC8vIFByZS1tYWRlIHRyYW5zcGFyZW50IFdlYk0gdXBsb2FkZWQgZGlyZWN0bHkg4oCUIHN0b3JlIGFzLWlzLgogICAgY2xlYW51cE9sZCgpOwogICAgZGIucHJlcGFyZSgnVVBEQVRFIGdob3N0cyBTRVQgd2VibV9wYXRoID0gPywgd2VicF9wYXRoID0gTlVMTCwgaW1hZ2VfcGF0aCA9IE5VTEwgV0hFUkUgaWQgPSA/JykKICAgICAgLnJ1bihyZXEuZmlsZS5maWxlbmFtZSwgZ2hvc3QuaWQpOwogICAgcmV0dXJuIHJlcy5qc29uKHsgb2s6IHRydWUsIHdlYm06IGAvdXBsb2Fkcy8ke3JlcS5maWxlLmZpbGVuYW1lfWAgfSk7CiAgfQoKICAvLyBQbGFpbiBpbWFnZSAoZ2lmL3BuZy9qcGcvd2VicCkg4oCUIHRoZSBvcmlnaW5hbCBiaWxsYm9hcmQgcGF0aC4gQ2xlYXIgYW55CiAgLy8gcHJldmlvdXMgdmlkZW8gc3ByaXRlcyBzbyB0aGUgZ2hvc3QgZmFsbHMgYmFjayBjbGVhbmx5IHRvIHRoZSBpbWFnZS4KICBjbGVhbnVwT2xkKCk7CiAgZGIucHJlcGFyZSgnVVBEQVRFIGdob3N0cyBTRVQgaW1hZ2VfcGF0aCA9ID8sIHdlYm1fcGF0aCA9IE5VTEwsIHdlYnBfcGF0aCA9IE5VTEwgV0hFUkUgaWQgPSA/JykKICAgIC5ydW4ocmVxLmZpbGUuZmlsZW5hbWUsIGdob3N0LmlkKTsKICByZXMuanNvbih7IG9rOiB0cnVlLCBpbWFnZTogYC91cGxvYWRzLyR7cmVxLmZpbGUuZmlsZW5hbWV9YCB9KTsKfSk7Cgpyb3V0ZXIuZGVsZXRlKCcvZ2hvc3RzLzppZCcsIChyZXEsIHJlcykgPT4gewogIGNvbnN0IGdob3N0ID0gZGIucHJlcGFyZSgnU0VMRUNUICogRlJPTSBnaG9zdHMgV0hFUkUgaWQgPSA/JykuZ2V0KHJlcS5wYXJhbXMuaWQpOwogIGlmICghZ2hvc3QpIHJldHVybiByZXMuc3RhdHVzKDQwNCkuanNvbih7IGVycm9yOiAnbm90IGZvdW5kJyB9KTsKICByZW1vdmVVcGxvYWQoZ2hvc3QuaW1hZ2VfcGF0aCk7CiAgcmVtb3ZlVXBsb2FkKGdob3N0LndlYm1fcGF0aCk7CiAgcmVtb3ZlVXBsb2FkKGdob3N0LndlYnBfcGF0aCk7CiAgZGIucHJlcGFyZSgnREVMRVRFIEZST00gZ2hvc3RzIFdIRVJFIGlkID0gPycpLnJ1bihnaG9zdC5pZCk7CiAgcmVzLmpzb24oeyBvazogdHJ1ZSB9KTsKfSk7CgovKiAtLS0tLS0tLS0tLS0tLS0tIFNldHMgLS0tLS0tLS0tLS0tLS0tLSAqLwoKcm91dGVyLmdldCgnL3NldHMnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBzZXRzID0gZGIucHJlcGFyZSgnU0VMRUNUICogRlJPTSBzZXRzIE9SREVSIEJZIHNldF9udW1iZXIsIHNldF9uYW1lJykuYWxsKCk7CiAgY29uc3QgZ2V0Um9zdGVyID0gZGIucHJlcGFyZSgKICAgIGBTRUxFQ1QgZy5pZCwgZy5uYW1lLCBnLnR5cGUsIGcucmFyaXR5LCBnLmlzX2Jvc3MKICAgICBGUk9NIGdob3N0cyBnIEpPSU4gc2V0X2dob3N0cyBzZyBPTiBzZy5naG9zdF9pZCA9IGcuaWQKICAgICBXSEVSRSBzZy5zZXRfaWQgPSA/IE9SREVSIEJZIGcuaXNfYm9zcyBERVNDLCBnLnJhcml0eSBERVNDLCBnLm5hbWVgCiAgKTsKICByZXMuanNvbih7CiAgICBzZXRzOiBzZXRzLm1hcCgocykgPT4gKHsgLi4ucywgcm9zdGVyOiBnZXRSb3N0ZXIuYWxsKHMuaWQpIH0pKSwKICB9KTsKfSk7Cgpyb3V0ZXIucG9zdCgnL3NldHMnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBiID0gcmVxLmJvZHkgfHwge307CiAgaWYgKCFiLmNvZGUgfHwgIWIuc2V0TmFtZSkgcmV0dXJuIHJlcy5zdGF0dXMoNDAwKS5qc29uKHsgZXJyb3I6ICdjb2RlIGFuZCBzZXROYW1lIHJlcXVpcmVkJyB9KTsKICB0cnkgewogICAgY29uc3QgaW5mbyA9IGRiCiAgICAgIC5wcmVwYXJlKAogICAgICAgICdJTlNFUlQgSU5UTyBzZXRzIChjb2RlLCBzZXRfbnVtYmVyLCBzZXRfbmFtZSwgYm9zc19naG9zdF9pZCwgZW5hYmxlZCkgVkFMVUVTICg/LD8sPyw/LD8pJwogICAgICApCiAgICAgIC5ydW4oYi5jb2RlLCBiLnNldE51bWJlciB8fCBudWxsLCBiLnNldE5hbWUsIGIuYm9zc0dob3N0SWQgfHwgbnVsbCwgYi5lbmFibGVkID09PSBmYWxzZSA/IDAgOiAxKTsKICAgIHJlcy5zdGF0dXMoMjAxKS5qc29uKHsgaWQ6IGluZm8ubGFzdEluc2VydFJvd2lkIH0pOwogIH0gY2F0Y2ggKGUpIHsKICAgIGlmIChTdHJpbmcoZSkuaW5jbHVkZXMoJ1VOSVFVRScpKSByZXR1cm4gcmVzLnN0YXR1cyg0MDkpLmpzb24oeyBlcnJvcjogJ2NvZGUgYWxyZWFkeSBleGlzdHMnIH0pOwogICAgdGhyb3cgZTsKICB9Cn0pOwoKcm91dGVyLnBhdGNoKCcvc2V0cy86aWQnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBzZXQgPSBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIHNldHMgV0hFUkUgaWQgPSA/JykuZ2V0KHJlcS5wYXJhbXMuaWQpOwogIGlmICghc2V0KSByZXR1cm4gcmVzLnN0YXR1cyg0MDQpLmpzb24oeyBlcnJvcjogJ25vdCBmb3VuZCcgfSk7CiAgY29uc3QgYiA9IHJlcS5ib2R5IHx8IHt9OwogIGNvbnN0IG1hcCA9IHsKICAgIGNvZGU6ICdjb2RlJywgc2V0TnVtYmVyOiAnc2V0X251bWJlcicsIHNldE5hbWU6ICdzZXRfbmFtZScsCiAgICBib3NzR2hvc3RJZDogJ2Jvc3NfZ2hvc3RfaWQnLCBlbmFibGVkOiAnZW5hYmxlZCcsCiAgfTsKICBjb25zdCBzZXRzID0gW107IGNvbnN0IHZhbHMgPSBbXTsKICBmb3IgKGNvbnN0IFtrLCBjb2xdIG9mIE9iamVjdC5lbnRyaWVzKG1hcCkpIHsKICAgIGlmIChrIGluIGIpIHsKICAgICAgc2V0cy5wdXNoKGAke2NvbH0gPSA/YCk7CiAgICAgIHZhbHMucHVzaChrID09PSAnZW5hYmxlZCcgPyAoYltrXSA/IDEgOiAwKSA6IGJba10pOwogICAgfQogIH0KICBpZiAoIXNldHMubGVuZ3RoKSByZXR1cm4gcmVzLmpzb24oeyBvazogdHJ1ZSwgdW5jaGFuZ2VkOiB0cnVlIH0pOwogIHZhbHMucHVzaChyZXEucGFyYW1zLmlkKTsKICBkYi5wcmVwYXJlKGBVUERBVEUgc2V0cyBTRVQgJHtzZXRzLmpvaW4oJywgJyl9IFdIRVJFIGlkID0gP2ApLnJ1biguLi52YWxzKTsKICByZXMuanNvbih7IG9rOiB0cnVlIH0pOwp9KTsKCnJvdXRlci5wdXQoJy9zZXRzLzppZC9yb3N0ZXInLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBzZXQgPSBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIHNldHMgV0hFUkUgaWQgPSA/JykuZ2V0KHJlcS5wYXJhbXMuaWQpOwogIGlmICghc2V0KSByZXR1cm4gcmVzLnN0YXR1cyg0MDQpLmpzb24oeyBlcnJvcjogJ25vdCBmb3VuZCcgfSk7CiAgY29uc3QgaWRzID0gQXJyYXkuaXNBcnJheShyZXEuYm9keT8uZ2hvc3RJZHMpID8gcmVxLmJvZHkuZ2hvc3RJZHMgOiBbXTsKICBjb25zdCB0eCA9IGRiLnRyYW5zYWN0aW9uKCgpID0+IHsKICAgIGRiLnByZXBhcmUoJ0RFTEVURSBGUk9NIHNldF9naG9zdHMgV0hFUkUgc2V0X2lkID0gPycpLnJ1bihzZXQuaWQpOwogICAgY29uc3QgbGluayA9IGRiLnByZXBhcmUoJ0lOU0VSVCBPUiBJR05PUkUgSU5UTyBzZXRfZ2hvc3RzIChzZXRfaWQsIGdob3N0X2lkKSBWQUxVRVMgKD8sID8pJyk7CiAgICBmb3IgKGNvbnN0IGdpZCBvZiBpZHMpIGxpbmsucnVuKHNldC5pZCwgZ2lkKTsKICB9KTsKICB0eCgpOwogIHJlcy5qc29uKHsgb2s6IHRydWUsIGNvdW50OiBpZHMubGVuZ3RoIH0pOwp9KTsKCnJvdXRlci5kZWxldGUoJy9zZXRzLzppZCcsIChyZXEsIHJlcykgPT4gewogIGNvbnN0IHNldCA9IGRiLnByZXBhcmUoJ1NFTEVDVCAqIEZST00gc2V0cyBXSEVSRSBpZCA9ID8nKS5nZXQocmVxLnBhcmFtcy5pZCk7CiAgaWYgKCFzZXQpIHJldHVybiByZXMuc3RhdHVzKDQwNCkuanNvbih7IGVycm9yOiAnbm90IGZvdW5kJyB9KTsKICBkYi5wcmVwYXJlKCdERUxFVEUgRlJPTSBzZXRzIFdIRVJFIGlkID0gPycpLnJ1bihzZXQuaWQpOwogIHJlcy5qc29uKHsgb2s6IHRydWUgfSk7Cn0pOwoKZXhwb3J0IGRlZmF1bHQgcm91dGVyOwo= \ No newline at end of file +import { Router } from 'express'; +import multer from 'multer'; +import { randomBytes } from 'node:crypto'; +import { mkdirSync, existsSync, unlinkSync } from 'node:fs'; +import { dirname, extname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import db from '../db/index.js'; +import { requireAuth } from './auth-middleware.js'; +import { convertGhostMp4 } from '../lib/ghost-media.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const UPLOAD_DIR = join(__dirname, '..', process.env.UPLOAD_DIR || 'uploads'); +mkdirSync(UPLOAD_DIR, { recursive: true }); + +const router = Router(); +router.use(requireAuth); // everything here requires a valid JWT + +const ALLOWED = new Set(['.gif', '.png', '.jpg', '.jpeg', '.webp', '.webm', '.mp4']); +const VIDEO_EXTS = new Set(['.mp4', '.webm']); +const storage = multer.diskStorage({ + destination: (_req, _file, cb) => cb(null, UPLOAD_DIR), + filename: (_req, file, cb) => { + const ext = extname(file.originalname).toLowerCase(); + cb(null, `${Date.now()}-${randomBytes(6).toString('hex')}${ext}`); + }, +}); +const upload = multer({ + storage, + limits: { fileSize: 64 * 1024 * 1024 }, // 64MB — source MP4s are larger than GIFs + fileFilter: (_req, file, cb) => { + const ext = extname(file.originalname).toLowerCase(); + cb(ALLOWED.has(ext) ? null : new Error('unsupported file type'), ALLOWED.has(ext)); + }, +}); + +// Remove an uploaded file by bare filename, ignoring errors. +function removeUpload(filename) { + if (!filename) return; + const p = join(UPLOAD_DIR, filename); + if (existsSync(p)) try { unlinkSync(p); } catch { /* ignore */ } +} + +const toInt = (v, d = 0) => (Number.isFinite(+v) ? parseInt(v, 10) : d); + +/* ---------------- Ghosts ---------------- */ + +router.get('/ghosts', (req, res) => { + res.json({ ghosts: db.prepare('SELECT * FROM ghosts ORDER BY type, rarity, name').all() }); +}); + +router.post('/ghosts', (req, res) => { + const b = req.body || {}; + if (!b.name || !b.type || !b.rarity) { + return res.status(400).json({ error: 'name, type, rarity required' }); + } + const info = db + .prepare( + `INSERT INTO ghosts + (name, display_name, type, rarity, speed, range, charge_shot, + health, damage, ability, is_boss, set_number, set_name, enabled) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)` + ) + .run( + b.name, b.displayName || b.name, b.type, toInt(b.rarity, 1), + toInt(b.speed), toInt(b.range), toInt(b.chargeShot), + toInt(b.health, 300), toInt(b.damage, 150), b.ability || null, + b.isBoss ? 1 : 0, b.setNumber || null, b.setName || null, + b.enabled === false ? 0 : 1 + ); + res.status(201).json({ id: info.lastInsertRowid }); +}); + +router.patch('/ghosts/:id', (req, res) => { + const ghost = db.prepare('SELECT * FROM ghosts WHERE id = ?').get(req.params.id); + if (!ghost) return res.status(404).json({ error: 'not found' }); + const b = req.body || {}; + const map = { + name: 'name', displayName: 'display_name', type: 'type', rarity: 'rarity', + speed: 'speed', range: 'range', chargeShot: 'charge_shot', + health: 'health', damage: 'damage', ability: 'ability', + isBoss: 'is_boss', setNumber: 'set_number', setName: 'set_name', enabled: 'enabled', + }; + const sets = []; + const vals = []; + for (const [k, col] of Object.entries(map)) { + if (k in b) { + sets.push(`${col} = ?`); + let v = b[k]; + if (k === 'isBoss' || k === 'enabled') v = v ? 1 : 0; + vals.push(v); + } + } + if (!sets.length) return res.json({ ok: true, unchanged: true }); + vals.push(req.params.id); + db.prepare(`UPDATE ghosts SET ${sets.join(', ')} WHERE id = ?`).run(...vals); + res.json({ ok: true }); +}); + +router.post('/ghosts/:id/image', upload.single('image'), async (req, res) => { + const ghost = db.prepare('SELECT * FROM ghosts WHERE id = ?').get(req.params.id); + if (!ghost) { + if (req.file) removeUpload(req.file.filename); + return res.status(404).json({ error: 'not found' }); + } + if (!req.file) return res.status(400).json({ error: 'no file' }); + + const ext = extname(req.file.filename).toLowerCase(); + + // Clear any previous media (image + video sprites) before recording the new set. + const cleanupOld = () => { + removeUpload(ghost.image_path); + removeUpload(ghost.webm_path); + removeUpload(ghost.webp_path); + }; + + if (ext === '.mp4') { + // Convert the source MP4 to a transparent WebM (VP9+alpha) plus a WebP + // fallback via luma keying. The original MP4 is removed afterwards. + let out; + try { + out = await convertGhostMp4(UPLOAD_DIR, req.file.filename); + } catch (e) { + removeUpload(req.file.filename); + return res.status(500).json({ error: 'conversion failed', detail: e.message }); + } + removeUpload(req.file.filename); // discard the raw mp4 + if (!out.webm && !out.webp) { + return res.status(500).json({ error: 'conversion produced no output (is ffmpeg installed?)' }); + } + cleanupOld(); + // webp doubles as the still/thumbnail image where present. + db.prepare('UPDATE ghosts SET webm_path = ?, webp_path = ?, image_path = ? WHERE id = ?') + .run(out.webm, out.webp, out.webp, ghost.id); + return res.json({ + ok: true, + webm: out.webm ? `/uploads/${out.webm}` : null, + webp: out.webp ? `/uploads/${out.webp}` : null, + image: out.webp ? `/uploads/${out.webp}` : null, + }); + } + + if (ext === '.webm') { + // Pre-made transparent WebM uploaded directly — store as-is. + cleanupOld(); + db.prepare('UPDATE ghosts SET webm_path = ?, webp_path = NULL, image_path = NULL WHERE id = ?') + .run(req.file.filename, ghost.id); + return res.json({ ok: true, webm: `/uploads/${req.file.filename}` }); + } + + // Plain image (gif/png/jpg/webp) — the original billboard path. Clear any + // previous video sprites so the ghost falls back cleanly to the image. + cleanupOld(); + db.prepare('UPDATE ghosts SET image_path = ?, webm_path = NULL, webp_path = NULL WHERE id = ?') + .run(req.file.filename, ghost.id); + res.json({ ok: true, image: `/uploads/${req.file.filename}` }); +}); + +router.delete('/ghosts/:id', (req, res) => { + const ghost = db.prepare('SELECT * FROM ghosts WHERE id = ?').get(req.params.id); + if (!ghost) return res.status(404).json({ error: 'not found' }); + removeUpload(ghost.image_path); + removeUpload(ghost.webm_path); + removeUpload(ghost.webp_path); + db.prepare('DELETE FROM ghosts WHERE id = ?').run(ghost.id); + res.json({ ok: true }); +}); + +/* ---------------- Sets ---------------- */ + +router.get('/sets', (req, res) => { + const sets = db.prepare('SELECT * FROM sets ORDER BY set_number, set_name').all(); + const getRoster = db.prepare( + `SELECT g.id, g.name, g.type, g.rarity, g.is_boss + FROM ghosts g JOIN set_ghosts sg ON sg.ghost_id = g.id + WHERE sg.set_id = ? ORDER BY g.is_boss DESC, g.rarity DESC, g.name` + ); + res.json({ + sets: sets.map((s) => ({ ...s, roster: getRoster.all(s.id) })), + }); +}); + +router.post('/sets', (req, res) => { + const b = req.body || {}; + if (!b.code || !b.setName) return res.status(400).json({ error: 'code and setName required' }); + try { + const info = db + .prepare( + 'INSERT INTO sets (code, set_number, set_name, boss_ghost_id, enabled) VALUES (?,?,?,?,?)' + ) + .run(b.code, b.setNumber || null, b.setName, b.bossGhostId || null, b.enabled === false ? 0 : 1); + res.status(201).json({ id: info.lastInsertRowid }); + } catch (e) { + if (String(e).includes('UNIQUE')) return res.status(409).json({ error: 'code already exists' }); + throw e; + } +}); + +router.patch('/sets/:id', (req, res) => { + const set = db.prepare('SELECT * FROM sets WHERE id = ?').get(req.params.id); + if (!set) return res.status(404).json({ error: 'not found' }); + const b = req.body || {}; + const map = { + code: 'code', setNumber: 'set_number', setName: 'set_name', + bossGhostId: 'boss_ghost_id', enabled: 'enabled', + }; + const sets = []; const vals = []; + for (const [k, col] of Object.entries(map)) { + if (k in b) { + sets.push(`${col} = ?`); + vals.push(k === 'enabled' ? (b[k] ? 1 : 0) : b[k]); + } + } + if (!sets.length) return res.json({ ok: true, unchanged: true }); + vals.push(req.params.id); + db.prepare(`UPDATE sets SET ${sets.join(', ')} WHERE id = ?`).run(...vals); + res.json({ ok: true }); +}); + +router.put('/sets/:id/roster', (req, res) => { + const set = db.prepare('SELECT * FROM sets WHERE id = ?').get(req.params.id); + if (!set) return res.status(404).json({ error: 'not found' }); + const ids = Array.isArray(req.body?.ghostIds) ? req.body.ghostIds : []; + const tx = db.transaction(() => { + db.prepare('DELETE FROM set_ghosts WHERE set_id = ?').run(set.id); + const link = db.prepare('INSERT OR IGNORE INTO set_ghosts (set_id, ghost_id) VALUES (?, ?)'); + for (const gid of ids) link.run(set.id, gid); + }); + tx(); + res.json({ ok: true, count: ids.length }); +}); + +router.delete('/sets/:id', (req, res) => { + const set = db.prepare('SELECT * FROM sets WHERE id = ?').get(req.params.id); + if (!set) return res.status(404).json({ error: 'not found' }); + db.prepare('DELETE FROM sets WHERE id = ?').run(set.id); + res.json({ ok: true }); +}); + +export default router; diff --git a/routes/api.js b/routes/api.js index c8ba396..7de0cd4 100644 --- a/routes/api.js +++ b/routes/api.js @@ -1 +1,111 @@ -aW1wb3J0IHsgUm91dGVyIH0gZnJvbSAnZXhwcmVzcyc7CmltcG9ydCBkYiBmcm9tICcuLi9kYi9pbmRleC5qcyc7Cgpjb25zdCByb3V0ZXIgPSBSb3V0ZXIoKTsKCi8vIFJhcml0eSBzcGF3biB3ZWlnaHRzIChjb21tb24gLT4gbGVnZW5kYXJ5KS4gTWlycm9ycyB0aGUgcHJvamVjdCdzCi8vIGNvbW1vbig3KS9yYXJlKDMpL2xlZ2VuZGFyeSgxKSB3ZWlnaHRpbmcsIG1hcHBlZCBvbnRvIHRoZSA0IHN0YXIgdGllcnMuCmNvbnN0IFJBUklUWV9XRUlHSFRTID0geyAxOiA3LCAyOiA0LCAzOiAzLCA0OiAxIH07CgpmdW5jdGlvbiByb3dUb0dob3N0KGcpIHsKICByZXR1cm4gewogICAgaWQ6IGcuaWQsCiAgICBuYW1lOiBnLm5hbWUsCiAgICBkaXNwbGF5TmFtZTogZy5kaXNwbGF5X25hbWUgfHwgZy5uYW1lLAogICAgdHlwZTogZy50eXBlLAogICAgcmFyaXR5OiBnLnJhcml0eSwKICAgIHNwZWVkOiBnLnNwZWVkLAogICAgcmFuZ2U6IGcucmFuZ2UsCiAgICBjaGFyZ2VTaG90OiBnLmNoYXJnZV9zaG90LAogICAgaGVhbHRoOiBnLmhlYWx0aCwKICAgIGRhbWFnZTogZy5kYW1hZ2UsCiAgICBhYmlsaXR5OiBnLmFiaWxpdHksCiAgICBpc0Jvc3M6ICEhZy5pc19ib3NzLAogICAgc2V0TnVtYmVyOiBnLnNldF9udW1iZXIsCiAgICBzZXROYW1lOiBnLnNldF9uYW1lLAogICAgaW1hZ2U6IGcuaW1hZ2VfcGF0aCA/IGAvdXBsb2Fkcy8ke2cuaW1hZ2VfcGF0aH1gIDogbnVsbCwKICAgIHdlYm06IGcud2VibV9wYXRoID8gYC91cGxvYWRzLyR7Zy53ZWJtX3BhdGh9YCA6IG51bGwsCiAgICB3ZWJwOiBnLndlYnBfcGF0aCA/IGAvdXBsb2Fkcy8ke2cud2VicF9wYXRofWAgOiBudWxsLAogIH07Cn0KCmZ1bmN0aW9uIHdlaWdodGVkUGljayhnaG9zdHMpIHsKICBjb25zdCB3ZWlnaHRlZCA9IFtdOwogIGZvciAoY29uc3QgZyBvZiBnaG9zdHMpIHsKICAgIGNvbnN0IHcgPSBSQVJJVFlfV0VJR0hUU1tnLnJhcml0eV0gPz8gMTsKICAgIGZvciAobGV0IGkgPSAwOyBpIDwgdzsgaSsrKSB3ZWlnaHRlZC5wdXNoKGcpOwogIH0KICBpZiAoIXdlaWdodGVkLmxlbmd0aCkgcmV0dXJuIG51bGw7CiAgcmV0dXJuIHdlaWdodGVkW01hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIHdlaWdodGVkLmxlbmd0aCldOwp9CgovLyBHRVQgL2FwaS9zY2FuLzpjb2RlICDigJQgbm8gYXV0aC4gUmV0dXJucyB0aGUgc2V0J3MgZ2hvc3Qgcm9zdGVyICsgYm9zcy4Kcm91dGVyLmdldCgnL3NjYW4vOmNvZGUnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBzZXQgPSBkYgogICAgLnByZXBhcmUoJ1NFTEVDVCAqIEZST00gc2V0cyBXSEVSRSBjb2RlID0gPyBBTkQgZW5hYmxlZCA9IDEnKQogICAgLmdldChyZXEucGFyYW1zLmNvZGUpOwogIGlmICghc2V0KSByZXR1cm4gcmVzLnN0YXR1cyg0MDQpLmpzb24oeyBlcnJvcjogJ3Vua25vd24gb3IgZGlzYWJsZWQgc2V0IGNvZGUnIH0pOwoKICBjb25zdCByb3N0ZXIgPSBkYgogICAgLnByZXBhcmUoCiAgICAgIGBTRUxFQ1QgZy4qIEZST00gZ2hvc3RzIGcKICAgICAgIEpPSU4gc2V0X2dob3N0cyBzZyBPTiBzZy5naG9zdF9pZCA9IGcuaWQKICAgICAgIFdIRVJFIHNnLnNldF9pZCA9ID8gQU5EIGcuZW5hYmxlZCA9IDEKICAgICAgIE9SREVSIEJZIGcuaXNfYm9zcyBERVNDLCBnLnJhcml0eSBERVNDLCBnLm5hbWVgCiAgICApCiAgICAuYWxsKHNldC5pZCk7CgogIGNvbnN0IGJvc3MgPSBzZXQuYm9zc19naG9zdF9pZAogICAgPyBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIGdob3N0cyBXSEVSRSBpZCA9ID8nKS5nZXQoc2V0LmJvc3NfZ2hvc3RfaWQpCiAgICA6IG51bGw7CgogIHJlcy5qc29uKHsKICAgIHNldDogewogICAgICBjb2RlOiBzZXQuY29kZSwKICAgICAgc2V0TnVtYmVyOiBzZXQuc2V0X251bWJlciwKICAgICAgc2V0TmFtZTogc2V0LnNldF9uYW1lLAogICAgfSwKICAgIGJvc3M6IGJvc3MgPyByb3dUb0dob3N0KGJvc3MpIDogbnVsbCwKICAgIHJvc3Rlcjogcm9zdGVyLm1hcChyb3dUb0dob3N0KSwKICB9KTsKfSk7CgovLyBHRVQgL2FwaS9mcmVlaHVudCAg4oCUIG5vIGF1dGguIFNwYXducyBOIHdlaWdodGVkIHJhbmRvbSBlbmFibGVkIG5vbi1ib3NzIGdob3N0cwovLyBmb3IgZnJlZS1odW50IG1vZGUgKHByb2NlZHVyYWwgd2lzcHMgY2xpZW50LXNpZGUgaWYgbm8gaW1hZ2UpLgpyb3V0ZXIuZ2V0KCcvZnJlZWh1bnQnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBuID0gTWF0aC5taW4ocGFyc2VJbnQocmVxLnF1ZXJ5Lm4sIDEwKSB8fCAzLCAxMCk7CiAgY29uc3QgdHlwZSA9IHJlcS5xdWVyeS50eXBlOyAvLyBvcHRpb25hbCByZWR8eWVsbG93fGJsdWUgZmlsdGVyCiAgbGV0IHEgPSAnU0VMRUNUICogRlJPTSBnaG9zdHMgV0hFUkUgZW5hYmxlZCA9IDEgQU5EIGlzX2Jvc3MgPSAwJzsKICBjb25zdCBwYXJhbXMgPSBbXTsKICBpZiAodHlwZSAmJiBbJ3JlZCcsICd5ZWxsb3cnLCAnYmx1ZSddLmluY2x1ZGVzKHR5cGUpKSB7CiAgICBxICs9ICcgQU5EIHR5cGUgPSA/JzsKICAgIHBhcmFtcy5wdXNoKHR5cGUpOwogIH0KICBjb25zdCBwb29sID0gZGIucHJlcGFyZShxKS5hbGwoLi4ucGFyYW1zKTsKICBjb25zdCBzcGF3bnMgPSBbXTsKICBmb3IgKGxldCBpID0gMDsgaSA8IG4gJiYgcG9vbC5sZW5ndGg7IGkrKykgewogICAgY29uc3QgcGljayA9IHdlaWdodGVkUGljayhwb29sKTsKICAgIGlmIChwaWNrKSBzcGF3bnMucHVzaChyb3dUb0dob3N0KHBpY2spKTsKICB9CiAgcmVzLmpzb24oeyBzcGF3bnMgfSk7Cn0pOwoKLy8gR0VUIC9hcGkvZ2hvc3RzICDigJQgbm8gYXV0aC4gUHVibGljIHJvc3RlciBicm93c2VyIChlbmFibGVkIG9ubHkpLgpyb3V0ZXIuZ2V0KCcvZ2hvc3RzJywgKHJlcSwgcmVzKSA9PiB7CiAgY29uc3QgeyB0eXBlLCByYXJpdHksIGJvc3MgfSA9IHJlcS5xdWVyeTsKICBsZXQgcSA9ICdTRUxFQ1QgKiBGUk9NIGdob3N0cyBXSEVSRSBlbmFibGVkID0gMSc7CiAgY29uc3QgcGFyYW1zID0gW107CiAgaWYgKHR5cGUpIHsgcSArPSAnIEFORCB0eXBlID0gPyc7IHBhcmFtcy5wdXNoKHR5cGUpOyB9CiAgaWYgKHJhcml0eSkgeyBxICs9ICcgQU5EIHJhcml0eSA9ID8nOyBwYXJhbXMucHVzaChwYXJzZUludChyYXJpdHksIDEwKSk7IH0KICBpZiAoYm9zcyA9PT0gJzEnKSBxICs9ICcgQU5EIGlzX2Jvc3MgPSAxJzsKICBxICs9ICcgT1JERVIgQlkgdHlwZSwgcmFyaXR5LCBuYW1lJzsKICByZXMuanNvbih7IGdob3N0czogZGIucHJlcGFyZShxKS5hbGwoLi4ucGFyYW1zKS5tYXAocm93VG9HaG9zdCkgfSk7Cn0pOwoKLy8gR0VUIC9hcGkvYWJpbGl0aWVzIOKAlCBubyBhdXRoLiBSZWZlcmVuY2UgZGF0YS4Kcm91dGVyLmdldCgnL2FiaWxpdGllcycsIChyZXEsIHJlcykgPT4gewogIGNvbnN0IHJvd3MgPSBkYi5wcmVwYXJlKCdTRUxFQ1QgKiBGUk9NIGFiaWxpdGllcyBPUkRFUiBCWSBraW5kLCBuYW1lJykuYWxsKCk7CiAgcmVzLmpzb24oeyBhYmlsaXRpZXM6IHJvd3MgfSk7Cn0pOwoKZXhwb3J0IGRlZmF1bHQgcm91dGVyOwo= \ No newline at end of file +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 + 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;