// ts/popover.ts function setupPopover() { const main = document.querySelector("main"); const popover = document.querySelector("#popover"); main.addEventListener("mouseover", (event) => { const target = event.target; if (target.dataset.tooltip) { popover.textContent = target.dataset.tooltip; const rect = target.getBoundingClientRect(); popover.style.left = `${rect.left}px`; popover.style.top = `${rect.bottom}px`; popover.showPopover(); } }); main.addEventListener("mouseout", (event) => { const target = event.target; if (target.dataset.tooltip) popover.hidePopover(); }); } // ts/roi.ts var roiCache = {}; async function getROI(cx) { 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 }; } var lowVolume = document.querySelector("input#low-volume"); var cxSelect = document.querySelector("select#cx"); var expertise = { AGRICULTURE: "agri", CHEMISTRY: "chem", CONSTRUCTION: "const", ELECTRONICS: "elec", FOOD_INDUSTRIES: "food ind", FUEL_REFINING: "fuel", MANUFACTURING: "mfg", METALLURGY: "metal", RESOURCE_EXTRACTION: "res ext" }; var expertiseSelect = document.querySelector("select#expertise"); for (const key of Object.keys(expertise)) { const option = document.createElement("option"); option.value = key; option.textContent = key.replace("_", " ").toLowerCase(); expertiseSelect.appendChild(option); } var buildingSelect = document.querySelector("select#building"); var formatDecimal = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, maximumSignificantDigits: 6, roundingPriority: "lessPrecision" }).format; var formatWhole = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 }).format; 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]; const buildingTickers = new Set(profits.map((p) => p.building)); const buildings = 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 = ""; for (const p of profits) { const volumeRatio = p.output_per_day / p.average_traded_7d; if (!lowVolume.checked && volumeRatio > 0.05) continue; if (expertiseSelect.value !== "" && p.expertise !== expertiseSelect.value) continue; if (selectedBuilding !== "" && p.building !== selectedBuilding) continue; const tr = document.createElement("tr"); const profitPerArea = p.profit_per_day / p.area; const breakEven = p.profit_per_day > 0 ? (p.capex + 3 * p.cost_per_day) / p.profit_per_day : Infinity; tr.innerHTML = ` ${p.outputs.map((o) => o.ticker).join(", ")} ${expertise[p.expertise]} ${formatDecimal(profitPerArea)} ${formatDecimal(breakEven)}d ${formatWhole(p.capex)} ${formatWhole(p.cost_per_day)} ${formatDecimal(p.logistics_per_area)} ${formatDecimal(p.output_per_day)}
${formatWhole(p.average_traded_7d)} `; const output = tr.querySelector("td"); output.dataset.tooltip = p.recipe; const profitCell = tr.querySelectorAll("td")[2]; const revenue = p.outputs.reduce((sum, o) => sum + o.amount * o.vwap_7d, 0); const inputCost = p.input_costs.reduce((sum, o) => sum + o.amount * o.vwap_7d, 0); profitCell.dataset.tooltip = formatMatPrices(p.outputs) + ` ` + formatMatPrices(p.input_costs) + ` ` + "worker consumables: " + formatWhole(p.worker_consumable_cost_per_day) + ` ` + `(${formatWhole(revenue)} - ${formatWhole(inputCost)}) × ${formatDecimal(p.runs_per_day)} runs ` + `- ${formatWhole(p.worker_consumable_cost_per_day)} = ${formatDecimal(p.profit_per_day)} ` + `${formatDecimal(p.profit_per_day)} / ${formatWhole(p.area)} area = ${formatDecimal(profitPerArea)}`; tbody.appendChild(tr); } document.getElementById("last-updated").textContent = `last updated: ${lastModified.toLocaleString(undefined, { dateStyle: "full", timeStyle: "long", hour12: false })}`; } function color(n, low, high) { const scale = Math.min(Math.max((n - low) / (high - low), 0), 1); return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`; } function formatMatPrices(matPrices) { return matPrices.map(({ ticker, amount, vwap_7d }) => `${ticker}: ${amount} × ${formatDecimal(vwap_7d)} = ${formatWhole(amount * vwap_7d)}`).join(` `); } setupPopover(); lowVolume.addEventListener("change", render); cxSelect.addEventListener("change", render); expertiseSelect.addEventListener("change", render); buildingSelect.addEventListener("change", render); render(); //# debugId=55BEB0C86795158164756E2164756E21