| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- // 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;
- 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";
- var savedBuilding = localStorage.getItem("roi-building") || "";
- var currentSortKey = localStorage.getItem("roi-sort-key") || "break_even";
- var currentSortAsc = localStorage.getItem("roi-sort-asc") !== "false";
- var headersInitialized = false;
- var metricControlsInitialized = false;
- var capexMetric = localStorage.getItem("roi-capex-metric") || "vwap";
- var opexMetric = localStorage.getItem("roi-opex-metric") || "vwap";
- var revenueMetric = localStorage.getItem("roi-revenue-metric") || "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];
- if (!metricControlsInitialized) {
- const controls = document.createElement("div");
- controls.style.marginBottom = "15px";
- controls.innerHTML = `
- <label style="margin-right: 15px;">CapEx Price:
- <select id="capex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
- </label>
- <label style="margin-right: 15px;">OpEx Price:
- <select id="opex-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
- </label>
- <label>Revenue (Outputs) Price:
- <select id="revenue-metric"><option value="vwap">VWAP</option><option value="bid">Bid</option><option value="ask">Ask</option></select>
- </label>
- `;
- const table = document.querySelector("table");
- if (table)
- table.parentNode?.insertBefore(controls, table);
- document.getElementById("capex-metric").value = capexMetric;
- document.getElementById("opex-metric").value = opexMetric;
- document.getElementById("revenue-metric").value = revenueMetric;
- document.getElementById("capex-metric").addEventListener("change", (e) => {
- capexMetric = e.target.value;
- render();
- });
- document.getElementById("opex-metric").addEventListener("change", (e) => {
- opexMetric = e.target.value;
- render();
- });
- document.getElementById("revenue-metric").addEventListener("change", (e) => {
- revenueMetric = e.target.value;
- render();
- });
- metricControlsInitialized = true;
- }
- if (!headersInitialized) {
- const ths = document.querySelectorAll("th");
- const keys = [
- "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 = "";
- if (keys[i] === "market_capacity_area") {
- th.textContent = "Market Cap (Areas)";
- th.dataset.tooltip = `Click to sort.
- Market Capacity: 7-day average traded volume ÷ daily output per area. Indicates how many areas you can build before saturating the market.`;
- } else if (keys[i] === "break_even") {
- th.dataset.tooltip = `Click to sort.
- Break Even: (CapEx + 3 days of OpEx) ÷ daily profit. Includes 3 days of operating costs as working capital.`;
- } 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 = 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 = '<option value="">(all)</option>';
- 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;
- });
- const profitsWithMetrics = 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 = a[currentSortKey];
- let valB = b[currentSortKey];
- 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");
- tr.innerHTML = `
- <td>${p.outputs.map((o) => o.ticker).join(", ")}</td>
- <td>${expertise[p.expertise]}</td>
- <td style="color: ${color(p.profit_per_area, 0, 300)}">${formatDecimal(p.profit_per_area)}</td>
- <td><span style="color: ${color(p.break_even, 30, 3)}">${formatDecimal(p.break_even)}</span>d</td>
- <td style="color: ${color(p.capex_val, 300000, 40000)}">${formatWhole(p.capex_val)}</td>
- <td style="color: ${color(p.opex_val, 40000, 1000)}">${formatWhole(p.opex_val)}</td>
- <td style="color: ${color(p.logistics_per_area, 2, 0.2)}">${formatDecimal(p.logistics_per_area)}</td>
- <td style="color: ${color(p.market_capacity_area, 20, 500)}">${formatWhole(p.market_capacity_area)}</td>
- `;
- 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) + `
- ` + formatMatPrices(p.input_costs, opexMetric, p.runs_per_day) + `
- ` + `(${formatWhole(p.revenue_val)} - ${formatWhole(p.opex_val)}) = ${formatDecimal(p.profit_per_day)}
- ` + `${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);
- }
- document.getElementById("last-updated").textContent = `last updated: ${lastModified.toLocaleString(undefined, { dateStyle: "full", timeStyle: "long", hour12: false })}`;
- saveState();
- }
- 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);
- }
- 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, metric, runs_per_day) {
- 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(`
- `);
- }
- setupPopover();
- lowVolume.addEventListener("change", render);
- cxSelect.addEventListener("change", render);
- expertiseSelect.addEventListener("change", render);
- buildingSelect.addEventListener("change", render);
- render();
- //# debugId=2FB8029285CE8DDA64756E2164756E21
|