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}; } type MetricType = 'vwap' | 'bid' | 'ask'; 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 formatSigFig = new Intl.NumberFormat(undefined, { notation: 'compact', maximumSignificantDigits: 3, }).format; if (localStorage.getItem('roi-cx')) cxSelect.value = localStorage.getItem('roi-cx')!; if (localStorage.getItem('roi-expertise')) expertiseSelect.value = localStorage.getItem('roi-expertise')!; if (localStorage.getItem('roi-low-volume')) lowVolume.checked = localStorage.getItem('roi-low-volume') === 'true'; let savedBuilding = localStorage.getItem('roi-building') || ''; let storedSortKey = localStorage.getItem('roi-sort-key') as any; if (storedSortKey === 'logistics_per_base') storedSortKey = 'normalized_logistics_per_base'; let currentSortKey: keyof ProfitWithMetrics | 'outputs' = storedSortKey || 'break_even'; let currentSortAsc: boolean = localStorage.getItem('roi-sort-asc') !== 'false'; let headersInitialized = false; let metricControlsInitialized = false; let capexMetric: MetricType = (localStorage.getItem('roi-capex-metric') as MetricType) || 'vwap'; let opexMetric: MetricType = (localStorage.getItem('roi-opex-metric') as MetricType) || 'vwap'; let revenueMetric: MetricType = (localStorage.getItem('roi-revenue-metric') as MetricType) || 'vwap'; let roundTripOption: string = localStorage.getItem('roi-round-trip') || 'omit'; let showNegativeProfit: boolean = localStorage.getItem('roi-show-negative') !== 'false'; let workingCapitalOption: string = localStorage.getItem('roi-working-capital-opt') || 'dynamic'; let targetPermitOption: string = localStorage.getItem('roi-target-permit') || '2'; let percentileMode: 'relative' | 'absolute' = (localStorage.getItem('roi-pct-mode') as 'relative' | 'absolute') || 'relative'; 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]; 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') as HTMLSelectElement).value = capexMetric; (document.getElementById('opex-metric') as HTMLSelectElement).value = opexMetric; (document.getElementById('revenue-metric') as HTMLSelectElement).value = revenueMetric; (document.getElementById('show-negative') as HTMLInputElement).checked = showNegativeProfit; (document.getElementById('round-trip') as HTMLSelectElement).value = roundTripOption; (document.getElementById('working-capital') as HTMLSelectElement).value = workingCapitalOption; (document.getElementById('target-permit') as HTMLSelectElement).value = targetPermitOption; (document.getElementById('percentile-mode') as HTMLSelectElement).value = percentileMode; 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(); }); document.getElementById('show-negative')!.addEventListener('change', (e) => { showNegativeProfit = (e.target as HTMLInputElement).checked; render(); }); document.getElementById('round-trip')!.addEventListener('change', (e) => { roundTripOption = (e.target as HTMLSelectElement).value; render(); }); document.getElementById('working-capital')!.addEventListener('change', (e) => { workingCapitalOption = (e.target as HTMLSelectElement).value; render(); }); document.getElementById('target-permit')!.addEventListener('change', (e) => { targetPermitOption = (e.target as HTMLSelectElement).value; render(); }); document.getElementById('percentile-mode')!.addEventListener('change', (e) => { percentileMode = (e.target as HTMLSelectElement).value as 'relative' | 'absolute'; render(); }); metricControlsInitialized = true; } if (!headersInitialized) { const ths = document.querySelectorAll('th'); const keys: (keyof ProfitWithMetrics | 'outputs')[] = [ 'outputs', 'expertise', 'profit_per_base', 'break_even',  'capex_val', 'opex_val', 'normalized_logistics_per_base', 'market_capacity_base' ]; const pctExplainer = '\n(Percentiles: 100.0% is the most desirable outcome. Toggle between Absolute and Normalized (weighted by market cash flow) using the controls.)'; ths.forEach((th, i) => { if (keys[i]) { th.style.cursor = 'pointer'; th.title = '';  if (keys[i] === 'profit_per_base') { th.textContent = 'Profit/Base'; th.dataset.tooltip = 'Click to sort.\n\nDaily profit scaled to a full 500-area planetary base.' + pctExplainer; } else if (keys[i] === 'capex_val') { th.textContent = 'CapEx/Base'; th.dataset.tooltip = 'Click to sort.\n\nTotal capital expenditure scaled to a full 500-area planetary base.\nIncludes base construction, and optional Working Capital, HQ Upgrades, and Ship CapEx (use "Omit From Calculation" to exclude).' + pctExplainer; } else if (keys[i] === 'opex_val') { th.textContent = 'OpEx/Base'; th.dataset.tooltip = 'Click to sort.\n\nDaily operational expenditure (input materials + worker consumables) scaled to a full 500-area planetary base.' + pctExplainer; } else if (keys[i] === 'normalized_logistics_per_base') { th.textContent = 'Logistics/Base'; th.dataset.tooltip = 'Click to sort.\n\nDaily logistics bottleneck scaled to a full 500-area planetary base. The suffix indicates whether Weight (t) or Volume (m³) of Inputs (I) or Outputs (O) is the limiting bottleneck.\nSorts and percentiles are strictly normalized based on ship capacity limits (3000t or 1000m³).' + pctExplainer; } else if (keys[i] === 'market_capacity_base') { th.textContent = 'Market Cap (Bases)'; th.dataset.tooltip = 'Click to sort.\n\nMarket Capacity: 7-day average traded volume ÷ daily output per base. Indicates how many full 500-area bases you can build before saturating the market.' + pctExplainer; } else if (keys[i] === 'break_even') { th.dataset.tooltip = 'Click to sort.\n\nBreak Even: CapEx ÷ daily profit. Note that CapEx dynamically includes optional logistics and HQ upgrades to accurately reflect operational readiness.' + pctExplainer; } else { th.dataset.tooltip = '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 || savedBuilding; 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 = ''; savedBuilding = '';  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; }); let profitsWithMetrics: ProfitWithMetrics[] = filteredProfits.map(p => { const bases = p.area / 500; const opex_val = p.opex[opexMetric] / bases; const revenue_val = p.revenue[revenueMetric] / bases; let capex_val = p.capex[capexMetric] / bases; let activeWorkingCapitalDays = 0; if (workingCapitalOption !== 'omit') { if (workingCapitalOption === 'dynamic') { activeWorkingCapitalDays = p.normalized_logistics_per_base > 0  ? 1 / p.normalized_logistics_per_base  : 0; } else { activeWorkingCapitalDays = parseInt(workingCapitalOption, 10); } capex_val += (opex_val * activeWorkingCapitalDays); } let hq_capex = 0; if (targetPermitOption !== 'omit') { const targetPermit = parseInt(targetPermitOption, 10); if (targetPermit >= 3) { const hqLevelStr = (targetPermit - 1).toString(); if (p.hq_costs && p.hq_costs[hqLevelStr]) { hq_capex = p.hq_costs[hqLevelStr][capexMetric]; capex_val += hq_capex; } } } let shipsNeeded = 0; let activeShipCapex = 0; if (roundTripOption !== 'omit') { const roundTripHours = parseInt(roundTripOption, 10); shipsNeeded = p.normalized_logistics_per_base * (roundTripHours / 24); activeShipCapex = shipsNeeded * 800_000; capex_val += activeShipCapex; } const profit_per_base = revenue_val - opex_val; const break_even = profit_per_base > 0 ? capex_val / profit_per_base : Infinity; // EXTREME DETAIL: We determine the specific market weight of this recipe line. // Multiplying the revenue (value per day per base) by the market capacity (max bases allowed) // yields the total cash flow value of all available trades in the global FIO market for this bottleneck. const market_cash_flow = revenue_val * p.market_capacity_base; return {  ...p,  capex_val,  opex_val,  revenue_val,  profit_per_base,  break_even,  hq_capex, activeWorkingCapitalDays, activeShipCapex, shipsNeeded, market_cash_flow }; }); if (!showNegativeProfit) { profitsWithMetrics = profitsWithMetrics.filter(p => p.profit_per_base > 0); } // EXTREME DETAIL: Overhauled the extraction arrays to store both the numerical value AND the weight parameter. const numSortObj = (a: {val: number, weight: number}, b: {val: number, weight: number}) => (a.val < b.val ? -1 : a.val > b.val ? 1 : 0); const arrProfit = profitsWithMetrics.map(p => ({val: p.profit_per_base, weight: p.market_cash_flow})).sort(numSortObj); const arrBreak = profitsWithMetrics.map(p => ({val: p.break_even, weight: p.market_cash_flow})).sort(numSortObj); const arrCapex = profitsWithMetrics.map(p => ({val: p.capex_val, weight: p.market_cash_flow})).sort(numSortObj); const arrOpex = profitsWithMetrics.map(p => ({val: p.opex_val, weight: p.market_cash_flow})).sort(numSortObj); const arrLog = profitsWithMetrics.map(p => ({val: p.normalized_logistics_per_base, weight: p.market_cash_flow})).sort(numSortObj); const arrCap = profitsWithMetrics.map(p => ({val: p.market_capacity_base, weight: p.market_cash_flow})).sort(numSortObj); 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 tr = document.createElement('tr'); // Map the raw value to the selected percentile rank. const pctProfit = getPercentiles(p.profit_per_base, arrProfit, false)[percentileMode === 'absolute' ? 'abs' : 'rel']; const pctBreak = getPercentiles(p.break_even, arrBreak, true)[percentileMode === 'absolute' ? 'abs' : 'rel']; const pctCapex = getPercentiles(p.capex_val, arrCapex, true)[percentileMode === 'absolute' ? 'abs' : 'rel']; const pctOpex = getPercentiles(p.opex_val, arrOpex, true)[percentileMode === 'absolute' ? 'abs' : 'rel']; const pctLog = getPercentiles(p.normalized_logistics_per_base, arrLog, true)[percentileMode === 'absolute' ? 'abs' : 'rel']; const pctCap = getPercentiles(p.market_capacity_base, arrCap, false)[percentileMode === 'absolute' ? 'abs' : 'rel']; // Interplate the format string requested directly into the span wrapper. tr.innerHTML = ` ${p.outputs.map(o => o.ticker).join(', ')} ${expertise[p.expertise]} ${formatSigFig(p.profit_per_base)} ${pctProfit} ${formatSigFig(p.break_even)}d ${pctBreak} ${formatSigFig(p.capex_val)} ${pctCapex} ${formatSigFig(p.opex_val)} ${pctOpex} ${formatSigFig(p.logistics_per_base)} ${p.logistics_bottleneck} ${pctLog} ${formatSigFig(p.market_capacity_base)} ${pctCap} `; const output = tr.querySelector('td')!; output.dataset.tooltip = p.recipe; const profitCell = tr.querySelectorAll('td')[2]; const runs_per_base = p.runs_per_day / (p.area / 500); profitCell.dataset.tooltip = formatMatPrices(p.outputs, revenueMetric, runs_per_base) + '\n\n' + formatMatPrices(p.input_costs, opexMetric, runs_per_base) + '\n' + '+ worker consumables\n\n' + `(${formatSigFig(p.revenue_val)} - ${formatSigFig(p.opex_val)}) = ${formatSigFig(p.profit_per_base)}`; const capexCell = tr.querySelectorAll('td')[4]; capexCell.dataset.tooltip = `Base Construction: ${formatSigFig(p.capex[capexMetric] / (p.area / 500))}`; if (workingCapitalOption !== 'omit') { capexCell.dataset.tooltip += `\nWorking Capital (${formatSigFig(p.activeWorkingCapitalDays)} days): ${formatSigFig(p.opex_val * p.activeWorkingCapitalDays)}`; } if (targetPermitOption !== 'omit' && p.hq_capex > 0) { capexCell.dataset.tooltip += `\nHQ Upgrade (Permit ${targetPermitOption}): ${formatSigFig(p.hq_capex)}`; } if (roundTripOption !== 'omit') { capexCell.dataset.tooltip += `\nShip CapEx: ${formatSigFig(p.activeShipCapex)} (${formatSigFig(p.shipsNeeded)} ships)`; } const marketCell = tr.querySelectorAll('td')[7]; marketCell.dataset.tooltip = `Market Capacity: ${formatSigFig(p.average_traded_7d)} traded/day ÷ ${formatSigFig(p.output_per_day / (p.area / 500))} produced/day/base = ${formatSigFig(p.market_capacity_base)} equivalent bases`; tbody.appendChild(tr); } document.getElementById('last-updated')!.textContent = `last updated: ${lastModified.toLocaleString(undefined, {dateStyle: 'full', timeStyle: 'long', hour12: false})}`; saveState(); } // EXTREME DETAIL: Overhauled function to calculate both Absolute and Relative Volume-Weighted Percentiles. // To ensure the mathematical max bounds cleanly hit 100.0%, the denominator is extracted dynamically  // relative to the highest data point present in the array. function getPercentiles(val: number, sortedArr: {val: number, weight: number}[], invert: boolean = false): {abs: string, rel: string} { if (sortedArr.length < 2) return {abs: "100.0%", rel: "100.0%"}; let lessCount = 0; let lessWeight = 0; for (let i = 0; i < sortedArr.length; i++) { if (sortedArr[i].val < val) { lessCount++; lessWeight += sortedArr[i].weight; } else { break;  } } const maxVal = sortedArr[sortedArr.length - 1].val; let maxLessCount = 0; let maxLessWeight = 0; for (let i = 0; i < sortedArr.length; i++) { if (sortedArr[i].val < maxVal) { maxLessCount++; maxLessWeight += sortedArr[i].weight; } else { break; } } let absDecimal = maxLessCount > 0 ? lessCount / maxLessCount : 1.0; let relDecimal = maxLessWeight > 0 ? lessWeight / maxLessWeight : 1.0; if (invert) { absDecimal = 1.0 - absDecimal; relDecimal = 1.0 - relDecimal; } return { abs: (absDecimal * 100).toFixed(1) + "%", rel: (relDecimal * 100).toFixed(1) + "%" }; } function saveState() { localStorage.setItem('roi-cx', cxSelect.value); localStorage.setItem('roi-expertise', expertiseSelect.value); localStorage.setItem('roi-building', buildingSelect.value); localStorage.setItem('roi-low-volume', lowVolume.checked.toString()); localStorage.setItem('roi-sort-key', currentSortKey); localStorage.setItem('roi-sort-asc', currentSortAsc.toString()); localStorage.setItem('roi-capex-metric', capexMetric); localStorage.setItem('roi-opex-metric', opexMetric); localStorage.setItem('roi-revenue-metric', revenueMetric); localStorage.setItem('roi-show-negative', showNegativeProfit.toString()); localStorage.setItem('roi-round-trip', roundTripOption); localStorage.setItem('roi-working-capital-opt', workingCapitalOption); localStorage.setItem('roi-target-permit', targetPermitOption); localStorage.setItem('roi-pct-mode', percentileMode); } 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)`; } 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}: ${formatSigFig(daily_amount)} × ${formatSigFig(val)} = ${formatSigFig(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_base: number normalized_logistics_per_base: number logistics_bottleneck: string output_per_day: number average_traded_7d: number market_capacity_base: number hq_costs: Record } interface ProfitWithMetrics extends Profit { capex_val: number; opex_val: number; revenue_val: number; profit_per_day: number; profit_per_base: number; break_even: number; hq_capex: number; activeWorkingCapitalDays: number; activeShipCapex: number; shipsNeeded: number; market_cash_flow: number; // Exported weight tracker } 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; }