From 1f20359fa3a4c28d827df2ed97ba68d9684deda6 Mon Sep 17 00:00:00 2001 From: Oleksandr Shuryha Date: Thu, 26 Mar 2026 11:33:03 +0100 Subject: [PATCH] initial --- .gitignore | 29 + CHANGELOG.md | 42 + README.md | 50 + backend/data/faction-assets/arachne.png | Bin 0 -> 6919 bytes backend/data/faction-assets/cyberacme.png | Bin 0 -> 4332 bytes backend/data/faction-assets/mida.png | Bin 0 -> 3843 bytes backend/data/faction-assets/nucaloric.png | Bin 0 -> 1827 bytes backend/data/faction-assets/sekiguchi.png | Bin 0 -> 2665 bytes backend/data/faction-assets/traxus.png | Bin 0 -> 2453 bytes backend/data/fonts/Ki-Bold.otf | Bin 0 -> 42012 bytes backend/data/fonts/Ki-Regular.otf | Bin 0 -> 40372 bytes backend/references/api.js | 745 ++++++++ backend/references/faction-upgrades.js | 1479 ++++++++++++++++ backend/references/faction-upgrades.live.js | 1479 ++++++++++++++++ backend/server.js | 592 +++++++ frontend/index.html | 13 + frontend/public/favicon.ico | Bin 0 -> 3262 bytes frontend/public/fonts/Ki-Bold.otf | Bin 0 -> 42012 bytes frontend/public/fonts/Ki-Regular.otf | Bin 0 -> 40372 bytes frontend/src/App.tsx | 814 +++++++++ frontend/src/main.tsx | 10 + frontend/src/marathonApi.ts | 157 ++ frontend/src/styles.css | 509 ++++++ frontend/src/types.ts | 43 + package-lock.json | 1729 +++++++++++++++++++ package.json | 26 + project.md | 29 + scripts/dev-all.mjs | 62 + tsconfig.json | 18 + vite.config.ts | 23 + 30 files changed, 7849 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 backend/data/faction-assets/arachne.png create mode 100644 backend/data/faction-assets/cyberacme.png create mode 100644 backend/data/faction-assets/mida.png create mode 100644 backend/data/faction-assets/nucaloric.png create mode 100644 backend/data/faction-assets/sekiguchi.png create mode 100644 backend/data/faction-assets/traxus.png create mode 100644 backend/data/fonts/Ki-Bold.otf create mode 100644 backend/data/fonts/Ki-Regular.otf create mode 100644 backend/references/api.js create mode 100644 backend/references/faction-upgrades.js create mode 100644 backend/references/faction-upgrades.live.js create mode 100644 backend/server.js create mode 100644 frontend/index.html create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/public/fonts/Ki-Bold.otf create mode 100644 frontend/public/fonts/Ki-Regular.otf create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/marathonApi.ts create mode 100644 frontend/src/styles.css create mode 100644 frontend/src/types.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 project.md create mode 100644 scripts/dev-all.mjs create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df021c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Dependencies +node_modules/ + +# Build output +dist/ +frontend/dist/ + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +*.log + +# Environment/local config +.env +.env.* +!.env.example + +# OS/editor noise +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# Runtime data/cache +backend/data/catalog.db +backend/data/*.db +backend/data/*.db-* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0e1823f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +## 0.1.0 - 2026-03-25 +- Created initial frontend-only React + TypeScript project scaffold with Vite. +- Added typed Marathon API client for fetching/searching items from `/api/items/all`. +- Implemented initial app features: + - Item search by name. + - Top 5 relevant search results with icons. + - Click-to-add items into to-do list. + - Quantity editing per item. + - Completed state toggle. + - Item deletion. +- Added simple, minimal UI styling. +- Added `README.md` documentation and local setup instructions. + +## 0.2.0 - 2026-03-25 +- Fetched faction upgrades data from `https://marathondb.gg/js/data/faction-upgrades.js` and added it to app data sources. +- Extended search results with faction upgrades that include salvage requirements in their levels. +- Added upgrade click behavior: clicking an upgrade adds all mapped salvage items to the to-do list and increases quantity when an item already exists. +- Added image URL fallback candidates for search/list icons with extension order ending in `.webp`. + +## 0.3.0 - 2026-03-25 +- Added a local proxy server (`proxy/server.js`) using Node + SQLite (`proxy/catalog.db`). +- Proxy now fetches Marathon items and faction upgrades from source URLs, builds a combined searchable catalog, and serves it at `GET /api/catalog`. +- Added automatic catalog refresh every 24 hours and a manual refresh endpoint `POST /api/catalog/refresh`. +- Frontend data loading now uses `/api/catalog` instead of hitting external source endpoints directly. +- Added Vite dev proxy configuration for `/api` and `/health` to `http://localhost:8787`. +- Added a right-side faction upgrade tag in search results, using faction color and ` upgrade` label text. +- Added temporary faction-color highlight animation on affected to-do items when clicking an upgrade search result. + +## 0.4.0 - 2026-03-25 +- Reorganized repository into explicit `frontend/` and `backend/` directories. +- Moved React app files to `frontend/` and proxy API to `backend/server.js`. +- Moved SQLite catalog storage path to `backend/data/catalog.db`. +- Added `backend/references/` for source reference files (`api.js`, faction upgrades snapshots). +- Updated scripts/config/docs for the new structure (`dev:frontend`, `dev:backend`, Vite root set to `frontend`). +- Added `dev:all` script to start backend and frontend together with one command. +- Replaced inline `dev:all` command with `scripts/dev-all.mjs` for reliable Windows process spawning. +- Upgrade search results now use faction assets from `backend/data/faction-assets/*.png` via `/api/faction-assets/:file`. +- Upgrade search result rows now have border color based on faction color. +- Upgrade search results now expose per-level salvage and require clicking a level button (`L1`, `L2`, ...) to add items. +- Faction-color border is now applied around the upgrade icon frame (not the whole search row). diff --git a/README.md b/README.md new file mode 100644 index 0000000..23b75ab --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# marathon.todo + +A frontend-only React + TypeScript app for planning what to loot (or do) in raid in Bungie's Marathon. + +## Features +- Search for Marathon items by name. +- Show up to 5 relevant results with item icons. +- Search also includes faction upgrades with salvage requirements. +- Add an item to a to-do list by clicking a search result. +- Clicking an upgrade search result adds all mapped salvage materials to the to-do list. +- Set quantity per to-do entry. +- Mark entries completed. +- Delete entries. +- Persist to-do entries in browser `localStorage`. + +## Data Source +- Frontend fetches a local proxy endpoint: `/api/catalog` +- Proxy source data: + - Items: `https://items.marathondb.gg/api/items` + - Faction upgrades: `https://marathondb.gg/js/data/faction-upgrades.js` +- Proxy stores processed catalog in SQLite (`backend/data/catalog.db`) and refreshes every 24h. + +## Project Structure +- `frontend/`: Vite + React app (`frontend/src`, `frontend/index.html`) +- `backend/`: local proxy API (`backend/server.js`) and SQLite data (`backend/data/`) +- `backend/references/`: downloaded reference source files used during development + +## Tech Stack +- React +- TypeScript +- Vite + +## Run Locally +1. Install dependencies: + ```bash + npm install + ``` +2. Start backend + frontend together: + ```bash + npm run dev:all + ``` +3. Or run separately if preferred: + ```bash + npm run dev:backend + npm run dev:frontend + ``` +4. Build production assets: + ```bash + npm run build + ``` diff --git a/backend/data/faction-assets/arachne.png b/backend/data/faction-assets/arachne.png new file mode 100644 index 0000000000000000000000000000000000000000..275c923ad9e969701135d52ffcaa65b2653a8d18 GIT binary patch literal 6919 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 GqW=eZIPCWT literal 0 HcmV?d00001 diff --git a/backend/data/faction-assets/cyberacme.png b/backend/data/faction-assets/cyberacme.png new file mode 100644 index 0000000000000000000000000000000000000000..b4068b2804d79c7531de8ea7d0770af8a3d7ab62 GIT binary patch literal 4332 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 FV1gc>R4@Pl literal 0 HcmV?d00001 diff --git a/backend/data/faction-assets/nucaloric.png b/backend/data/faction-assets/nucaloric.png new file mode 100644 index 0000000000000000000000000000000000000000..583812857d37c7ba05cb986dade4d9bbd3ae8634 GIT binary patch literal 1827 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

5k+ziB_h%JbIZxDd*MC@MmT^K5d|cBuZeYE zZY7W!AV_ZMwde&DvH83W7`s$-b5GhBut3ygvg#iInnWm19$T*?nlC$+LWmNvqQaAL zLp%T>bpQZAzqeS~Z9c9-NFla}&XaxJ8DowZrOyU6;;GA^vFt|GL&cGv5-P5sTtxni z?@l%G;MBe`s!;a{ojy5=AhiWi^Ek!x1f)&`0zUW3*@{>+i9p|JK2jbNC1UY><^j!% z*n6-^xydo++)_dE?TSyoxQhsCo0jHjnodJrDiO^;I+ev^#34U$wnaF^GuJu$+Xcpg z3x}0g$>&5|OZ8o%gm6mZwbdwB9H8LPv+58fE0^u8-_! z&Q8xVD+?-cO2(cG<0?+4#bPl}SrkcXdQOCMzz@Eg=Nn{biR6YjS!c+BPfrWQT!G(y za58*eGN1VxV>CpAKvee1q~$=;^{MZsM-uvVB9i`eIy5>@^=61L#4j_?iKSQ=C~#27 zbSNZ>)UidrESX&7v-F7p-FT1Z12vFuf=S=a;V7~VfSC{^yWE4k<|?wHZSDg-?!tgI zitx!gJ1-CgLjbu!WT_6QdASOiEIK0ZOKewqQl^hp$Sw3pManYIS>)3)AQnbv+aMw$ z@=NUnhuPgk#1_#6(NX>ZNeS#05n3bia=l1r8K&!ENVN!sXzjU(xJRU!%O21rA}c=} zg#cizsEB83At_z0RGszqFGa*%WP~CaXAB9HO>5;Msjyyn4I;k(zakPTk>?F2Ng+kF zh+u~vO%ZVxp#)4Ukp>kuGU`YoEucV_9&EJt{-y|6G@9uCQj6%_R0^7Gz@B|3=<)9& zVoqeLoDvlD93wkQSSkuI#0~R8Hgt ziXT$MViEgq*=h$){Fdlpi{Lzp{UC}~L}*G+K_x*D7_X2rq=>~aI=rF^UYEa<9=3>H zn`)=9V4h|RB9nE2-huUaH? zk$gj5Z<*>|?);;=WnB+ZA{O)E;B|mEeN>CQA%OrN(L#zi=LnM!1+$@)mE$WBi|-S2 zo6{C4aAQPPQ9O)?f><&96KXaWzMi~FHlrwtk%=G{M3STDM(3&1d49&vj-vaa$iu^f z`Lx^ZAmVpEc&>E1-7X&3#Jh9n&Ye4V?)+{K4haAN000F2f9(k)0000000000004^s XfQg5_<_1Q%00000NkvXXu0mjf@{`Kr literal 0 HcmV?d00001 diff --git a/backend/data/faction-assets/traxus.png b/backend/data/faction-assets/traxus.png new file mode 100644 index 0000000000000000000000000000000000000000..058a7d75cdc29180d361e11f1abdc0262fa9336d GIT binary patch literal 2453 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 Ti8t(p00000NkvXXu0mjfTikIo literal 0 HcmV?d00001 diff --git a/backend/data/fonts/Ki-Bold.otf b/backend/data/fonts/Ki-Bold.otf new file mode 100644 index 0000000000000000000000000000000000000000..9df9c963425cb00bea36f967fe5b35cb43edf40c GIT binary patch literal 42012 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 bLZ6|TgdIBfGf^t+@UhxPx_{nf%K!X73cyuS literal 0 HcmV?d00001 diff --git a/backend/data/fonts/Ki-Regular.otf b/backend/data/fonts/Ki-Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..ad6e98c09cedda0c60cdbf696d9d9a5ec549a562 GIT binary patch literal 40372 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

qPruEq`mTn6gqu+Xz0%Pv!y$K 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 L`aRmZ^vVARGc4GK literal 0 HcmV?d00001 diff --git a/backend/references/api.js b/backend/references/api.js new file mode 100644 index 0000000..97e3063 --- /dev/null +++ b/backend/references/api.js @@ -0,0 +1,745 @@ +// Marathon API Client +// Provides API access for all database pages + +// ─── Session Cache ──────────────────────────────────────────────── +// Thin sessionStorage layer — caches GET responses with a 5-min TTL +// so repeat visits within the same tab session avoid redundant fetches. +const ApiCache = (function () { + const TTL = 5 * 60 * 1000; // 5 minutes + const PREFIX = 'mdb_'; + + function _key(url) { return PREFIX + url; } + + return { + get(url) { + try { + const raw = sessionStorage.getItem(_key(url)); + if (!raw) return null; + const entry = JSON.parse(raw); + if (Date.now() - entry.ts > TTL) { + sessionStorage.removeItem(_key(url)); + return null; + } + return entry.data; + } catch { return null; } + }, + set(url, data) { + try { + sessionStorage.setItem(_key(url), JSON.stringify({ ts: Date.now(), data })); + } catch { /* quota exceeded — silently skip */ } + }, + clear() { + try { + Object.keys(sessionStorage) + .filter(k => k.startsWith(PREFIX)) + .forEach(k => sessionStorage.removeItem(k)); + } catch { /* ignore */ } + } + }; +})(); + +const DISPLAY_NAME_KEYS = new Set([ + 'name', + 'display_name', + 'weapon_name', + 'runner_name', + 'collection_name', + 'faction_name' +]); + +function normalizeDisplayName(value) { + if (typeof value !== 'string') return value; + const trimmed = value.trim(); + if (!trimmed) return value; + + return trimmed + .split(/\s+/) + .map(token => token + .split(/([\-/'’])/) + .map(part => { + if (!part || /[\-/'’]/.test(part)) return part; + if (!/[A-Za-z]/.test(part)) return part; + if (/^[ivxlcdm]+$/i.test(part) && part.length <= 6) return part.toUpperCase(); + if (/^[A-Z0-9]+$/.test(part) && part.length <= 5) return part; + return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(); + }) + .join('')) + .join(' '); +} + +function normalizeDisplayNamesInPayload(payload) { + if (Array.isArray(payload)) { + return payload.map(normalizeDisplayNamesInPayload); + } + if (!payload || typeof payload !== 'object') { + return payload; + } + + const normalized = {}; + for (const [key, value] of Object.entries(payload)) { + if (DISPLAY_NAME_KEYS.has(key) && typeof value === 'string') { + normalized[key] = normalizeDisplayName(value); + } else { + normalized[key] = normalizeDisplayNamesInPayload(value); + } + } + return normalized; +} + +if (typeof window !== 'undefined') { + window.MarathonNameUtils = window.MarathonNameUtils || {}; + window.MarathonNameUtils.normalizeDisplayName = normalizeDisplayName; + window.MarathonNameUtils.normalizeDisplayNamesInPayload = normalizeDisplayNamesInPayload; +} + +const MarathonAPI = (function() { + const API_BASE = 'https://helpbot.marathondb.gg'; + const CORES_API_BASE = 'https://cores.marathondb.gg'; + const IMPLANTS_API_BASE = 'https://implants.marathondb.gg'; + const MODS_API_BASE = 'https://mods.marathondb.gg'; + const CONTRACTS_API_BASE = 'https://marathon-contracts-api.heymarathondb.workers.dev'; + + // Generic fetch wrapper (with session cache for GETs) + async function fetchAPI(endpoint, options = {}) { + try { + const { headers: extraHeaders, ...rest } = options; + const method = (rest.method || 'GET').toUpperCase(); + const url = `${API_BASE}${endpoint}`; + + // Cache hit — return immediately for safe methods + if (method === 'GET') { + const cached = ApiCache.get(url); + if (cached) return cached; + } + + const needsContentType = method !== 'GET' && method !== 'HEAD'; + const headers = { + ...(needsContentType ? { 'Content-Type': 'application/json' } : {}), + ...(extraHeaders || {}), + }; + const response = await fetch(url, { + ...rest, + headers, + }); + + const data = await response.json().catch(() => null); + + if (!response.ok) { + const err = new Error(`API Error: ${response.status}`); + err.status = response.status; + err.body = data; + throw err; + } + + if (method === 'GET' && data) ApiCache.set(url, data); + return data; + } catch (error) { + if (error.status) throw error; // Re-throw API errors with body attached + console.error('API fetch error:', error); + return null; + } + } + + // Cores-specific fetch wrapper (uses cores.marathondb.gg exclusively) + async function fetchCoresAPI(endpoint, options = {}) { + try { + const { headers: extraHeaders, ...rest } = options; + const method = (rest.method || 'GET').toUpperCase(); + const url = `${CORES_API_BASE}${endpoint}`; + + if (method === 'GET') { + const cached = ApiCache.get(url); + if (cached) return cached; + } + + const needsContentType = method !== 'GET' && method !== 'HEAD'; + const headers = { + ...(needsContentType ? { 'Content-Type': 'application/json' } : {}), + ...(extraHeaders || {}), + }; + const response = await fetch(url, { + ...rest, + headers, + }); + + const data = await response.json().catch(() => null); + + if (!response.ok) { + const err = new Error(`Cores API Error: ${response.status}`); + err.status = response.status; + err.body = data; + throw err; + } + + if (method === 'GET' && data) ApiCache.set(url, data); + return data; + } catch (error) { + if (error.status) throw error; + console.error('Cores API fetch error:', error); + return null; + } + } + + // Implants-specific fetch wrapper (uses implants.marathondb.gg exclusively) + async function fetchImplantsAPI(endpoint, options = {}) { + try { + const { headers: extraHeaders, ...rest } = options; + const method = (rest.method || 'GET').toUpperCase(); + const url = `${IMPLANTS_API_BASE}${endpoint}`; + + if (method === 'GET') { + const cached = ApiCache.get(url); + if (cached) return cached; + } + + const needsContentType = method !== 'GET' && method !== 'HEAD'; + const headers = { + ...(needsContentType ? { 'Content-Type': 'application/json' } : {}), + ...(extraHeaders || {}), + }; + const response = await fetch(url, { + ...rest, + headers, + }); + + const data = await response.json().catch(() => null); + + if (!response.ok) { + const err = new Error(`Implants API Error: ${response.status}`); + err.status = response.status; + err.body = data; + throw err; + } + + if (method === 'GET' && data) ApiCache.set(url, data); + return data; + } catch (error) { + if (error.status) throw error; + console.error('Implants API fetch error:', error); + return null; + } + } + + // Mods-specific fetch wrapper (uses mods.marathondb.gg exclusively) + async function fetchModsAPI(endpoint, options = {}) { + try { + const { headers: extraHeaders, ...rest } = options; + const method = (rest.method || 'GET').toUpperCase(); + const url = `${MODS_API_BASE}${endpoint}`; + + if (method === 'GET') { + const cached = ApiCache.get(url); + if (cached) return cached; + } + + const needsContentType = method !== 'GET' && method !== 'HEAD'; + const headers = { + ...(needsContentType ? { 'Content-Type': 'application/json' } : {}), + ...(extraHeaders || {}), + }; + const response = await fetch(url, { + ...rest, + headers, + }); + + const data = await response.json().catch(() => null); + + if (!response.ok) { + const err = new Error(`Mods API Error: ${response.status}`); + err.status = response.status; + err.body = data; + throw err; + } + + if (method === 'GET' && data) ApiCache.set(url, data); + return data; + } catch (error) { + if (error.status) throw error; + console.error('Mods API fetch error:', error); + return null; + } + } + + // Contracts-specific fetch wrapper (uses marathon-contracts-api.heymarathondb.workers.dev) + async function fetchContractsAPI(endpoint) { + try { + const url = `${CONTRACTS_API_BASE}${endpoint}`; + const cached = ApiCache.get(url); + if (cached) return cached; + + const response = await fetch(url); + const data = await response.json().catch(() => null); + + if (!response.ok) { + const err = new Error(`Contracts API Error: ${response.status}`); + err.status = response.status; + err.body = data; + throw err; + } + + if (data) ApiCache.set(url, data); + return data; + } catch (error) { + if (error.status) throw error; + console.error('Contracts API fetch error:', error); + return null; + } + } + + return { + // ============ API BASE ============ + getApiBase: function() { + return API_BASE; + }, + + // ============ GENERIC GET ============ + get: async function(endpoint) { + const result = await fetchAPI(`/api${endpoint}`); + // API already returns { success, data } format + if (result && result.success !== undefined) { + return result; + } + // Wrap raw data in standard format + return { success: !!result, data: result }; + }, + + // ============ WEAPONS ============ + getWeapons: async function(category = null) { + const endpoint = category ? `/api/weapons?category=${category}` : '/api/weapons'; + return await fetchAPI(endpoint); + }, + + getWeaponBySlug: async function(slug) { + return await fetchAPI(`/api/weapons/${slug}`); + }, + + getWeaponHistory: async function(slug) { + return await fetchAPI(`/api/weapons/${slug}/history`); + }, + + // ============ CATEGORIES ============ + getCategories: async function() { + return await fetchAPI('/api/categories'); + }, + + // ============ RUNNERS ============ + getRunners: async function() { + return await fetchAPI('/api/runners'); + }, + + getRunnerBySlug: async function(slug) { + return await fetchAPI(`/api/runners/${slug}`); + }, + + getRunnerHistory: async function(slug) { + return await fetchAPI(`/api/runners/${slug}/history`); + }, + + compareRunners: async function(season = null) { + const endpoint = season ? `/api/runners/compare?season=${season}` : '/api/runners/compare'; + return await fetchAPI(endpoint); + }, + + // ============ ITEMS ============ + // Unified endpoint - returns all items from all tables + getAllItems: async function() { + return await fetchAPI('/api/items/all'); + }, + + getItems: async function(type = null, rarity = null) { + let endpoint = '/api/items'; + const params = []; + if (type) params.push(`type=${type}`); + if (rarity) params.push(`rarity=${rarity}`); + if (params.length) endpoint += '?' + params.join('&'); + return await fetchAPI(endpoint); + }, + + getItemsByType: async function(type) { + return await fetchAPI(`/api/items/types/${type}`); + }, + + getItemBySlug: async function(slug) { + return await fetchAPI(`/api/items/${slug}`); + }, + + // Get available item types for filter dropdowns (API v2.0.0) + getItemTypes: async function() { + return await fetchAPI('/api/items/types'); + }, + + // ============ MODS ============ + getMods: async function() { + return await fetchModsAPI('/api/mods'); + }, + + getModBySlug: async function(slug) { + return await fetchModsAPI(`/api/mods/${slug}`); + }, + + getModsForWeapon: async function(weaponSlug) { + return await fetchModsAPI(`/api/weapons/${encodeURIComponent(weaponSlug)}/mods`); + }, + + getModSlotsForWeapon: async function(weaponSlug) { + return await fetchModsAPI(`/api/weapons/${encodeURIComponent(weaponSlug)}/slots`); + }, + + // ============ SEASONS ============ + getSeasons: async function() { + return await fetchAPI('/api/seasons'); + }, + + getCurrentSeason: async function() { + return await fetchAPI('/api/seasons/current'); + }, + + // ============ LOADOUTS ============ + createLoadout: async function(loadout) { + return await fetchAPI('/api/loadouts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(loadout), + }); + }, + + getLoadout: async function(shareCode) { + return await fetchAPI(`/api/loadouts/${shareCode}`); + }, + + getLoadouts: async function(params = {}) { + const qs = Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&'); + return await fetchAPI(`/api/loadouts${qs ? '?' + qs : ''}`); + }, + + deleteLoadout: async function(shareCode, adminKey) { + return await fetchAPI(`/api/loadouts/${shareCode}`, { + method: 'DELETE', + headers: { 'X-Admin-Key': adminKey }, + }); + }, + + // ============ STAT RANGES ============ + getWeaponStatRanges: async function() { + return await fetchAPI('/api/weapons/stat-ranges'); + }, + + getRunnerStatRanges: async function() { + return await fetchAPI('/api/runners/stat-ranges'); + }, + + // ============ ASSET URLs ============ + getWeaponIconUrl: function(iconPath, resolution = 'low') { + if (!iconPath) return `${API_BASE}/assets/weapons/placeholder.png`; + const filename = iconPath.split('/').pop(); + return `${API_BASE}/assets/weapons/${encodeURIComponent(filename)}`; + }, + + // Get weapon icon with resolution option ('low' = 180x135, 'high' = 800x600) + getWeaponIconUrlBySlug: function(slug, resolution = 'low') { + if (!slug) return `${API_BASE}/assets/weapons/placeholder.png`; + const suffix = resolution === 'high' ? '800x600' : '180x135'; + return `${API_BASE}/assets/weapons/${slug}-${suffix}.png`; + }, + + getRunnerIconUrl: function(iconPath) { + if (!iconPath) return `${API_BASE}/assets/runners/placeholder.png`; + // If it's already a full path from API (e.g., "assets/runners/thief-300x460.png") + if (iconPath.startsWith('assets/')) { + return `${API_BASE}/${iconPath}`; + } + // If slug is passed instead of full path, construct the URL + const slug = iconPath.toLowerCase().replace(/\.png$/, ''); + return `${API_BASE}/assets/runners/${slug}-300x460.png`; + }, + + // Get runner icon with resolution option ('low' = 150x230, 'high' = 300x460) + getRunnerIconUrlBySlug: function(slug, resolution = 'high') { + if (!slug) return `${API_BASE}/assets/runners/placeholder.png`; + const suffix = resolution === 'low' ? '150x230' : '300x460'; + return `${API_BASE}/assets/runners/${slug}-${suffix}.png`; + }, + + getItemIconUrl: function(iconPath) { + if (!iconPath) return `${API_BASE}/assets/items/placeholder.png`; + // If it's already a full path from API (e.g., "assets/items/consumables/patch-kit-64x64.png") + if (iconPath.startsWith('assets/')) { + return `${API_BASE}/${iconPath}`; + } + // Fallback: assume it's just a filename or slug + return `${API_BASE}/assets/items/${encodeURIComponent(iconPath)}`; + }, + + getStatIconUrl: function(statKey) { + // Map stat keys to icon filenames + const statIcons = { + 'heat_capacity': 'heat-capacity.jpg', + 'agility': 'agility.jpg', + 'loot_speed': 'loot-speed.jpg', + 'self_repair_speed': 'self-repair-speed.jpg', + 'finisher_siphon': 'finisher-siphon.jpg', + 'revive_speed': 'revive-speed.jpg', + 'hardware': 'hardware.jpg', + 'firewall': 'firewall.jpg' + }; + const filename = statIcons[statKey] || 'placeholder.png'; + return `/assets/icons/${filename}`; + }, + + // ============ CONSUMABLES ============ + getConsumables: async function(type = null, rarity = null) { + let endpoint = '/api/consumables'; + const params = []; + if (type) params.push(`type=${type}`); + if (rarity) params.push(`rarity=${rarity}`); + if (params.length > 0) endpoint += `?${params.join('&')}`; + return await fetchAPI(endpoint); + }, + + // ============ CORES ============ + getCores: async function(runnerType = null, rarity = null, purchaseable = null, activeOnly = null) { + let endpoint = '/api/cores'; + const params = []; + if (runnerType) params.push(`runner=${runnerType}`); + if (rarity) params.push(`rarity=${rarity}`); + if (purchaseable) params.push(`purchaseable=true`); + if (activeOnly !== null) params.push(`active=${activeOnly}`); + if (params.length > 0) endpoint += `?${params.join('&')}`; + return await fetchCoresAPI(endpoint); + }, + + getCoresByRunner: async function(runnerType) { + return await fetchCoresAPI(`/api/cores/runner/${runnerType}`); + }, + + getCoreBySlug: async function(slug) { + // Returns detailed core info with full balance history + return await fetchCoresAPI(`/api/cores/${slug}`); + }, + + getCoreChangelog: async function() { + // Chronological changelog of all balance changes + return await fetchCoresAPI('/api/cores/changelog'); + }, + + getCoreIconUrl: function(iconPath) { + if (!iconPath) return `${CORES_API_BASE}/assets/items/cores/placeholder.png`; + // If it's already a full URL from the API response + if (iconPath.startsWith('http')) return iconPath; + // If it's a relative path from API + if (iconPath.startsWith('assets/')) { + return `${CORES_API_BASE}/${iconPath}`; + } + // Fallback: assume it's a slug and construct the path + return `${CORES_API_BASE}/assets/items/cores/${encodeURIComponent(iconPath)}-72x72.png`; + }, + + // ============ IMPLANTS ============ + getImplants: async function(slot = null, rarity = null, page = null, limit = null) { + let endpoint = '/api/implants'; + const params = []; + if (slot) params.push(`slot=${slot}`); + if (rarity) params.push(`rarity=${rarity}`); + if (page) params.push(`page=${page}`); + if (limit) params.push(`limit=${limit}`); + if (params.length > 0) endpoint += `?${params.join('&')}`; + return await fetchImplantsAPI(endpoint); + }, + + getImplantBySlug: async function(slug) { + return await fetchImplantsAPI(`/api/implants/${slug}`); + }, + + getImplantSlots: async function() { + return await fetchImplantsAPI('/api/implants/slots'); + }, + + getImplantsBySlot: async function(slot) { + return await fetchImplantsAPI(`/api/implants/slot/${slot}`); + }, + + getTraits: async function() { + return await fetchImplantsAPI('/api/traits'); + }, + + // ============ FACTIONS (Contracts API) ============ + getFactions: async function() { + return await fetchContractsAPI('/api/factions'); + }, + + getFactionBySlug: async function(slug) { + return await fetchContractsAPI(`/api/factions/${slug}`); + }, + + getFactionContracts: async function(slug, type = null) { + let endpoint = `/api/factions/${slug}/contracts`; + const params = []; + if (type) params.push(`type=${type}`); + if (params.length > 0) endpoint += `?${params.join('&')}`; + return await fetchContractsAPI(endpoint); + }, + + getFactionUpgrades: async function(slug, category = null, tier = null) { + let endpoint = `/api/factions/${slug}/upgrades`; + const params = []; + if (category) params.push(`category=${category}`); + if (tier) params.push(`tier=${tier}`); + if (params.length > 0) endpoint += `?${params.join('&')}`; + return await fetchContractsAPI(endpoint); + }, + + getFactionReputation: async function(slug) { + return await fetchContractsAPI(`/api/factions/${slug}/reputation`); + }, + + // ============ CONTRACTS (Contracts API) ============ + getContracts: async function(options = {}) { + let endpoint = '/api/contracts'; + const params = []; + if (options.type) params.push(`type=${options.type}`); + if (options.faction) params.push(`faction=${options.faction}`); + if (options.difficulty) params.push(`difficulty=${options.difficulty}`); + if (options.map) params.push(`map=${options.map}`); + if (options.scope) params.push(`scope=${options.scope}`); + if (options.tag) params.push(`tag=${options.tag}`); + if (options.chain) params.push(`chain=${options.chain}`); + if (options.season) params.push(`season=${options.season}`); + if (options.active !== undefined) params.push(`active=${options.active}`); + if (params.length > 0) endpoint += `?${params.join('&')}`; + return await fetchContractsAPI(endpoint); + }, + + getContractBySlug: async function(slug) { + return await fetchContractsAPI(`/api/contracts/${slug}`); + }, + + getContractRotation: async function() { + return await fetchContractsAPI('/api/contracts/rotation'); + }, + + getContractTags: async function() { + return await fetchContractsAPI('/api/contract-tags'); + }, + + // ============ DATABASE STATS ============ + // New unified stats endpoint (API v2.0.0) + getStats: async function() { + return await fetchAPI('/api/stats'); + }, + + // Legacy method - now uses the new /api/stats endpoint + getDbStats: async function() { + try { + const stats = await this.getStats(); + if (stats) { + return { + weapons: stats.weapons?.total || 0, + runners: stats.runners?.total || 0, + items: stats.items?.total || 0, + mods: stats.mods?.total || 0, + cores: stats.cores?.total || 0, + factions: stats.factions?.total || 0, + cosmetics: stats.cosmetics?.total || 0, + maps: stats.maps?.total || 0, + implants: stats.implants?.total || 0, + contracts: stats.contracts?.total || 0 + }; + } + return { weapons: 0, runners: 0, items: 0, mods: 0 }; + } catch (error) { + console.error('Error fetching stats:', error); + return { weapons: 0, runners: 0, items: 0, mods: 0 }; + } + } + }; +})(); + +// Weapons API Client — https://weapons.marathondb.gg +const WeaponsAPI = (function() { + const BASE = 'https://weapons.marathondb.gg'; + + async function fetchAPI(endpoint) { + try { + const url = `${BASE}${endpoint}`; + const cached = ApiCache.get(url); + if (cached) return cached; + + const response = await fetch(url); + const data = await response.json().catch(() => null); + if (!response.ok) { + const err = new Error(`Weapons API Error: ${response.status}`); + err.status = response.status; + err.body = data; + throw err; + } + if (data) ApiCache.set(url, data); + return data; + } catch (error) { + if (error.status) throw error; + console.error('WeaponsAPI fetch error:', error); + return null; + } + } + + // Pick the current-season stats row and flatten into weapon object. + // Prefer is_current === true; fall back to the row with the highest season_id (latest). + function normalizeWeapon(weapon) { + if (!weapon) return weapon; + if (!Array.isArray(weapon.stats) || weapon.stats.length === 0) return weapon; + const current = weapon.stats.reduce((a, b) => (b.season_id > a.season_id ? b : a)); + return { ...weapon, stats: current }; + } + + return { + getWeapons: async function(category = null) { + const qs = category ? `?category=${encodeURIComponent(category)}` : ''; + return await fetchAPI(`/api/weapons${qs}`); + }, + getCategories: async function() { + return await fetchAPI('/api/weapons/categories'); + }, + getWeaponBySlug: async function(slug) { + const result = await fetchAPI(`/api/weapons/${encodeURIComponent(slug)}`); + if (result?.data) result.data = normalizeWeapon(result.data); + return result; + }, + getStatRanges: async function() { + return await fetchAPI('/api/weapons/stat-ranges'); + }, + normalizeWeapon, + }; +})(); + +// Twitch API Client (separate endpoint — 2-min cache for live data) +const TwitchAPI = (function() { + const API_BASE = 'https://twitch.rnk.gg/api/v2/categories/407314011'; + const TWITCH_TTL = 2 * 60 * 1000; + const _twitchCache = {}; + + async function fetchAPI(endpoint) { + try { + const url = `${API_BASE}${endpoint}`; + const hit = _twitchCache[url]; + if (hit && Date.now() - hit.ts < TWITCH_TTL) return hit.data; + + const response = await fetch(url); + if (!response.ok) throw new Error(`Twitch API Error: ${response.status}`); + const data = await response.json(); + _twitchCache[url] = { ts: Date.now(), data }; + return data; + } catch (error) { + console.error('Twitch API error:', error); + return null; + } + } + + return { + getCurrentStats: async function() { + return await fetchAPI('/current-v2'); + }, + + getHistory: async function(range = '7d') { + return await fetchAPI(`/history-v2?range=${range}`); + } + }; +})(); diff --git a/backend/references/faction-upgrades.js b/backend/references/faction-upgrades.js new file mode 100644 index 0000000..f0b5da7 --- /dev/null +++ b/backend/references/faction-upgrades.js @@ -0,0 +1,1479 @@ +// Faction Upgrade Definitions — Static data for the upgrade checklist +// Data sourced from user's verified Faction Upgrades v2 spreadsheet (Mar 2026). + +const FACTION_UPGRADES = { + // ═══════════════════════════════════════════════════════════════════ + // CYBERACME + // ═══════════════════════════════════════════════════════════════════ + cyberacme: { + name: 'CyberAcme', + color: '#01d838', + agent: 'Oni', + materials: [ + { slug: 'unstable-diode', name: 'Unstable Diode', icon: 'https://items.marathondb.gg/images/items/unstable-diode.webp' }, + { slug: 'unstable-gel', name: 'Unstable Gel', icon: 'https://items.marathondb.gg/images/items/unstable-gel.webp' }, + { slug: 'unstable-gunmetal', name: 'Unstable Gunmetal', icon: 'https://items.marathondb.gg/images/items/unstable-gunmetal.webp' }, + { slug: 'unstable-biomass', name: 'Unstable Biomass', icon: 'https://items.marathondb.gg/images/items/unstable-biomass.webp' }, + { slug: 'unstable-lead', name: 'Unstable Lead', icon: 'https://items.marathondb.gg/images/items/unstable-lead.webp' }, + ], + upgrades: [ + // ── Inventory ── + { + slug: 'cyac-expansion', + name: 'Expansion', + category: 'inventory', + description: 'Gain additional rows of vault capacity for the rest of the current season.', + maxLevel: 5, + levels: [ + { level: 1, rank: 3, credits: 2500, effect: 'Vault Size +8 Rows', salvage: [{ slug: 'unstable-diode', amount: 12 }] }, + { level: 2, rank: 7, credits: 4000, effect: 'Vault Size +8 Rows', salvage: [{ slug: 'unstable-diode', amount: 22 }, { slug: 'unstable-gunmetal', amount: 12 }] }, + { level: 3, rank: 12, credits: 5000, effect: 'Vault Size +6 Rows', salvage: [{ slug: 'unstable-diode', amount: 27 }, { slug: 'unstable-gunmetal', amount: 15 }] }, + { level: 4, rank: 18, credits: 7000, effect: 'Vault Size +4 Rows', salvage: [{ slug: 'unstable-diode', amount: 30 }, { slug: 'unstable-gunmetal', amount: 18 }] }, + { level: 5, rank: 28, credits: 10000, effect: 'Vault Size +4 Rows', salvage: [{ slug: 'unstable-diode', amount: 50 }, { slug: 'unstable-gunmetal', amount: 30 }] }, + ], + }, + { + slug: 'cyac-credit-limit', + name: 'Credit Limit', + category: 'inventory', + description: 'Raises your credit wallet\'s capacity for the rest of the season.', + maxLevel: 5, + levels: [ + { level: 1, rank: 4, credits: 2500, effect: 'Credit Wallet Capacity +20k', salvage: [] }, + { level: 2, rank: 8, credits: 4000, effect: 'Credit Wallet Capacity +50k', salvage: [] }, + { level: 3, rank: 12, credits: 7000, effect: 'Credit Wallet Capacity +200k', salvage: [] }, + { level: 4, rank: 18, credits: 10000, effect: 'Credit Wallet Capacity +700k', salvage: [] }, + { level: 5, rank: 25, credits: 50000, effect: 'Credit Wallet Capacity +9,000k', salvage: [] }, + ], + }, + // ── Function ── + { + slug: 'cyac-informant-exe', + name: 'Informant.exe', + category: 'function', + description: 'Increases data card credit rewards by 50%. This bonus additively stacks with other Informant upgrades.', + maxLevel: 2, + levels: [ + { level: 1, rank: 2, credits: 1500, effect: 'Data Card Credit Value +50%', salvage: [] }, + { level: 2, rank: 15, credits: 2000, effect: 'Data Card Credit Value +50%', salvage: [] }, + ], + }, + { + slug: 'cyac-soundproof-exe', + name: 'Soundproof.exe', + category: 'function', + description: 'You make less noise while looting.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: 'Reduced looting noise', salvage: [] }, + ], + }, + { + slug: 'cyac-loose-change-exe', + name: 'Loose Change.exe', + category: 'function', + description: 'Opening a container rewards you with 25 credits.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: '+25 Credits per container', salvage: [] }, + ], + }, + { + slug: 'cyac-fixative-exe', + name: 'Fixative.exe', + category: 'function', + description: 'ROOK gains an increased chance of finding Matter Fixatives when defeating UESC.', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 3500, effect: 'Increased Matter Fixative drops', salvage: [] }, + ], + }, + { + slug: 'cyac-slider-exe', + name: 'Slider.exe', + category: 'function', + description: 'Your sprint slide generates less heat.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 7000, effect: 'Reduced slide heat', salvage: [] }, + ], + }, + // ── Armory ── + { + slug: 'cyac-carrier', + name: 'Carrier', + category: 'armory', + description: 'Unlocks Enhanced backpacks for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: '8XS Base Pack', salvage: [] }, + ], + }, + { + slug: 'cyac-carrier-plus', + name: 'Carrier+', + category: 'armory', + description: 'Unlocks Deluxe backpacks for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 15, credits: 4000, effect: '16XS Base Pack', salvage: [] }, + ], + }, + { + slug: 'cyac-enhanced-weaponry', + name: 'Enhanced Weaponry', + category: 'armory', + description: 'Unlocks Enhanced Overrun AR, V11 Punch, and CE Tactical Sidearm for purchase from CyberAcme.', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 2500, effect: 'Unlock Enhanced weapons', salvage: [] }, + ], + }, + { + slug: 'cyac-deluxe-weaponry', + name: 'Deluxe Weaponry', + category: 'armory', + description: 'Unlocks Deluxe Overrun AR, V11 Punch, and CE Tactical Sidearm for purchase from CyberAcme.', + maxLevel: 1, + levels: [ + { level: 1, rank: 14, credits: 4000, effect: 'Unlock Deluxe weapons', salvage: [] }, + ], + }, + { + slug: 'cyac-locksmith', + name: 'Locksmith', + category: 'armory', + description: 'Unlocks lockbox key for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 2500, effect: 'Lockbox Key (Item)', salvage: [] }, + ], + }, + { + slug: 'cyac-keymaker', + name: 'Keymaker', + category: 'armory', + description: 'Unlocks Deluxe Key templates for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 16, credits: 4000, effect: 'Deluxe Key Template (Item)', salvage: [] }, + ], + }, + { + slug: 'cyac-keymaker-plus', + name: 'Keymaker+', + category: 'armory', + description: 'Unlocks Superior Key templates for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 28, credits: 7000, effect: 'Superior Key Template (Item)', salvage: [] }, + ], + }, + // ── Stat ── + { + slug: 'cyac-heat-sink-exe', + name: 'Heat Sink.exe', + category: 'stat', + description: 'Heat Capacity increases the number of movement actions (sprint, sliding) you can perform before overheating.', + maxLevel: 2, + levels: [ + { level: 1, rank: 1, credits: 2500, effect: 'Heat Capacity +20', salvage: [{ slug: 'unstable-biomass', amount: 12 }] }, + { level: 2, rank: 12, credits: 3500, effect: 'Heat Capacity +20', salvage: [{ slug: 'unstable-biomass', amount: 24 }, { slug: 'unstable-lead', amount: 12 }] }, + ], + }, + { + slug: 'cyac-scavenger-exe', + name: 'Scavenger.exe', + category: 'stat', + description: 'Loot Speed increases how quickly items are revealed when looting containers.', + maxLevel: 3, + levels: [ + { level: 1, rank: 1, credits: 750, effect: 'Loot Speed +20', salvage: [] }, + { level: 2, rank: 4, credits: 2500, effect: 'Loot Speed +20', salvage: [] }, + { level: 3, rank: 16, credits: 4000, effect: 'Loot Speed +20', salvage: [] }, + ], + }, + { + slug: 'cyac-quick-vent-exe', + name: 'Quick Vent.exe', + category: 'stat', + description: 'Your heat recovery begins more quickly after actions that generate heat.', + maxLevel: 2, + levels: [ + { level: 1, rank: 4, credits: 2500, effect: 'Heat Recovery Speed -20%', salvage: [{ slug: 'unstable-gel', amount: 8 }] }, + { level: 2, rank: 20, credits: 4000, effect: 'Heat Recovery Speed -20%', salvage: [{ slug: 'unstable-gel', amount: 16 }] }, + ], + }, + { + slug: 'cyac-active-cool-exe', + name: 'Active Cool.exe', + category: 'stat', + description: 'Your generated heat recovers more quickly.', + maxLevel: 2, + levels: [ + { level: 1, rank: 13, credits: 3500, effect: 'Heat Recovery Rate +15%', salvage: [{ slug: 'unstable-gel', amount: 24 }] }, + { level: 2, rank: 23, credits: 5000, effect: 'Heat Recovery Rate +15%', salvage: [{ slug: 'unstable-gel', amount: 30 }] }, + ], + }, + { + slug: 'cyac-firm-stance-exe', + name: 'Firm Stance.exe', + category: 'stat', + description: 'Fall Resistance reduces the amount of damage you take after falling.', + maxLevel: 3, + levels: [ + { level: 1, rank: 1, credits: 750, effect: 'Fall Resistance +20', salvage: [] }, + { level: 2, rank: 11, credits: 4000, effect: 'Fall Resistance +20', salvage: [] }, + { level: 3, rank: 26, credits: 5000, effect: 'Fall Resistance +20', salvage: [] }, + ], + }, + { + slug: 'cyac-loot-siphon-exe', + name: 'Loot Siphon.exe', + category: 'stat', + description: 'Grants bonus tactical ability energy when opening an unlooted container.', + maxLevel: 2, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: 'Tactical Energy on Container Loot +5%', salvage: [] }, + { level: 2, rank: 17, credits: 4000, effect: 'Tactical Energy on Container Loot +5%', salvage: [] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 6, name: 'Capstone I', reward: 'Bonus Pay' }, + { rank: 2, nodesRequired: 12, name: 'Capstone II', reward: 'CyberAcme Treasure Reputation +20%' }, + { rank: 3, nodesRequired: 18, name: 'Capstone III', reward: 'Unlocks Enhanced CyberAcme Sponsorship Kits for purchase' }, + { rank: 4, nodesRequired: 24, name: 'Capstone IV', reward: 'Stipend — Rook will now start runs with a small amount of credits in addition to their basic gear' }, + { rank: 5, nodesRequired: 30, name: 'Capstone V', reward: 'Max Looter — Unlocks Superior Backpacks for purchase in the Armory (24XS Backpack)' }, + { rank: 6, nodesRequired: 38, name: 'Capstone VI', reward: 'Carrier — Start with Deluxe Backpack' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // NUCALORIC + // ═══════════════════════════════════════════════════════════════════ + nucaloric: { + name: 'NuCaloric', + color: '#ff125d', + agent: 'Gaius', + materials: [ + { slug: 'unstable-biomass', name: 'Unstable Biomass', icon: 'https://items.marathondb.gg/images/items/unstable-biomass.webp' }, + { slug: 'sparkleaf', name: 'Sparkleaf', icon: 'https://items.marathondb.gg/images/items/sparkleaf.webp' }, + { slug: 'reclaimed-biostripping', name: 'Reclaimed Biostripping', icon: 'https://items.marathondb.gg/images/items/reclaimed-biostripping.webp' }, + { slug: 'dermachem-pack', name: 'Dermachem Pack', icon: 'https://items.marathondb.gg/images/items/dermachem-pack.webp' }, + { slug: 'tarax-seed', name: 'Tarax Seed', icon: 'https://items.marathondb.gg/images/items/tarax-seed.webp' }, + { slug: 'biolens-seed', name: 'Biolens Seed', icon: 'https://items.marathondb.gg/images/items/biolens-seed.webp' }, + { slug: 'sterilized-biostripping', name: 'Sterilized Biostripping', icon: 'https://items.marathondb.gg/images/items/sterilized-biostripping.webp' }, + { slug: 'neurochem-pack', name: 'Neurochem Pack', icon: 'https://items.marathondb.gg/images/items/neurochem-pack.webp' }, + { slug: 'neural-insulation', name: 'Neural Insulation', icon: 'https://items.marathondb.gg/images/items/neural-insulation.webp' }, + { slug: 'hazard-capsule', name: 'Hazard Capsule', icon: 'https://items.marathondb.gg/images/items/hazard-capsule.webp' }, + { slug: 'enzyme-replicator', name: 'Enzyme Replicator', icon: 'https://items.marathondb.gg/images/items/enzyme-replicator.webp' }, + ], + upgrades: [ + // ── Armory ── + { + slug: 'nucal-safeguard', + name: 'Safeguard', + category: 'armory', + description: 'Unlocks daily free Shield Charges in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 1, credits: 750, effect: 'Free Daily Shield Charges', salvage: [{ slug: 'unstable-biomass', amount: 16 }] }, + ], + }, + { + slug: 'nucal-advanced-shields', + name: 'Advanced Shields', + category: 'armory', + description: 'Unlocks Advanced Shield Charges for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 6, credits: 1500, effect: 'Advanced Shield Charge', salvage: [{ slug: 'reclaimed-biostripping', amount: 10 }, { slug: 'unstable-biomass', amount: 10 }] }, + ], + }, + { + slug: 'nucal-safeguard-plus', + name: 'Safeguard+', + category: 'armory', + description: 'Unlocks daily free Advanced Shield Charges in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 11, credits: 2000, effect: 'Free Daily Advanced Shield Charges', salvage: [{ slug: 'sterilized-biostripping', amount: 6 }, { slug: 'sparkleaf', amount: 16 }] }, + ], + }, + { + slug: 'nucal-shield-stock', + name: 'Shield Stock', + category: 'armory', + description: 'Increases Advanced Shield Charge stock in the Armory by 5.', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 1500, effect: 'Shield Charge Stock +5', salvage: [{ slug: 'reclaimed-biostripping', amount: 15 }, { slug: 'sparkleaf', amount: 8 }] }, + ], + }, + { + slug: 'nucal-shielded', + name: 'Shielded', + category: 'armory', + description: 'Unlocks Enhanced shield implants for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: 'Protector V1', salvage: [{ slug: 'reclaimed-biostripping', amount: 12 }, { slug: 'unstable-biomass', amount: 13 }] }, + ], + }, + { + slug: 'nucal-armored', + name: 'Armored', + category: 'armory', + description: 'Unlocks Deluxe shield implants for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 20, credits: 3500, effect: 'Protector V2', salvage: [{ slug: 'biolens-seed', amount: 7 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + { + slug: 'nucal-restore', + name: 'Restore', + category: 'armory', + description: 'Unlocks daily free Patch Kits in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: 'Free Daily Patch Kits', salvage: [{ slug: 'unstable-biomass', amount: 23 }] }, + ], + }, + { + slug: 'nucal-advanced-patch', + name: 'Advanced Patch', + category: 'armory', + description: 'Unlocks Advanced Patch Kits for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 8, credits: 1500, effect: 'Advanced Patch Kit', salvage: [{ slug: 'dermachem-pack', amount: 10 }, { slug: 'unstable-biomass', amount: 13 }] }, + ], + }, + { + slug: 'nucal-restore-plus', + name: 'Restore+', + category: 'armory', + description: 'Unlocks daily free Advanced Patch Kits in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 2000, effect: 'Free Daily Advanced Patch Kits', salvage: [{ slug: 'neurochem-pack', amount: 5 }, { slug: 'sparkleaf', amount: 16 }] }, + ], + }, + { + slug: 'nucal-patch-stock', + name: 'Patch Stock', + category: 'armory', + description: 'Increases Advanced Patch Kit stock in the Armory by 5.', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 1500, effect: 'Patch Kit Stock +5', salvage: [{ slug: 'dermachem-pack', amount: 8 }, { slug: 'unstable-biomass', amount: 11 }] }, + ], + }, + { + slug: 'nucal-panacea-kit', + name: 'Panacea Kit', + category: 'armory', + description: 'Unlocks Panacea Kits for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 5000, effect: 'Panacea Kit', salvage: [{ slug: 'hazard-capsule', amount: 2 }, { slug: 'neural-insulation', amount: 7 }] }, + ], + }, + { + slug: 'nucal-regen', + name: 'Regen', + category: 'armory', + description: 'Unlocks Regen V2 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 3, credits: 750, effect: 'Regen V2', salvage: [{ slug: 'unstable-biomass', amount: 10 }] }, + ], + }, + { + slug: 'nucal-regen-plus', + name: 'Regen+', + category: 'armory', + description: 'Unlocks Regen V3 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: 'Regen V3', salvage: [{ slug: 'reclaimed-biostripping', amount: 28 }, { slug: 'sparkleaf', amount: 14 }] }, + ], + }, + { + slug: 'nucal-regen-plus-plus', + name: 'Regen++', + category: 'armory', + description: 'Unlocks Regen V4 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 21, credits: 3500, effect: 'Regen V4', salvage: [{ slug: 'biolens-seed', amount: 7 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + { + slug: 'nucal-advanced-mch', + name: 'Advanced MCH', + category: 'armory', + description: 'Unlocks Advanced Mechanic\'s Kits for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 6, credits: 1500, effect: 'Advanced Mechanic\'s Kit', salvage: [{ slug: 'reclaimed-biostripping', amount: 8 }, { slug: 'unstable-biomass', amount: 9 }] }, + ], + }, + { + slug: 'nucal-advanced-os', + name: 'Advanced OS', + category: 'armory', + description: 'Unlocks Advanced OS Debugs for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: 'Advanced OS Debug', salvage: [{ slug: 'reclaimed-biostripping', amount: 10 }, { slug: 'unstable-biomass', amount: 10 }] }, + ], + }, + { + slug: 'nucal-helping-hands', + name: 'Helping Hands', + category: 'armory', + description: 'Unlocks Helping Hands V2 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 2, credits: 750, effect: 'Helping Hands V2', salvage: [{ slug: 'unstable-biomass', amount: 10 }] }, + ], + }, + { + slug: 'nucal-helping-hands-plus', + name: 'Helping Hands+', + category: 'armory', + description: 'Unlocks Helping Hands V3 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 11, credits: 1500, effect: 'Helping Hands V3', salvage: [{ slug: 'dermachem-pack', amount: 10 }, { slug: 'sparkleaf', amount: 13 }] }, + ], + }, + { + slug: 'nucal-helping-hands-plus-plus', + name: 'Helping Hands++', + category: 'armory', + description: 'Unlocks Helping Hands V4 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 22, credits: 3500, effect: 'Helping Hands V4', salvage: [{ slug: 'biolens-seed', amount: 9 }, { slug: 'tarax-seed', amount: 14 }] }, + ], + }, + { + slug: 'nucal-self-revive', + name: 'Self-Revive', + category: 'armory', + description: 'Unlocks Self-Revives for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 1500, effect: 'Self-Revive', salvage: [{ slug: 'neurochem-pack', amount: 4 }, { slug: 'sparkleaf', amount: 14 }] }, + ], + }, + // ── Stat ── + { + slug: 'nucal-null-hazard-exe', + name: 'NULL_HAZARD.EXE', + category: 'stat', + description: 'Hazard Tolerance increases your maximum data buffer protection, which is restored by using HEC consumables.', + maxLevel: 2, + levels: [ + { level: 1, rank: 4, credits: 750, effect: 'Hazard Tolerance +50', salvage: [{ slug: 'unstable-biomass', amount: 19 }] }, + { level: 2, rank: 14, credits: 1500, effect: 'Hazard Tolerance +50', salvage: [{ slug: 'sterilized-biostripping', amount: 5 }, { slug: 'sparkleaf', amount: 12 }] }, + ], + }, + { + slug: 'nucal-tciv-resist-exe', + name: 'TCIV_RESIST.EXE', + category: 'stat', + description: 'Ticks, lightning, and Heat Cascade deal reduced damage.', + maxLevel: 1, + levels: [ + { level: 1, rank: 21, credits: 3500, effect: 'Reduced environmental damage', salvage: [{ slug: 'biolens-seed', amount: 5 }, { slug: 'tarax-seed', amount: 7 }] }, + ], + }, + { + slug: 'nucal-reinforce-exe', + name: 'REINFORCE.EXE', + category: 'stat', + description: 'Hardware reduces the duration of negative status effects that debilitate your Runner\'s physical chassis (Frost, Immobilize, Overheat, Toxin).', + maxLevel: 3, + levels: [ + { level: 1, rank: 6, credits: 1500, effect: 'Hardware +20', salvage: [{ slug: 'reclaimed-biostripping', amount: 8 }, { slug: 'unstable-biomass', amount: 9 }] }, + { level: 2, rank: 16, credits: 2000, effect: 'Hardware +20', salvage: [{ slug: 'sterilized-biostripping', amount: 7 }, { slug: 'sparkleaf', amount: 25 }] }, + { level: 3, rank: 26, credits: 5000, effect: 'Hardware +20', salvage: [{ slug: 'hazard-capsule', amount: 2 }] }, + ], + }, + { + slug: 'nucal-unfazed-exe', + name: 'UNFAZED.EXE', + category: 'stat', + description: 'Firewall reduces the duration of status effects that degrade your Runner\'s electronic systems (EMP, Hack).', + maxLevel: 3, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: 'Firewall +20', salvage: [{ slug: 'dermachem-pack', amount: 7 }, { slug: 'unstable-biomass', amount: 7 }] }, + { level: 2, rank: 18, credits: 2000, effect: 'Firewall +20', salvage: [{ slug: 'neurochem-pack', amount: 8 }, { slug: 'tarax-seed', amount: 5 }] }, + { level: 3, rank: 27, credits: 5000, effect: 'Firewall +20', salvage: [{ slug: 'hazard-capsule', amount: 2 }, { slug: 'tarax-seed', amount: 9 }] }, + ], + }, + { + slug: 'nucal-recovery-exe', + name: 'RECOVERY.EXE', + category: 'stat', + description: 'Self-Repair Speed increases how quickly your consumables restore missing health or shields.', + maxLevel: 3, + levels: [ + { level: 1, rank: 9, credits: 1500, effect: 'Self-Repair Speed +20', salvage: [{ slug: 'dermachem-pack', amount: 10 }, { slug: 'unstable-biomass', amount: 13 }] }, + { level: 2, rank: 19, credits: 2000, effect: 'Self-Repair Speed +20', salvage: [{ slug: 'neurochem-pack', amount: 10 }, { slug: 'tarax-seed', amount: 6 }] }, + { level: 3, rank: 29, credits: 5000, effect: 'Self-Repair Speed +20', salvage: [{ slug: 'hazard-capsule', amount: 3 }, { slug: 'enzyme-replicator', amount: 3 }] }, + ], + }, + // ── Function ── + { + slug: 'nucal-shield-comm', + name: 'Shield Comm', + category: 'function', + description: 'NuCaloric standard contracts will now award Shield Charges in addition to other rewards.', + maxLevel: 1, + levels: [ + { level: 1, rank: 15, credits: 2000, effect: 'Shield Charges from contracts', salvage: [{ slug: 'sterilized-biostripping', amount: 12 }, { slug: 'tarax-seed', amount: 6 }] }, + ], + }, + { + slug: 'nucal-health-comm', + name: 'Health Comm', + category: 'function', + description: 'NuCaloric standard contracts will now award Patch Kits in addition to other rewards.', + maxLevel: 1, + levels: [ + { level: 1, rank: 24, credits: 5000, effect: 'Patch Kits from contracts', salvage: [{ slug: 'hazard-capsule', amount: 2 }] }, + ], + }, + { + slug: 'nucal-resist-comm', + name: 'Resist Comm', + category: 'function', + description: 'NuCaloric standard contracts will now award Mechanic\'s Kits or OS Reboots in addition to other rewards.', + maxLevel: 1, + levels: [ + { level: 1, rank: 22, credits: 3500, effect: 'Mechanic\'s Kits/OS Reboots from contracts', salvage: [{ slug: 'biolens-seed', amount: 9 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + { + slug: 'nucal-field-medic-exe', + name: 'FIELD_MEDIC.EXE', + category: 'function', + description: 'Health and shield consumables take less time to use.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: 'Faster consumable use', salvage: [{ slug: 'hazard-capsule', amount: 3 }, { slug: 'enzyme-replicator', amount: 8 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 6, name: 'Capstone I', reward: 'Rook will now start runs with a Patch Kit and Shield Charge' }, + { rank: 2, nodesRequired: 12, name: 'Capstone II', reward: 'Treasure Hunter — Increases NuCaloric Treasure Reputation by 20%' }, + { rank: 3, nodesRequired: 18, name: 'Capstone III', reward: 'Unlocks Enhanced NuCaloric Sponsorship Kits for purchase. NuCaloric will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 24, name: 'Capstone IV', reward: '2nd Chance.exe — Self-Revives have a small chance to not be consumed on use' }, + { rank: 5, nodesRequired: 30, name: 'Capstone V', reward: 'Hush.exe — You make less noise while healing' }, + { rank: 6, nodesRequired: 38, name: 'Capstone VI', reward: 'Reinforced — Unlocks Superior Shield Implants for purchase in the Armory. NuCaloric will now barter their wares for certain Prestige Salvage.' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // TRAXUS + // ═══════════════════════════════════════════════════════════════════ + traxus: { + name: 'Traxus', + color: '#ff7300', + agent: 'Vulcan', + materials: [ + { slug: 'unstable-gunmetal', name: 'Unstable Gunmetal', icon: 'https://items.marathondb.gg/images/items/unstable-gunmetal.webp' }, + { slug: 'deimosite-rods', name: 'Deimosite Rods', icon: 'https://items.marathondb.gg/images/items/deimosite-rods.webp' }, + { slug: 'altered-wire', name: 'Altered Wire', icon: 'https://items.marathondb.gg/images/items/altered-wire.webp' }, + { slug: 'plasma-filament', name: 'Plasma Filament', icon: 'https://items.marathondb.gg/images/items/plasma-filament.webp' }, + { slug: 'tachyon-filament', name: 'Tachyon Filament', icon: 'https://items.marathondb.gg/images/items/tachyon-filament.webp' }, + { slug: 'anomalous-wire', name: 'Anomalous Wire', icon: 'https://items.marathondb.gg/images/items/anomalous-wire.webp' }, + { slug: 'cetinite-rods', name: 'Cetinite Rods', icon: 'https://items.marathondb.gg/images/items/cetinite-rods.webp' }, + { slug: 'predictive-framework', name: 'Predictive Framework', icon: 'https://items.marathondb.gg/images/items/predictive-framework.webp' }, + { slug: 'ballistic-turbine', name: 'Ballistic Turbine', icon: 'https://items.marathondb.gg/images/items/ballistic-turbine.webp' }, + { slug: 'reflex-coil', name: 'Reflex Coil', icon: 'https://items.marathondb.gg/images/items/reflex-coil.webp' }, + { slug: 'alien-alloy', name: 'Alien Alloy', icon: 'https://items.marathondb.gg/images/items/alien-alloy.webp' }, + ], + upgrades: [ + // ── Armory ── + { + slug: 'trax-smg-mods', + name: 'SMG Mods', + category: 'armory', + description: 'Unlocks rotating Enhanced SMG mods from the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 1, credits: 750, effect: 'Unlock Enhanced SMG mods (2 items)', salvage: [{ slug: 'unstable-gunmetal', amount: 10 }] }, + { level: 2, rank: 6, credits: 1500, effect: 'Unlock additional Enhanced SMG mods (2 items)', salvage: [{ slug: 'deimosite-rods', amount: 7 }, { slug: 'unstable-gunmetal', amount: 6 }] }, + ], + }, + { + slug: 'trax-deluxe-smg-mods', + name: 'Deluxe SMG Mods', + category: 'armory', + description: 'Unlocks rotating Deluxe SMG mods from the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 18, credits: 3500, effect: 'Unlock deluxe SMG mods (3 items)', salvage: [{ slug: 'predictive-framework', amount: 4 }, { slug: 'tachyon-filament', amount: 6 }] }, + ], + }, + { + slug: 'trax-enhanced-heavy-submachine-gun', + name: 'Enhanced Heavy Submachine Gun', + category: 'armory', + description: 'Unlocks Enhanced "Bully SMG" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: 'Unlocks Bully SMG for purchase', salvage: [{ slug: 'deimosite-rods', amount: 23 }, { slug: 'altered-wire', amount: 9 }] }, + ], + }, + { + slug: 'trax-enhanced-volt-submachine-gun', + name: 'Enhanced Volt Submachine Gun', + category: 'armory', + description: 'Unlocks Enhanced "V22 Volt Thrower" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 15, credits: 2000, effect: 'Unlocks V22 Voltthrower for purchase', salvage: [{ slug: 'cetinite-rods', amount: 12 }, { slug: 'tachyon-filament', amount: 5 }] }, + ], + }, + { + slug: 'trax-ar-mods', + name: 'AR Mods', + category: 'armory', + description: 'Unlocks rotating Enhanced AR mods from the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 2, credits: 750, effect: 'Unlock Enhanced AR mods (2 items)', salvage: [{ slug: 'unstable-gunmetal', amount: 10 }] }, + { level: 2, rank: 2, credits: 1500, effect: 'Unlock Enhanced AR mods (2 items)', salvage: [{ slug: 'altered-wire', amount: 7 }, { slug: 'unstable-gunmetal', amount: 6 }] }, + ], + }, + { + slug: 'trax-deluxe-ar-mods', + name: 'Deluxe AR Mods', + category: 'armory', + description: 'Unlocks a rotating Deluxe AR mod in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 22, credits: 5000, effect: 'Unlock Deluxe AR mods (3 items)', salvage: [{ slug: 'alien-alloy', amount: 2 }] }, + ], + }, + { + slug: 'trax-enhanced-light-ar', + name: 'Enhanced Light AR', + category: 'armory', + description: 'Unlocks Enhanced "M77 AR" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 14, credits: 2000, effect: 'Unlocks M77 AR for purchase in the armory', salvage: [{ slug: 'anomalous-wire', amount: 10 }, { slug: 'tachyon-filament', amount: 4 }] }, + ], + }, + { + slug: 'trax-enhanced-chips', + name: 'Enhanced Chips', + category: 'armory', + description: 'Unlocks a set of enhanced weapon chip mods from the Armory', + maxLevel: 2, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: 'Unlock 3 weapon chips in the armory', salvage: [{ slug: 'altered-wire', amount: 7 }, { slug: 'unstable-gunmetal', amount: 6 }] }, + { level: 2, rank: 10, credits: 1500, effect: 'Unlock 3 weapon chips in the armory', salvage: [{ slug: 'altered-wire', amount: 19 }, { slug: 'plasma-filament', amount: 9 }] }, + ], + }, + { + slug: 'trax-deluxe-chips', + name: 'Deluxe Chips', + category: 'armory', + description: 'Unlocks a set of Deluxe weapon chip mods in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: 'Unlock 4 weapon chips in the armory', salvage: [{ slug: 'alien-alloy', amount: 3 }, { slug: 'reflex-coil', amount: 11 }] }, + ], + }, + { + slug: 'trax-volt-mods', + name: 'Volt Mods', + category: 'armory', + description: 'Unlocks rotating Enhanced volt weapon mod from the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 3, credits: 750, effect: '', salvage: [{ slug: 'unstable-gunmetal', amount: 13 }] }, + { level: 2, rank: 11, credits: 1500, effect: '', salvage: [{ slug: 'deimosite-rods', amount: 12 }, { slug: 'altered-wire', amount: 6 }] }, + { level: 3, rank: 17, credits: 1500, effect: '', salvage: [{ slug: 'cetinite-rods', amount: 12 }, { slug: 'altered-wire', amount: 11 }] }, + ], + }, + { + slug: 'trax-volt-pr', + name: 'Volt PR', + category: 'armory', + description: 'Unlocks the V66 Lookout for purchase in the Armory (Weapon)', + maxLevel: 2, + levels: [ + { level: 1, rank: 8, credits: 1500, effect: 'Unlocks item for purchase', salvage: [{ slug: 'deimosite-rods', amount: 19 }, { slug: 'altered-wire', amount: 7 }] }, + { level: 2, rank: 18, credits: 3500, effect: 'Unlocks item for purchase', salvage: [{ slug: 'predictive-framework', amount: 5 }, { slug: 'tachyon-filament', amount: 7 }] }, + ], + }, + { + slug: 'trax-deluxe-volt-mods', + name: 'Deluxe Volt Mods', + category: 'armory', + description: 'Unlocks rotating Deluxe Volt weapon mod in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 20, credits: 3500, effect: 'Unlocks item for purchase', salvage: [{ slug: 'predictive-framework', amount: 7 }, { slug: 'reflex-coil', amount: 3 }] }, + ], + }, + { + slug: 'trax-precision-mods', + name: 'Precision Mods', + category: 'armory', + description: 'Unlocks rotating enhanced precision weapon mod from the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 7, credits: 750, effect: '', salvage: [{ slug: 'unstable-gunmetal', amount: 19 }] }, + { level: 2, rank: 13, credits: 1500, effect: '', salvage: [{ slug: 'deimosite-rods', amount: 19 }, { slug: 'altered-wire', amount: 7 }] }, + { level: 3, rank: 19, credits: 2000, effect: '', salvage: [{ slug: 'cetinite-rods', amount: 4 }, { slug: 'tachyon-filament', amount: 4 }] }, + ], + }, + { + slug: 'trax-mips-sniper', + name: 'MIPS Sniper', + category: 'armory', + description: 'Unlocks Enhanced "Longshot" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 9, credits: 1500, effect: 'Unlocks "Longshot" for purchase in the armory', salvage: [{ slug: 'anomalous-wire', amount: 5 }, { slug: 'plasma-filament', amount: 10 }] }, + ], + }, + { + slug: 'trax-enhanced-hardline-pr', + name: 'Enhanced Hardline PR', + category: 'armory', + description: 'Unlocks Enhanced "Hardline PR" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 2000, effect: 'Unlocks "Hardline PR" for purchase in the armory', salvage: [{ slug: 'anomalous-wire', amount: 8 }, { slug: 'plasma-filament', amount: 21 }] }, + ], + }, + { + slug: 'trax-deluxe-precision-mods', + name: 'Deluxe Precision Mods', + category: 'armory', + description: 'Unlocks rotating Deluxe precision weapon mod in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 5000, effect: '', salvage: [{ slug: 'alien-alloy', amount: 2 }, { slug: 'tachyon-filament', amount: 9 }] }, + ], + }, + // ── Stat ── + { + slug: 'trax-tracker-exe', + name: 'Tracker.exe', + category: 'stat', + description: 'Ping duration increases how long your ping persist on hostile targets.', + maxLevel: 2, + levels: [ + { level: 1, rank: 15, credits: 2000, effect: 'Ping Duration +30', salvage: [{ slug: 'anomalous-wire', amount: 7 }, { slug: 'plasma-filament', amount: 21 }] }, + { level: 2, rank: 26, credits: 5000, effect: 'Ping Duration +30', salvage: [{ slug: 'alien-alloy', amount: 2 }] }, + ], + }, + { + slug: 'trax-tad-boost', + name: 'Tad Boost', + category: 'stat', + description: 'Expands the ping\'s area of effect when using a TAD', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 750, effect: 'Tad Ping Area +20m', salvage: [{ slug: 'unstable-gunmetal', amount: 19 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 5, name: 'Capstone I', reward: 'Proficient — Rook will now start runs with an Enhanced weapon. Traxus will now barter their wares for certain Deluxe Salvage.' }, + { rank: 2, nodesRequired: 10, name: 'Capstone II', reward: 'Treasure Hunter — Increases faction rep gained from Traxus Treasures' }, + { rank: 3, nodesRequired: 15, name: 'Capstone III', reward: 'Unlocks Enhanced Traxus Sponsorship Kits for purchase. Traxus will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 20, name: 'Capstone IV', reward: 'Bonus Mod — Traxus standard contracts will now award a bonus weapon mod in addition to other rewards' }, + { rank: 5, nodesRequired: 25, name: 'Capstone V', reward: 'Deluxe Weapons — Unlocks Deluxe weapons for purchase in the Armory' }, + { rank: 6, nodesRequired: 28, name: 'Capstone VI', reward: 'Superior Mods — Unlocks Superior weapon mods in the Armory. Traxus will now barter their wares for certain Prestige Salvage.' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // MIDA + // ═══════════════════════════════════════════════════════════════════ + mida: { + name: 'MIDA', + color: '#be72e4', + agent: 'Gantry', + materials: [ + { slug: 'unstable-lead', name: 'Unstable Lead', icon: 'https://items.marathondb.gg/images/items/unstable-lead.webp' }, + { slug: 'surveillance-lens', name: 'Surveillance Lens', icon: 'https://items.marathondb.gg/images/items/surveillance-lens.webp' }, + { slug: 'dynamic-compounds', name: 'Dynamic Compounds', icon: 'https://items.marathondb.gg/images/items/dynamic-compounds.webp' }, + { slug: 'volatile-compounds', name: 'Volatile Compounds', icon: 'https://items.marathondb.gg/images/items/volatile-compounds.webp' }, + { slug: 'thoughtwave-lens', name: 'Thoughtwave Lens', icon: 'https://items.marathondb.gg/images/items/thoughtwave-lens.webp' }, + { slug: 'biolens-seed', name: 'Biolens Seed', icon: 'https://items.marathondb.gg/images/items/biolens-seed.webp' }, + { slug: 'ballistic-turbine', name: 'Ballistic Turbine', icon: 'https://items.marathondb.gg/images/items/ballistic-turbine.webp' }, + { slug: 'hazard-capsule', name: 'Hazard Capsule', icon: 'https://items.marathondb.gg/images/items/hazard-capsule.webp' }, + { slug: 'alien-alloy', name: 'Alien Alloy', icon: 'https://items.marathondb.gg/images/items/alien-alloy.webp' }, + ], + upgrades: [ + // ── Stat ── + { + slug: 'mida-flex-matrix-exe', + name: 'Flex Matrix.exe', + category: 'stat', + description: 'Agility increases your movement speed and jump height.', + maxLevel: 3, + levels: [ + { level: 1, rank: 3, credits: 750, effect: 'Agility +20', salvage: [{ slug: 'unstable-lead', amount: 16 }] }, + { level: 2, rank: 11, credits: 1500, effect: 'Agility +20', salvage: [{ slug: 'surveillance-lens', amount: 28 }, { slug: 'dynamic-compounds', amount: 10 }] }, + { level: 3, rank: 16, credits: 2000, effect: 'Agility +20', salvage: [{ slug: 'thoughtwave-lens', amount: 8 }, { slug: 'dynamic-compounds', amount: 26 }] }, + ], + }, + // ── Armory ── + { + slug: 'mida-survivor', + name: 'Survivor', + category: 'armory', + description: 'Unlocks Survivor Kit V2 Implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 6, credits: 1500, effect: '', salvage: [{ slug: 'surveillance-lens', amount: 13 }, { slug: 'unstable-lead', amount: 9 }] }, + { level: 2, rank: 12, credits: 1500, effect: '', salvage: [{ slug: 'thoughtwave-lens', amount: 6 }, { slug: 'dynamic-compounds', amount: 11 }] }, + { level: 3, rank: 25, credits: 3500, effect: '', salvage: [{ slug: 'biolens-seed', amount: 10 }, { slug: 'ballistic-turbine', amount: 3 }] }, + ], + }, + { + slug: 'mida-graceful', + name: 'Graceful', + category: 'armory', + description: 'Unlocks Graceful Landing Upgrades V2 Implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 4, credits: 1500, effect: '', salvage: [{ slug: 'surveillance-lens', amount: 9 }, { slug: 'unstable-lead', amount: 5 }] }, + { level: 2, rank: 11, credits: 2000, effect: '', salvage: [{ slug: 'thoughtwave-lens', amount: 8 }, { slug: 'dynamic-compounds', amount: 26 }] }, + { level: 3, rank: 23, credits: 3500, effect: '', salvage: [{ slug: 'biolens-seed', amount: 8 }, { slug: 'ballistic-turbine', amount: 3 }] }, + ], + }, + { + slug: 'mida-sprinter', + name: 'Sprinter', + category: 'armory', + description: 'Unlocks Bionic Leg Upgrades V2 Implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 2, credits: 750, effect: '', salvage: [{ slug: 'unstable-lead', amount: 13 }] }, + { level: 2, rank: 9, credits: 1500, effect: '', salvage: [{ slug: 'thoughtwave-lens', amount: 5 }, { slug: 'dynamic-compounds', amount: 8 }] }, + { level: 3, rank: 24, credits: 3500, effect: '', salvage: [{ slug: 'biolens-seed', amount: 10 }, { slug: 'ballistic-turbine', amount: 3 }] }, + ], + }, + { + slug: 'mida-cardio-kick', + name: 'Cardio Kick', + category: 'armory', + description: 'Unlocks "Cardio Kick Packs" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 1500, effect: '', salvage: [{ slug: 'thoughtwave-lens', amount: 4 }, { slug: 'dynamic-compounds', amount: 7 }] }, + ], + }, + // ── Function ── + { + slug: 'mida-full-throttle', + name: 'Full Throttle', + category: 'function', + description: 'Gain the effects of cardio kick for a short duration at the beginning of each run', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: '', salvage: [{ slug: 'alien-alloy', amount: 3 }, { slug: 'ballistic-turbine', amount: 11 }] }, + ], + }, + { + slug: 'mida-cloud-cover', + name: 'Cloud Cover', + category: 'function', + description: 'Automatically deploy smoke cloud when activating an exfil site', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: '', salvage: [{ slug: 'hazard-capsule', amount: 3 }, { slug: 'biolens-seed', amount: 12 }] }, + ], + }, + { + slug: 'mida-anti-virus', + name: 'Anti-Virus', + category: 'function', + description: 'Gain a small portion of active Anti Virus protection at the beginning of each run', + maxLevel: 3, + levels: [ + { level: 1, rank: 11, credits: 1500, effect: 'Active Anti Virus protection. 40 seconds at the start of a match', salvage: [{ slug: 'surveillance-lens', amount: 28 }, { slug: 'dynamic-compounds', amount: 10 }] }, + { level: 2, rank: 18, credits: 2000, effect: 'Active Anti Virus protection. 40 seconds at the start of a match', salvage: [{ slug: 'thoughtwave-lens', amount: 12 }, { slug: 'volatile-compounds', amount: 4 }] }, + { level: 3, rank: 26, credits: 5000, effect: 'Active Anti Virus protection. 40 seconds at the start of a match', salvage: [{ slug: 'hazard-capsule', amount: 12 }] }, + ], + }, + // ── Armory ── + { + slug: 'mida-anti-virus-packs', + name: 'Anti-Virus Packs', + category: 'armory', + description: 'Unlocks "Anti Virus Packs" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: '', salvage: [{ slug: 'unstable-lead', amount: 23 }] }, + ], + }, + { + slug: 'mida-hot-potato', + name: 'Hot Potato', + category: 'armory', + description: 'Unlocks "Heat Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 3, credits: 750, effect: '', salvage: [{ slug: 'unstable-lead', amount: 16 }] }, + ], + }, + { + slug: 'mida-explosives', + name: 'Explosives', + category: 'armory', + description: 'Unlocks "Frag Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 750, effect: '', salvage: [{ slug: 'unstable-lead', amount: 16 }] }, + ], + }, + { + slug: 'mida-bullseye', + name: 'Bullseye', + category: 'armory', + description: 'Unlocks "Flecette Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 8, credits: 1500, effect: '', salvage: [{ slug: 'dynamic-compounds', amount: 15 }, { slug: 'surveillance-lens', amount: 8 }] }, + ], + }, + { + slug: 'mida-eyes-open', + name: 'Eyes Open', + category: 'armory', + description: 'Unlocks Proximity Sendor for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 9, credits: 1500, effect: '', salvage: [{ slug: 'dynamic-compounds', amount: 25 }, { slug: 'surveillance-lens', amount: 14 }] }, + ], + }, + { + slug: 'mida-bad-step', + name: 'Bad Step', + category: 'armory', + description: 'Unlocks Claymores for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 1, credits: 750, effect: '', salvage: [{ slug: 'unstable-lead', amount: 13 }] }, + ], + }, + { + slug: 'mida-got-em', + name: 'Got Em', + category: 'armory', + description: 'Unlocks Trap Packs for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'dynamic-compounds', amount: 19 }, { slug: 'surveillance-lens', amount: 9 }] }, + ], + }, + { + slug: 'mida-chemist', + name: 'Chemist', + category: 'armory', + description: 'Unlocks "Chem Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 11, credits: 1500, effect: '', salvage: [{ slug: 'volatile-compounds', amount: 4 }, { slug: 'surveillance-lens', amount: 10 }] }, + ], + }, + { + slug: 'mida-lights-out', + name: 'Lights Out', + category: 'armory', + description: 'Unlocks "EMP Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 2000, effect: '', salvage: [{ slug: 'volatile-compounds', amount: 6 }, { slug: 'surveillance-lens', amount: 16 }] }, + ], + }, + { + slug: 'mida-spare-rounds', + name: 'Spare Rounds', + category: 'armory', + description: 'Unlocks "Ammo Crates" for purchase in the Armory', + maxLevel: 2, + levels: [ + { level: 1, rank: 8, credits: 1500, effect: '', salvage: [{ slug: 'dynamic-compounds', amount: 19 }, { slug: 'surveillance-lens', amount: 9 }] }, + { level: 2, rank: 11, credits: 2000, effect: '', salvage: [{ slug: 'volatile-compounds', amount: 6 }, { slug: 'surveillance-lens', amount: 16 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 5, name: 'Capstone I', reward: 'Castling — Rook will now start runs with a stack of Claymores. MIDA will now barter their wares for certain Deluxe Salvage.' }, + { rank: 2, nodesRequired: 10, name: 'Capstone II', reward: 'Treasure Hunter — Increases rep gain from MIDA Treasures +20%' }, + { rank: 3, nodesRequired: 15, name: 'Capstone III', reward: 'Unlocks Enhanced MIDA Sponsorship Kits for purchase. MIDA will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 20, name: 'Capstone IV', reward: 'Bonus Equipment — MIDA standard contracts will now award at least a grenade or gadget' }, + { rank: 5, nodesRequired: 25, name: 'Capstone V', reward: 'Dome Up — Unlocks Bubble Shields for purchase in the Armory' }, + { rank: 6, nodesRequired: 29, name: 'Capstone VI', reward: 'Steady Hand.exe — Allows you to disarm Claymore mines. MIDA will now barter their wares for certain Prestige Salvage.' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // ARACHNE + // ═══════════════════════════════════════════════════════════════════ + arachne: { + name: 'Arachne', + color: '#e40b0d', + agent: 'Charter', + materials: [ + { slug: 'unstable-gel', name: 'Unstable Gel', icon: 'https://items.marathondb.gg/images/items/unstable-gel.webp' }, + { slug: 'drone-resin', name: 'Drone Resin', icon: 'https://items.marathondb.gg/images/items/drone-resin.webp' }, + { slug: 'drone-node', name: 'Drone Node', icon: 'https://items.marathondb.gg/images/items/drone-node.webp' }, + { slug: 'biomata-resin', name: 'Biomata Resin', icon: 'https://items.marathondb.gg/images/items/biomata-resin.webp' }, + { slug: 'enzyme-replicator', name: 'Enzyme Replicator', icon: 'https://items.marathondb.gg/images/items/enzyme-replicator.webp' }, + { slug: 'biomata-node', name: 'Biomata Node', icon: 'https://items.marathondb.gg/images/items/biomata-node.webp' }, + { slug: 'reflex-coil', name: 'Reflex Coil', icon: 'https://items.marathondb.gg/images/items/reflex-coil.webp' }, + { slug: 'synapse-cube', name: 'Synapse Cube', icon: 'https://items.marathondb.gg/images/items/synapse-cube.webp' }, + { slug: 'hazard-capsule', name: 'Hazard Capsule', icon: 'https://items.marathondb.gg/images/items/hazard-capsule.webp' }, + ], + upgrades: [ + // ── Stat ── + { + slug: 'arach-hard-strike-exe', + name: 'Hard Strike.exe', + category: 'stat', + description: 'Melee Damage increases the damage of your melee and knife attacks.', + maxLevel: 3, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: 'Melee Damage +20', salvage: [{ slug: 'drone-resin', amount: 7 }, { slug: 'unstable-gel', amount: 6 }] }, + { level: 2, rank: 16, credits: 2000, effect: 'Melee Damage +20', salvage: [{ slug: 'biomata-resin', amount: 8 }, { slug: 'drone-node', amount: 22 }] }, + { level: 3, rank: 22, credits: 3500, effect: 'Melee Damage +20', salvage: [{ slug: 'reflex-coil', amount: 6 }, { slug: 'biomata-node', amount: 6 }] }, + ], + }, + { + slug: 'arach-cutthroat', + name: 'Cutthroat', + category: 'stat', + description: 'Finisher Siphon increases the amount your shields recharge after you perform a finisher on a runner.', + maxLevel: 3, + levels: [ + { level: 1, rank: 6, credits: 750, effect: 'Finisher Siphon +20', salvage: [{ slug: 'unstable-gel', amount: 16 }] }, + { level: 2, rank: 18, credits: 2000, effect: 'Finisher Siphon +20', salvage: [{ slug: 'biomata-resin', amount: 12 }, { slug: 'biomata-node', amount: 4 }] }, + { level: 3, rank: 25, credits: 3500, effect: 'Finisher Siphon +20', salvage: [{ slug: 'reflex-coil', amount: 7 }, { slug: 'enzyme-replicator', amount: 3 }] }, + ], + }, + // ── Armory ── + { + slug: 'arach-knife-fight', + name: 'Knife Fight', + category: 'armory', + description: 'Unlocks Knife Fight V2 implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 4, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 13 }] }, + { level: 2, rank: 11, credits: 1500, effect: '', salvage: [{ slug: 'drone-node', amount: 23 }, { slug: 'drone-resin', amount: 12 }] }, + { level: 3, rank: 24, credits: 3500, effect: '', salvage: [{ slug: 'enzyme-replicator', amount: 9 }, { slug: 'reflex-coil', amount: 3 }] }, + ], + }, + { + slug: 'arach-hurting-hands', + name: 'Hurting Hands', + category: 'armory', + description: 'Unlocks Hurting Hands V2 implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 1, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 10 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'drone-node', amount: 19 }, { slug: 'drone-resin', amount: 9 }] }, + { level: 3, rank: 23, credits: 3500, effect: '', salvage: [{ slug: 'enzyme-replicator', amount: 7 }, { slug: 'reflex-coil', amount: 3 }] }, + ], + }, + // ── Stat ── + { + slug: 'arach-reboot', + name: 'Reboot', + category: 'stat', + description: 'Revive speed increases how quickly you can self revive or revive downed crew members', + maxLevel: 3, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: 'Revive speed +20', salvage: [{ slug: 'drone-resin', amount: 19 }, { slug: 'unstable-gel', amount: 17 }] }, + { level: 2, rank: 19, credits: 3500, effect: 'Revive speed +20', salvage: [{ slug: 'reflex-coil', amount: 5 }, { slug: 'biomata-node', amount: 5 }] }, + { level: 3, rank: 27, credits: 5000, effect: 'Revive speed +20', salvage: [{ slug: 'synapse-cube', amount: 2 }, { slug: 'biomata-resin', amount: 9 }] }, + ], + }, + // ── Function ── + { + slug: 'arach-leech', + name: 'Leech', + category: 'function', + description: 'Knife attacks restore a small amount of health', + maxLevel: 1, + levels: [ + { level: 1, rank: 28, credits: 5000, effect: '', salvage: [{ slug: 'synapse-cube', amount: 2 }] }, + ], + }, + { + slug: 'arach-heat-death', + name: 'Heat Death', + category: 'function', + description: 'Eliminating a hostile reduces your heat buildup.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: '', salvage: [{ slug: 'hazard-capsule', amount: 2 }, { slug: 'enzyme-replicator', amount: 11 }] }, + ], + }, + // ── Armory ── + { + slug: 'arach-lmg-mods', + name: 'LMG Mods', + category: 'armory', + description: 'Unlocks a set of Enhanced LMG mods from the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 1, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 13 }] }, + ], + }, + { + slug: 'arach-shotgun-mods', + name: 'Shotgun Mods', + category: 'armory', + description: 'Unlocks a set of enhanced shotgun mods from the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 3, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 19 }] }, + ], + }, + { + slug: 'arach-railgun-mods', + name: 'Railgun Mods', + category: 'armory', + description: 'Unlocks a set of Enhanced railgun mods from the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 1500, effect: '', salvage: [{ slug: 'drone-resin', amount: 8 }, { slug: 'unstable-gel', amount: 9 }] }, + ], + }, + { + slug: 'arach-mips-railgun', + name: 'MIPS Railgun', + category: 'armory', + description: 'Unlocks the ARES RG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 2, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 13 }] }, + ], + }, + { + slug: 'arach-mips-shotgun', + name: 'MIPS Shotgun', + category: 'armory', + description: 'Unlocks the WSTR Combat Shotgun for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: '', salvage: [{ slug: 'drone-node', amount: 7 }, { slug: 'unstable-gel', amount: 8 }] }, + ], + }, + { + slug: 'arach-enhanced-retaliator-lmg', + name: 'Enhanced Retaliator LMG', + category: 'armory', + description: 'Unlocks the Enhanced, Retaliator LMG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'biomata-node', amount: 4 }, { slug: 'drone-resin', amount: 11 }] }, + ], + }, + { + slug: 'arach-enhanced-mips-shotgun', + name: 'Enhanced MIPS Shotgun', + category: 'armory', + description: 'Unlocks the Enhanced WSTR Combat Shotgun for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 13, credits: 2000, effect: '', salvage: [{ slug: 'biomata-node', amount: 6 }, { slug: 'drone-resin', amount: 18 }] }, + ], + }, + { + slug: 'arach-enhanced-mips-railgun', + name: 'Enhanced MIPS Railgun', + category: 'armory', + description: 'Unlocks the Enhanced, ARES RG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 15, credits: 2000, effect: '', salvage: [{ slug: 'biomata-resin', amount: 12 }, { slug: 'biomata-resin', amount: 4 }] }, + ], + }, + { + slug: 'arach-deluxe-retaliator-lmg', + name: 'Deluxe Retaliator LMG', + category: 'armory', + description: 'Unlocks the Deluxe, Retaliator LMG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 20, credits: 3500, effect: '', salvage: [{ slug: 'enzyme-replicator', amount: 9 }, { slug: 'reflex-coil', amount: 3 }] }, + ], + }, + { + slug: 'arach-deluxe-mips-shotgun', + name: 'Deluxe MIPS Shotgun', + category: 'armory', + description: 'Unlocks the Enhanced WSTR Combat Shotgun for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 23, credits: 5000, effect: '', salvage: [{ slug: 'synapse-cube', amount: 2 }, { slug: 'biomata-resin', amount: 9 }] }, + ], + }, + { + slug: 'arach-deluxe-mips-railgun', + name: 'Deluxe MIPS Railgun', + category: 'armory', + description: 'Unlocks the Deluxe ARES RG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 5000, effect: '', salvage: [{ slug: 'hazard-capsule', amount: 2 }, { slug: 'enzyme-replicator', amount: 7 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 5, name: 'Capstone I', reward: 'Boosted — Rook will now start runs with implants. Arachne will now barter their wares for certain Deluxe Salvage.' }, + { rank: 2, nodesRequired: 10, name: 'Capstone II', reward: 'Treasure Hunter — Increases faction rep gain from Arachne Treasures. Arachne Treasure Reputation +20%' }, + { rank: 3, nodesRequired: 15, name: 'Capstone III', reward: 'Unlocks Enhanced Arachne Sponsorship Kits for purchase. Arachne will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 20, name: 'Capstone IV', reward: 'Boomstick — Rook will now start runs with a WSTR Combat Shotgun and MIPS Rounds' }, + { rank: 5, nodesRequired: 25, name: 'Capstone V', reward: 'Factory Reset.exe — Reviving a crew member grants healing over time. The healing is interrupted upon taking damage.' }, + { rank: 6, nodesRequired: 28, name: 'Capstone VI', reward: 'Superior Armament — Unlocks Superior Retaliator LMG, WSTR Combat Shotgun, and ARES RG for purchase in the Armory. Arachne will now barter their wares for certain Prestige Salvage.' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // SEKIGUCHI + // ═══════════════════════════════════════════════════════════════════ + sekiguchi: { + name: 'Sekiguchi', + color: '#73f2c9', + agent: 'Nona', + materials: [ + { slug: 'unstable-diode', name: 'Unstable Diode', icon: 'https://items.marathondb.gg/images/items/unstable-diode.webp' }, + { slug: 'fractal-circuit', name: 'Fractal Circuit', icon: 'https://items.marathondb.gg/images/items/fractal-circuit.webp' }, + { slug: 'storage-drive', name: 'Storage Drive', icon: 'https://items.marathondb.gg/images/items/storage-drive.webp' }, + { slug: 'amygdala-drive', name: 'Amygdala Drive', icon: 'https://items.marathondb.gg/images/items/amygdala-drive.webp' }, + { slug: 'neural-insulation', name: 'Neural Insulation', icon: 'https://items.marathondb.gg/images/items/neural-insulation.webp' }, + { slug: 'paradox-circuit', name: 'Paradox Circuit', icon: 'https://items.marathondb.gg/images/items/paradox-circuit.webp' }, + { slug: 'predictive-framework', name: 'Predictive Framework', icon: 'https://items.marathondb.gg/images/items/predictive-framework.webp' }, + { slug: 'synapse-cube', name: 'Synapse Cube', icon: 'https://items.marathondb.gg/images/items/synapse-cube.webp' }, + { slug: 'alien-alloy', name: 'Alien Alloy', icon: 'https://items.marathondb.gg/images/items/alien-alloy.webp' }, + ], + upgrades: [ + // ── Stat ── + { + slug: 'sek-tac-amp-exe', + name: 'Tac Amp.exe', + category: 'stat', + description: 'Tactical Recovery reduces the cooldown of your tactical and trait abilities.', + maxLevel: 2, + levels: [ + { level: 1, rank: 2, credits: 750, effect: 'Tactical Recovery +30', salvage: [{ slug: 'unstable-diode', amount: 16 }] }, + { level: 2, rank: 14, credits: 2000, effect: 'Tactical Recovery +30', salvage: [{ slug: 'paradox-circuit', amount: 8 }, { slug: 'storage-drive', amount: 30 }] }, + ], + }, + { + slug: 'sek-prime-amp', + name: 'Prime Amp', + category: 'stat', + description: 'Prime Recovery reduces the cooldown of your prime ability.', + maxLevel: 1, + levels: [ + { level: 1, rank: 24, credits: 5000, effect: 'Prime Recovery +30', salvage: [{ slug: 'synapse-cube', amount: 2 }] }, + ], + }, + // ── Function ── + { + slug: 'sek-lethal-amp-exe', + name: 'Lethal Amp.EXE', + category: 'function', + description: 'Downing a Runner grants you tactical ability energy. Eliminating a Runner grants you prime ability energy.', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 3500, effect: '', salvage: [{ slug: 'predictive-framework', amount: 7 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + // ── Armory ── + { + slug: 'sek-energy-amp', + name: 'Energy Amp', + category: 'armory', + description: 'Unlocks Energy Amps for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 1, credits: 750, effect: '', salvage: [{ slug: 'unstable-diode', amount: 10 }] }, + ], + }, + { + slug: 'sek-amped', + name: 'Amped', + category: 'armory', + description: 'Unlocks daily free Energy Amps in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: 'Free Daily Energy Amps in the Armory', salvage: [{ slug: 'fractal-circuit', amount: 23 }, { slug: 'storage-drive', amount: 9 }] }, + ], + }, + { + slug: 'sek-amp-stock', + name: 'Amp Stock', + category: 'armory', + description: 'Increases available stock of Energy Amps in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 21, credits: 2000, effect: '', salvage: [{ slug: 'paradox-circuit', amount: 12 }, { slug: 'amygdala-drive', amount: 5 }] }, + ], + }, + // ── Stat ── + { + slug: 'sek-scab-factory', + name: 'Scab Factory', + category: 'stat', + description: 'Increases the time it takes to bleed out when downed.', + maxLevel: 2, + levels: [ + { level: 1, rank: 13, credits: 2000, effect: 'DBNO Time +30 seconds', salvage: [{ slug: 'amygdala-drive', amount: 7 }, { slug: 'fractal-circuit', amount: 20 }] }, + { level: 2, rank: 23, credits: 3500, effect: 'DBNO Time +30 seconds', salvage: [{ slug: 'predictive-framework', amount: 9 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + // ── Function ── + { + slug: 'sek-head-start', + name: 'Head Start', + category: 'function', + description: 'Partially fills your tactical ability charge at the start of a run.', + maxLevel: 2, + levels: [ + { level: 1, rank: 4, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 10 }, { slug: 'unstable-diode', amount: 10 }] }, + { level: 2, rank: 14, credits: 2000, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 7 }, { slug: 'fractal-circuit', amount: 20 }] }, + ], + }, + { + slug: 'sek-primed-exe', + name: 'Primed.EXE', + category: 'function', + description: 'Partially fills your prime ability charge at the start of a run.', + maxLevel: 2, + levels: [ + { level: 1, rank: 26, credits: 5000, effect: '', salvage: [{ slug: 'alien-alloy', amount: 2 }, { slug: 'neural-insulation', amount: 7 }] }, + { level: 2, rank: 'VIP', credits: 5000, effect: '', salvage: [{ slug: 'alien-alloy', amount: 3 }, { slug: 'neural-insulation', amount: 11 }] }, + ], + }, + // ── Armory ── + { + slug: 'sek-capacitors', + name: 'Capacitors', + category: 'armory', + description: 'Unlocks Augmented Capacitors V2 implant for purchase in the Armory.', + maxLevel: 3, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 10 }, { slug: 'unstable-diode', amount: 10 }] }, + { level: 2, rank: 11, credits: 1500, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 8 }, { slug: 'fractal-circuit', amount: 13 }] }, + { level: 3, rank: 24, credits: 1500, effect: '', salvage: [{ slug: 'predictive-framework', amount: 9 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + { + slug: 'sek-harvester', + name: 'Harvester', + category: 'armory', + description: 'Unlocks Energy Harvesting V2 implant for purchase in the Armory.', + maxLevel: 3, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 7 }, { slug: 'unstable-diode', amount: 6 }] }, + { level: 2, rank: 9, credits: 2000, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 23 }, { slug: 'storage-drive', amount: 9 }] }, + { level: 3, rank: 23, credits: 3500, effect: '', salvage: [{ slug: 'neural-insulation', amount: 7 }, { slug: 'predictive-framework', amount: 3 }] }, + ], + }, + { + slug: 'sek-triage', + name: 'Triage', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Triage in the Armory', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 4 }, { slug: 'fractal-circuit', amount: 8 }] }, + ], + }, + { + slug: 'sek-destroyer', + name: 'Destroyer', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Destroyer in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'paradox-circuit', amount: 4 }, { slug: 'storage-drive', amount: 8 }] }, + ], + }, + { + slug: 'sek-assassin', + name: 'Assassin', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Assassin in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 4 }, { slug: 'fractal-circuit', amount: 8 }] }, + ], + }, + { + slug: 'sek-vandal', + name: 'Vandal', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Vandal in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'paradox-circuit', amount: 4 }, { slug: 'storage-drive', amount: 8 }] }, + ], + }, + { + slug: 'sek-recon', + name: 'Recon', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Recon in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 4 }, { slug: 'fractal-circuit', amount: 8 }] }, + ], + }, + { + slug: 'sek-thief', + name: 'Thief', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Thief in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'paradox-circuit', amount: 4 }, { slug: 'storage-drive', amount: 8 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 5, name: 'Capstone I', reward: 'Specialized — Rook will now start runs with Runner Cores. Sekiguchi will now barter their wares for certain Deluxe Salvage.' }, + { rank: 2, nodesRequired: 10, name: 'Capstone II', reward: 'Treasure Hunter — Increases faction rep gained from Sekiguchi Treasures by 20%' }, + { rank: 3, nodesRequired: 15, name: 'Capstone III', reward: 'Unlocks Enhanced Sekiguchi Sponsorship Kits for purchase. Sekiguchi will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 20, name: 'Capstone IV', reward: 'Commission — Sekiguchi standard contracts will now award cores in addition to other rewards' }, + { rank: 5, nodesRequired: 25, name: 'Capstone V', reward: 'Quiet Exit.exe — Rook gains the effects of Signal Mask after activating an exfil' }, + { rank: 6, nodesRequired: 32, name: 'Capstone VI', reward: 'Core All — Unlocks Superior Cores for each runner in the Armory. Sekiguchi will now barter their wares for certain Prestige Salvage.' }, + ], + }, +}; + +// Make available globally +if (typeof window !== 'undefined') window.FACTION_UPGRADES = FACTION_UPGRADES; diff --git a/backend/references/faction-upgrades.live.js b/backend/references/faction-upgrades.live.js new file mode 100644 index 0000000..f0b5da7 --- /dev/null +++ b/backend/references/faction-upgrades.live.js @@ -0,0 +1,1479 @@ +// Faction Upgrade Definitions — Static data for the upgrade checklist +// Data sourced from user's verified Faction Upgrades v2 spreadsheet (Mar 2026). + +const FACTION_UPGRADES = { + // ═══════════════════════════════════════════════════════════════════ + // CYBERACME + // ═══════════════════════════════════════════════════════════════════ + cyberacme: { + name: 'CyberAcme', + color: '#01d838', + agent: 'Oni', + materials: [ + { slug: 'unstable-diode', name: 'Unstable Diode', icon: 'https://items.marathondb.gg/images/items/unstable-diode.webp' }, + { slug: 'unstable-gel', name: 'Unstable Gel', icon: 'https://items.marathondb.gg/images/items/unstable-gel.webp' }, + { slug: 'unstable-gunmetal', name: 'Unstable Gunmetal', icon: 'https://items.marathondb.gg/images/items/unstable-gunmetal.webp' }, + { slug: 'unstable-biomass', name: 'Unstable Biomass', icon: 'https://items.marathondb.gg/images/items/unstable-biomass.webp' }, + { slug: 'unstable-lead', name: 'Unstable Lead', icon: 'https://items.marathondb.gg/images/items/unstable-lead.webp' }, + ], + upgrades: [ + // ── Inventory ── + { + slug: 'cyac-expansion', + name: 'Expansion', + category: 'inventory', + description: 'Gain additional rows of vault capacity for the rest of the current season.', + maxLevel: 5, + levels: [ + { level: 1, rank: 3, credits: 2500, effect: 'Vault Size +8 Rows', salvage: [{ slug: 'unstable-diode', amount: 12 }] }, + { level: 2, rank: 7, credits: 4000, effect: 'Vault Size +8 Rows', salvage: [{ slug: 'unstable-diode', amount: 22 }, { slug: 'unstable-gunmetal', amount: 12 }] }, + { level: 3, rank: 12, credits: 5000, effect: 'Vault Size +6 Rows', salvage: [{ slug: 'unstable-diode', amount: 27 }, { slug: 'unstable-gunmetal', amount: 15 }] }, + { level: 4, rank: 18, credits: 7000, effect: 'Vault Size +4 Rows', salvage: [{ slug: 'unstable-diode', amount: 30 }, { slug: 'unstable-gunmetal', amount: 18 }] }, + { level: 5, rank: 28, credits: 10000, effect: 'Vault Size +4 Rows', salvage: [{ slug: 'unstable-diode', amount: 50 }, { slug: 'unstable-gunmetal', amount: 30 }] }, + ], + }, + { + slug: 'cyac-credit-limit', + name: 'Credit Limit', + category: 'inventory', + description: 'Raises your credit wallet\'s capacity for the rest of the season.', + maxLevel: 5, + levels: [ + { level: 1, rank: 4, credits: 2500, effect: 'Credit Wallet Capacity +20k', salvage: [] }, + { level: 2, rank: 8, credits: 4000, effect: 'Credit Wallet Capacity +50k', salvage: [] }, + { level: 3, rank: 12, credits: 7000, effect: 'Credit Wallet Capacity +200k', salvage: [] }, + { level: 4, rank: 18, credits: 10000, effect: 'Credit Wallet Capacity +700k', salvage: [] }, + { level: 5, rank: 25, credits: 50000, effect: 'Credit Wallet Capacity +9,000k', salvage: [] }, + ], + }, + // ── Function ── + { + slug: 'cyac-informant-exe', + name: 'Informant.exe', + category: 'function', + description: 'Increases data card credit rewards by 50%. This bonus additively stacks with other Informant upgrades.', + maxLevel: 2, + levels: [ + { level: 1, rank: 2, credits: 1500, effect: 'Data Card Credit Value +50%', salvage: [] }, + { level: 2, rank: 15, credits: 2000, effect: 'Data Card Credit Value +50%', salvage: [] }, + ], + }, + { + slug: 'cyac-soundproof-exe', + name: 'Soundproof.exe', + category: 'function', + description: 'You make less noise while looting.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: 'Reduced looting noise', salvage: [] }, + ], + }, + { + slug: 'cyac-loose-change-exe', + name: 'Loose Change.exe', + category: 'function', + description: 'Opening a container rewards you with 25 credits.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: '+25 Credits per container', salvage: [] }, + ], + }, + { + slug: 'cyac-fixative-exe', + name: 'Fixative.exe', + category: 'function', + description: 'ROOK gains an increased chance of finding Matter Fixatives when defeating UESC.', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 3500, effect: 'Increased Matter Fixative drops', salvage: [] }, + ], + }, + { + slug: 'cyac-slider-exe', + name: 'Slider.exe', + category: 'function', + description: 'Your sprint slide generates less heat.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 7000, effect: 'Reduced slide heat', salvage: [] }, + ], + }, + // ── Armory ── + { + slug: 'cyac-carrier', + name: 'Carrier', + category: 'armory', + description: 'Unlocks Enhanced backpacks for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: '8XS Base Pack', salvage: [] }, + ], + }, + { + slug: 'cyac-carrier-plus', + name: 'Carrier+', + category: 'armory', + description: 'Unlocks Deluxe backpacks for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 15, credits: 4000, effect: '16XS Base Pack', salvage: [] }, + ], + }, + { + slug: 'cyac-enhanced-weaponry', + name: 'Enhanced Weaponry', + category: 'armory', + description: 'Unlocks Enhanced Overrun AR, V11 Punch, and CE Tactical Sidearm for purchase from CyberAcme.', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 2500, effect: 'Unlock Enhanced weapons', salvage: [] }, + ], + }, + { + slug: 'cyac-deluxe-weaponry', + name: 'Deluxe Weaponry', + category: 'armory', + description: 'Unlocks Deluxe Overrun AR, V11 Punch, and CE Tactical Sidearm for purchase from CyberAcme.', + maxLevel: 1, + levels: [ + { level: 1, rank: 14, credits: 4000, effect: 'Unlock Deluxe weapons', salvage: [] }, + ], + }, + { + slug: 'cyac-locksmith', + name: 'Locksmith', + category: 'armory', + description: 'Unlocks lockbox key for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 2500, effect: 'Lockbox Key (Item)', salvage: [] }, + ], + }, + { + slug: 'cyac-keymaker', + name: 'Keymaker', + category: 'armory', + description: 'Unlocks Deluxe Key templates for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 16, credits: 4000, effect: 'Deluxe Key Template (Item)', salvage: [] }, + ], + }, + { + slug: 'cyac-keymaker-plus', + name: 'Keymaker+', + category: 'armory', + description: 'Unlocks Superior Key templates for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 28, credits: 7000, effect: 'Superior Key Template (Item)', salvage: [] }, + ], + }, + // ── Stat ── + { + slug: 'cyac-heat-sink-exe', + name: 'Heat Sink.exe', + category: 'stat', + description: 'Heat Capacity increases the number of movement actions (sprint, sliding) you can perform before overheating.', + maxLevel: 2, + levels: [ + { level: 1, rank: 1, credits: 2500, effect: 'Heat Capacity +20', salvage: [{ slug: 'unstable-biomass', amount: 12 }] }, + { level: 2, rank: 12, credits: 3500, effect: 'Heat Capacity +20', salvage: [{ slug: 'unstable-biomass', amount: 24 }, { slug: 'unstable-lead', amount: 12 }] }, + ], + }, + { + slug: 'cyac-scavenger-exe', + name: 'Scavenger.exe', + category: 'stat', + description: 'Loot Speed increases how quickly items are revealed when looting containers.', + maxLevel: 3, + levels: [ + { level: 1, rank: 1, credits: 750, effect: 'Loot Speed +20', salvage: [] }, + { level: 2, rank: 4, credits: 2500, effect: 'Loot Speed +20', salvage: [] }, + { level: 3, rank: 16, credits: 4000, effect: 'Loot Speed +20', salvage: [] }, + ], + }, + { + slug: 'cyac-quick-vent-exe', + name: 'Quick Vent.exe', + category: 'stat', + description: 'Your heat recovery begins more quickly after actions that generate heat.', + maxLevel: 2, + levels: [ + { level: 1, rank: 4, credits: 2500, effect: 'Heat Recovery Speed -20%', salvage: [{ slug: 'unstable-gel', amount: 8 }] }, + { level: 2, rank: 20, credits: 4000, effect: 'Heat Recovery Speed -20%', salvage: [{ slug: 'unstable-gel', amount: 16 }] }, + ], + }, + { + slug: 'cyac-active-cool-exe', + name: 'Active Cool.exe', + category: 'stat', + description: 'Your generated heat recovers more quickly.', + maxLevel: 2, + levels: [ + { level: 1, rank: 13, credits: 3500, effect: 'Heat Recovery Rate +15%', salvage: [{ slug: 'unstable-gel', amount: 24 }] }, + { level: 2, rank: 23, credits: 5000, effect: 'Heat Recovery Rate +15%', salvage: [{ slug: 'unstable-gel', amount: 30 }] }, + ], + }, + { + slug: 'cyac-firm-stance-exe', + name: 'Firm Stance.exe', + category: 'stat', + description: 'Fall Resistance reduces the amount of damage you take after falling.', + maxLevel: 3, + levels: [ + { level: 1, rank: 1, credits: 750, effect: 'Fall Resistance +20', salvage: [] }, + { level: 2, rank: 11, credits: 4000, effect: 'Fall Resistance +20', salvage: [] }, + { level: 3, rank: 26, credits: 5000, effect: 'Fall Resistance +20', salvage: [] }, + ], + }, + { + slug: 'cyac-loot-siphon-exe', + name: 'Loot Siphon.exe', + category: 'stat', + description: 'Grants bonus tactical ability energy when opening an unlooted container.', + maxLevel: 2, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: 'Tactical Energy on Container Loot +5%', salvage: [] }, + { level: 2, rank: 17, credits: 4000, effect: 'Tactical Energy on Container Loot +5%', salvage: [] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 6, name: 'Capstone I', reward: 'Bonus Pay' }, + { rank: 2, nodesRequired: 12, name: 'Capstone II', reward: 'CyberAcme Treasure Reputation +20%' }, + { rank: 3, nodesRequired: 18, name: 'Capstone III', reward: 'Unlocks Enhanced CyberAcme Sponsorship Kits for purchase' }, + { rank: 4, nodesRequired: 24, name: 'Capstone IV', reward: 'Stipend — Rook will now start runs with a small amount of credits in addition to their basic gear' }, + { rank: 5, nodesRequired: 30, name: 'Capstone V', reward: 'Max Looter — Unlocks Superior Backpacks for purchase in the Armory (24XS Backpack)' }, + { rank: 6, nodesRequired: 38, name: 'Capstone VI', reward: 'Carrier — Start with Deluxe Backpack' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // NUCALORIC + // ═══════════════════════════════════════════════════════════════════ + nucaloric: { + name: 'NuCaloric', + color: '#ff125d', + agent: 'Gaius', + materials: [ + { slug: 'unstable-biomass', name: 'Unstable Biomass', icon: 'https://items.marathondb.gg/images/items/unstable-biomass.webp' }, + { slug: 'sparkleaf', name: 'Sparkleaf', icon: 'https://items.marathondb.gg/images/items/sparkleaf.webp' }, + { slug: 'reclaimed-biostripping', name: 'Reclaimed Biostripping', icon: 'https://items.marathondb.gg/images/items/reclaimed-biostripping.webp' }, + { slug: 'dermachem-pack', name: 'Dermachem Pack', icon: 'https://items.marathondb.gg/images/items/dermachem-pack.webp' }, + { slug: 'tarax-seed', name: 'Tarax Seed', icon: 'https://items.marathondb.gg/images/items/tarax-seed.webp' }, + { slug: 'biolens-seed', name: 'Biolens Seed', icon: 'https://items.marathondb.gg/images/items/biolens-seed.webp' }, + { slug: 'sterilized-biostripping', name: 'Sterilized Biostripping', icon: 'https://items.marathondb.gg/images/items/sterilized-biostripping.webp' }, + { slug: 'neurochem-pack', name: 'Neurochem Pack', icon: 'https://items.marathondb.gg/images/items/neurochem-pack.webp' }, + { slug: 'neural-insulation', name: 'Neural Insulation', icon: 'https://items.marathondb.gg/images/items/neural-insulation.webp' }, + { slug: 'hazard-capsule', name: 'Hazard Capsule', icon: 'https://items.marathondb.gg/images/items/hazard-capsule.webp' }, + { slug: 'enzyme-replicator', name: 'Enzyme Replicator', icon: 'https://items.marathondb.gg/images/items/enzyme-replicator.webp' }, + ], + upgrades: [ + // ── Armory ── + { + slug: 'nucal-safeguard', + name: 'Safeguard', + category: 'armory', + description: 'Unlocks daily free Shield Charges in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 1, credits: 750, effect: 'Free Daily Shield Charges', salvage: [{ slug: 'unstable-biomass', amount: 16 }] }, + ], + }, + { + slug: 'nucal-advanced-shields', + name: 'Advanced Shields', + category: 'armory', + description: 'Unlocks Advanced Shield Charges for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 6, credits: 1500, effect: 'Advanced Shield Charge', salvage: [{ slug: 'reclaimed-biostripping', amount: 10 }, { slug: 'unstable-biomass', amount: 10 }] }, + ], + }, + { + slug: 'nucal-safeguard-plus', + name: 'Safeguard+', + category: 'armory', + description: 'Unlocks daily free Advanced Shield Charges in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 11, credits: 2000, effect: 'Free Daily Advanced Shield Charges', salvage: [{ slug: 'sterilized-biostripping', amount: 6 }, { slug: 'sparkleaf', amount: 16 }] }, + ], + }, + { + slug: 'nucal-shield-stock', + name: 'Shield Stock', + category: 'armory', + description: 'Increases Advanced Shield Charge stock in the Armory by 5.', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 1500, effect: 'Shield Charge Stock +5', salvage: [{ slug: 'reclaimed-biostripping', amount: 15 }, { slug: 'sparkleaf', amount: 8 }] }, + ], + }, + { + slug: 'nucal-shielded', + name: 'Shielded', + category: 'armory', + description: 'Unlocks Enhanced shield implants for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: 'Protector V1', salvage: [{ slug: 'reclaimed-biostripping', amount: 12 }, { slug: 'unstable-biomass', amount: 13 }] }, + ], + }, + { + slug: 'nucal-armored', + name: 'Armored', + category: 'armory', + description: 'Unlocks Deluxe shield implants for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 20, credits: 3500, effect: 'Protector V2', salvage: [{ slug: 'biolens-seed', amount: 7 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + { + slug: 'nucal-restore', + name: 'Restore', + category: 'armory', + description: 'Unlocks daily free Patch Kits in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: 'Free Daily Patch Kits', salvage: [{ slug: 'unstable-biomass', amount: 23 }] }, + ], + }, + { + slug: 'nucal-advanced-patch', + name: 'Advanced Patch', + category: 'armory', + description: 'Unlocks Advanced Patch Kits for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 8, credits: 1500, effect: 'Advanced Patch Kit', salvage: [{ slug: 'dermachem-pack', amount: 10 }, { slug: 'unstable-biomass', amount: 13 }] }, + ], + }, + { + slug: 'nucal-restore-plus', + name: 'Restore+', + category: 'armory', + description: 'Unlocks daily free Advanced Patch Kits in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 2000, effect: 'Free Daily Advanced Patch Kits', salvage: [{ slug: 'neurochem-pack', amount: 5 }, { slug: 'sparkleaf', amount: 16 }] }, + ], + }, + { + slug: 'nucal-patch-stock', + name: 'Patch Stock', + category: 'armory', + description: 'Increases Advanced Patch Kit stock in the Armory by 5.', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 1500, effect: 'Patch Kit Stock +5', salvage: [{ slug: 'dermachem-pack', amount: 8 }, { slug: 'unstable-biomass', amount: 11 }] }, + ], + }, + { + slug: 'nucal-panacea-kit', + name: 'Panacea Kit', + category: 'armory', + description: 'Unlocks Panacea Kits for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 5000, effect: 'Panacea Kit', salvage: [{ slug: 'hazard-capsule', amount: 2 }, { slug: 'neural-insulation', amount: 7 }] }, + ], + }, + { + slug: 'nucal-regen', + name: 'Regen', + category: 'armory', + description: 'Unlocks Regen V2 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 3, credits: 750, effect: 'Regen V2', salvage: [{ slug: 'unstable-biomass', amount: 10 }] }, + ], + }, + { + slug: 'nucal-regen-plus', + name: 'Regen+', + category: 'armory', + description: 'Unlocks Regen V3 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: 'Regen V3', salvage: [{ slug: 'reclaimed-biostripping', amount: 28 }, { slug: 'sparkleaf', amount: 14 }] }, + ], + }, + { + slug: 'nucal-regen-plus-plus', + name: 'Regen++', + category: 'armory', + description: 'Unlocks Regen V4 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 21, credits: 3500, effect: 'Regen V4', salvage: [{ slug: 'biolens-seed', amount: 7 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + { + slug: 'nucal-advanced-mch', + name: 'Advanced MCH', + category: 'armory', + description: 'Unlocks Advanced Mechanic\'s Kits for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 6, credits: 1500, effect: 'Advanced Mechanic\'s Kit', salvage: [{ slug: 'reclaimed-biostripping', amount: 8 }, { slug: 'unstable-biomass', amount: 9 }] }, + ], + }, + { + slug: 'nucal-advanced-os', + name: 'Advanced OS', + category: 'armory', + description: 'Unlocks Advanced OS Debugs for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: 'Advanced OS Debug', salvage: [{ slug: 'reclaimed-biostripping', amount: 10 }, { slug: 'unstable-biomass', amount: 10 }] }, + ], + }, + { + slug: 'nucal-helping-hands', + name: 'Helping Hands', + category: 'armory', + description: 'Unlocks Helping Hands V2 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 2, credits: 750, effect: 'Helping Hands V2', salvage: [{ slug: 'unstable-biomass', amount: 10 }] }, + ], + }, + { + slug: 'nucal-helping-hands-plus', + name: 'Helping Hands+', + category: 'armory', + description: 'Unlocks Helping Hands V3 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 11, credits: 1500, effect: 'Helping Hands V3', salvage: [{ slug: 'dermachem-pack', amount: 10 }, { slug: 'sparkleaf', amount: 13 }] }, + ], + }, + { + slug: 'nucal-helping-hands-plus-plus', + name: 'Helping Hands++', + category: 'armory', + description: 'Unlocks Helping Hands V4 implant for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 22, credits: 3500, effect: 'Helping Hands V4', salvage: [{ slug: 'biolens-seed', amount: 9 }, { slug: 'tarax-seed', amount: 14 }] }, + ], + }, + { + slug: 'nucal-self-revive', + name: 'Self-Revive', + category: 'armory', + description: 'Unlocks Self-Revives for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 1500, effect: 'Self-Revive', salvage: [{ slug: 'neurochem-pack', amount: 4 }, { slug: 'sparkleaf', amount: 14 }] }, + ], + }, + // ── Stat ── + { + slug: 'nucal-null-hazard-exe', + name: 'NULL_HAZARD.EXE', + category: 'stat', + description: 'Hazard Tolerance increases your maximum data buffer protection, which is restored by using HEC consumables.', + maxLevel: 2, + levels: [ + { level: 1, rank: 4, credits: 750, effect: 'Hazard Tolerance +50', salvage: [{ slug: 'unstable-biomass', amount: 19 }] }, + { level: 2, rank: 14, credits: 1500, effect: 'Hazard Tolerance +50', salvage: [{ slug: 'sterilized-biostripping', amount: 5 }, { slug: 'sparkleaf', amount: 12 }] }, + ], + }, + { + slug: 'nucal-tciv-resist-exe', + name: 'TCIV_RESIST.EXE', + category: 'stat', + description: 'Ticks, lightning, and Heat Cascade deal reduced damage.', + maxLevel: 1, + levels: [ + { level: 1, rank: 21, credits: 3500, effect: 'Reduced environmental damage', salvage: [{ slug: 'biolens-seed', amount: 5 }, { slug: 'tarax-seed', amount: 7 }] }, + ], + }, + { + slug: 'nucal-reinforce-exe', + name: 'REINFORCE.EXE', + category: 'stat', + description: 'Hardware reduces the duration of negative status effects that debilitate your Runner\'s physical chassis (Frost, Immobilize, Overheat, Toxin).', + maxLevel: 3, + levels: [ + { level: 1, rank: 6, credits: 1500, effect: 'Hardware +20', salvage: [{ slug: 'reclaimed-biostripping', amount: 8 }, { slug: 'unstable-biomass', amount: 9 }] }, + { level: 2, rank: 16, credits: 2000, effect: 'Hardware +20', salvage: [{ slug: 'sterilized-biostripping', amount: 7 }, { slug: 'sparkleaf', amount: 25 }] }, + { level: 3, rank: 26, credits: 5000, effect: 'Hardware +20', salvage: [{ slug: 'hazard-capsule', amount: 2 }] }, + ], + }, + { + slug: 'nucal-unfazed-exe', + name: 'UNFAZED.EXE', + category: 'stat', + description: 'Firewall reduces the duration of status effects that degrade your Runner\'s electronic systems (EMP, Hack).', + maxLevel: 3, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: 'Firewall +20', salvage: [{ slug: 'dermachem-pack', amount: 7 }, { slug: 'unstable-biomass', amount: 7 }] }, + { level: 2, rank: 18, credits: 2000, effect: 'Firewall +20', salvage: [{ slug: 'neurochem-pack', amount: 8 }, { slug: 'tarax-seed', amount: 5 }] }, + { level: 3, rank: 27, credits: 5000, effect: 'Firewall +20', salvage: [{ slug: 'hazard-capsule', amount: 2 }, { slug: 'tarax-seed', amount: 9 }] }, + ], + }, + { + slug: 'nucal-recovery-exe', + name: 'RECOVERY.EXE', + category: 'stat', + description: 'Self-Repair Speed increases how quickly your consumables restore missing health or shields.', + maxLevel: 3, + levels: [ + { level: 1, rank: 9, credits: 1500, effect: 'Self-Repair Speed +20', salvage: [{ slug: 'dermachem-pack', amount: 10 }, { slug: 'unstable-biomass', amount: 13 }] }, + { level: 2, rank: 19, credits: 2000, effect: 'Self-Repair Speed +20', salvage: [{ slug: 'neurochem-pack', amount: 10 }, { slug: 'tarax-seed', amount: 6 }] }, + { level: 3, rank: 29, credits: 5000, effect: 'Self-Repair Speed +20', salvage: [{ slug: 'hazard-capsule', amount: 3 }, { slug: 'enzyme-replicator', amount: 3 }] }, + ], + }, + // ── Function ── + { + slug: 'nucal-shield-comm', + name: 'Shield Comm', + category: 'function', + description: 'NuCaloric standard contracts will now award Shield Charges in addition to other rewards.', + maxLevel: 1, + levels: [ + { level: 1, rank: 15, credits: 2000, effect: 'Shield Charges from contracts', salvage: [{ slug: 'sterilized-biostripping', amount: 12 }, { slug: 'tarax-seed', amount: 6 }] }, + ], + }, + { + slug: 'nucal-health-comm', + name: 'Health Comm', + category: 'function', + description: 'NuCaloric standard contracts will now award Patch Kits in addition to other rewards.', + maxLevel: 1, + levels: [ + { level: 1, rank: 24, credits: 5000, effect: 'Patch Kits from contracts', salvage: [{ slug: 'hazard-capsule', amount: 2 }] }, + ], + }, + { + slug: 'nucal-resist-comm', + name: 'Resist Comm', + category: 'function', + description: 'NuCaloric standard contracts will now award Mechanic\'s Kits or OS Reboots in addition to other rewards.', + maxLevel: 1, + levels: [ + { level: 1, rank: 22, credits: 3500, effect: 'Mechanic\'s Kits/OS Reboots from contracts', salvage: [{ slug: 'biolens-seed', amount: 9 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + { + slug: 'nucal-field-medic-exe', + name: 'FIELD_MEDIC.EXE', + category: 'function', + description: 'Health and shield consumables take less time to use.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: 'Faster consumable use', salvage: [{ slug: 'hazard-capsule', amount: 3 }, { slug: 'enzyme-replicator', amount: 8 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 6, name: 'Capstone I', reward: 'Rook will now start runs with a Patch Kit and Shield Charge' }, + { rank: 2, nodesRequired: 12, name: 'Capstone II', reward: 'Treasure Hunter — Increases NuCaloric Treasure Reputation by 20%' }, + { rank: 3, nodesRequired: 18, name: 'Capstone III', reward: 'Unlocks Enhanced NuCaloric Sponsorship Kits for purchase. NuCaloric will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 24, name: 'Capstone IV', reward: '2nd Chance.exe — Self-Revives have a small chance to not be consumed on use' }, + { rank: 5, nodesRequired: 30, name: 'Capstone V', reward: 'Hush.exe — You make less noise while healing' }, + { rank: 6, nodesRequired: 38, name: 'Capstone VI', reward: 'Reinforced — Unlocks Superior Shield Implants for purchase in the Armory. NuCaloric will now barter their wares for certain Prestige Salvage.' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // TRAXUS + // ═══════════════════════════════════════════════════════════════════ + traxus: { + name: 'Traxus', + color: '#ff7300', + agent: 'Vulcan', + materials: [ + { slug: 'unstable-gunmetal', name: 'Unstable Gunmetal', icon: 'https://items.marathondb.gg/images/items/unstable-gunmetal.webp' }, + { slug: 'deimosite-rods', name: 'Deimosite Rods', icon: 'https://items.marathondb.gg/images/items/deimosite-rods.webp' }, + { slug: 'altered-wire', name: 'Altered Wire', icon: 'https://items.marathondb.gg/images/items/altered-wire.webp' }, + { slug: 'plasma-filament', name: 'Plasma Filament', icon: 'https://items.marathondb.gg/images/items/plasma-filament.webp' }, + { slug: 'tachyon-filament', name: 'Tachyon Filament', icon: 'https://items.marathondb.gg/images/items/tachyon-filament.webp' }, + { slug: 'anomalous-wire', name: 'Anomalous Wire', icon: 'https://items.marathondb.gg/images/items/anomalous-wire.webp' }, + { slug: 'cetinite-rods', name: 'Cetinite Rods', icon: 'https://items.marathondb.gg/images/items/cetinite-rods.webp' }, + { slug: 'predictive-framework', name: 'Predictive Framework', icon: 'https://items.marathondb.gg/images/items/predictive-framework.webp' }, + { slug: 'ballistic-turbine', name: 'Ballistic Turbine', icon: 'https://items.marathondb.gg/images/items/ballistic-turbine.webp' }, + { slug: 'reflex-coil', name: 'Reflex Coil', icon: 'https://items.marathondb.gg/images/items/reflex-coil.webp' }, + { slug: 'alien-alloy', name: 'Alien Alloy', icon: 'https://items.marathondb.gg/images/items/alien-alloy.webp' }, + ], + upgrades: [ + // ── Armory ── + { + slug: 'trax-smg-mods', + name: 'SMG Mods', + category: 'armory', + description: 'Unlocks rotating Enhanced SMG mods from the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 1, credits: 750, effect: 'Unlock Enhanced SMG mods (2 items)', salvage: [{ slug: 'unstable-gunmetal', amount: 10 }] }, + { level: 2, rank: 6, credits: 1500, effect: 'Unlock additional Enhanced SMG mods (2 items)', salvage: [{ slug: 'deimosite-rods', amount: 7 }, { slug: 'unstable-gunmetal', amount: 6 }] }, + ], + }, + { + slug: 'trax-deluxe-smg-mods', + name: 'Deluxe SMG Mods', + category: 'armory', + description: 'Unlocks rotating Deluxe SMG mods from the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 18, credits: 3500, effect: 'Unlock deluxe SMG mods (3 items)', salvage: [{ slug: 'predictive-framework', amount: 4 }, { slug: 'tachyon-filament', amount: 6 }] }, + ], + }, + { + slug: 'trax-enhanced-heavy-submachine-gun', + name: 'Enhanced Heavy Submachine Gun', + category: 'armory', + description: 'Unlocks Enhanced "Bully SMG" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: 'Unlocks Bully SMG for purchase', salvage: [{ slug: 'deimosite-rods', amount: 23 }, { slug: 'altered-wire', amount: 9 }] }, + ], + }, + { + slug: 'trax-enhanced-volt-submachine-gun', + name: 'Enhanced Volt Submachine Gun', + category: 'armory', + description: 'Unlocks Enhanced "V22 Volt Thrower" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 15, credits: 2000, effect: 'Unlocks V22 Voltthrower for purchase', salvage: [{ slug: 'cetinite-rods', amount: 12 }, { slug: 'tachyon-filament', amount: 5 }] }, + ], + }, + { + slug: 'trax-ar-mods', + name: 'AR Mods', + category: 'armory', + description: 'Unlocks rotating Enhanced AR mods from the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 2, credits: 750, effect: 'Unlock Enhanced AR mods (2 items)', salvage: [{ slug: 'unstable-gunmetal', amount: 10 }] }, + { level: 2, rank: 2, credits: 1500, effect: 'Unlock Enhanced AR mods (2 items)', salvage: [{ slug: 'altered-wire', amount: 7 }, { slug: 'unstable-gunmetal', amount: 6 }] }, + ], + }, + { + slug: 'trax-deluxe-ar-mods', + name: 'Deluxe AR Mods', + category: 'armory', + description: 'Unlocks a rotating Deluxe AR mod in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 22, credits: 5000, effect: 'Unlock Deluxe AR mods (3 items)', salvage: [{ slug: 'alien-alloy', amount: 2 }] }, + ], + }, + { + slug: 'trax-enhanced-light-ar', + name: 'Enhanced Light AR', + category: 'armory', + description: 'Unlocks Enhanced "M77 AR" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 14, credits: 2000, effect: 'Unlocks M77 AR for purchase in the armory', salvage: [{ slug: 'anomalous-wire', amount: 10 }, { slug: 'tachyon-filament', amount: 4 }] }, + ], + }, + { + slug: 'trax-enhanced-chips', + name: 'Enhanced Chips', + category: 'armory', + description: 'Unlocks a set of enhanced weapon chip mods from the Armory', + maxLevel: 2, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: 'Unlock 3 weapon chips in the armory', salvage: [{ slug: 'altered-wire', amount: 7 }, { slug: 'unstable-gunmetal', amount: 6 }] }, + { level: 2, rank: 10, credits: 1500, effect: 'Unlock 3 weapon chips in the armory', salvage: [{ slug: 'altered-wire', amount: 19 }, { slug: 'plasma-filament', amount: 9 }] }, + ], + }, + { + slug: 'trax-deluxe-chips', + name: 'Deluxe Chips', + category: 'armory', + description: 'Unlocks a set of Deluxe weapon chip mods in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: 'Unlock 4 weapon chips in the armory', salvage: [{ slug: 'alien-alloy', amount: 3 }, { slug: 'reflex-coil', amount: 11 }] }, + ], + }, + { + slug: 'trax-volt-mods', + name: 'Volt Mods', + category: 'armory', + description: 'Unlocks rotating Enhanced volt weapon mod from the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 3, credits: 750, effect: '', salvage: [{ slug: 'unstable-gunmetal', amount: 13 }] }, + { level: 2, rank: 11, credits: 1500, effect: '', salvage: [{ slug: 'deimosite-rods', amount: 12 }, { slug: 'altered-wire', amount: 6 }] }, + { level: 3, rank: 17, credits: 1500, effect: '', salvage: [{ slug: 'cetinite-rods', amount: 12 }, { slug: 'altered-wire', amount: 11 }] }, + ], + }, + { + slug: 'trax-volt-pr', + name: 'Volt PR', + category: 'armory', + description: 'Unlocks the V66 Lookout for purchase in the Armory (Weapon)', + maxLevel: 2, + levels: [ + { level: 1, rank: 8, credits: 1500, effect: 'Unlocks item for purchase', salvage: [{ slug: 'deimosite-rods', amount: 19 }, { slug: 'altered-wire', amount: 7 }] }, + { level: 2, rank: 18, credits: 3500, effect: 'Unlocks item for purchase', salvage: [{ slug: 'predictive-framework', amount: 5 }, { slug: 'tachyon-filament', amount: 7 }] }, + ], + }, + { + slug: 'trax-deluxe-volt-mods', + name: 'Deluxe Volt Mods', + category: 'armory', + description: 'Unlocks rotating Deluxe Volt weapon mod in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 20, credits: 3500, effect: 'Unlocks item for purchase', salvage: [{ slug: 'predictive-framework', amount: 7 }, { slug: 'reflex-coil', amount: 3 }] }, + ], + }, + { + slug: 'trax-precision-mods', + name: 'Precision Mods', + category: 'armory', + description: 'Unlocks rotating enhanced precision weapon mod from the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 7, credits: 750, effect: '', salvage: [{ slug: 'unstable-gunmetal', amount: 19 }] }, + { level: 2, rank: 13, credits: 1500, effect: '', salvage: [{ slug: 'deimosite-rods', amount: 19 }, { slug: 'altered-wire', amount: 7 }] }, + { level: 3, rank: 19, credits: 2000, effect: '', salvage: [{ slug: 'cetinite-rods', amount: 4 }, { slug: 'tachyon-filament', amount: 4 }] }, + ], + }, + { + slug: 'trax-mips-sniper', + name: 'MIPS Sniper', + category: 'armory', + description: 'Unlocks Enhanced "Longshot" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 9, credits: 1500, effect: 'Unlocks "Longshot" for purchase in the armory', salvage: [{ slug: 'anomalous-wire', amount: 5 }, { slug: 'plasma-filament', amount: 10 }] }, + ], + }, + { + slug: 'trax-enhanced-hardline-pr', + name: 'Enhanced Hardline PR', + category: 'armory', + description: 'Unlocks Enhanced "Hardline PR" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 2000, effect: 'Unlocks "Hardline PR" for purchase in the armory', salvage: [{ slug: 'anomalous-wire', amount: 8 }, { slug: 'plasma-filament', amount: 21 }] }, + ], + }, + { + slug: 'trax-deluxe-precision-mods', + name: 'Deluxe Precision Mods', + category: 'armory', + description: 'Unlocks rotating Deluxe precision weapon mod in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 5000, effect: '', salvage: [{ slug: 'alien-alloy', amount: 2 }, { slug: 'tachyon-filament', amount: 9 }] }, + ], + }, + // ── Stat ── + { + slug: 'trax-tracker-exe', + name: 'Tracker.exe', + category: 'stat', + description: 'Ping duration increases how long your ping persist on hostile targets.', + maxLevel: 2, + levels: [ + { level: 1, rank: 15, credits: 2000, effect: 'Ping Duration +30', salvage: [{ slug: 'anomalous-wire', amount: 7 }, { slug: 'plasma-filament', amount: 21 }] }, + { level: 2, rank: 26, credits: 5000, effect: 'Ping Duration +30', salvage: [{ slug: 'alien-alloy', amount: 2 }] }, + ], + }, + { + slug: 'trax-tad-boost', + name: 'Tad Boost', + category: 'stat', + description: 'Expands the ping\'s area of effect when using a TAD', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 750, effect: 'Tad Ping Area +20m', salvage: [{ slug: 'unstable-gunmetal', amount: 19 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 5, name: 'Capstone I', reward: 'Proficient — Rook will now start runs with an Enhanced weapon. Traxus will now barter their wares for certain Deluxe Salvage.' }, + { rank: 2, nodesRequired: 10, name: 'Capstone II', reward: 'Treasure Hunter — Increases faction rep gained from Traxus Treasures' }, + { rank: 3, nodesRequired: 15, name: 'Capstone III', reward: 'Unlocks Enhanced Traxus Sponsorship Kits for purchase. Traxus will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 20, name: 'Capstone IV', reward: 'Bonus Mod — Traxus standard contracts will now award a bonus weapon mod in addition to other rewards' }, + { rank: 5, nodesRequired: 25, name: 'Capstone V', reward: 'Deluxe Weapons — Unlocks Deluxe weapons for purchase in the Armory' }, + { rank: 6, nodesRequired: 28, name: 'Capstone VI', reward: 'Superior Mods — Unlocks Superior weapon mods in the Armory. Traxus will now barter their wares for certain Prestige Salvage.' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // MIDA + // ═══════════════════════════════════════════════════════════════════ + mida: { + name: 'MIDA', + color: '#be72e4', + agent: 'Gantry', + materials: [ + { slug: 'unstable-lead', name: 'Unstable Lead', icon: 'https://items.marathondb.gg/images/items/unstable-lead.webp' }, + { slug: 'surveillance-lens', name: 'Surveillance Lens', icon: 'https://items.marathondb.gg/images/items/surveillance-lens.webp' }, + { slug: 'dynamic-compounds', name: 'Dynamic Compounds', icon: 'https://items.marathondb.gg/images/items/dynamic-compounds.webp' }, + { slug: 'volatile-compounds', name: 'Volatile Compounds', icon: 'https://items.marathondb.gg/images/items/volatile-compounds.webp' }, + { slug: 'thoughtwave-lens', name: 'Thoughtwave Lens', icon: 'https://items.marathondb.gg/images/items/thoughtwave-lens.webp' }, + { slug: 'biolens-seed', name: 'Biolens Seed', icon: 'https://items.marathondb.gg/images/items/biolens-seed.webp' }, + { slug: 'ballistic-turbine', name: 'Ballistic Turbine', icon: 'https://items.marathondb.gg/images/items/ballistic-turbine.webp' }, + { slug: 'hazard-capsule', name: 'Hazard Capsule', icon: 'https://items.marathondb.gg/images/items/hazard-capsule.webp' }, + { slug: 'alien-alloy', name: 'Alien Alloy', icon: 'https://items.marathondb.gg/images/items/alien-alloy.webp' }, + ], + upgrades: [ + // ── Stat ── + { + slug: 'mida-flex-matrix-exe', + name: 'Flex Matrix.exe', + category: 'stat', + description: 'Agility increases your movement speed and jump height.', + maxLevel: 3, + levels: [ + { level: 1, rank: 3, credits: 750, effect: 'Agility +20', salvage: [{ slug: 'unstable-lead', amount: 16 }] }, + { level: 2, rank: 11, credits: 1500, effect: 'Agility +20', salvage: [{ slug: 'surveillance-lens', amount: 28 }, { slug: 'dynamic-compounds', amount: 10 }] }, + { level: 3, rank: 16, credits: 2000, effect: 'Agility +20', salvage: [{ slug: 'thoughtwave-lens', amount: 8 }, { slug: 'dynamic-compounds', amount: 26 }] }, + ], + }, + // ── Armory ── + { + slug: 'mida-survivor', + name: 'Survivor', + category: 'armory', + description: 'Unlocks Survivor Kit V2 Implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 6, credits: 1500, effect: '', salvage: [{ slug: 'surveillance-lens', amount: 13 }, { slug: 'unstable-lead', amount: 9 }] }, + { level: 2, rank: 12, credits: 1500, effect: '', salvage: [{ slug: 'thoughtwave-lens', amount: 6 }, { slug: 'dynamic-compounds', amount: 11 }] }, + { level: 3, rank: 25, credits: 3500, effect: '', salvage: [{ slug: 'biolens-seed', amount: 10 }, { slug: 'ballistic-turbine', amount: 3 }] }, + ], + }, + { + slug: 'mida-graceful', + name: 'Graceful', + category: 'armory', + description: 'Unlocks Graceful Landing Upgrades V2 Implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 4, credits: 1500, effect: '', salvage: [{ slug: 'surveillance-lens', amount: 9 }, { slug: 'unstable-lead', amount: 5 }] }, + { level: 2, rank: 11, credits: 2000, effect: '', salvage: [{ slug: 'thoughtwave-lens', amount: 8 }, { slug: 'dynamic-compounds', amount: 26 }] }, + { level: 3, rank: 23, credits: 3500, effect: '', salvage: [{ slug: 'biolens-seed', amount: 8 }, { slug: 'ballistic-turbine', amount: 3 }] }, + ], + }, + { + slug: 'mida-sprinter', + name: 'Sprinter', + category: 'armory', + description: 'Unlocks Bionic Leg Upgrades V2 Implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 2, credits: 750, effect: '', salvage: [{ slug: 'unstable-lead', amount: 13 }] }, + { level: 2, rank: 9, credits: 1500, effect: '', salvage: [{ slug: 'thoughtwave-lens', amount: 5 }, { slug: 'dynamic-compounds', amount: 8 }] }, + { level: 3, rank: 24, credits: 3500, effect: '', salvage: [{ slug: 'biolens-seed', amount: 10 }, { slug: 'ballistic-turbine', amount: 3 }] }, + ], + }, + { + slug: 'mida-cardio-kick', + name: 'Cardio Kick', + category: 'armory', + description: 'Unlocks "Cardio Kick Packs" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 1500, effect: '', salvage: [{ slug: 'thoughtwave-lens', amount: 4 }, { slug: 'dynamic-compounds', amount: 7 }] }, + ], + }, + // ── Function ── + { + slug: 'mida-full-throttle', + name: 'Full Throttle', + category: 'function', + description: 'Gain the effects of cardio kick for a short duration at the beginning of each run', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: '', salvage: [{ slug: 'alien-alloy', amount: 3 }, { slug: 'ballistic-turbine', amount: 11 }] }, + ], + }, + { + slug: 'mida-cloud-cover', + name: 'Cloud Cover', + category: 'function', + description: 'Automatically deploy smoke cloud when activating an exfil site', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: '', salvage: [{ slug: 'hazard-capsule', amount: 3 }, { slug: 'biolens-seed', amount: 12 }] }, + ], + }, + { + slug: 'mida-anti-virus', + name: 'Anti-Virus', + category: 'function', + description: 'Gain a small portion of active Anti Virus protection at the beginning of each run', + maxLevel: 3, + levels: [ + { level: 1, rank: 11, credits: 1500, effect: 'Active Anti Virus protection. 40 seconds at the start of a match', salvage: [{ slug: 'surveillance-lens', amount: 28 }, { slug: 'dynamic-compounds', amount: 10 }] }, + { level: 2, rank: 18, credits: 2000, effect: 'Active Anti Virus protection. 40 seconds at the start of a match', salvage: [{ slug: 'thoughtwave-lens', amount: 12 }, { slug: 'volatile-compounds', amount: 4 }] }, + { level: 3, rank: 26, credits: 5000, effect: 'Active Anti Virus protection. 40 seconds at the start of a match', salvage: [{ slug: 'hazard-capsule', amount: 12 }] }, + ], + }, + // ── Armory ── + { + slug: 'mida-anti-virus-packs', + name: 'Anti-Virus Packs', + category: 'armory', + description: 'Unlocks "Anti Virus Packs" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: '', salvage: [{ slug: 'unstable-lead', amount: 23 }] }, + ], + }, + { + slug: 'mida-hot-potato', + name: 'Hot Potato', + category: 'armory', + description: 'Unlocks "Heat Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 3, credits: 750, effect: '', salvage: [{ slug: 'unstable-lead', amount: 16 }] }, + ], + }, + { + slug: 'mida-explosives', + name: 'Explosives', + category: 'armory', + description: 'Unlocks "Frag Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 750, effect: '', salvage: [{ slug: 'unstable-lead', amount: 16 }] }, + ], + }, + { + slug: 'mida-bullseye', + name: 'Bullseye', + category: 'armory', + description: 'Unlocks "Flecette Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 8, credits: 1500, effect: '', salvage: [{ slug: 'dynamic-compounds', amount: 15 }, { slug: 'surveillance-lens', amount: 8 }] }, + ], + }, + { + slug: 'mida-eyes-open', + name: 'Eyes Open', + category: 'armory', + description: 'Unlocks Proximity Sendor for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 9, credits: 1500, effect: '', salvage: [{ slug: 'dynamic-compounds', amount: 25 }, { slug: 'surveillance-lens', amount: 14 }] }, + ], + }, + { + slug: 'mida-bad-step', + name: 'Bad Step', + category: 'armory', + description: 'Unlocks Claymores for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 1, credits: 750, effect: '', salvage: [{ slug: 'unstable-lead', amount: 13 }] }, + ], + }, + { + slug: 'mida-got-em', + name: 'Got Em', + category: 'armory', + description: 'Unlocks Trap Packs for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'dynamic-compounds', amount: 19 }, { slug: 'surveillance-lens', amount: 9 }] }, + ], + }, + { + slug: 'mida-chemist', + name: 'Chemist', + category: 'armory', + description: 'Unlocks "Chem Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 11, credits: 1500, effect: '', salvage: [{ slug: 'volatile-compounds', amount: 4 }, { slug: 'surveillance-lens', amount: 10 }] }, + ], + }, + { + slug: 'mida-lights-out', + name: 'Lights Out', + category: 'armory', + description: 'Unlocks "EMP Grenade" for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 12, credits: 2000, effect: '', salvage: [{ slug: 'volatile-compounds', amount: 6 }, { slug: 'surveillance-lens', amount: 16 }] }, + ], + }, + { + slug: 'mida-spare-rounds', + name: 'Spare Rounds', + category: 'armory', + description: 'Unlocks "Ammo Crates" for purchase in the Armory', + maxLevel: 2, + levels: [ + { level: 1, rank: 8, credits: 1500, effect: '', salvage: [{ slug: 'dynamic-compounds', amount: 19 }, { slug: 'surveillance-lens', amount: 9 }] }, + { level: 2, rank: 11, credits: 2000, effect: '', salvage: [{ slug: 'volatile-compounds', amount: 6 }, { slug: 'surveillance-lens', amount: 16 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 5, name: 'Capstone I', reward: 'Castling — Rook will now start runs with a stack of Claymores. MIDA will now barter their wares for certain Deluxe Salvage.' }, + { rank: 2, nodesRequired: 10, name: 'Capstone II', reward: 'Treasure Hunter — Increases rep gain from MIDA Treasures +20%' }, + { rank: 3, nodesRequired: 15, name: 'Capstone III', reward: 'Unlocks Enhanced MIDA Sponsorship Kits for purchase. MIDA will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 20, name: 'Capstone IV', reward: 'Bonus Equipment — MIDA standard contracts will now award at least a grenade or gadget' }, + { rank: 5, nodesRequired: 25, name: 'Capstone V', reward: 'Dome Up — Unlocks Bubble Shields for purchase in the Armory' }, + { rank: 6, nodesRequired: 29, name: 'Capstone VI', reward: 'Steady Hand.exe — Allows you to disarm Claymore mines. MIDA will now barter their wares for certain Prestige Salvage.' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // ARACHNE + // ═══════════════════════════════════════════════════════════════════ + arachne: { + name: 'Arachne', + color: '#e40b0d', + agent: 'Charter', + materials: [ + { slug: 'unstable-gel', name: 'Unstable Gel', icon: 'https://items.marathondb.gg/images/items/unstable-gel.webp' }, + { slug: 'drone-resin', name: 'Drone Resin', icon: 'https://items.marathondb.gg/images/items/drone-resin.webp' }, + { slug: 'drone-node', name: 'Drone Node', icon: 'https://items.marathondb.gg/images/items/drone-node.webp' }, + { slug: 'biomata-resin', name: 'Biomata Resin', icon: 'https://items.marathondb.gg/images/items/biomata-resin.webp' }, + { slug: 'enzyme-replicator', name: 'Enzyme Replicator', icon: 'https://items.marathondb.gg/images/items/enzyme-replicator.webp' }, + { slug: 'biomata-node', name: 'Biomata Node', icon: 'https://items.marathondb.gg/images/items/biomata-node.webp' }, + { slug: 'reflex-coil', name: 'Reflex Coil', icon: 'https://items.marathondb.gg/images/items/reflex-coil.webp' }, + { slug: 'synapse-cube', name: 'Synapse Cube', icon: 'https://items.marathondb.gg/images/items/synapse-cube.webp' }, + { slug: 'hazard-capsule', name: 'Hazard Capsule', icon: 'https://items.marathondb.gg/images/items/hazard-capsule.webp' }, + ], + upgrades: [ + // ── Stat ── + { + slug: 'arach-hard-strike-exe', + name: 'Hard Strike.exe', + category: 'stat', + description: 'Melee Damage increases the damage of your melee and knife attacks.', + maxLevel: 3, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: 'Melee Damage +20', salvage: [{ slug: 'drone-resin', amount: 7 }, { slug: 'unstable-gel', amount: 6 }] }, + { level: 2, rank: 16, credits: 2000, effect: 'Melee Damage +20', salvage: [{ slug: 'biomata-resin', amount: 8 }, { slug: 'drone-node', amount: 22 }] }, + { level: 3, rank: 22, credits: 3500, effect: 'Melee Damage +20', salvage: [{ slug: 'reflex-coil', amount: 6 }, { slug: 'biomata-node', amount: 6 }] }, + ], + }, + { + slug: 'arach-cutthroat', + name: 'Cutthroat', + category: 'stat', + description: 'Finisher Siphon increases the amount your shields recharge after you perform a finisher on a runner.', + maxLevel: 3, + levels: [ + { level: 1, rank: 6, credits: 750, effect: 'Finisher Siphon +20', salvage: [{ slug: 'unstable-gel', amount: 16 }] }, + { level: 2, rank: 18, credits: 2000, effect: 'Finisher Siphon +20', salvage: [{ slug: 'biomata-resin', amount: 12 }, { slug: 'biomata-node', amount: 4 }] }, + { level: 3, rank: 25, credits: 3500, effect: 'Finisher Siphon +20', salvage: [{ slug: 'reflex-coil', amount: 7 }, { slug: 'enzyme-replicator', amount: 3 }] }, + ], + }, + // ── Armory ── + { + slug: 'arach-knife-fight', + name: 'Knife Fight', + category: 'armory', + description: 'Unlocks Knife Fight V2 implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 4, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 13 }] }, + { level: 2, rank: 11, credits: 1500, effect: '', salvage: [{ slug: 'drone-node', amount: 23 }, { slug: 'drone-resin', amount: 12 }] }, + { level: 3, rank: 24, credits: 3500, effect: '', salvage: [{ slug: 'enzyme-replicator', amount: 9 }, { slug: 'reflex-coil', amount: 3 }] }, + ], + }, + { + slug: 'arach-hurting-hands', + name: 'Hurting Hands', + category: 'armory', + description: 'Unlocks Hurting Hands V2 implant for purchase in the Armory', + maxLevel: 3, + levels: [ + { level: 1, rank: 1, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 10 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'drone-node', amount: 19 }, { slug: 'drone-resin', amount: 9 }] }, + { level: 3, rank: 23, credits: 3500, effect: '', salvage: [{ slug: 'enzyme-replicator', amount: 7 }, { slug: 'reflex-coil', amount: 3 }] }, + ], + }, + // ── Stat ── + { + slug: 'arach-reboot', + name: 'Reboot', + category: 'stat', + description: 'Revive speed increases how quickly you can self revive or revive downed crew members', + maxLevel: 3, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: 'Revive speed +20', salvage: [{ slug: 'drone-resin', amount: 19 }, { slug: 'unstable-gel', amount: 17 }] }, + { level: 2, rank: 19, credits: 3500, effect: 'Revive speed +20', salvage: [{ slug: 'reflex-coil', amount: 5 }, { slug: 'biomata-node', amount: 5 }] }, + { level: 3, rank: 27, credits: 5000, effect: 'Revive speed +20', salvage: [{ slug: 'synapse-cube', amount: 2 }, { slug: 'biomata-resin', amount: 9 }] }, + ], + }, + // ── Function ── + { + slug: 'arach-leech', + name: 'Leech', + category: 'function', + description: 'Knife attacks restore a small amount of health', + maxLevel: 1, + levels: [ + { level: 1, rank: 28, credits: 5000, effect: '', salvage: [{ slug: 'synapse-cube', amount: 2 }] }, + ], + }, + { + slug: 'arach-heat-death', + name: 'Heat Death', + category: 'function', + description: 'Eliminating a hostile reduces your heat buildup.', + maxLevel: 1, + levels: [ + { level: 1, rank: 'VIP', credits: 5000, effect: '', salvage: [{ slug: 'hazard-capsule', amount: 2 }, { slug: 'enzyme-replicator', amount: 11 }] }, + ], + }, + // ── Armory ── + { + slug: 'arach-lmg-mods', + name: 'LMG Mods', + category: 'armory', + description: 'Unlocks a set of Enhanced LMG mods from the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 1, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 13 }] }, + ], + }, + { + slug: 'arach-shotgun-mods', + name: 'Shotgun Mods', + category: 'armory', + description: 'Unlocks a set of enhanced shotgun mods from the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 3, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 19 }] }, + ], + }, + { + slug: 'arach-railgun-mods', + name: 'Railgun Mods', + category: 'armory', + description: 'Unlocks a set of Enhanced railgun mods from the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 4, credits: 1500, effect: '', salvage: [{ slug: 'drone-resin', amount: 8 }, { slug: 'unstable-gel', amount: 9 }] }, + ], + }, + { + slug: 'arach-mips-railgun', + name: 'MIPS Railgun', + category: 'armory', + description: 'Unlocks the ARES RG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 2, credits: 750, effect: '', salvage: [{ slug: 'unstable-gel', amount: 13 }] }, + ], + }, + { + slug: 'arach-mips-shotgun', + name: 'MIPS Shotgun', + category: 'armory', + description: 'Unlocks the WSTR Combat Shotgun for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 7, credits: 1500, effect: '', salvage: [{ slug: 'drone-node', amount: 7 }, { slug: 'unstable-gel', amount: 8 }] }, + ], + }, + { + slug: 'arach-enhanced-retaliator-lmg', + name: 'Enhanced Retaliator LMG', + category: 'armory', + description: 'Unlocks the Enhanced, Retaliator LMG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'biomata-node', amount: 4 }, { slug: 'drone-resin', amount: 11 }] }, + ], + }, + { + slug: 'arach-enhanced-mips-shotgun', + name: 'Enhanced MIPS Shotgun', + category: 'armory', + description: 'Unlocks the Enhanced WSTR Combat Shotgun for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 13, credits: 2000, effect: '', salvage: [{ slug: 'biomata-node', amount: 6 }, { slug: 'drone-resin', amount: 18 }] }, + ], + }, + { + slug: 'arach-enhanced-mips-railgun', + name: 'Enhanced MIPS Railgun', + category: 'armory', + description: 'Unlocks the Enhanced, ARES RG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 15, credits: 2000, effect: '', salvage: [{ slug: 'biomata-resin', amount: 12 }, { slug: 'biomata-resin', amount: 4 }] }, + ], + }, + { + slug: 'arach-deluxe-retaliator-lmg', + name: 'Deluxe Retaliator LMG', + category: 'armory', + description: 'Unlocks the Deluxe, Retaliator LMG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 20, credits: 3500, effect: '', salvage: [{ slug: 'enzyme-replicator', amount: 9 }, { slug: 'reflex-coil', amount: 3 }] }, + ], + }, + { + slug: 'arach-deluxe-mips-shotgun', + name: 'Deluxe MIPS Shotgun', + category: 'armory', + description: 'Unlocks the Enhanced WSTR Combat Shotgun for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 23, credits: 5000, effect: '', salvage: [{ slug: 'synapse-cube', amount: 2 }, { slug: 'biomata-resin', amount: 9 }] }, + ], + }, + { + slug: 'arach-deluxe-mips-railgun', + name: 'Deluxe MIPS Railgun', + category: 'armory', + description: 'Unlocks the Deluxe ARES RG for purchase in the Armory', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 5000, effect: '', salvage: [{ slug: 'hazard-capsule', amount: 2 }, { slug: 'enzyme-replicator', amount: 7 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 5, name: 'Capstone I', reward: 'Boosted — Rook will now start runs with implants. Arachne will now barter their wares for certain Deluxe Salvage.' }, + { rank: 2, nodesRequired: 10, name: 'Capstone II', reward: 'Treasure Hunter — Increases faction rep gain from Arachne Treasures. Arachne Treasure Reputation +20%' }, + { rank: 3, nodesRequired: 15, name: 'Capstone III', reward: 'Unlocks Enhanced Arachne Sponsorship Kits for purchase. Arachne will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 20, name: 'Capstone IV', reward: 'Boomstick — Rook will now start runs with a WSTR Combat Shotgun and MIPS Rounds' }, + { rank: 5, nodesRequired: 25, name: 'Capstone V', reward: 'Factory Reset.exe — Reviving a crew member grants healing over time. The healing is interrupted upon taking damage.' }, + { rank: 6, nodesRequired: 28, name: 'Capstone VI', reward: 'Superior Armament — Unlocks Superior Retaliator LMG, WSTR Combat Shotgun, and ARES RG for purchase in the Armory. Arachne will now barter their wares for certain Prestige Salvage.' }, + ], + }, + + // ═══════════════════════════════════════════════════════════════════ + // SEKIGUCHI + // ═══════════════════════════════════════════════════════════════════ + sekiguchi: { + name: 'Sekiguchi', + color: '#73f2c9', + agent: 'Nona', + materials: [ + { slug: 'unstable-diode', name: 'Unstable Diode', icon: 'https://items.marathondb.gg/images/items/unstable-diode.webp' }, + { slug: 'fractal-circuit', name: 'Fractal Circuit', icon: 'https://items.marathondb.gg/images/items/fractal-circuit.webp' }, + { slug: 'storage-drive', name: 'Storage Drive', icon: 'https://items.marathondb.gg/images/items/storage-drive.webp' }, + { slug: 'amygdala-drive', name: 'Amygdala Drive', icon: 'https://items.marathondb.gg/images/items/amygdala-drive.webp' }, + { slug: 'neural-insulation', name: 'Neural Insulation', icon: 'https://items.marathondb.gg/images/items/neural-insulation.webp' }, + { slug: 'paradox-circuit', name: 'Paradox Circuit', icon: 'https://items.marathondb.gg/images/items/paradox-circuit.webp' }, + { slug: 'predictive-framework', name: 'Predictive Framework', icon: 'https://items.marathondb.gg/images/items/predictive-framework.webp' }, + { slug: 'synapse-cube', name: 'Synapse Cube', icon: 'https://items.marathondb.gg/images/items/synapse-cube.webp' }, + { slug: 'alien-alloy', name: 'Alien Alloy', icon: 'https://items.marathondb.gg/images/items/alien-alloy.webp' }, + ], + upgrades: [ + // ── Stat ── + { + slug: 'sek-tac-amp-exe', + name: 'Tac Amp.exe', + category: 'stat', + description: 'Tactical Recovery reduces the cooldown of your tactical and trait abilities.', + maxLevel: 2, + levels: [ + { level: 1, rank: 2, credits: 750, effect: 'Tactical Recovery +30', salvage: [{ slug: 'unstable-diode', amount: 16 }] }, + { level: 2, rank: 14, credits: 2000, effect: 'Tactical Recovery +30', salvage: [{ slug: 'paradox-circuit', amount: 8 }, { slug: 'storage-drive', amount: 30 }] }, + ], + }, + { + slug: 'sek-prime-amp', + name: 'Prime Amp', + category: 'stat', + description: 'Prime Recovery reduces the cooldown of your prime ability.', + maxLevel: 1, + levels: [ + { level: 1, rank: 24, credits: 5000, effect: 'Prime Recovery +30', salvage: [{ slug: 'synapse-cube', amount: 2 }] }, + ], + }, + // ── Function ── + { + slug: 'sek-lethal-amp-exe', + name: 'Lethal Amp.EXE', + category: 'function', + description: 'Downing a Runner grants you tactical ability energy. Eliminating a Runner grants you prime ability energy.', + maxLevel: 1, + levels: [ + { level: 1, rank: 25, credits: 3500, effect: '', salvage: [{ slug: 'predictive-framework', amount: 7 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + // ── Armory ── + { + slug: 'sek-energy-amp', + name: 'Energy Amp', + category: 'armory', + description: 'Unlocks Energy Amps for purchase in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 1, credits: 750, effect: '', salvage: [{ slug: 'unstable-diode', amount: 10 }] }, + ], + }, + { + slug: 'sek-amped', + name: 'Amped', + category: 'armory', + description: 'Unlocks daily free Energy Amps in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 10, credits: 1500, effect: 'Free Daily Energy Amps in the Armory', salvage: [{ slug: 'fractal-circuit', amount: 23 }, { slug: 'storage-drive', amount: 9 }] }, + ], + }, + { + slug: 'sek-amp-stock', + name: 'Amp Stock', + category: 'armory', + description: 'Increases available stock of Energy Amps in the Armory.', + maxLevel: 1, + levels: [ + { level: 1, rank: 21, credits: 2000, effect: '', salvage: [{ slug: 'paradox-circuit', amount: 12 }, { slug: 'amygdala-drive', amount: 5 }] }, + ], + }, + // ── Stat ── + { + slug: 'sek-scab-factory', + name: 'Scab Factory', + category: 'stat', + description: 'Increases the time it takes to bleed out when downed.', + maxLevel: 2, + levels: [ + { level: 1, rank: 13, credits: 2000, effect: 'DBNO Time +30 seconds', salvage: [{ slug: 'amygdala-drive', amount: 7 }, { slug: 'fractal-circuit', amount: 20 }] }, + { level: 2, rank: 23, credits: 3500, effect: 'DBNO Time +30 seconds', salvage: [{ slug: 'predictive-framework', amount: 9 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + // ── Function ── + { + slug: 'sek-head-start', + name: 'Head Start', + category: 'function', + description: 'Partially fills your tactical ability charge at the start of a run.', + maxLevel: 2, + levels: [ + { level: 1, rank: 4, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 10 }, { slug: 'unstable-diode', amount: 10 }] }, + { level: 2, rank: 14, credits: 2000, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 7 }, { slug: 'fractal-circuit', amount: 20 }] }, + ], + }, + { + slug: 'sek-primed-exe', + name: 'Primed.EXE', + category: 'function', + description: 'Partially fills your prime ability charge at the start of a run.', + maxLevel: 2, + levels: [ + { level: 1, rank: 26, credits: 5000, effect: '', salvage: [{ slug: 'alien-alloy', amount: 2 }, { slug: 'neural-insulation', amount: 7 }] }, + { level: 2, rank: 'VIP', credits: 5000, effect: '', salvage: [{ slug: 'alien-alloy', amount: 3 }, { slug: 'neural-insulation', amount: 11 }] }, + ], + }, + // ── Armory ── + { + slug: 'sek-capacitors', + name: 'Capacitors', + category: 'armory', + description: 'Unlocks Augmented Capacitors V2 implant for purchase in the Armory.', + maxLevel: 3, + levels: [ + { level: 1, rank: 5, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 10 }, { slug: 'unstable-diode', amount: 10 }] }, + { level: 2, rank: 11, credits: 1500, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 8 }, { slug: 'fractal-circuit', amount: 13 }] }, + { level: 3, rank: 24, credits: 1500, effect: '', salvage: [{ slug: 'predictive-framework', amount: 9 }, { slug: 'neural-insulation', amount: 3 }] }, + ], + }, + { + slug: 'sek-harvester', + name: 'Harvester', + category: 'armory', + description: 'Unlocks Energy Harvesting V2 implant for purchase in the Armory.', + maxLevel: 3, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 7 }, { slug: 'unstable-diode', amount: 6 }] }, + { level: 2, rank: 9, credits: 2000, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 23 }, { slug: 'storage-drive', amount: 9 }] }, + { level: 3, rank: 23, credits: 3500, effect: '', salvage: [{ slug: 'neural-insulation', amount: 7 }, { slug: 'predictive-framework', amount: 3 }] }, + ], + }, + { + slug: 'sek-triage', + name: 'Triage', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Triage in the Armory', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 4 }, { slug: 'fractal-circuit', amount: 8 }] }, + ], + }, + { + slug: 'sek-destroyer', + name: 'Destroyer', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Destroyer in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'paradox-circuit', amount: 4 }, { slug: 'storage-drive', amount: 8 }] }, + ], + }, + { + slug: 'sek-assassin', + name: 'Assassin', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Assassin in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 4 }, { slug: 'fractal-circuit', amount: 8 }] }, + ], + }, + { + slug: 'sek-vandal', + name: 'Vandal', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Vandal in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'paradox-circuit', amount: 4 }, { slug: 'storage-drive', amount: 8 }] }, + ], + }, + { + slug: 'sek-recon', + name: 'Recon', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Recon in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'storage-drive', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'amygdala-drive', amount: 4 }, { slug: 'fractal-circuit', amount: 8 }] }, + ], + }, + { + slug: 'sek-thief', + name: 'Thief', + category: 'armory', + description: 'Unlocks 2 Enhanced cores for Thief in the Armory.', + maxLevel: 2, + levels: [ + { level: 1, rank: 3, credits: 1500, effect: '', salvage: [{ slug: 'fractal-circuit', amount: 8 }, { slug: 'unstable-diode', amount: 9 }] }, + { level: 2, rank: 10, credits: 1500, effect: '', salvage: [{ slug: 'paradox-circuit', amount: 4 }, { slug: 'storage-drive', amount: 8 }] }, + ], + }, + ], + capstones: [ + { rank: 1, nodesRequired: 5, name: 'Capstone I', reward: 'Specialized — Rook will now start runs with Runner Cores. Sekiguchi will now barter their wares for certain Deluxe Salvage.' }, + { rank: 2, nodesRequired: 10, name: 'Capstone II', reward: 'Treasure Hunter — Increases faction rep gained from Sekiguchi Treasures by 20%' }, + { rank: 3, nodesRequired: 15, name: 'Capstone III', reward: 'Unlocks Enhanced Sekiguchi Sponsorship Kits for purchase. Sekiguchi will now also barter wares for certain Superior Salvage.' }, + { rank: 4, nodesRequired: 20, name: 'Capstone IV', reward: 'Commission — Sekiguchi standard contracts will now award cores in addition to other rewards' }, + { rank: 5, nodesRequired: 25, name: 'Capstone V', reward: 'Quiet Exit.exe — Rook gains the effects of Signal Mask after activating an exfil' }, + { rank: 6, nodesRequired: 32, name: 'Capstone VI', reward: 'Core All — Unlocks Superior Cores for each runner in the Armory. Sekiguchi will now barter their wares for certain Prestige Salvage.' }, + ], + }, +}; + +// Make available globally +if (typeof window !== 'undefined') window.FACTION_UPGRADES = FACTION_UPGRADES; diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..70d89bd --- /dev/null +++ b/backend/server.js @@ -0,0 +1,592 @@ +import { createServer } from 'node:http'; +import { existsSync, mkdirSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; +import vm from 'node:vm'; + +const PORT = Number(process.env.PORT || 8787); +const REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000; +const DB_PATH = resolve('backend', 'data', 'catalog.db'); +const FACTION_ASSETS_DIR = resolve('backend', 'data', 'faction-assets'); +const ITEMS_URL = 'https://items.marathondb.gg/api/items'; +const UPGRADES_URL = 'https://marathondb.gg/js/data/faction-upgrades.js'; + +mkdirSync(dirname(DB_PATH), { recursive: true }); + +const db = new DatabaseSync(DB_PATH); +db.exec(` + CREATE TABLE IF NOT EXISTS catalog_cache ( + id INTEGER PRIMARY KEY CHECK (id = 1), + payload TEXT NOT NULL, + updated_at_ms INTEGER NOT NULL + ) +`); + +db.exec(` + CREATE TABLE IF NOT EXISTS popularity_stats ( + entry_type TEXT NOT NULL CHECK (entry_type IN ('item', 'upgrade')), + slug TEXT NOT NULL, + add_count INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (entry_type, slug) + ) +`); + +const upsertCacheStatement = db.prepare(` + INSERT INTO catalog_cache (id, payload, updated_at_ms) + VALUES (1, ?, ?) + ON CONFLICT(id) DO UPDATE SET payload = excluded.payload, updated_at_ms = excluded.updated_at_ms +`); + +const selectCacheStatement = db.prepare('SELECT payload, updated_at_ms FROM catalog_cache WHERE id = 1'); +const incrementPopularityStatement = db.prepare(` + INSERT INTO popularity_stats (entry_type, slug, add_count) + VALUES (?, ?, 1) + ON CONFLICT(entry_type, slug) DO UPDATE SET add_count = popularity_stats.add_count + 1 +`); +const selectPopularStatement = db.prepare(` + SELECT entry_type, slug, add_count + FROM popularity_stats + ORDER BY add_count DESC, slug ASC + LIMIT ? +`); + +let refreshPromise = null; + +function asArray(value) { + return Array.isArray(value) ? value : []; +} + +function normalizeName(value) { + return typeof value === 'string' ? value.trim() : ''; +} + +function normalizeColor(value) { + if (typeof value !== 'string') { + return '#3a4f77'; + } + + const trimmed = value.trim(); + return trimmed || '#3a4f77'; +} + +function getName(raw) { + const candidates = [raw.name, raw.display_name, raw.item_name, raw.title]; + for (const candidate of candidates) { + const name = normalizeName(candidate); + if (name) { + return name; + } + } + + return ''; +} + +function getSlug(raw) { + const candidates = [raw.slug, raw.id, raw.item_id, raw.uuid, raw.name]; + for (const candidate of candidates) { + if (typeof candidate === 'string' && candidate.trim()) { + return candidate.trim().toLowerCase().replace(/\s+/g, '-'); + } + } + + return `item-${Math.random().toString(36).slice(2, 10)}`; +} + +function getIconPath(raw) { + const candidates = [raw.icon, raw.icon_path, raw.image, raw.image_url]; + for (const candidate of candidates) { + if (typeof candidate === 'string' && candidate.trim()) { + return candidate.trim(); + } + } + + return ''; +} + +function getRarity(raw) { + const candidates = [raw.rarity, raw.rarity_name, raw.item_rarity, raw.quality, raw.tier]; + for (const candidate of candidates) { + if (typeof candidate === 'string' && candidate.trim()) { + return candidate.trim().toLowerCase(); + } + } + + return ''; +} + +function getItemIconUrl(iconPath, slug) { + const itemImageBase = 'https://items.marathondb.gg/images/items'; + const apiBase = 'https://helpbot.marathondb.gg'; + + if (!iconPath) { + return `${itemImageBase}/${encodeURIComponent(slug)}`; + } + + if (iconPath.startsWith('http://') || iconPath.startsWith('https://')) { + return iconPath; + } + + if (iconPath.startsWith('assets/')) { + return `${apiBase}/${iconPath}`; + } + + return `${itemImageBase}/${encodeURIComponent(slug)}`; +} + +function extractRawItems(payload) { + if (Array.isArray(payload)) { + return payload.filter((row) => typeof row === 'object' && row !== null); + } + + if (typeof payload !== 'object' || payload === null) { + return []; + } + + const dataArray = asArray(payload.data); + if (dataArray.length > 0) { + return dataArray.filter((row) => typeof row === 'object' && row !== null); + } + + const dataAsObject = payload.data; + if (dataAsObject && typeof dataAsObject === 'object' && Array.isArray(dataAsObject.items)) { + return dataAsObject.items.filter((row) => typeof row === 'object' && row !== null); + } + + return []; +} + +function parseFactionUpgradesFromScript(source) { + const context = { window: {} }; + vm.createContext(context); + + const script = new vm.Script(`\n${source}\n;globalThis.__factionUpgrades = typeof FACTION_UPGRADES !== 'undefined' ? FACTION_UPGRADES : window.FACTION_UPGRADES;\n`); + script.runInContext(context, { timeout: 5000 }); + + const parsed = context.__factionUpgrades; + if (!parsed || typeof parsed !== 'object') { + throw new Error('Could not parse FACTION_UPGRADES from faction-upgrades.js'); + } + + return parsed; +} + +function toNumber(value) { + if (typeof value === 'number') { + return value; + } + + if (typeof value === 'string') { + return Number(value); + } + + return Number.NaN; +} + +function getFactionAssetUrl(factionKey) { + const normalizedKey = normalizeName(factionKey).toLowerCase().replace(/[^a-z0-9_-]/g, ''); + return `/api/faction-assets/${normalizedKey}.png`; +} + +function buildUpgradeResults(factionUpgrades, itemsBySlug) { + const results = []; + + for (const [factionKey, factionValue] of Object.entries(factionUpgrades)) { + if (!factionValue || typeof factionValue !== 'object') { + continue; + } + + const faction = factionValue; + const factionName = normalizeName(faction.name) || factionKey; + const factionColor = normalizeColor(faction.color); + const upgrades = asArray(faction.upgrades); + + for (const upgradeEntry of upgrades) { + if (!upgradeEntry || typeof upgradeEntry !== 'object') { + continue; + } + + const upgrade = upgradeEntry; + const upgradeName = normalizeName(upgrade.name); + if (!upgradeName) { + continue; + } + + const mappedLevels = []; + const levels = asArray(upgrade.levels); + + for (const [levelIndex, levelEntry] of levels.entries()) { + if (!levelEntry || typeof levelEntry !== 'object') { + continue; + } + + const levelNumber = Math.max(1, Math.floor(toNumber(levelEntry.level)) || levelIndex + 1); + const salvageBySlug = new Map(); + const salvage = asArray(levelEntry.salvage); + for (const salvageEntry of salvage) { + if (!salvageEntry || typeof salvageEntry !== 'object') { + continue; + } + + const salvageSlug = normalizeName(salvageEntry.slug).toLowerCase(); + const amount = Math.floor(toNumber(salvageEntry.amount)); + if (!salvageSlug || !Number.isFinite(amount) || amount <= 0) { + continue; + } + + const current = salvageBySlug.get(salvageSlug) || 0; + salvageBySlug.set(salvageSlug, current + amount); + } + + const mappedSalvage = Array.from(salvageBySlug.entries()) + .map(([slug, amount]) => ({ slug, amount, item: itemsBySlug.get(slug) })) + .filter((entry) => Boolean(entry.item)) + .map((entry) => ({ + slug: entry.slug, + amount: entry.amount, + name: entry.item.name, + iconUrl: entry.item.iconUrl, + rarity: entry.item.rarity, + })); + + mappedLevels.push({ + level: levelNumber, + salvage: mappedSalvage, + }); + } + + const levelsWithSalvage = mappedLevels.filter((entry) => entry.salvage.length > 0); + if (levelsWithSalvage.length === 0) { + continue; + } + + mappedLevels.sort((a, b) => a.level - b.level); + + const rawUpgradeSlug = normalizeName(upgrade.slug).toLowerCase().replace(/\s+/g, '-'); + const fallbackSlug = `${factionKey}-${upgradeName.toLowerCase().replace(/\s+/g, '-')}`; + const upgradeSlug = rawUpgradeSlug || fallbackSlug; + + results.push({ + id: `upgrade-${upgradeSlug}`, + slug: upgradeSlug, + name: `${upgradeName} (${factionName})`, + factionName, + factionColor, + iconUrl: getFactionAssetUrl(factionKey), + levels: mappedLevels, + isUpgrade: true, + }); + } + } + + return results; +} + +function buildCatalog(itemsPayload, factionUpgradesPayload, updatedAtMs) { + const rows = extractRawItems(itemsPayload); + + const items = rows + .map((row) => { + const name = getName(row); + if (!name) { + return null; + } + + const slug = getSlug(row); + const iconPath = getIconPath(row); + const rarity = getRarity(row); + return { + id: slug, + slug, + name, + iconUrl: getItemIconUrl(iconPath, slug), + rarity, + }; + }) + .filter(Boolean); + + const itemsBySlug = new Map(items.map((item) => [item.slug, item])); + const upgrades = buildUpgradeResults(factionUpgradesPayload, itemsBySlug); + + return { + updatedAt: new Date(updatedAtMs).toISOString(), + items, + upgrades, + }; +} + +function readCachedCatalog() { + const row = selectCacheStatement.get(); + if (!row) { + return null; + } + + try { + return { + payload: JSON.parse(row.payload), + updatedAtMs: row.updated_at_ms, + }; + } catch { + return null; + } +} + +function writeCatalog(catalog, updatedAtMs) { + upsertCacheStatement.run(JSON.stringify(catalog), updatedAtMs); +} + +async function fetchJson(url) { + const response = await fetch(url, { + headers: { + 'User-Agent': 'marathon-todo-proxy/1.0', + Accept: 'application/json,text/javascript,*/*;q=0.8', + }, + }); + + if (!response.ok) { + throw new Error(`Request failed for ${url}: ${response.status}`); + } + + return response.json(); +} + +async function fetchText(url) { + const response = await fetch(url, { + headers: { + 'User-Agent': 'marathon-todo-proxy/1.0', + Accept: 'text/javascript,*/*;q=0.8', + }, + }); + + if (!response.ok) { + throw new Error(`Request failed for ${url}: ${response.status}`); + } + + return response.text(); +} + +async function refreshCatalog() { + if (refreshPromise) { + return refreshPromise; + } + + refreshPromise = (async () => { + const [itemsPayload, upgradesScript] = await Promise.all([fetchJson(ITEMS_URL), fetchText(UPGRADES_URL)]); + const factionUpgradesPayload = parseFactionUpgradesFromScript(upgradesScript); + const updatedAtMs = Date.now(); + const catalog = buildCatalog(itemsPayload, factionUpgradesPayload, updatedAtMs); + + writeCatalog(catalog, updatedAtMs); + return catalog; + })().finally(() => { + refreshPromise = null; + }); + + return refreshPromise; +} + +function isStale(updatedAtMs) { + return Date.now() - updatedAtMs >= REFRESH_INTERVAL_MS; +} + +function sendJson(response, statusCode, payload) { + const body = JSON.stringify(payload); + response.writeHead(statusCode, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-store', + 'Content-Length': Buffer.byteLength(body), + }); + response.end(body); +} + +function sendBinary(response, statusCode, contentType, body) { + response.writeHead(statusCode, { + 'Content-Type': contentType, + 'Cache-Control': 'no-store', + 'Content-Length': body.byteLength, + }); + response.end(body); +} + +function readRequestBody(request) { + return new Promise((resolveBody, rejectBody) => { + const chunks = []; + + request.on('data', (chunk) => { + chunks.push(chunk); + }); + + request.on('end', () => { + resolveBody(Buffer.concat(chunks).toString('utf-8')); + }); + + request.on('error', (error) => { + rejectBody(error); + }); + }); +} + +async function readJsonBody(request) { + const raw = await readRequestBody(request); + if (!raw.trim()) { + return {}; + } + + try { + return JSON.parse(raw); + } catch { + throw new Error('Invalid JSON body'); + } +} + +const server = createServer(async (request, response) => { + const requestUrl = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`); + + if (request.method === 'GET' && requestUrl.pathname === '/health') { + sendJson(response, 200, { ok: true }); + return; + } + + if (request.method === 'POST' && requestUrl.pathname === '/api/catalog/refresh') { + try { + const catalog = await refreshCatalog(); + sendJson(response, 200, catalog); + } catch (error) { + sendJson(response, 500, { error: error instanceof Error ? error.message : 'Unknown refresh error' }); + } + return; + } + + if (request.method === 'GET' && requestUrl.pathname.startsWith('/api/faction-assets/')) { + const filename = requestUrl.pathname.replace('/api/faction-assets/', ''); + if (!/^[a-z0-9_-]+\.png$/i.test(filename)) { + sendJson(response, 400, { error: 'Invalid faction asset path' }); + return; + } + + const assetPath = resolve(FACTION_ASSETS_DIR, filename); + if (!assetPath.startsWith(FACTION_ASSETS_DIR) || !existsSync(assetPath)) { + sendJson(response, 404, { error: 'Faction asset not found' }); + return; + } + + try { + const asset = readFileSync(assetPath); + sendBinary(response, 200, 'image/png', asset); + } catch (error) { + sendJson(response, 500, { error: error instanceof Error ? error.message : 'Failed to read faction asset' }); + } + return; + } + + if (request.method === 'GET' && requestUrl.pathname === '/api/catalog') { + const cached = readCachedCatalog(); + if (cached) { + if (isStale(cached.updatedAtMs)) { + void refreshCatalog().catch((error) => { + console.error('[proxy] background refresh failed:', error); + }); + } + sendJson(response, 200, cached.payload); + return; + } + + try { + const catalog = await refreshCatalog(); + sendJson(response, 200, catalog); + } catch (error) { + sendJson(response, 500, { error: error instanceof Error ? error.message : 'Unknown fetch error' }); + } + return; + } + + if (request.method === 'POST' && requestUrl.pathname === '/api/popularity/track') { + try { + const body = await readJsonBody(request); + const entryType = body?.type === 'upgrade' ? 'upgrade' : body?.type === 'item' ? 'item' : ''; + const slug = normalizeName(body?.slug).toLowerCase(); + + if (!entryType || !slug) { + sendJson(response, 400, { error: 'Payload must include type ("item" or "upgrade") and slug.' }); + return; + } + + incrementPopularityStatement.run(entryType, slug); + sendJson(response, 200, { ok: true }); + } catch (error) { + sendJson(response, 400, { error: error instanceof Error ? error.message : 'Invalid request' }); + } + return; + } + + if (request.method === 'GET' && requestUrl.pathname === '/api/popularity') { + const limitParam = Number(requestUrl.searchParams.get('limit')); + const limit = Number.isFinite(limitParam) ? Math.min(Math.max(Math.floor(limitParam), 1), 20) : 5; + + const cached = readCachedCatalog(); + let catalog = cached?.payload; + + if (!catalog) { + try { + catalog = await refreshCatalog(); + } catch (error) { + sendJson(response, 500, { + error: error instanceof Error ? error.message : 'Failed to load catalog for popularity', + }); + return; + } + } + + const rows = selectPopularStatement.all(limit * 4); + const itemBySlug = new Map((Array.isArray(catalog.items) ? catalog.items : []).map((item) => [item.slug, item])); + const upgradeBySlug = new Map( + (Array.isArray(catalog.upgrades) ? catalog.upgrades : []).map((upgrade) => [upgrade.slug, upgrade]), + ); + + const picks = []; + + for (const row of rows) { + if (picks.length >= limit) { + break; + } + + const slug = normalizeName(row.slug).toLowerCase(); + if (!slug) { + continue; + } + + if (row.entry_type === 'item') { + const item = itemBySlug.get(slug); + if (item) { + picks.push(item); + } + continue; + } + + if (row.entry_type === 'upgrade') { + const upgrade = upgradeBySlug.get(slug); + if (upgrade) { + picks.push(upgrade); + } + } + } + + sendJson(response, 200, { picks }); + return; + } + + sendJson(response, 404, { error: 'Not found' }); +}); + +server.listen(PORT, () => { + console.log(`[proxy] listening on http://localhost:${PORT}`); +}); + +setInterval(() => { + void refreshCatalog().catch((error) => { + console.error('[proxy] scheduled refresh failed:', error); + }); +}, REFRESH_INTERVAL_MS); + +void refreshCatalog().catch((error) => { + console.error('[proxy] initial refresh failed:', error); +}); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..4691b7e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + marathon.todo + + +

+ + + diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6dbe8abfbfe99c1135c1289e838a1162be2ef0d0 GIT binary patch literal 3262 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 bLZ6|TgdIBfGf^t+@UhxPx_{nf%K!X73cyuS literal 0 HcmV?d00001 diff --git a/frontend/public/fonts/Ki-Regular.otf b/frontend/public/fonts/Ki-Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..ad6e98c09cedda0c60cdbf696d9d9a5ec549a562 GIT binary patch literal 40372 zcmce;2V4|a*El>g%NEy&hH1_h%v=XVoZ#A5@W1s zc4G@7Vp*&~U`52m-l8UItT{t2#_!xayOfmYegD7z_kAYJ%+B0<&TXfkdxwu2HJod} zWpf&CME~$`Hl5Eb38{PCynaooymj?la<3~wI#CGvcv|s-C zr#|zX6F1KOEFj)E`?ChiEU-U2aV@zr`?D7NCD@;JTw}ho{n@~|@Pq8n^|%iF4EwW@ z^XI>|KiB6v@qen%*dBKwkn4o_I&*GZ2$#xD{FrM81hjTOohd&*Mehv4bF0)iDd-ax>D? zXV3BP(IW*1J{`xsd)zF1I}~R$otuFLQ`l1?7LUbwcEc-OJak|a@o6R&jZL2spAws% z7#ExD7CR?BJ}q$$zGnX@t_f?vdc#DBtH z=3fdegr34!;e>Em)4^%7)5lIW)7z#srl)R=-P*WyaPx5UcMEmf>b9qKlh$oouW0>6 z8<#c_ZKkw|C&EkQmJFgBx1R6K53)o2Lukq%9v3bkM9yis(*;vDLVV)p;?|Tw>}`kW ziV&B#{>%<>8sQNl|ML0En=g;NJpA&|%N;Mby^MP~;AM}OjsJZ5=evJq{n_J1%!>|H z*KYdXY;&Xd`VZH)U*B?l!}V3yKfdmMz3sK5*K)26zBcfx_3G-YZ(m)kyJP1eteO8W zU37wxLc+nN{MQ{VTLC83U;Bp+s4M*`3iDVju zOeGp=do(wO8;ksPJU4-x$W7uVBkzyJPQ1;%!@bA7&wYY&<120hx1QU`eZzgtean5% zZRR#{Tez*f|7Idb=eX~<{irugrF6vy@wf0zGPjVo;{8o-3HKTQ zD(}J<@W=Sm{7?LG{sfnUUk>BCFXPYirTjVmE`N_N=H~Jz`9l6I_ZBB}i@6WDrQ9;^ zBW^kOA@?b_5^JpBzT{SOtGFlpP(F&A%@5~C@R8g+eiT2FABNv4#o@n~t8ujd;U9`6 zCywX&`n7l*${n8TUD=bX?8R617ApNUN>eBD>MNQVS2ji4YKpF8G4$qr7d>>-tzu?y5WNgEM z-_KdNGu%1uB6pp;!#&|%@EX1zFY>MUc6=A!j}PI)_<`8HXnq18$0zc$_)PwF{#|}4 zFY}-A>-q2b9sFMY0DqJ}#TVls+K_SF#36jd1OGzM2s)vj&`4+^v=Z719zvkdUl=Hi z5Jn3Vg*YKmm?g{?-WEO-Rtl?yjlw2jhp=1tQ8*}AgwujmC>JgZH-tOFGYzjXXc}so zYual(G<`H7ngNLw=5@^yjjZ`hvtIMPW`|~<=AfoP^ONSR#-_QZxubcg zd9JB)(l{BNUUh2fWOnM{)XmAqsgF}Xr-4o*oJKoMa*B6KcADcf-|0=K_nba*`poGo zr>~v1IPG%U?{wJdgj11InbRewn@+ztJ$6!@UTSsP2HM8j7TPx2&RP#`Pis?a$f=+Gkp#6Lj@- zjdV?PCS7}7SDm-cUl*c_&<)qc=qBo>>1OKEbn|rCx_5QUbf4;0>DKAK)$P#b=#J`6 z>WXw1bvJZB>mKNy>4@G*UtcfjP5Sowu6l31zdlSqR3D?Cs-L0H(!Z{MSHDufTEAJp zUw=wpp}(sKgU-;z(Af}R7-onyBpb2}iwz$bRvI=Kju?J2oXJQ{^uT}WEtK6t)LTE_ zkd)ZCv{|U!vRla1wD`>USauDWl`tzcerBwC>mM7Jkscq%uKnW@)8aBxrYFbGjjMU= zKW$ceY+PJ?YIpkcGH=75@}B-A_(V2~2nbwD+ggz6_c8lGNW>diZ%rs9m6$B3!1X*2L| zV0vQmwD?4J9jG=pQN0ah@Dtf}pxU%V^)|2?)4)NANrP&DCe=JL+Ln#>woe zzUrsm`VV3KOlH?12sau3hOu&~HKl!gS!o}?VXRy#yN0DE#HS_XAqvGPHB_=1w z&avNFBmLBI_wydbpro_wC>-f@{EKGeoWZWqYLhe6TQqBP2D?V5C8j21&`WgfW@ps? z5v@)|hI$)a)2OVP$FVh!^J*T)u^!E5*KyS|Iluae^`cieyM=~^&}Oi*vDHr@Vd{%` zb{jqiM_>kfoK^Ekt9yC&Qg6N0TZnpNQ}5;7ll}5#-@Lup4EU%7?c?D|zxjBuq4e?a zR)6`ZH?|ghJnR)16dwZ8tzkGG1?TgB(C;`3JVd8_!mReatmK5rGDkBZMn#pk2q^HK5nsQ6S`_VH22(?{)} zkBZMn#pk2q^HK5nsQ7$Td_F2Z9~GakiqBWY=d0rLRq^?%_tK#!j@u`c? z$5+MYtK#!j@%gIwd{unDD!$$-zTPUnP!&pO@6icqv6=B%>^hoJLKeG@o|YJ&7C$F( zPL|_gKzv$CZ0fYB$#cfC((~B$wTxM42%(fpn-f0`*Qb|WgW!vQk-v-gJb$JbTj~bLOyzgtYir+7SA35bHrgb$h$V#m#DTv0O$`wGY7TNWENO3J^_!YPGv>{n5wBv2Po0JgPQ{;6{phe=yL(pi#oCSn zj|rXPCj1w_2znpv9s}wc;ZRL|50zr-&vDpOGyZ?KrxyNS+f!3fd7p+>&~&s$Hlsa~ z#Qld~iuP1C+EuI3s@lZ8&cBb2fHbtbK0_;IF1H4)lZ~kEZ)cX)PG)Jn&;5W_)-F`} z_n^|h5ACfSv|n<$e6+gqxI^4Qw9P)`3b-TOQ8WpTq20EU`-wZrokH{AG*^hWPZ4*P zE9Oc#D|enNMU$ZnZLM-N94>G+^psYhNwETrh%4Mx?izOkt-hORB7DGq$S*_de>wj# z`V?01pYkiwZ1@lVIsXN}ivN;d&40zO;n(u((3IG~Z{)w`zu~{-zeBTP6Tg|?!f)lb z@!QeF*vbFE@8Wm!d(ha}$N$LZ@ca2({s8wepT{5M^Z7&kVg3jjAr@xapu%s=6u^3V9^{O@S!D10S%od1I-d=>wKJHg#TKVUri zPtX@D2xtU330gFS^nwA6A)`>ATZGo}n`l=qL38X4?j7M3p@HBmG(tg>FK3p$8gRo`RR) zE%*q&f**QXdI`M+f1!^MfQDCJAxH=oLWF*3f`tlULbxzMh(Kd(kT6&nA`BIV3B%Da zixfr*QNk!8T8Kd-Z47#U#tGwv323fO5+(~%gjiuJ8gA2scwxGbAk07$E=iavBnv4* zDjIXMh1Y~MVUCb4WC)o;mM~YC$NQu8z7)L*1};OGj}EIWE>l>5{;${2g*A_xkM65v zE>2h|ye=#f-r&9vvV}K=#ll;{5;P3o5#Hs#72Xq;qW4-xi#}EO0FA|E!bie#;bY+w zVTJH1nvS0d{}DbHz7SRkU!ocLm9R!wE36aN3mb6s8TqTk-w7QV8~H%~Ja&6B0y(5< zrU}$cLJRUO%{Q77%?qdgPO{T6Z9{FUc9pKBZmw>%UUpv;Oe< zZ`A*|{@3--ywc*8?ytP{%9>ZoUU}4jZ{X2jRD+cbzHt_uTRM9?k9S_-e8yR6*tDTv z!-R(KG+f^>r{T#)ts3=cw4_m9qsy;Ocy*IY50{T!wuxTiF!AfgPK|pu9@aRnab9Dq zRA2IzQlzueZP(_mLtQ7ku5>-@`e&1&O-h?iZRXo-LbL2<-!&_4&NcUI9@qSh=2x5l z*`isCo-L-fc)i8S7Q0$pXz{qEsb&9`Gg>Zdxv%BRRspTXwVK!J(^k7%T{CG-157(i zx6Q50G3FKKa<}$wW88jpyV}~Rb+^`2S}$n*Y3rZbG;H%hn-gvBw|%v3&$g4>{@9kZ zYtb&eT}r!E?X2ydweQ(Jq5bmqN88`;FrmZFj;%X}be!06QO6%TmUR5BQ^QU{oiaL| z>-3=WsLr!Hf7*Fh=ZemXyV*U}{hIsBE}gmz>5|^1q-%q&J-WtpUD>T+H;-=Px_#R% zx7&s8&ANMaAJlzD_s_cD?h(@CwH|Nw$o1&v5$o}##~DwvXK&Auo*AB>d0z8s={3mf z9j{~F2JbfBL%fr{S9Yd;FPye?5BmJ}ekNH38)3?vuJ~@5P^&tVx1HuE61J(tc33LjK z3)~rasc-$h-TFrMeY5XZefRZ!64WVZKu}!J>p_;FtHI-fFN7FEB0|za)`UFo=h-i& z-`o9m^!usb)qYR=>-snC@7aG$|CIhq`+wO#xBu->&(P7K--VtHy%A;zYY~8&W#dZ|Jb0%ZF|m)@WGzu(yYOGwkrN zUxvF44;wyV`267;hVLD29e#I&?}!m2=8o7j;#{OLa&YA5k!MGmMlK$CF-jBV7qud4 z|EPMSd`88L`fAjX=tj{KqGv~c5xqOw79+&8is>ISF=k%OmY8d!>y7pqoiuvm=u@K~ zjcGil?HI2yVPi7Kygz31nEWvp$0%d_jLjJP`PegKAC7A|?*6!!tS)}$XNJ(yHAdD`R^lTS?%rUXvOobsP32dDfR+c4HIc3bS_ zsm@c~rw*99dg|l2#&NylX2#{lorxpU8cj1#^P0AFn$1~uc6NRf8Y>l9E|3k4+u6ht z_yQJ5Hf4ICPP~@(qPgT|bXMBIVHrydE__-j?o@o0b`l&OL=NjoplcyM>73VPLLUpP zutY(c3wT)|#3J55uB1s1$%;-<4+(zj10n48z|vUfp2S3fu08SSAbjo%#*Arqra+CEIVpKc(l_2Msn zpG`U<|7-wjq9g~6pX~Fl0Y1^6n|Erq>}Mdul_?T=S1(o!SQGuNTp^#^x2NJGgY%Mt z)J#}>6zW-EgyqdFm(7KV)<=aO6+SLWEEID~;3%|}+;d}YT+cmx|G|Vqz9yduqdS|) zM#ZqCW?XwzRUlzQ6?dhZ3|`@~Oqx}7sp=*ZFYOa$x`YrX&DnP1fm>`V2g>&FNVc*3|dHe1>JUZ6HT&9Co{$<3Sv<@0boK14z z<`L)84d6tMO5&5wqjRrc&)@&68T<*a>kv7rjmcSwfn{`ktSc1P!8#>IDuiXYD2PvC z<)uO%B1<$7`IAK2UkZROrb2z|k(MWBWsUwxf#o^=BwhRiNe6Rv@n5j`fXdgU=LJ#i z%vEdj?M6nmHVx84mrE7Up4Nj7{x;IZnN@GISd9*i5M(k&{xfOf_hP;p57VG zTVQY&k1#d2;JgHb$zXf|gDnIvQ3QWt?NkBy@Fw?``7F z%dX0VA~K}1!QeM_V#%lhsicuf+!+bsRT(4(3tN>UTq9*67vXq?cvso(YSoJus+xbK zQzWIa{EZem_NpN6Ch;!m+9!Z?#AWgL$dN~nk4CpQVL8{TCbBzg-IrFj*l?XeZpFX~ z@d>5R_D1o3>4we6WSaq;gU%2$0_j6Kn96jIjvjq{J9kR&fXP$*O=H83=)@--jFf@6 zTb1-IzWCy^OPg()VsMw>AaS>W`!j1B=ov_Qk`!>$;YbXXp@%Yg7t_Ru6M_wz?}pajz)@FM9z zI{Ej)g=r(LCGgXU&+Z+Z;O!AJ&Rqq+F0-Mnv-U>P-tfc zzhIqPRAl>3F-ISnLUg1qN;8I)&4WT)*+>WG2Z=@2l_y;VkS}b*IfHk95}PmPY>KNe8TGp!kMD_>xp6L zNH=qDeVbDw!C-(Do%#luS-&SZUCExbf4Vtm-w3*UkLXpxp zi$8CJT$|>+YiFYki@6(-XTZl+SX_dl1-`48h!j%n+Qld;U5&P?1`_ncHQ&t$Udk~^ zRJt3HbtxAZ+d5OGDaOwCpb(T}C6tq$tk4v@dKhitk|(|`f{%+~X%Q|qjn&nQv4eB$ z#d*Y!r1D~f4bFkBnAl*cVkp8TWQC>0u(k&JJjt6?D=&i6S#;4BP*6S;wsN|Nhc9dz zk#HIcD!(4^sm)z+Q(`}O^0>A71lX=4;@_l7;TmHr2sn1CD5?gK3FP`rxj(=w5Tt* zELuihJr@BjWJ8I*r#$h;_bR~U^WqCKG&R82C=HR&x~M|bL?r?ul7EL`6cwGP}dZFT4b&E^3r5u0wH<-3u#EI@A$@;$>kW=~;OadKQv} zx>|x5npEzQ0>~#i#jnZ_MX(Jy^Cwj(leN_#R25~9GzfaWI7xaAf`xw!(mPkSC_pXf z=_y!uN}T-^)u2dEFPX8m#78G;mjWE)6p}zhj-837-@~5S0Z{6|lu7=E4dnm4+L|%n>-BR)nV_*AOabRnd}YW~R>*KeTh-SI{J>9(qOiM}G}d>F ziSE!j`tU;&CEb3ExY!!bkVdL4QRds%l$cWt@e(oHf}x#hy-XRZR-8+k+bS0ak{f!0 zzcXYlX%`qwOlF(w-Glk}OyEcS!@+NQ4*@p%))Pa%E>} z3?ozJk0M~@i`4RvfLh;JBbHhCM;B=v#esYbLQmmJsOBZew0_pXWHMQf#l=)i;!(sS zcZr91`4mz{0+~c6^^j#{5G4!bS5-)CYZ$*8Z**Rpxjqwz{hchAL#5E#3av{UJB!;7 zy)Tt2U3In!?Vk77XUK5|F}13XRq3uTFW2sUe?z()YpCi~s`S!dAD8EAnkR>cYZv59 z+c;b{WXoD{Qc#e5wm%|6GGm}s3cFx+onRDmpgFR=x(gslnutR6VUm60*9PY*mi? z6_<-T2RX>)wn)iF@j{9!7jP}lt44=SO|>suzEDY`oNX2s^C?uCpHP}3b6Zz})&X>Y z0}_r`kePU)3Q#x%oYBteJ9T%N8NBrJ`INkqjfeA)Cfp=0wd&87BuD5N*iR zxFBk^H8^t`3>0no-n|!1C-i+{_m-OxE9eKFMrG9*zPIeO@TC^GkEj~MF6r>MoAON8 zt@7cO3UN{~WT04Wcitb|78}Gh9{GB45;_oEKP@RmQ_c{e z6SvF$Q^yA^HYmaRip6^-$o_^3oj6Gbm+(^J`YF=c&ttF-IuhI#2m2!^gUuD|JUn2z z^{JsMSnu`e_yYNXfvPlUo9?pmpsj$f%QS~vmH9_sCHq- zTgh^QoUkJ0%XCBkP&sb5eD08M(_52<3{6TLGBk19DU-7d z-J>j5Es8r^t4!V^D_Uel{Y)mw5Jg6kMpPBXOHap>R?P2g(~FB9z^fMW>Q#Deq=%6( zQifzoDzO4kgpO`~|3XxGN$b8N+n7Hj z?_i^|u}(9YbTfB!ad~{fmR3E4bMTo|(i-8D6hyKw6 zRcsgFHHvSgNt$gWdyYu2p#Uy`2Klf$51cLhF-wUB#6u+(%`qiGN;6u>B595hh7Uq| z*;j5Jt(%mQ9y>K-LyieVea`xgd-krMGs$#FS3Y%bP~WMsK_(*VgZ50hfb{WO9%%PJ zKIj6SEU^U;Xc3>mM&*#yXG+3=0s9gzn)Br1B6+X{*V0?s^09k8(9ZQ3JGH;LK)2`n z^+zn5(x#Xa_2M%!q`kCrf_|KG<-(qwg(fjq-Zy_!;ui)nZ}S&l zd@JuXoQm5X(r1ba@#~E_yVlN~VG=JT&6}5K5-Vnn`(C$m?c9WfjJdHBUfYm^T(On{ z`8g_(8BADY)s`PG7TaI~`01eyw2?_0Du{C_C6l>9$P@ z)wd}a3A`zX6r8ObL3BF8j~Ln7thB7!CY`Gs(GRD8SB*uFqe89s(U!{vF0j_}rUhB_ zLSmNC-COdb?!?xuC(QC%t$1x>j=VEUmPcyE<@+aT=PX(@2N(R4Tu9m-cgqE~TF}`{ zM>y9TEEC?ry0a1&3`HMS+zpb{Z33F*zaE6u2YH-p7*U`BcV&Up{qUIk4-V(wHI?h{ z#T@MH9y7L^$>Mr)>(-N}wj>QR1?h*)Od2{YY3oTuq^^7`TliKOCJjA6t@?iQkJPFk zFU!L=$y9Nt|7l@5T$YAy(mP(H$nsF+6yWpdG{Cz%GkI#7%N=NnYVZleQn>Xdlk+M8QD<0V3 z?X4wiV4O4o7Uz0_Wg-s2!3{VDLrnNF?j}jX3Q4o8T1%Nmr5&!LES&8;=)BvSx8$#l zXn6R*?%kFN7V)A%IqFK6toUm?$EuyYH-N3N!?jlJNVaOli|>&QI0KFb=)}wKsSUXJ z9&JE3y?Aj<5o-ZdVGDN4zy2yi8=O;O?9-=uP@d%%@BUC-`%AK1_8b~_XtHJZ?wv>h zYikB)MZe{HC^6(rxqM+S5`(;N9wUVn?Urx;VihBXsoO&W?8LBf<9?MGRyJOaj`(X0kWp&DZfaMm{l)Gp7%;Iw1 zmSTZ-Ey*^qD*L>$R9a{xuU7R%Ck{!i)JP)XUazU}3^*)5N0n-6HnFm9h1JSSN&J10 z5h9|cH)?7f$$gTG&CbKyfjZ6USlM36Hje2ey-B$bv@6_wXk`e|br?0OL;F!j z9-CmD{_)Wxj~`L5G>Z6+{uR_0(i{ENsIS<)N(XRwt+u_?s;15*cm?{=(RX~tZ;z|L zMI4|b?)aqE{G5(BtNQZ+>Ft`Yw{Y03w%65R6Yo;kxDJdkR(i)VR?PWCp}^1_^lFzD zN$(niDi5gW?S84vIqLfbm4Pa5e9x+Q>^o&Uy;uD>6hGn=GkA!X=^z_z8&L@a!V9!gXG zxzbegT-hVZ)1W))LGVwWwhJQ+&>isCE?M41@QMDCcTJPk63`ueyv53;V!pT>3NP5l zrJQ^$eQNx;pR|&;5&dIDb;hnTQu@re3qt7JJKSfEA!FYj=F;h}RkQY3g&S|)mi}Y^ zjJ^JA@6lTrPCb@-7*n(Q*mM^|5YG1foK)N9oSnADl2}F+_RozXxqp{V4l*jw5?F^G zIEOc#f+oSGVsL+oeeT2SchL99iHgz6Egt=cevcnl)z#} z#;R8sAt=Z7R}cR9GlocR?vD)!nmRSeJT3evI*4!NPU>wQuJ1izvX99>?2-;l=Sx5| zpU}%!rk)B|ZxEG&jhk3dqkg}i&>CtN7)BbKs*bB>c`aAi8eDI;ZQ_A!hdV4Q8~tGi zvPl&AMEb%=#7ZwZ)-?{aJRAw?L#x1wn{492!;Xpz4r9fGhwYxU$4WGvh`-l+ldjzl zYFC_Jr{d>W5!pjc#R|HZDaI*uIVah~d^ZQm*WD0hzMH+Ry())FV%}k7$u*U4;~dkv z)73SOo{GF+NA3B+SUWG+Ui(?)KuO%6y$TJHXljVmz}SVnnE}~>5Bm}SH3x*(uTlKh z>|F-W%0|etiMvzMysx%y`-=X}OQc#Ro89nY=w0HHdp5cZT&-X#6@SkKFX>C8_;9rm zLcFD~j8>(Gbo;=hp5_E%LQ`$Dg);VjY(+Ff#$HxI89TMS+m`u5tG_XJsoMPZ95b#< z+mXk9H~psnz2Ml>U-QR%nksbABH$bij`ZwJx>4UGFrOjLXD!|Y)t*Dr4)Un$)KLx# zIS8#*p?J8b@}}CrYDlgIT0Y!SDNij(=cP91K^9eAcDfRYlCpXtkX!o{9jDwsBnL$# z`WDw1A63q=bAKP^<@+K>eGk59hfI0gxGE1wS#jpG1$B(t*rAO`e z$?`rTLgPeq<=w?NXk+Fz^eOs{3I+r4du*Qorzv$^XN2p@MQQt*uXb#oyJp6WxpQZj z6V}YzZX$b+NC|7_ZQr&QzhtdRoRNiQ1&)`}wFKH(Hyy$e%RBM>1dGEa*#r0Bj{eMc zCb*`TK^RlqWu4)NALK*BW%;#{QDm4bhk-$!HccLVLYBWdQ>eZ6-v03ze3UUN`mes= zZrV=KF`YZaAd5ye#5|F}*Og9%*~}-oHJdsm#naiqKcHgjGKX^Nbau6%>Sz7SVz?!( zM?vK*Z7}XqPTK{Qqfqr?s44VN8DbYyjzTpnf%!fesKbicE~xrh8EyYXP^msrDyYKk zf~ueOuge9awvX1s0|RZ}gTVt(m)Z>zXxNx6WOYlr(p4lIc*Do3wS!SKAyf64%Vzio*SjGPsa0 zE(0ru=CDQUjTNN7^tF+EN^VNus5}H+pcPDb>_mon-zILdJ3a@KaaK>)jG938j9mvY{pYM#ms?i50fzOHO4^z z!y)TY!*5q{9?WKF_P0O@+Q)wm(mH(G)mZ-4yh;@nYJs$2-x+bf7S$-sz*!t2)!GXA zN6KdsdM_}{T*J8Us&a!ydk#8S_5v!T9Kx+?B2 zrN(Ek12m|yTq_AmMQ_s~%rnVdW#a^~h^CY%Zj^tK0*(YS!6mL)dv58AY1;nBWDFyzmJ5Sv{!2V; zp5<+f*Je8gDKDD{2=^d2gaO{;0Gx?{Pcy)|*`F$}pfm6vI(RAD(Lq&(pd7KbcKaix zm1JqN5z=5a^{wD{7A1R*3x5kHNt{p$q%Fo#d8#5;U6=8aRn?W7U}aU`bugRNdtvgv zWmGGsk+xr8r?eM&eQichM9Sq^du~HLyi^|_r21wg$dQ-PG*w>xXHlz*|IP$(Ja)7YC4y`k-yLZL@hKs>!djLk&@(o3` zjV%OKl~I=puB2b;0_m>AOFJnywV?4{otXizV^dfzhEh-eFLb+EmW!%f&=>tNT%nOp zykNpc1AL3&ZUood8k-3p(tt0W1!q(fE5xg`{iMycK+I=ZCNKM7nQVxOR=L7?({{@Y zUA#)XTJg|Y{V!|tkIVOHoaSoixM@8IA9?t`X@}C}S3R`ueSzgc1QWB#5kXcP|2H!p zW~uA_e^mX@dCv$#O*T=D;cD7JR`7!nn!uNonybUksv^P)t)O{qj_C7B%$h~(0B4XT zuH;wG;O8o1ARR9E!SqN>bHuz%GkB8M2Y|PizQ)tBUjOJHJsp_Dxe&gWcGYS_&GPuz z(F90CTf5uX{JUmM1bBG(=%WXijb$QEdXlf~v@b2zu)6Gprqv+$V#;2syI8$$?mD2; z*e!Hd&ah?k_Wpu<+iLQ2e!mA!Y8P>|wp5*!zMH=~C1w8n6q7t_)%^_X_soD*yf7GaI_{k+Z6CVw$J$muPZht=G759wrhK4lz1tQ(g)! z%&4cT(Pb0b^mC*bT&AdXq{1?)BBLefas#&1?6|hW+~PV=yy~TEZ-Q z2rXtL8oP>ugyUlb=>VN$rlZ1K_F2s@8f|P-E=}31fBg7y`@J<&)KA{(m8JT$aiwq3E<-9Y&_sN%`&j$7=jVEN5fh+okb_(8Er7TphDt=(!m zF25adQ#M$4Vz?_xH+^1ak||RDxX4R3gvV3Y4|6ji2cub2%x6WMD%Y#r2`!o@_(4ri zSYs@#9BY4nx(HbobhqT&@-3RiEm^YFMGua$+8N0cU&!2Bi>+;$5O~cXgq|hbpXYL5VX0TL9+5Q zO$cG*ggn*--Uef5OdnC5aoFU!zezu=TeItj%ysecnVIqCY3t_xKs}P!j+((kW-u+Y zvO$rH%?5ia#2jS=`tquJGJC)jM*S}=<9R-L9kWm{x#mR!sd5B5VbH>!ZUx^=1zk+E zQ;(ru9~(qNJ^u>vIh-JF5>{?geh!+Om4hTG;!f3kz1KLjq)7n!eyg%`1GzK(ish&f13qqMlp`9REUfwUr z2T(J;h!SY8QQVik4?S4fa2k{UUsOmxB6qUasL8%zHGaezizOQLT8eT0zvSQ`fWIf< zXrbkwuTY3A^N%uv^XNLWr~i#RV2`P(IXYi5>wUZtdRA_v);nv&kM@kgXZO(Ct&A_K zYEaoA5QF=FhDbTpMKH<71wOmyYZLb%IgH07N@z*mrUh`3G(33~>c5$_DT`{(X;5D* zv;)$K=Mk13=A(-X8lIsuEEes^ST}uIW=6b8{8^syRp!?B7L1yjI#{MT2uc>JS$z=` zvKEg=;w6SkX{0=m#^~=KDtKlV?{Av-)y$-Mb7z|5lvVS-dvDS7nOPHAU099U+dC`^ zbTB5Y(=^OpMofi2Rx4X~17_={$1zv?dd%LK#TGXV$Ks{-?DHVuwv`X0(`z{diEEHD5gj$JD>UqY62RxT`*NsiK%(%?B%A`0%^JAgS8 zp>k-b49fyAzI0jZ?LD65OW_UxK#x-^m9p%J!KQogpcJcgm4icM*i1g9G)(tZ?Cab) z6w?&$$>+~&iA+=B)o5ppy@(ER=+6QHcCUnL2HVZsO*CO>zfnAuOYPO>`uohTv@bsz zHnrNGQJK})nR?diOnYHa*g~h@;k2zIg8YrwRh^|ygKDg3 zwM8~;(SB@^cy$HLr-}=erCnJlmkA-1Wj5s_9g3I&nT%0|W}k4(KvqR&<-owImU&k}p+$7LM)!4W zVOUn_Ol1f&rfR9ij-0b)%3Jo&7pyf}6VrPg5K#5_kt#q8M2+%eFJs?$VrQq|{MVG( zNmO;|YOl{MB;;|_ySypq$BHRA;!{oyF0kzuvoNGFDMv5TKtNiK@8!ZI2lqZ+Y$Ii~6=?@A;ejg51m(8>hMi-;Gh z)23o#f~C-{h{!t9s$VE^#k9(6xw+R&U+Kh!RmbM({U+?cR^zVa9od2?i{UF=AH*Ee zpR_QO7JAa6tRI+7RvnnjPJ#v#UIva&@GwC*BqED!6;g%@G)W+F((Qu>el}J0(0hzM zaNCSFJ10SJ{hd7O=A}gA$TC)i!Yv%IT$*cqC>!1X2aMty*_dv8C^*ju%Q4IOpz#*e zqjs@<1!IQa1=U@2_O>e~Q|o{xqW)WwUp}Ld9?X$sr!)t495$*VBI4idh_6z_zae5Z zySkw2Q8nLn5a6}D-Ymgz%Wai%cIO+#Uy*W9#%__Q9{Sya)yZb@dCG$MDQ0=r7d2|? z-}(*j@BNB(n`1fFZ*Yh_KRXC}9gdxbJx=nJzuUrxxDJo2vo9%kxovyH;S#eq$%aj0 z#9aP{O3Y|Eu!L>2LRilBP3m-@Y1?;$;pCgrA)|OJ7!$W|Wn<>{tzb;uz8ielC_-?0 z<@>6y0$6?|+9xbU@*YV5$ywYRi0K(Ss$M0YGQ=CobNAdZiNDiv z`kju`AHkU8u3{KathQ54B5i;;&eBF6bfZHg6ODB+G+)ecRIEcB7u{4G&6bxQlONMW zF&COx-OW&tx9O%yycmpQdC?8W^0HezGyT{gve~N{!AIHl>ARba(|3=FSjOimL98`sg>Ywm2RYacIv-`pCMTv9-HIkza zZKYPe>^t|wJ@$;*tt?}94`$4YPc~EwFS=8}9=UeFdk6H`i@*1P<>AVxp$x~(gAq1( z=aE>bcq&;^5$S=jF*CSf1T-WE^`t?`AZTpbVNKBWjopp;qX+fkK8Puz4j;7IE?Qt2 zVnt(xVI<2)(BYRy z<$v&{iMiAZjw-8~n^&e4D-vnuIomNqBl-oU-|jnOE!z9-Ib0P5P+uxAib>hW=pa61 zNh-7~CY{saI90lym0e%j*qDvQPk})?PK%$Q#qS-b47QHeNz+cychy)@GX?UQ^TY0! zal}^ax~C(!qK0i46yO~}8!IuN*Nd7$w3?OX;kv5kRn2j0jgmzD0L^6Ve=`M>fO}OX zq4)ZY->3_V0i`xGPc3#$AyPjUscMGByE*EyTKCjSpt09dQ*sI~F~8nt_NM-fTRu?B zW@59K@RKD9C^M8Z(&Nhgg1oK7*_oNgwe`&-_!X^Kb_DSV$E zT9g*!{!V29q!f{q&-Au^doP*pmUP#VCZPjKLvuC>j({Nj2UQESy(doU#bV)MMY!3k zniUjCr;X%fg!B{RjJS{Q!od%7(2U%b2gZXR=Pa_J=2|BTV^fyu$tZ}{9$fj&4*9se z_Py0_V*=_+?!*bI-9|=g7ko1JqYODzo<3dfFOT>pA@5BC(c#u}88;?(gpOLO3wI

qPruEq`mTn6gqu+Xz0%Pv!y$K zIBS;IU@KM`#6z=dw_;g_Y#^R5Ow!5STT4w8$jFg8(u_2Xl(nI8TThuED7z?jv?%RK zXH+$%nROqRFU`R&)g+&d!IU}5KK{(MV0kOlq&WPZlVRPGbFx%?nr+W=_%-_<(;UR- z+1=r1rufrbAm$t{e}v0*4`u`*18Hlll@auda*eKC7ItMG;<6`Lk_D8gol__HdQN$t zmSuV_`cZ3ihNt)LP>l!kKvaD%m+E`DRFBD}dQ2YG_egMivOrZ~?QYg;{WXQUS*yX; zY?gU^?><#>@*H|#F4Y5bsUDb1^}rXXiJh$St))Z%zp65*J|Z5T+BBCb5r9N}SJZ{8 z>?T(u;Nz;e=r^y1-fhNyz@}$u~J}8N*Y#y?&yI;Sy$QF!4BZ< z$~YPyQGTV;3?|`~CXw%~7D$wvH`u^K2G-8-l`w8u%WX)VhEeEG)H+v+cF!zk{b*ob z`tKCp^JabUHo69AgWe8{AxuV zh=(8=ano!?Urq#=UNo@5ujTsNdE>pzWIP>ZPt}rdeSx{q$5TuG9BFL|g4r;jh#vES zQ%|EQH*M71(||v+UiiJJDik;9>YzDtZWa}V&x>KXV{a}T!F}s$RHIhn=2MteOlJN1 zui2Yqo!%Z1q4Qot1OI)eDzf4_n|;~u|1n<@-3EW%5{xWlRT1>ex>rOE>CRSYR;oFq zSfnaTsq&5f>C+rpe$fCL%G!sA^M5fv)&DX&zq4r+dRE+ZxD!z?9o8NCZuV$%Tm6_> zvqqa@bd)C{z=Et=;9N`LGIS=Fp`q?CP{K$7=?n$^b#NbH)qw=7*l8>O<8)4~fT@3< zJ~hC>{HZr^0x?aLyGI1xqm}aq*1Tu2d$rLpFfk~?SA-mrh8{J{NPL&KA)K)yFZX&W`BHso0)?D zH>YrThTU_V!l6#P8r(W#R|{*V8q-Cd(6u1mzo1UEBV_iEJLlB_K3@j;G$&=YR5iOy ziPWQacm!xD`QX+Q+)9$kwxLqIb@tS&E0wMx$3}jZ{7|juI9z`!PjZyQWr&=pvH=tz z_)2ww{oAJdYIrrL6R0h&HYCGpmODj&$6%=I-1)Q2+cc6B0Uc{^%6)hUc1xuB`F>zB zd0KDl(Y;0;^w0|FFr*ZnYT}iP82@|N2jiE)c#wu!2_UT`tC2i&JxhoBf4SqG5f?S? zHq+IKY3B&Wy%caQKEf3mfYh~_#vACO+fxF+GCIU#9IVP>Y#cLhG1GYuj!q!?m2_9_ zT$oyY(m{>iu-73roXV2Q5&aQr>&gaU>dk&A0_}UFv(TtCv$Sz9Q90x)K2wI-o1%=V ze1%qORM{X@z4b>PNY5moG!1y1!?+3!(0CWJ(KeWHRg1ONH=oF`}$0n z;A0*P>l^^~XYohNU_iyyGR;xeLYlE9S&N{d`CGa>xQIxm5wKsU_`Dc_0kaTfG*XN5^zFVM2)L=6^u$(qMN$~^E%_X4o#(=6LWD#UG7yggp<`e<}Rp} znf+f6Dd>&c^UHW^DK&>+7slpD>);^L#!Sxr<-CH1wbJl9kPb35#&Ze|-ZR16G98&h z)}xY3Pc3Lzb85lw%)t{{1OX*9r22(zJUX>7*eO-z!g@U6{S@XG$R(*cBiCY<`Ofug zcFRW%zeeWcQ7L!?Oo0SZn4Vs^J$dBF8Oeh%(MSiunAR*R$2O-#M<=I5nn^GOP`bjn zEB_cutIDmw)l{r_|FhgsG97Y*?Ej10fK?8;;mz3iza55vQ@6CJ6#Q|Y zxI>1BS5Sy-VL~K?3XzVg5Lrrv$Wm2^bfiKg1ciwBkctt1Dn{H?F|wG7k;SSQaie0y zzXm_94>&-Na(Y$_eerOtioxIk?XOvWH_4}F?H@}=Z_m1o`{ZMWCjm$BEQziGgNfc` z(^(Jg`t8=Cw#@&1H3QFQE zT*nyceuD`Ziq#=tY*48~#jEo-GVUNnSzE~pVv?s9J(+^|fv*|>L)7mT!CN)WwZVJH zmEOZ86-eH~K-pW!W?9UsmRPbMl%TE>yZB;D(57TT0SmsIa%$Ya99MvN zve@46e~?T{WP6ybCDod2y{s^_T zk`<=jsAQsT_^*;l$)uGUDOsWFO_faJN}xlvWZFt8yGK za;Yj^f^jfHU$`BcG9z&iJ5FV2aqZ(&#!IAyEd<>(J+9G%xqj0_H^0lci>YH^Khgq8 z_ICb(U!N94x|!Y}F#{kbOYb%^vTc_!dAIE{2~)xb{6!|Qj+J68(y482k%T+(szp*4 zRU*AvTOy%BS1pmStV1Hncj%!QBk1vM{n*({^Q}q~N$z@J)a^G6VkUZbn#nzoKF{Em zBu{9CjB1Wb)|*fw;ptYbQ3TzSU_2Q?#^d=_TTY$azO|4I#Q)JAk-w(t|Ce1IszF^l zC#%xWDe7X?pEkIm$72J!YB4nKVJ3I}9vZ)c*(zvIv(UIJK?m9>=tNI~z)Lx@pCA?z z%wk(c9Ca_JI35mPr6?1g*(tQ-H2|N6lK8SxjdB z=kNP!m>03e9rQn(okDquno4CqGN%-2aD92$gt#Wx2ENQfkrayCx9)=jxa)F zJcdi2q{Ynz=m0A#5Y?BG*;b&1V}%PL;l6uk|+TK&~SoGoJ=zup-pjJAx+DZYxY>$sym` zCRd0`F`7IVjHuVrl(7&zcH)u!xCR;)vf6gyKI2rnTjzxuyxEC)Lv$1g4r2AbP6VKxri0n`g^$f)v$Io5L8qF>|vqM>9sak_)ZY%-gH9SCs z##7*JB~O~MF7t;U)~)-&B=4BNHfh&+w2j!|GyFKcf?8eJzAJ7ZJ)ZJO1sC3{E-WtE?^WEr^<$It0Dwjs`i9 zw?X8XE15u=pXrBo=t=!xdFuBIj&5DHdY!!8@I&VMcoRKQ0Go}pZ9i!QzE(bzsy?(R zA3~&sL_%Z~o)r>_`+$&7L|Wi;^i;F+m#`H196tblwE&i*g+I5~(W{m4{EZ3lHV+Z- zwgxZfq`{zpg?hZ_#8_W{c z%-wE=P}eIMxT41!XB~xrBhd8-8l~TzoNj%3V#JYO^0t+{bwtda0Tr-a8k9J$YqxD< z3eDT)uUCEX-IudAF5E5~3b*Co`{n`b7eDDppEon(3@soY_3i8{j}A|N4=OF%#& z#3W#VB(RL!5=oGtC<4o&yTW_8+8*d1d~cN%c&-OT5=7)r&_rYqSqOnBhXLjI>!JuM z;8DX?l>hfv-91ML%kuhlPfvGUUDZ`x$M5&~P5&U@TV~9DY~e$$$KHJ8Z)I4z84$SB z-Dl#&-hH-AIPOiGzv1|n>fhafn*GKHHLtx6q{_p-duPv@=uN9G6Uj|c?vGclTet3&@==8g%ga6G3)b!MisBw_ zItjlM;@n&Ua#INOlGW1_ogi zL2`x3Y5-LXLXs#3_K7u$*AGTRb8Q*0C$bHRT!|YG9T05*OYJdkT#wtvz5kWRFGhm{ zbzB|&PN1N^jzmFu?;;db5lB#d3h-j!dzb4!jrI6??T1G3G)?M&eeoc+4sNG;Qj0~v7Y)FbBxoc1zh^?G^(o!@vG#YVu~i;FFu5jms~>EJkNw~8Nc(YXYyU+jc%?GcXnM*|^8!V9Zu}Lg zMTR5LQC@WK-%z;+XlRHa?wMAZbN95eVVIwA4%<++&m-z%DpoF-;l=+M3l_|nv0!C| z2QDjC;+Nn_VVoccbU6I`Y)+6^Dn_^g63debUimnLLGpmPl?nNEBEcgb2V$&lFV5OE z&30q0-LD^bcf)>y26<(S?pNF(e*`xOD?4f1MQguk3wTf+;3j_Yh`y^}rSZ>Qn0DQc z5%Yy&!x)pDo%5-&4bLSj#=B2T|90X3Zmaw&DjNE$v30_QGJdueivljUF-iEOuhuXc zK=Q?ik~Scd8paxq68XGaoT`(j;O|1M_J!p&T69Ik##+;@>}<@$sJ&MDgb#2?gk_9% zh-LB_c+Hom0J;f9xviFzA@;h5f<=QNENyr{ht|?!tPkiK932}GDWU|DQhXR5=eqN5 z5IMv9;{#m#<^FohHFDkn=`k@S5;+*it=^vRaWYF zR{r7^OT>~1TQGozFLCQ0)7MCGt6NkU>(;?>X_e2n2AtkfIn9{6#^-a0Kkjp@c$Q3< zVER$XYis(cs`Ay$oddT(C$C=Zt0^rtWQ9X?Ibgx{Cxj>6s!6h~i9<3HRW#Ej1uar8b9LDi_iBTumk@83f2f&O8!bOXFT<&MC}n zns)@#Cm-mFwkcZem#uV6Tr1utv-*e1YvK{{0tlT^@LMnYns81L_uBxd?Ma`F=*-Uj zl){^*CQOtf0nM4Xu?V%?lo#p^EgKUT^56A&%<;j_EnjS*wPUE!Tbc!bhGqd)_R;;a zq;t_e1zYbBX+7fz^q@@AdIpAGq+N;5FjgT<8ap7cBLRWUupqFlFy<5)CIr@~Ketbe ztQT#=fXspAZg~VPQ$xN;^N1(von-}K(+o-_LAo2Cr(&h6_2Hw(H( z^PRWs91WZn1jWwkA~9z#5Eor_nKl%_eXls$!+l(i?4uW!uiftbyL0=xwL35#imJP~ z&c@dY-Ih-8rWt-Ry*Yj|-NH|X1=<`oncfUG*`_&cvP~pxGCc^J z#Osb?S0HgBEMNmAeZRY|KK4N5ICeC)n7rFxc4jXy6MJY|L_w#>`59*_-*o9g4x~ z&7k{?X;2eT9vM0XX10dtkQ|DpgKeCkc@v2i)9maYvOD{~S{4@;lFpN5aUnOt;sO-m zLKYWJ1}!c?X*g(cVadC0)6PPZ1MOBU$fALNnNmWUC^kieNbI7vMUkM(biIF0x?f1Lks@$r7{f8~Yvgd5n+X>1!A zL23nqzyn~X`C(XQUJmPEm9X%25C(rQYP#muhHE9-pJ1tL5A0w41nX1%VI67~tSr3= zW4t?~>S2WU98B=Gj_wql0rR`#qaTWXCb}}Z8fJDcz^tw(CM%{erYzE4{PcTfbW$r zeBb$n^WUyGSGuc6E`I8OZ+nNr=+MP zZ&IhEK1oB8#w3*_ElgUG^mfwDq+gPwle3ZsC67veIQbvR)yX@OKTST7{Ci5Plv`5< zrVLM+l(INwRm$d+Ln%L{I#N534v?)A2YHN=RdcGIn%{TSq)fxb!(Ea? z4>v_LZN7Cg6gns56QQ6y;V39BZ$921AI6)H?1wEF6~Bb zKj-Cve!ywy+)kBB6h-dg7PtT+a~7IFLZQsTEkQV22#%Y}i1i}HYkYOzE-;NakVe~$ zLoUH{_Q*7k{ELn#C*4jABuFyo{4hQa0|=1>Rgc9w56ZrnVlx;pgtUOK%sb;^5#atx zWl+4}6def*?*ZDJiwynTw$Yuu@&!|~5MMPOm<&B_($a>GHfd%ny9ekTs+pvi3AIeZX+tG5*BM!Afdx%%b47!t^r+-!DWJo;cu+oM zbfC}fb#jhSn<3)yrS3)sGX@zB{@X}Px6a8M19QU{beiVEACiI3x*lB75ju35;L_HA znq<3ax*@3Y#6>lhc_WvAmqEgW!?Zzg@s|p$QB;4cV=Z91(Z3O)8o$aBVuXW>r2naJ zFhq*Wlv44?Wynvdu@Xq!IP6E36c+o7)D058#qy8Vl)(}jLKjkV` zvp2p2JhIlQo3_=P8z+o50CPKnmYVqNLLfp^r|tVER;njyZvpZR~Od)h7pcX z0j`4Ead2~8zm2n-5AasC8v8xsia(Y|?e@`~zfvuV)U-h@g4#MEp8MCyTcmeTQ3t9- zj|EahM?yjujCnv+qQXswyd0(i;H9)vPmUv4#d~RRjKYg}-xlTFoRGvULl%gTT83!i$*S-pG7EDsM(y_i!o zp5{|C9HwywLuvP)m=r;$II?4m=E72+lqr}_LS4t~uSr_%+7(!02M-6y8dKpuL&?|R zTUc1!zVn#(zdpLyx=hW@hjhG5KYEwA1t~+25YXIWHY8|M&?K9qL5j@5 z&qdH0ER}N}GOGAj+5gkmwciFft$a zT@@x%nQt~Oe>z7*`n<$~^YC1<4i@osc5>?*9eziTcr{E6886x=kBUV$pdh@rVLUAx zP*%?7kBjZHO5vhsCRKF0Y#+W)C&sxGCR3~ zB}9-C&{z>b|7zb)2PZWL`oekbFI-sJT5p?|-}FhbaCk%*RYkpbtIVT59N9xrTKvi$ z3aXPMn6GAqQikTj93kw)$XsZyb|j0ufndJ^TKxj1;9); z&wstl#l?ED-tqjxXBQZMav_kLii&Rn)nxL@S?f_qW@~?KEhrlKV7*4(dO=D=y(t1~ z-pPr37Dg^n%rkWL2;p;Wo%X9P+qcj|@$hiAh{AKIPkQ)OeE%-fJBH3NC+dTZeDFpf zA2@XcC@?!1Z{!1A+i0LU5Uxm9s5oxQN$6p663zNFEy+dn$H!H;ZB9Wmy%6cvF&3af zgR_M}13bnL=%%df(V!s$He`c{pdmvbA{Tld(EJXm&4kg3c+*yNh|ofkmim?YVu-8y zTW4rkaQSe6#u8xz3COgXAznf6onxGeVFL<6_5aDr9*tf-+=L}jQoWo|O4butPlpd+ z_~%X}%A{);3s8>9_-p9ippTM)fg?$>3MB;Vp~>f)$$DgSAy^E?W1PHKNCftzli%u; z?Y|gE_2$dNkenuQ>2|#uVWO_XKufH!X5%TLeLSYCXBrA%*8_UDVqeG3W43ch#gDz(f#=YB5{WU$N)f((|qxxM=BiCF#S<|?6iRG0f627LR=~_9&Ky|4Xqy zi7g8D;#c8(HMS_WhP{FFH?c*rx7Y=o|HOXA`7c^B)3g*V6=vttv|^l()<&~vZH)FI z&bMjXaQ!3g8=QZuoyPfh+IKiVqn*L|*(i!XM#YQ+&KZki8?+1-ot-nhn6;WZ|B-Um zrF_bx53)OzJ9g<3)0N9877gp}u~)cy1B)4QS8-dGanFe1ZCUSv;n{6je&L9F+Ok4+ zc^Gs>r31gtVCW7NzIhLEM%;6%dvQ~ylt0P_PMtFQewID8e0nL%FQMbe@`t9DvoY#8 zULEgK$FkYv{=wO^GqYfa-h5s<(sOHg7CO4rk@7@C?$Xhrj?wDq#M5h(1Mm{h(ph`f zh26&bvB7LOD`w+a344&urJLAGtlxU`SG&Rb%}{x!X?LoejM64*i>y1#kf-peEw@~} zYPr~~9kX0CnRlXeq^%!I*Uo9@wM$x)_PzEaO7?>GgZ7hlR{L4|Mf+6?Xcw{6c}BZi z%hmFwckSPrL`dn3BlQU^nWeH0tQ#B12BCcKVH4S8b{{Kei`ZY-YVAhNqqWo8 zYdf@$wa>Hzi1AjIreZYXGh>U4D-=hXio=Y-3_mjTP^gJ4#!e&B7GuYY_^xzEV;M_?ra3|Qe%I>o~lZN-fSxAd(^S=3U|AyyU*dMYgETHm4#KEG$nUZ;B;v3N(R4>X$$ugI=fVZ~r~O{_OFjo;C;u;^b$k(S&f-o_)W{tC z&tp%Z?kvZBAJX2Btzc_$wh`N5T+4>rZ`oG%63#oTEgNYJ-zY9~%SFhvG3C#Gh6fOa zy_rvT7gAzx-GleCE#KzT*gmpVZrL%Izr*0$-b3y}XAEK5c@X{^a$%n3T>6sAGZ?R( ze)=syouM)uiLF2W4-4CqaUPi>^SMIdQ~6Wwx3_*ncj&A$e0Rn>LBB1hl-?M#%&62t zTQ(a4ALg@%a*o2Gw{0c1m)Uu?o1J6}*=)8)rLr9xi+g77QmOZ3?eI)?PSTyRNM{b~ zg)_PudI!y?qR{D`qK$l~d`N+nTMEY+B{{_mw89sx5 zJAH*J28IyK_{LiIC^zigOeNK#PI%S|Z0FhYc#HFJEW)-4es5s4*m|N2bFlZr6Zd4J z)IO7$n-67Z*2NyIhl<~fyN{~LRGVMKM)ibJLZKNdMjzXTH-=gg)_CD}>Z!0J|N8h- L`aRmZ^vVARGc4GK literal 0 HcmV?d00001 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..39c0c10 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,814 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { buildImageSearchCandidates, getAllItems, getPopularResults, searchResultsByName, trackResultAdded } from './marathonApi'; +import type { SearchItem, SearchResult, TodoItem, UpgradeLevel, UpgradeSearchResult } from './types'; + +const STORAGE_KEY = 'marathon.todo.items'; +const SHARE_PARAM = 'l'; +const SHARE_VERSION = '1'; +const RARITY_GLOW_COLORS: Record = { + standard: '#CACAD6', + enhanced: '#00FF7D', + deluxe: '#21B4FA', + superior: '#E912F6', + prestige: '#FFF40C', +}; + +function getRarityGlowColor(rarity?: string): string | null { + if (!rarity) { + return null; + } + + return RARITY_GLOW_COLORS[rarity.trim().toLowerCase()] ?? null; +} + +function isUpgradeResult(result: SearchResult): result is UpgradeSearchResult { + return 'isUpgrade' in result; +} + +function ItemIcon({ + src, + alt, + className, + size = 32, +}: { + src: string; + alt: string; + className?: string; + size?: number; +}) { + const [candidateIndex, setCandidateIndex] = useState(0); + const candidates = useMemo(() => buildImageSearchCandidates(src), [src]); + const currentCandidate = candidates[candidateIndex] ?? src; + + useEffect(() => { + setCandidateIndex(0); + }, [src]); + + return ( + {alt} { + if (candidates.length <= 1) { + return; + } + + setCandidateIndex((current) => Math.min(current + 1, candidates.length - 1)); + }} + /> + ); +} + +function loadTodoItems(): TodoItem[] { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) { + return []; + } + + const parsed = JSON.parse(raw) as unknown; + if (!Array.isArray(parsed)) { + return []; + } + + return parsed + .filter((row): row is TodoItem => { + return ( + typeof row === 'object' && + row !== null && + typeof row.id === 'string' && + typeof row.slug === 'string' && + typeof row.name === 'string' && + typeof row.iconUrl === 'string' && + (row.rarity === undefined || typeof row.rarity === 'string') && + typeof row.quantity === 'number' && + typeof row.completed === 'boolean' + ); + }) + .map((item) => ({ + ...item, + rarity: typeof item.rarity === 'string' ? item.rarity : undefined, + quantity: Math.max(1, Math.floor(item.quantity)), + })); + } catch { + return []; + } +} + +function toBase64Url(value: string): string { + const bytes = new TextEncoder().encode(value); + let binary = ''; + + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); +} + +function fromBase64Url(value: string): string | null { + try { + const normalized = value.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '='); + const binary = atob(padded); + const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); + return new TextDecoder().decode(bytes); + } catch { + return null; + } +} + +function encodeListForUrl(items: TodoItem[]): string { + if (items.length === 0) { + return ''; + } + + const payload = items + .map((item) => { + const quantity = Math.max(1, Math.floor(item.quantity)); + return `${encodeURIComponent(item.slug)},${quantity},${item.completed ? 1 : 0}`; + }) + .join(';'); + + return toBase64Url(`${SHARE_VERSION}|${payload}`); +} + +function decodeListFromUrl(encoded: string, allItems: SearchItem[]): TodoItem[] | null { + const decoded = fromBase64Url(encoded); + if (!decoded) { + return null; + } + + const [version, rawItems = ''] = decoded.split('|', 2); + if (version !== SHARE_VERSION) { + return null; + } + + if (!rawItems) { + return []; + } + + const bySlug = new Map(allItems.map((item) => [item.slug, item])); + + const mapped = rawItems + .split(';') + .map((entry, index): TodoItem | null => { + const [rawSlug, rawQuantity, rawCompleted] = entry.split(',', 3); + if (!rawSlug) { + return null; + } + + const slug = decodeURIComponent(rawSlug); + const match = bySlug.get(slug); + if (!match) { + return null; + } + + const quantity = Math.max(1, Number(rawQuantity) || 1); + const completed = rawCompleted === '1'; + + return { + id: `${slug}-${index}`, + slug: match.slug, + name: match.name, + iconUrl: match.iconUrl, + rarity: match.rarity, + quantity, + completed, + }; + }) + .filter((item): item is TodoItem => item !== null); + + return mapped; +} + +export default function App() { + const [minimalMode, setMinimalMode] = useState(false); + const [query, setQuery] = useState(''); + const [results, setResults] = useState([]); + const [todoItems, setTodoItems] = useState(() => loadTodoItems()); + const [searchLoading, setSearchLoading] = useState(false); + const [searchError, setSearchError] = useState(null); + const [popularResults, setPopularResults] = useState([]); + const [searchFocused, setSearchFocused] = useState(false); + const [popularLoading, setPopularLoading] = useState(false); + const [actionMessage, setActionMessage] = useState(null); + const [highlightColorsBySlug, setHighlightColorsBySlug] = useState>({}); + const highlightTimeoutsRef = useRef>({}); + + useEffect(() => { + let cancelled = false; + + const initializeCatalogAndSharedList = async () => { + const sharedState = new URL(window.location.href).searchParams.get(SHARE_PARAM); + + try { + const allItems = await getAllItems(); + if (cancelled) { + return; + } + + const rarityBySlug = new Map(allItems.map((item) => [item.slug, item.rarity])); + setTodoItems((prevItems) => + prevItems.map((item) => ({ + ...item, + rarity: item.rarity ?? rarityBySlug.get(item.slug), + })), + ); + + if (!sharedState) { + return; + } + + const decodedItems = decodeListFromUrl(sharedState, allItems); + if (decodedItems) { + setTodoItems(decodedItems); + } + } catch { + if (!cancelled && sharedState) { + setSearchError('Failed to load shared list.'); + } + } + }; + + void initializeCatalogAndSharedList(); + + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(todoItems)); + + const url = new URL(window.location.href); + const encoded = encodeListForUrl(todoItems); + if (encoded) { + url.searchParams.set(SHARE_PARAM, encoded); + } else { + url.searchParams.delete(SHARE_PARAM); + } + + const nextUrl = `${url.pathname}${url.search}${url.hash}`; + const currentUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`; + + if (nextUrl !== currentUrl) { + window.history.replaceState(null, '', nextUrl); + } + }, [todoItems]); + + useEffect(() => { + if (!actionMessage) { + return; + } + + const timeoutId = window.setTimeout(() => setActionMessage(null), 2200); + return () => window.clearTimeout(timeoutId); + }, [actionMessage]); + + useEffect(() => { + return () => { + for (const timeoutId of Object.values(highlightTimeoutsRef.current)) { + window.clearTimeout(timeoutId); + } + }; + }, []); + + useEffect(() => { + const trimmedQuery = query.trim(); + if (!trimmedQuery) { + setResults([]); + setSearchError(null); + return; + } + + const timeoutId = window.setTimeout(async () => { + setSearchLoading(true); + setSearchError(null); + + try { + const matchedResults = await searchResultsByName(trimmedQuery, 5); + setResults(matchedResults); + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + setSearchError(message); + setResults([]); + } finally { + setSearchLoading(false); + } + }, 250); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [query]); + + const completedCount = useMemo(() => todoItems.filter((item) => item.completed).length, [todoItems]); + + function triggerTodoHighlights(slugs: string[], color: string): void { + const uniqueSlugs = Array.from(new Set(slugs)); + if (uniqueSlugs.length === 0) { + return; + } + + setHighlightColorsBySlug((prev) => { + const next = { ...prev }; + for (const slug of uniqueSlugs) { + delete next[slug]; + } + return next; + }); + + window.requestAnimationFrame(() => { + setHighlightColorsBySlug((prev) => { + const next = { ...prev }; + for (const slug of uniqueSlugs) { + next[slug] = color; + } + return next; + }); + }); + + for (const slug of uniqueSlugs) { + const existingTimeoutId = highlightTimeoutsRef.current[slug]; + if (existingTimeoutId) { + window.clearTimeout(existingTimeoutId); + } + + highlightTimeoutsRef.current[slug] = window.setTimeout(() => { + setHighlightColorsBySlug((prev) => { + if (!prev[slug]) { + return prev; + } + + const next = { ...prev }; + delete next[slug]; + return next; + }); + delete highlightTimeoutsRef.current[slug]; + }, 1500); + } + } + + function addTodoItem(item: SearchItem): void { + setTodoItems((prevItems) => { + const existing = prevItems.find((entry) => entry.slug === item.slug); + if (existing) { + return prevItems.map((entry) => + entry.slug === item.slug ? { ...entry, quantity: entry.quantity + 1 } : entry, + ); + } + + return [ + { + id: `${item.slug}-${Date.now()}`, + slug: item.slug, + name: item.name, + iconUrl: item.iconUrl, + rarity: item.rarity, + quantity: 1, + completed: false, + }, + ...prevItems, + ]; + }); + + setQuery(''); + setResults([]); + } + + function addUpgradeLevelSalvage(upgrade: UpgradeSearchResult, level: UpgradeLevel): void { + if (level.salvage.length === 0) { + return; + } + + setTodoItems((prevItems) => { + const nextItems = [...prevItems]; + + for (const salvageEntry of level.salvage) { + const { slug } = salvageEntry; + const amount = Math.max(1, Math.floor(salvageEntry.amount)); + const existingIndex = nextItems.findIndex((entry) => entry.slug === slug); + + if (existingIndex >= 0) { + nextItems[existingIndex] = { + ...nextItems[existingIndex], + quantity: nextItems[existingIndex].quantity + amount, + }; + continue; + } + + nextItems.unshift({ + id: `${slug}-${Date.now()}-${amount}`, + slug, + name: salvageEntry.name, + iconUrl: salvageEntry.iconUrl, + rarity: salvageEntry.rarity, + quantity: amount, + completed: false, + }); + } + + return nextItems; + }); + + triggerTodoHighlights( + level.salvage.map((entry) => entry.slug), + upgrade.factionColor, + ); + + setQuery(''); + setResults([]); + } + + function addSearchResult(result: SearchResult): void { + if (isUpgradeResult(result)) { + return; + } + + void trackResultAdded(result); + addTodoItem(result); + } + + async function handleSearchFocus(): Promise { + setSearchFocused(true); + + if (query.trim()) { + return; + } + + setPopularLoading(true); + try { + const popular = await getPopularResults(5); + setPopularResults(popular); + } catch { + setPopularResults([]); + } finally { + setPopularLoading(false); + } + } + + function updateQuantity(id: string, quantity: number): void { + setTodoItems((prevItems) => + prevItems.map((item) => (item.id === id ? { ...item, quantity: Math.max(1, quantity) } : item)), + ); + } + + function incrementQuantity(id: string): void { + setTodoItems((prevItems) => + prevItems.map((item) => (item.id === id ? { ...item, quantity: item.quantity + 1 } : item)), + ); + } + + function decrementQuantity(id: string): void { + setTodoItems((prevItems) => + prevItems.map((item) => + item.id === id ? { ...item, quantity: Math.max(1, item.quantity - 1) } : item, + ), + ); + } + + function toggleCompleted(id: string): void { + setTodoItems((prevItems) => + prevItems.map((item) => (item.id === id ? { ...item, completed: !item.completed } : item)), + ); + } + + function deleteItem(id: string): void { + setTodoItems((prevItems) => prevItems.filter((item) => item.id !== id)); + } + + async function shareList(): Promise { + try { + await navigator.clipboard.writeText(window.location.href); + setActionMessage('Share link copied.'); + } catch { + setActionMessage('Failed to copy share link.'); + } + } + + function resetList(): void { + setTodoItems([]); + setActionMessage('List reset.'); + } + + return ( +

+
+
+
+

marathon.todo

+ {!minimalMode ?

Plan what to loot (or do) in your next Marathon raid.

: null} +
+ {!minimalMode ? ( + + ) : ( + + )} +
+
+ + {!minimalMode ? ( +
+ +
+ setQuery(event.target.value)} + onFocus={() => void handleSearchFocus()} + onBlur={() => window.setTimeout(() => setSearchFocused(false), 120)} + placeholder="Type an item name..." + /> + + {searchLoading ?

Searching...

: null} + {searchError ?

Search failed: {searchError}

: null} + + {query.trim() && !searchLoading && !searchError ? ( +
    + {results.length === 0 ?
  • No matching items.
  • : null} + {results.map((item) => ( +
  • + {isUpgradeResult(item) ? ( +
    +
    + + + + {item.name} + + {item.factionName} upgrade + +
    +
    + Level: + {item.levels.map((level) => ( + + ))} +
    +
    + ) : ( + + )} +
  • + ))} +
+ ) : null} + + {!query.trim() && searchFocused ? ( + <> +
    +
  • Popular picks
  • + {popularLoading ?
  • Loading popular picks...
  • : null} + {!popularLoading && popularResults.length === 0 ?
  • No popular picks yet.
  • : null} + {!popularLoading + ? popularResults.map((item) => ( +
  • + {isUpgradeResult(item) ? ( +
    +
    + + + + {item.name} + + {item.factionName} upgrade + +
    +
    + Level: + {item.levels.map((level) => ( + + ))} +
    +
    + ) : ( + + )} +
  • + )) + : null} +
+ + ) : null} +
+
+ ) : null} + +
+
+

To-do list

+ {!minimalMode && todoItems.length > 0 ? ( +
+ + +
+ ) : null} +
+ {!minimalMode ? ( +

+ Completed {completedCount} / {todoItems.length} +

+ ) : null} + {!minimalMode && actionMessage ?

{actionMessage}

: null} + + {todoItems.length === 0 ? ( +

{minimalMode ? 'No items yet.' : 'No items yet. Add one from search.'}

+ ) : null} + +
    + {todoItems.map((item) => ( +
  • toggleCompleted(item.id)} + > + + + + + + {item.name} + + + {minimalMode ? ( + x{item.quantity} + ) : ( + <> +
    event.stopPropagation()} + > + + updateQuantity(item.id, Number(event.target.value) || 1)} + aria-label={`Quantity for ${item.name}`} + /> + +
    + + + + )} +
  • + ))} +
+
+ + {!minimalMode ? ( +
+
+

Privacy Policy

+

+ This site stores your to-do list locally in your browser so your list can persist between visits and be + shared by link. No accounts are required and no personal data is sold. +

+
+ +
+

Contact

+

+ E-mail: alshuriga@gmail.com +

+
+ +
+ +

Marathon™ is owned by Bungie, Inc. This website is unofficial and has no affiliation with or endorsement from Bungie.

+
+
+ ) : null} +
+ ); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..8f7da6f --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/frontend/src/marathonApi.ts b/frontend/src/marathonApi.ts new file mode 100644 index 0000000..53b4de3 --- /dev/null +++ b/frontend/src/marathonApi.ts @@ -0,0 +1,157 @@ +import type { SearchItem, SearchResult, UpgradeSearchResult } from './types'; + +const IMAGE_EXTENSION_RE = /\.(png|jpe?g|gif|webp)(?:[?#].*)?$/i; +const IMAGE_FALLBACK_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp'] as const; + +interface CatalogResponse { + updatedAt: string; + items: SearchItem[]; + upgrades: UpgradeSearchResult[]; +} + +interface PopularResponse { + picks: SearchResult[]; +} + +let catalogPromise: Promise | null = null; + +function normalizeCatalog(payload: CatalogResponse): CatalogResponse { + const normalizedUpgrades = payload.upgrades.map((upgrade) => { + if (Array.isArray(upgrade.levels)) { + return upgrade; + } + + const legacyUpgrade = upgrade as UpgradeSearchResult & { salvage?: UpgradeSearchResult['levels'][number]['salvage'] }; + const salvage = Array.isArray(legacyUpgrade.salvage) ? legacyUpgrade.salvage : []; + + return { + ...upgrade, + levels: [{ level: 1, salvage }], + }; + }); + + return { + ...payload, + upgrades: normalizedUpgrades, + }; +} + +function scoreResult(name: string, query: string): number { + const lowerName = name.toLowerCase(); + + if (lowerName === query) { + return 0; + } + + if (lowerName.startsWith(query)) { + return 1; + } + + const index = lowerName.indexOf(query); + if (index >= 0) { + return 2 + index; + } + + return Number.POSITIVE_INFINITY; +} + +async function fetchCatalog(): Promise { + const response = await fetch('/api/catalog'); + if (!response.ok) { + throw new Error(`Failed to fetch catalog: ${response.status}`); + } + + const payload = (await response.json()) as CatalogResponse; + if (!Array.isArray(payload.items) || !Array.isArray(payload.upgrades)) { + throw new Error('Invalid catalog payload'); + } + + return normalizeCatalog(payload); +} + +async function getCatalog(): Promise { + if (!catalogPromise) { + catalogPromise = fetchCatalog().catch((error: unknown) => { + catalogPromise = null; + throw error; + }); + } + + return catalogPromise; +} + +export function buildImageSearchCandidates(iconUrl: string): string[] { + if (!iconUrl) { + return []; + } + + if (IMAGE_EXTENSION_RE.test(iconUrl)) { + return [iconUrl]; + } + + const candidates = [iconUrl, ...IMAGE_FALLBACK_EXTENSIONS.map((ext) => `${iconUrl}.${ext}`)]; + return Array.from(new Set(candidates)); +} + +export async function getAllItems(): Promise { + const catalog = await getCatalog(); + return catalog.items; +} + +export async function searchResultsByName(query: string, limit = 5): Promise { + const normalizedQuery = query.trim().toLowerCase(); + if (!normalizedQuery) { + return []; + } + + const catalog = await getCatalog(); + + const candidates: SearchResult[] = [ + ...catalog.items.filter((item) => item.name.toLowerCase().includes(normalizedQuery)), + ...catalog.upgrades.filter((upgrade) => upgrade.name.toLowerCase().includes(normalizedQuery)), + ]; + + return candidates + .sort((a, b) => { + const scoreDiff = scoreResult(a.name, normalizedQuery) - scoreResult(b.name, normalizedQuery); + if (scoreDiff !== 0) { + return scoreDiff; + } + + return a.name.localeCompare(b.name); + }) + .slice(0, limit); +} + +export async function getPopularResults(limit = 5): Promise { + const response = await fetch(`/api/popularity?limit=${Math.max(1, Math.floor(limit))}`); + if (!response.ok) { + throw new Error(`Failed to fetch popular picks: ${response.status}`); + } + + const payload = (await response.json()) as PopularResponse; + if (!Array.isArray(payload.picks)) { + throw new Error('Invalid popular picks payload'); + } + + return payload.picks; +} + +export async function trackResultAdded(result: SearchResult): Promise { + const payload = { + type: 'isUpgrade' in result ? 'upgrade' : 'item', + slug: result.slug, + }; + + try { + await fetch('/api/popularity/track', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + } catch { + // Ignore analytics failures so add-to-list UX is never blocked. + } +} diff --git a/frontend/src/styles.css b/frontend/src/styles.css new file mode 100644 index 0000000..940e492 --- /dev/null +++ b/frontend/src/styles.css @@ -0,0 +1,509 @@ +@font-face { + font-family: 'Ki-Regular'; + src: url('/fonts/Ki-Regular.otf') format('opentype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Ki-Bold'; + src: url('/fonts/Ki-Bold.otf') format('opentype'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +:root { + --ui-dark: #1e2125; + --ui-dark-elevated: #212529; + --ui-accent: #caf61d; + --color-text-main: #e8edf7; + --color-text-input: #f2f6ff; + --color-text-muted: #d6dbe8; + --color-text-done: #8d9bbb; + --color-border: #444444; + --color-hover: #31373d; + --color-error: #ff9aa2; + --color-rarity-fallback: #6f6f6f; + --shadow-card: 0 10px 30px rgba(0, 0, 0, 0.45); + --shadow-dropdown: 0 14px 34px rgba(2, 6, 12, 0.65); + --control-height: 34px; + font-family: 'Ki-Regular', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: var(--color-text-main); + background: var(--ui-dark); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + color: var(--color-text-main); + background: var(--ui-dark); +} + +body, +button, +input, +textarea, +select { + font-family: 'Ki-Regular', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.app { + max-width: 760px; + margin: 0 auto; + padding: 1rem; + min-height: 100vh; + display: flex; + flex-direction: column; + gap: 1rem; +} + +h1, +h2, +p { + margin: 0; +} + +h1, +h2 { + font-family: 'Ki-Bold', 'Ki-Regular', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: var(--ui-accent); +} + +label { + color: var(--ui-accent); +} + +.header-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.list-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; +} + +.header-actions { + display: flex; + gap: 0.5rem; +} + +.header-btn { + border: 1px solid var(--color-border); + background: var(--ui-dark-elevated); + color: var(--color-text-main); + border-radius: 0; + height: var(--control-height); + min-height: var(--control-height); + padding: 0 0.75rem; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.header-btn:hover { + background: var(--color-hover); +} + +.header-btn.danger { + border-color: var(--color-border); + background: var(--ui-dark-elevated); +} + +.header-btn.danger:hover { + background: var(--color-hover); +} + +.card { + background: var(--ui-dark-elevated); + border: 1px solid var(--color-border); + border-radius: 0; + padding: 1rem; + display: grid; + gap: 0.75rem; + box-shadow: var(--shadow-card); +} + +input[type='search'], +input[type='number'] { + width: 100%; + border: 1px solid var(--color-border); + border-radius: 0; + padding: 0.5rem; + color: var(--color-text-input); + background: var(--ui-dark); +} + +.results, +.todo-list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 0.5rem; +} + +.search-box { + position: relative; +} + +.search-status { + margin-top: 0.5rem; +} + +.results { + position: absolute; + top: calc(100% + 0.5rem); + left: 0; + right: 0; + z-index: 20; + max-height: 320px; + overflow-y: auto; + padding: 0.75rem; + border: 1px solid var(--color-border); + border-radius: 0; + background: var(--ui-dark); + box-shadow: var(--shadow-dropdown); +} + +.results button { + width: 100%; + border: 1px solid var(--color-border); + background: var(--ui-dark-elevated); + color: var(--color-text-main); + padding: 0.5rem; + border-radius: 0; + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.result-row { + width: 100%; + border: 1px solid var(--color-border); + background: var(--ui-dark-elevated); + color: var(--color-text-main); + padding: 0.5rem; + border-radius: 0; + display: grid; + gap: 0.5rem; +} + +.result-main { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.result-icon-frame { + position: relative; + width: 48px; + height: 48px; + border: none; + background: linear-gradient( + 135deg, + color-mix(in srgb, var(--rarity-glow-color, var(--color-rarity-fallback)) 30%, transparent), + color-mix(in srgb, var(--rarity-glow-color, var(--color-rarity-fallback)) 10%, transparent) + ); + border-radius: 0; + padding: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + overflow: visible; +} + +.result-icon-frame::before, +.todo-icon-frame::before { + content: none; +} + +.result-icon-frame > img, +.todo-icon-frame > img { + position: relative; + z-index: 1; + width: 100%; + height: 100%; + object-fit: contain; + display: block; +} + +.todo-icon-frame { + position: relative; + width: 52px; + height: 52px; + border: none; + background: linear-gradient( + 135deg, + color-mix(in srgb, var(--rarity-glow-color, var(--color-rarity-fallback)) 25%, transparent), + color-mix(in srgb, var(--rarity-glow-color, var(--color-rarity-fallback)) 10%, transparent) + ); + border-radius: 0; + padding: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + overflow: visible; +} + +.result-name { + margin-right: auto; + text-align: left; +} + +.upgrade-tag { + display: inline-block; + padding: 0.2rem 0.8rem; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; + color: var(--ui-accent); +} + +.results button:hover { + background: var(--color-hover); + border-color: var(--color-border); +} + +.upgrade-levels { + display: flex; + flex-wrap: nowrap; + gap: 0.35rem; + overflow-x: auto; + align-items: center; +} + +.level-label { + font-size: 0.75rem; + color: var(--ui-accent); + white-space: nowrap; +} + +.level-button { + width: 28px !important; + border: 1px solid var(--color-border); + border-radius: 0; + background: var(--ui-dark-elevated); + color: var(--color-text-main); + height: 28px; + min-width: 28px; + max-width: 28px; + padding: 0; + font-size: 0.7rem; + cursor: pointer; + flex: 0 0 28px; +} + +.level-button:hover:not(:disabled) { + background: var(--color-hover); +} + +.level-button:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.todo-item { + border: 1px solid var(--color-border); + border-radius: 0; + padding: 0.65rem; + display: grid; + grid-template-columns: auto 1fr auto auto; + align-items: center; + gap: 0.5rem; + background: var(--ui-dark-elevated); + cursor: pointer; + transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease; +} + +.todo-item:hover { + background: var(--color-hover); + border-color: var(--color-border); + box-shadow: inset 0 0 0 1px var(--color-hover); +} + +.minimal-item { + grid-template-columns: auto 1fr auto; + cursor: pointer; +} + +.minimal-item:hover { + background: var(--color-hover); + border-color: var(--color-border); + box-shadow: inset 0 0 0 1px var(--color-hover); +} + +.minimal-qty { + color: var(--ui-accent); + font-weight: 700; + min-width: 52px; + text-align: right; +} + +.todo-item.highlighted { + animation: todo-highlight 1.5s ease-out; +} + +.item-name { + overflow-wrap: anywhere; +} + +.quantity-field { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.quantity-field input { + width: 72px; + text-align: center; + -moz-appearance: textfield; +} + +.quantity-field input::-webkit-outer-spin-button, +.quantity-field input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.qty-arrow { + width: var(--control-height); + height: var(--control-height); + border: 1px solid var(--color-border); + border-radius: 0; + background: var(--ui-dark-elevated); + color: var(--color-text-main); + font-size: 1rem; + line-height: 1; + cursor: pointer; +} + +.qty-arrow:hover { + background: var(--color-hover); +} + +.todo-item.done .item-name { + text-decoration: line-through; + color: var(--color-text-done); +} + +.todo-item.done .minimal-qty, +.todo-item.done .quantity-field input { + text-decoration: line-through; + color: var(--color-text-done); +} + +.todo-item.done { + opacity: 0.6; +} + +@keyframes todo-highlight { + 0% { + background: color-mix(in srgb, var(--highlight-color, var(--color-rarity-fallback)) 24%, var(--ui-dark-elevated)); + } + 35% { + background: color-mix(in srgb, var(--highlight-color, var(--color-rarity-fallback)) 16%, var(--ui-dark-elevated)); + } + 100% { + background: var(--ui-dark-elevated); + } +} + +.delete { + border: 1px solid var(--color-border); + background: var(--ui-dark-elevated); + color: var(--color-text-main); + border-radius: 0; + height: var(--control-height); + min-height: var(--control-height); + padding: 0 0.75rem; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.delete:hover { + background: var(--color-hover); +} + +.status { + color: var(--ui-accent); + font-size: 0.9rem; +} + +.status.error { + color: var(--color-error); +} + +.legal-footer { + margin-top: auto; + border: 1px solid var(--color-border); + background: var(--ui-dark-elevated); + padding: 0.55rem 0.65rem; + display: grid; + gap: 0.4rem; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +.legal-section { + display: grid; + gap: 0.15rem; +} + +.legal-section h2 { + font-size: 0.72rem; + letter-spacing: 0.02em; +} + +.legal-section p { + color: var(--color-text-muted); + font-size: 0.68rem; + line-height: 1.25; +} + +.legal-section a { + color: var(--ui-accent); +} + +.legal-section a:hover { + text-decoration: underline; +} + +@media (max-width: 640px) { + .header-top { + flex-direction: column; + align-items: flex-start; + } + + .list-header { + flex-direction: column; + align-items: flex-start; + } + + .todo-item { + grid-template-columns: auto 1fr; + } + + .minimal-item { + grid-template-columns: auto 1fr auto; + } + + .quantity-field, + .delete { + grid-column: 1 / -1; + } +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..89add4e --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,43 @@ +export interface SearchItem { + id: string; + slug: string; + name: string; + iconUrl: string; + rarity?: string; +} + +export interface UpgradeSalvageItem { + slug: string; + amount: number; + name: string; + iconUrl: string; + rarity?: string; +} + +export interface UpgradeLevel { + level: number; + salvage: UpgradeSalvageItem[]; +} + +export interface UpgradeSearchResult { + id: string; + slug: string; + name: string; + iconUrl: string; + factionName: string; + factionColor: string; + levels: UpgradeLevel[]; + isUpgrade: true; +} + +export type SearchResult = SearchItem | UpgradeSearchResult; + +export interface TodoItem { + id: string; + slug: string; + name: string; + iconUrl: string; + rarity?: string; + quantity: number; + completed: boolean; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ad05e02 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1729 @@ +{ + "name": "marathon-todo", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "marathon-todo", + "version": "0.1.0", + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.2", + "typescript": "^5.6.2", + "vite": "^5.4.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.325", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", + "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2ad5b81 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "marathon-todo", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -p tsconfig.json && vite build", + "preview": "vite preview", + "proxy": "node backend/server.js", + "dev:frontend": "vite", + "dev:backend": "node backend/server.js", + "dev:all": "node scripts/dev-all.mjs" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.2", + "typescript": "^5.6.2", + "vite": "^5.4.8" + } +} diff --git a/project.md b/project.md new file mode 100644 index 0000000..5ebce8d --- /dev/null +++ b/project.md @@ -0,0 +1,29 @@ +# marathon.todo + +A web application that allows users to create a to-do list of what to loot (or do) in raid in Bungie's Marathon video game. + +## Tech Stack +- React +- TypeScript +- Frontend-only + +## Data Source +https://marathondb.gg/ provides a public API with all the necessary game data. + +The `app.js` file downloaded from this site can be used to generate a TypeScript client for the API. + +## Project Rules +- Save all added features and changes (except very minor ones) to the `CHANGELOG.md` file. +- Always update the `README.md` file accordingly. + +## Initial Features +- User can search for an item by name. +- Search results display up to 5 relevant items with their icons. +- User can add an item to the to-do list by clicking on it. +- User can specify the quantity for each added item. +- User can mark items as completed. +- User can delete items from the to-do list. + +## UI +- Keep the UI very simple for now. +- Minimal styling. \ No newline at end of file diff --git a/scripts/dev-all.mjs b/scripts/dev-all.mjs new file mode 100644 index 0000000..e25cbe7 --- /dev/null +++ b/scripts/dev-all.mjs @@ -0,0 +1,62 @@ +import { spawn } from 'node:child_process'; + +function spawnNpmScript(scriptName) { + const npmExecPath = process.env.npm_execpath; + if (npmExecPath) { + return spawn(process.execPath, [npmExecPath, 'run', scriptName], { + stdio: 'inherit', + env: process.env, + }); + } + + const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + return spawn(npmCommand, ['run', scriptName], { + stdio: 'inherit', + env: process.env, + shell: process.platform === 'win32', + }); +} + +const backend = spawnNpmScript('dev:backend'); +const frontend = spawnNpmScript('dev:frontend'); + +let shuttingDown = false; + +function shutdown(code = 0) { + if (shuttingDown) { + return; + } + + shuttingDown = true; + + if (backend && !backend.killed) { + backend.kill('SIGTERM'); + } + + if (frontend && !frontend.killed) { + frontend.kill('SIGTERM'); + } + + process.exit(code); +} + +backend.on('exit', (code) => { + shutdown(code ?? 0); +}); + +frontend.on('exit', (code) => { + shutdown(code ?? 0); +}); + +backend.on('error', (error) => { + console.error('[dev:all] backend failed:', error); + shutdown(1); +}); + +frontend.on('error', (error) => { + console.error('[dev:all] frontend failed:', error); + shutdown(1); +}); + +process.on('SIGINT', () => shutdown(0)); +process.on('SIGTERM', () => shutdown(0)); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3bc4423 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "allowImportingTsExtensions": false, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["frontend/src"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..971513a --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + root: 'frontend', + plugins: [react()], + build: { + outDir: '../dist', + emptyOutDir: true, + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:8787', + changeOrigin: true, + }, + '/health': { + target: 'http://localhost:8787', + changeOrigin: true, + }, + }, + }, +});