| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- // ts/cache.ts
- var fetchCache = new Map;
- async function cachedFetchJSON(url, options) {
- if (fetchCache.has(url))
- return fetchCache.get(url);
- const response = await fetch(url, options).then((r) => r.json());
- fetchCache.set(url, response);
- return response;
- }
- // 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/production.ts
- var blueprint;
- var BUY;
- var renderTarget;
- var cx;
- var daysPerBundle;
- var apiKey = document.querySelector("#api-key");
- function setupProduction(bp, buy, cxCode, days, target) {
- blueprint = bp;
- BUY = buy;
- cx = cxCode;
- daysPerBundle = days;
- renderTarget = target;
- const storedApiKey = localStorage.getItem("punoted-api-key");
- if (storedApiKey)
- apiKey.value = storedApiKey;
- document.querySelector("#fetch").addEventListener("click", render);
- setupPopover();
- render();
- }
- async function render() {
- const loader = document.querySelector("#loader");
- loader.style.display = "block";
- try {
- await _render();
- if (apiKey.value)
- localStorage.setItem("punoted-api-key", apiKey.value);
- } catch (e) {
- renderTarget.innerHTML = e instanceof Error ? e.message : String(e);
- }
- loader.style.display = "none";
- }
- async function _render() {
- const [allPrices, { recipes, extractables }, buildingList, storage] = await Promise.all([
- cachedFetchJSON("https://refined-prun.github.io/refined-prices/all.json"),
- recipeForMats(),
- cachedFetchJSON("https://api.prunplanner.org/data/buildings/"),
- fetchStorage()
- ]);
- const prices = Object.fromEntries(allPrices.filter((price) => price.ExchangeCode === cx).map((price) => [price.MaterialTicker, price]));
- const buildings = Object.fromEntries(buildingList.map((b) => [b.building_ticker, b]));
- const production = {};
- const extract = {};
- const buy = {};
- const analysisNodes = [];
- let cost = 0;
- for (const [mat, amount] of Object.entries(blueprint)) {
- const node = analyzeMat(mat, amount, production, extract, buy, prices, recipes, extractables, storage);
- cost += node.cost;
- analysisNodes.push(node);
- }
- const requiredMats = { ...buy };
- for (const buildingProduction of Object.values(production))
- for (const [mat, amount] of Object.entries(buildingProduction))
- requiredMats[mat] = (requiredMats[mat] ?? 0) + amount;
- const expertiseGroups = {};
- for (const building of buildingList) {
- if (!(building.building_ticker in production))
- continue;
- if (!expertiseGroups[building.expertise])
- expertiseGroups[building.expertise] = [];
- expertiseGroups[building.expertise].push(building.building_ticker);
- }
- renderTarget.innerHTML = "";
- renderTarget.append(renderAnalysis(analysisNodes), element("p", { textContent: `total cost: ${formatWhole(cost)}` }), renderProduction(expertiseGroups, production, storage, prices, recipes, buildings), renderMatList("extract", extract, storage), renderMatList("buy", buy, storage));
- }
- function renderMatList(header, mats, storage) {
- const section = element("section");
- section.append(element("h2", { textContent: header }));
- const matsSorted = Object.entries(mats).sort(([a], [b]) => a.localeCompare(b));
- for (const [mat, amount] of matsSorted) {
- const div = element("div", { textContent: `${formatAmount(amount)}x${mat}` });
- const storageText = element("span", { textContent: ` (${formatAmount(storage[mat] ?? 0)})` });
- const percent = Math.min((storage[mat] ?? 0) / (amount * 2), 1);
- storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80 )`;
- div.append(storageText);
- section.append(div);
- }
- return section;
- }
- async function recipeForMats() {
- const [allRecipes, materials] = await Promise.all([
- cachedFetchJSON("https://api.prunplanner.org/data/recipes/"),
- cachedFetchJSON("https://api.prunplanner.org/data/materials/")
- ]);
- const extractables = new Set(materials.filter((m) => ["ores", "minerals", "liquids", "gases"].includes(m.category_name)).map((m) => m.ticker));
- const chosenRecipes = {
- AL: "6xALO 1xO 1xC 1xFLX=>4xAL",
- C: "4xHCP=>4xC",
- DRF: "50xNFI 1xDCS=>1xDRF",
- FE: "6xFEO 1xC 1xO 1xFLX=>4xFE",
- GL: "2xSIO 1xNA 1xSEN=>10xGL",
- HCP: "14xH2O 1xNS=>8xHCP",
- RE: "8xREO 1xC 1xO 1xFLX=>5xRE",
- RG: "10xGL 15xPG 1xSEN=>10xRG",
- SI: "3xSIO 1xC 1xO 1xFLX=>1xSI"
- };
- const matRecipes = {};
- for (const recipe of allRecipes)
- for (const output of recipe.outputs) {
- if (extractables.has(output.material_ticker))
- continue;
- if (chosenRecipes[output.material_ticker] && recipe.recipe_name != chosenRecipes[output.material_ticker])
- continue;
- const recipes = matRecipes[output.material_ticker];
- if (recipes)
- recipes.push(recipe);
- else
- matRecipes[output.material_ticker] = [recipe];
- }
- const matRecipe = {};
- for (const [mat, recipes] of Object.entries(matRecipes))
- if (recipes.length === 1)
- matRecipe[mat] = recipes[0];
- return { recipes: matRecipe, extractables };
- }
- function analyzeMat(mat, amount, production, extract, buy, prices, recipes, extractables, storage) {
- const price = prices[mat];
- if (!price)
- throw new Error(`missing price for ${mat}`);
- const traded = price.AverageTraded30D ?? 0;
- const inStorage = storage[mat] ?? 0;
- if (BUY.has(mat)) {
- const matPrice = price.VWAP30D ?? price.Ask;
- if (matPrice == null)
- throw new Error(`missing ask price for ${mat}`);
- buy[mat] = (buy[mat] ?? 0) + amount;
- return {
- ticker: mat,
- amount,
- inStorage,
- acquisition: `buy: ${formatAmount(matPrice)}, daily traded ${formatFixed(traded, 1)}`,
- children: [],
- cost: matPrice * amount
- };
- }
- if (extractables.has(mat)) {
- extract[mat] = (extract[mat] ?? 0) + amount;
- return { ticker: mat, amount, inStorage, acquisition: `extract`, children: [], cost: 0 };
- }
- const recipe = recipes[mat];
- if (!recipe)
- throw new Error(`no recipe for ${mat}`);
- const building = recipe.building_ticker;
- if (!production[building])
- production[building] = {};
- production[building][mat] = (production[building][mat] ?? 0) + amount;
- const liquid = traded > amount * 2 ? "liquid" : "not liquid";
- let totalCost = 0;
- const children = [];
- for (const inputMat of recipe.inputs) {
- const inputAmount = inputMat.material_amount * amount / recipe.outputs[0].material_amount;
- const node = analyzeMat(inputMat.material_ticker, inputAmount, production, extract, buy, prices, recipes, extractables, storage);
- totalCost += node.cost;
- children.push(node);
- }
- return {
- ticker: mat,
- amount,
- inStorage,
- acquisition: `make (${building}, ${liquid})`,
- children,
- cost: totalCost
- };
- }
- function renderAnalysis(nodes) {
- const section = element("section");
- for (const node of nodes)
- section.append(renderAnalysisNode(node));
- return section;
- }
- function renderAnalysisNode(node, level = 0) {
- const amountText = element("span", { textContent: `${formatAmount(node.amount)}x${node.ticker} ` });
- const storageText = element("span", { textContent: `(${formatAmount(node.inStorage)})` });
- const percent = Math.min(node.inStorage / node.amount, 1);
- storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80 )`;
- const acquisitionText = element("span", { textContent: " " + node.acquisition });
- let el;
- if (node.children.length === 0) {
- el = element("div", { className: "analysis-node" });
- el.append(amountText, storageText, acquisitionText);
- } else {
- el = element("details", { className: "analysis-node", open: level > 0 && percent < 1 });
- const summary = element("summary");
- summary.append(amountText, storageText, acquisitionText);
- el.append(summary);
- for (const child of node.children)
- el.append(renderAnalysisNode(child, level + 1));
- el.append(document.createTextNode(`total cost: ${formatWhole(node.cost)}`));
- }
- if (level === 0)
- el.classList.add("root");
- return el;
- }
- var WORKER_CONSUMPTION = {
- pioneers: { COF: 0.5, DW: 4, RAT: 4, OVE: 0.5, PWO: 0.2 },
- settlers: { DW: 5, RAT: 6, KOM: 1, EXO: 0.5, REP: 0.2, PT: 0.5 },
- technicians: { DW: 7.5, RAT: 7, ALE: 1, MED: 0.5, SC: 0.1, HMS: 0.5, SCN: 0.1 },
- engineers: { DW: 10, MED: 0.5, GIN: 1, FIM: 7, VG: 0.2, HSS: 0.2, PDA: 0.1 },
- scientists: { DW: 10, MED: 0.5, WIN: 1, MEA: 7, NST: 0.1, LC: 0.2, WS: 0.05 }
- };
- function buildingDailyCost(building, prices) {
- let cost = 0;
- for (const [workerType, mats] of Object.entries(WORKER_CONSUMPTION)) {
- const workers = building[workerType];
- for (const [mat, per100] of Object.entries(mats)) {
- const price = prices[mat].VWAP30D;
- if (price == null)
- throw new Error(`no price for ${mat}`);
- cost += price * workers * per100 / 100;
- }
- }
- return cost;
- }
- function renderProduction(expertiseGroups, production, storage, prices, recipes, buildings) {
- const section = element("section");
- section.append(element("h2", { textContent: "production" }));
- const matInputs = {};
- const matConsumers = {};
- for (const [expertise, productionBuildings] of Object.entries(expertiseGroups)) {
- for (const building of productionBuildings) {
- for (const [mat, totalAmount] of Object.entries(production[building])) {
- const recipe = recipes[mat];
- if (!matInputs[mat])
- matInputs[mat] = [];
- const outputPerRun = recipe.outputs.find((o) => o.material_ticker === mat).material_amount;
- for (const input of recipe.inputs) {
- const ticker = input.material_ticker;
- const amount = input.material_amount * totalAmount / outputPerRun;
- matInputs[mat].push({ upstreamMat: ticker, amount });
- if (!matConsumers[ticker])
- matConsumers[ticker] = [];
- matConsumers[ticker].push({ downstreamMat: mat, expertise, amount });
- }
- }
- }
- }
- let totalConsumablesCost = 0;
- for (const [expertise, productionBuildings] of Object.entries(expertiseGroups)) {
- section.append(element("h3", { textContent: expertise.toLocaleLowerCase() }));
- const imports = {};
- const exportTo = {};
- for (const building of productionBuildings) {
- const buildingRow = element("div", { className: "building-row" });
- const mats = Object.entries(production[building]);
- let buildingMins = 0;
- for (const [mat, amount] of mats) {
- buildingRow.append(document.createTextNode(" "));
- buildingRow.append(renderProductionBuildingMat(expertise, mat, amount, storage, matInputs, matConsumers, imports, exportTo));
- const recipe = recipes[mat];
- const outputPerRun = recipe.outputs.find((o) => o.material_ticker === mat).material_amount;
- buildingMins += amount / outputPerRun * (recipe.time_ms / 1000 / 60);
- }
- const numBuildings = buildingMins / (24 * 60) / daysPerBundle / 1.605;
- const consumablesCost = buildingDailyCost(buildings[building], prices) * Math.round(Math.max(1, numBuildings));
- totalConsumablesCost += consumablesCost;
- buildingRow.prepend(document.createTextNode(`${formatFixed(numBuildings, 1)}x${building} (${formatWhole(consumablesCost)}/d)`));
- section.append(buildingRow);
- }
- const importDetails = element("details");
- importDetails.append(element("summary", { textContent: "imports" }));
- for (const [mat, amount] of Object.entries(imports)) {
- if (recipes[mat] && buildings[recipes[mat].building_ticker].expertise == expertise)
- continue;
- importDetails.append(element("div", { textContent: `${formatAmount(amount)}x${mat}` }));
- }
- section.append(importDetails);
- const exportDetails = element("details");
- exportDetails.append(element("summary", { textContent: "exports" }));
- for (const [expertise2, mats] of Object.entries(exportTo)) {
- const exportToRow = element("div", { textContent: expertise2.toLocaleLowerCase() + ": " });
- exportToRow.textContent += Object.entries(mats).map(([mat, amount]) => `${amount}x${mat}`).join(" ");
- exportDetails.append(exportToRow);
- }
- section.append(exportDetails);
- }
- section.append(element("h4", { textContent: `total consumables cost: ${formatWhole(totalConsumablesCost)}/day,
- ${formatWhole(totalConsumablesCost * daysPerBundle)}/bundle` }));
- return section;
- }
- function renderProductionBuildingMat(expertise, mat, amount, storage, matInputs, matConsumers, imports, exportTo) {
- const inStorage = storage[mat] ?? 0;
- const wrapper = element("span");
- const amountText = element("span", { textContent: `${formatAmount(amount)}x${mat}` });
- wrapper.append(amountText);
- const storageText = element("span", { textContent: ` (${formatAmount(inStorage)})` });
- wrapper.append(storageText);
- const percent = Math.min(inStorage / (amount * 2), 1);
- storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80 )`;
- if (percent >= 1)
- amountText.style.color = "#777";
- else {
- const produceable = Object.values(matInputs[mat]).every((input) => storage[input.upstreamMat] ?? 0 >= input.amount);
- amountText.style.color = produceable ? "#0aa" : "#f80";
- }
- let tooltip = "";
- const inputs = matInputs[mat];
- if (inputs) {
- tooltip += inputs.map((i) => `${formatAmount(i.amount)}x${i.upstreamMat} (${storage[i.upstreamMat] ?? 0}) → ` + `${formatAmount(amount)}x${mat}`).join(`
- `) + `
- `;
- for (const input of inputs)
- imports[input.upstreamMat] = (imports[input.upstreamMat] ?? 0) + input.amount;
- }
- const consumers = matConsumers[mat];
- if (consumers) {
- tooltip += consumers.map((c) => `${formatAmount(c.amount)}x${mat} → ${c.downstreamMat} (${c.expertise.toLocaleLowerCase()})`).join(`
- `);
- for (const consumer of consumers) {
- if (consumer.expertise == expertise)
- continue;
- if (!exportTo[consumer.expertise])
- exportTo[consumer.expertise] = {};
- exportTo[consumer.expertise][mat] = (exportTo[consumer.expertise][mat] ?? 0) + consumer.amount;
- }
- }
- amountText.dataset.tooltip = tooltip;
- return wrapper;
- }
- async function fetchStorage() {
- if (!apiKey.value)
- return {};
- const users = await fetch("https://api.punoted.net/v1/storages/", { headers: { "X-Data-Token": apiKey.value } }).then((r) => r.json());
- const items = {};
- for (const user of users)
- for (const storage of user.Storages)
- for (const item of storage.StorageItems)
- items[item.MaterialTicker] = (items[item.MaterialTicker] ?? 0) + item.MaterialAmount;
- return items;
- }
- function element(tagName, properties = {}) {
- const node = document.createElement(tagName);
- Object.assign(node, properties);
- return node;
- }
- function formatAmount(n) {
- return n.toLocaleString(undefined, { maximumFractionDigits: 3 });
- }
- function formatFixed(n, digits) {
- return n.toLocaleString(undefined, {
- minimumFractionDigits: digits,
- maximumFractionDigits: digits
- });
- }
- function formatWhole(n) {
- return n.toLocaleString(undefined, { maximumFractionDigits: 0 });
- }
- export {
- setupProduction
- };
- //# debugId=02424DECF53B716064756E2164756E21
|