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 + ? `