import {setupPopover} from './popover'; const roiCache: Record> = {}; async function getROI(cx: string) { const response = await fetch(`/roi_${cx.toLowerCase()}.json`); const lastModified = new Date(response.headers.get('last-modified')!); const profits = await response.json(); return {lastModified, profits}; } const lowVolume = document.querySelector('input#low-volume') as HTMLInputElement; const cxSelect = document.querySelector('select#cx') as HTMLSelectElement; const expertise = { AGRICULTURE: 'agri', CHEMISTRY: 'chem', CONSTRUCTION: 'const', ELECTRONICS: 'elec', FOOD_INDUSTRIES: 'food ind', FUEL_REFINING: 'fuel', MANUFACTURING: 'mfg', METALLURGY: 'metal', RESOURCE_EXTRACTION: 'res ext', } as const; const expertiseSelect = document.querySelector('select#expertise') as HTMLSelectElement; for (const key of Object.keys(expertise)) { const option = document.createElement('option'); option.value = key; option.textContent = key.replace('_', ' ').toLowerCase(); expertiseSelect.appendChild(option); } const buildingSelect = document.querySelector('select#building') as HTMLSelectElement; const formatDecimal = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: 'lessPrecision'}).format; const formatWhole = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0}).format; let currentSortKey: keyof ProfitWithMetrics | 'outputs' = 'break_even'; let currentSortAsc: boolean = true; let headersInitialized = false; let metricControlsInitialized = false; // EXTREME DETAIL: Global state trackers to determine which pricing metric is applied to which column mathematically. type MetricType = 'vwap' | 'bid' | 'ask'; let capexMetric: MetricType = 'vwap'; let opexMetric: MetricType = 'vwap'; let revenueMetric: MetricType = 'vwap'; async function render() { const tbody = document.querySelector('tbody')!; tbody.innerHTML = ''; const cx = cxSelect.value; if (!roiCache[cx]) roiCache[cx] = getROI(cx); const {lastModified, profits} = await roiCache[cx]; // EXTREME DETAIL: We dynamically inject three select dropdowns prior to rendering the table. // We bind event listeners to them to update the global MetricType states and force a re-render. if (!metricControlsInitialized) { const controls = document.createElement('div'); controls.style.marginBottom = '15px'; controls.innerHTML = ` `; const table = document.querySelector('table'); if (table) table.parentNode?.insertBefore(controls, table); document.getElementById('capex-metric')!.addEventListener('change', (e) => { capexMetric = (e.target as HTMLSelectElement).value as MetricType; render(); }); document.getElementById('opex-metric')!.addEventListener('change', (e) => { opexMetric = (e.target as HTMLSelectElement).value as MetricType; render(); }); document.getElementById('revenue-metric')!.addEventListener('change', (e) => { revenueMetric = (e.target as HTMLSelectElement).value as MetricType; render(); }); metricControlsInitialized = true; } if (!headersInitialized) { const ths = document.querySelectorAll('th'); // Note that 'capex' and 'cost_per_day' here map dynamically to the newly derived states later in the file. const keys: (keyof ProfitWithMetrics | 'outputs')[] = [ 'outputs', 'expertise', 'profit_per_area', 'break_even', 'capex_val', 'opex_val', 'logistics_per_area', 'market_capacity_area' ]; ths.forEach((th, i) => { if (keys[i]) { th.style.cursor = 'pointer'; th.title = 'Click to sort'; th.addEventListener('click', () => { if (currentSortKey === keys[i]) { currentSortAsc = !currentSortAsc; } else { currentSortKey = keys[i]; currentSortAsc = keys[i] === 'break_even' ? true : false; } render(); }); } }); headersInitialized = true; } const buildingTickers = new Set(profits.map(p => p.building)); const buildings: {ticker: string, expertise: keyof typeof expertise}[] = Array.from(buildingTickers) .map((building) => ({ticker: building, expertise: profits.find(p => p.building === building)!.expertise})) .sort((a, b) => a.ticker.localeCompare(b.ticker)); let selectedBuilding = buildingSelect.value; let buildingFound = false; buildingSelect.innerHTML = ''; for (const building of buildings) if (expertiseSelect.value === '' || expertiseSelect.value === building.expertise) { const option = document.createElement('option'); option.value = building.ticker; option.textContent = building.ticker; if (building.ticker === selectedBuilding) { buildingFound = true; option.selected = true; } buildingSelect.appendChild(option); } if (!buildingFound) selectedBuilding = ''; const filteredProfits = profits.filter(p => { const volumeRatio = p.output_per_day / p.average_traded_7d; if (!lowVolume.checked && volumeRatio > 0.05) return false; if (expertiseSelect.value !== '' && p.expertise !== expertiseSelect.value) return false; if (selectedBuilding !== '' && p.building !== selectedBuilding) return false; return true; }); // EXTREME DETAIL: We map over the filtered array to compute the final derivation. // By executing this map BEFORE the sort algorithm runs, the columns dynamically organize themselves // perfectly around whichever permutations the user selected in the dropdowns. const profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => { const capex_val = p.capex[capexMetric]; const opex_val = p.opex[opexMetric]; const revenue_val = p.revenue[revenueMetric]; const profit_per_day = revenue_val - opex_val; const profit_per_area = profit_per_day / p.area; const break_even = profit_per_day > 0 ? (capex_val + 3 * opex_val) / profit_per_day : Infinity; return { ...p, capex_val, opex_val, revenue_val, profit_per_day, profit_per_area, break_even }; }); profitsWithMetrics.sort((a, b) => { let valA: any = a[currentSortKey as keyof ProfitWithMetrics]; let valB: any = b[currentSortKey as keyof ProfitWithMetrics]; if (currentSortKey === 'outputs') { valA = a.outputs.map(o => o.ticker).join(', '); valB = b.outputs.map(o => o.ticker).join(', '); } if (valA < valB) return currentSortAsc ? -1 : 1; if (valA > valB) return currentSortAsc ? 1 : -1; return 0; }); for (const p of profitsWithMetrics) { const volumeRatio = p.output_per_day / p.average_traded_7d; const tr = document.createElement('tr'); tr.innerHTML = ` ${p.outputs.map(o => o.ticker).join(', ')} ${expertise[p.expertise]} ${formatDecimal(p.profit_per_area)} ${formatDecimal(p.break_even)}d ${formatWhole(p.capex_val)} ${formatWhole(p.opex_val)} ${formatDecimal(p.logistics_per_area)} ${formatWhole(p.market_capacity_area)} `; const output = tr.querySelector('td')!; output.dataset.tooltip = p.recipe; const profitCell = tr.querySelectorAll('td')[2]; profitCell.dataset.tooltip = formatMatPrices(p.outputs, revenueMetric, p.runs_per_day) + '\n\n' + formatMatPrices(p.input_costs, opexMetric, p.runs_per_day) + '\n' + `(${formatWhole(p.revenue_val)} - ${formatWhole(p.opex_val)}) = ${formatDecimal(p.profit_per_day)}\n` + `${formatDecimal(p.profit_per_day)} / ${formatWhole(p.area)} area = ${formatDecimal(p.profit_per_area)}`; const marketCell = tr.querySelectorAll('td')[7]; marketCell.dataset.tooltip = `Market Capacity: ${formatWhole(p.average_traded_7d)} traded/day ÷ ${formatDecimal(p.output_per_day / p.area)} produced/day/area = ${formatWhole(p.market_capacity_area)} equivalent areas`; tbody.appendChild(tr); } } function color(n: number, low: number, high: number): string { const scale = Math.min(Math.max((n - low) / (high - low), 0), 1); return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`; } // EXTREME DETAIL: formatMatPrices relies on evaluating the user's selected metric ('vwap', 'bid', or 'ask'). // To avoid math crashes, if a user requests a Bid/Ask display for an item lacking that specific liquidity, // the fallback logic defaults to the standard 7-day VWAP. function formatMatPrices(matPrices: MatPrice[], metric: MetricType, runs_per_day: number): string { return matPrices.map(({ticker, amount, vwap_7d, bid, ask}) => { const val = metric === 'vwap' ? vwap_7d : metric === 'bid' ? (bid ?? vwap_7d) : (ask ?? vwap_7d); const daily_amount = amount * runs_per_day; return `${ticker}: ${formatDecimal(daily_amount)} × ${formatDecimal(val)} = ${formatWhole(daily_amount * val)}`; }).join('\n'); } setupPopover(); lowVolume.addEventListener('change', render); cxSelect.addEventListener('change', render); expertiseSelect.addEventListener('change', render); buildingSelect.addEventListener('change', render); render(); interface Metrics { vwap: number; bid: number; ask: number; } interface Profit { outputs: MatPrice[] recipe: string expertise: keyof typeof expertise building: string area: number capex: Metrics opex: Metrics revenue: Metrics input_costs: MatPrice[] runs_per_day: number logistics_per_area: number output_per_day: number average_traded_7d: number market_capacity_area: number } // Interface extension utilized for the dynamic frontend mapping array interface ProfitWithMetrics extends Profit { capex_val: number; opex_val: number; revenue_val: number; profit_per_day: number; profit_per_area: number; break_even: number; } interface MatPrice { ticker: string amount: number vwap_7d: number bid: number | null ask: number | null } interface Building { building_type: 'INFRASTRUCTURE' | 'PLANETARY' | 'PRODUCTION'; building_ticker: string; expertise: keyof typeof expertise; }