commit 1f20359fa3a4c28d827df2ed97ba68d9684deda6 Author: Oleksandr Shuryha Date: Thu Mar 26 11:33:03 2026 +0100 initial 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 0000000..275c923 Binary files /dev/null and b/backend/data/faction-assets/arachne.png differ diff --git a/backend/data/faction-assets/cyberacme.png b/backend/data/faction-assets/cyberacme.png new file mode 100644 index 0000000..b4068b2 Binary files /dev/null and b/backend/data/faction-assets/cyberacme.png differ diff --git a/backend/data/faction-assets/mida.png b/backend/data/faction-assets/mida.png new file mode 100644 index 0000000..b830188 Binary files /dev/null and b/backend/data/faction-assets/mida.png differ diff --git a/backend/data/faction-assets/nucaloric.png b/backend/data/faction-assets/nucaloric.png new file mode 100644 index 0000000..5838128 Binary files /dev/null and b/backend/data/faction-assets/nucaloric.png differ diff --git a/backend/data/faction-assets/sekiguchi.png b/backend/data/faction-assets/sekiguchi.png new file mode 100644 index 0000000..522b33d Binary files /dev/null and b/backend/data/faction-assets/sekiguchi.png differ diff --git a/backend/data/faction-assets/traxus.png b/backend/data/faction-assets/traxus.png new file mode 100644 index 0000000..058a7d7 Binary files /dev/null and b/backend/data/faction-assets/traxus.png differ diff --git a/backend/data/fonts/Ki-Bold.otf b/backend/data/fonts/Ki-Bold.otf new file mode 100644 index 0000000..9df9c96 Binary files /dev/null and b/backend/data/fonts/Ki-Bold.otf differ diff --git a/backend/data/fonts/Ki-Regular.otf b/backend/data/fonts/Ki-Regular.otf new file mode 100644 index 0000000..ad6e98c Binary files /dev/null and b/backend/data/fonts/Ki-Regular.otf differ 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 0000000..6dbe8ab Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/fonts/Ki-Bold.otf b/frontend/public/fonts/Ki-Bold.otf new file mode 100644 index 0000000..9df9c96 Binary files /dev/null and b/frontend/public/fonts/Ki-Bold.otf differ diff --git a/frontend/public/fonts/Ki-Regular.otf b/frontend/public/fonts/Ki-Regular.otf new file mode 100644 index 0000000..ad6e98c Binary files /dev/null and b/frontend/public/fonts/Ki-Regular.otf differ 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, + }, + }, + }, +});