import {cachedFetchJSON} from './cache'; const buy = new Set([ // definitely buy 'C', 'FLX', 'H', 'H2O', 'HAL', 'HCP', 'HE', 'LST', 'MG', 'N', 'NA', 'NCS', 'NS', 'O', 'PE', 'PG', 'S', 'TCL', 'THF', // maybe buy 'AU', 'BRM', 'CU', 'FE', 'LI', 'RG', 'ROM', 'SI', 'TI', // skip 'LFE', 'LHP', 'MFE', 'SFE', 'SSC', ]) const blueprint = { 'FFC': 1, 'FSE': 1, 'LFE': 2, 'MFE': 2, 'QCR': 1, 'SFE': 1, 'LCB': 1, 'MFL': 1, 'MSL': 1, 'LHP': 94, 'SSC': 128, 'BR1': 1, 'CQM': 1, } render(); async function render() { const main = document.querySelector('main.shipbuilding'); if (!main) throw new Error('missing shipbuilding container'); main.innerHTML = '

Loading...

'; const [allPrices, recipes, buildings] = await Promise.all([ cachedFetchJSON('https://refined-prun.github.io/refined-prices/all.json') as Promise, recipeForMats(), cachedFetchJSON('https://api.prunplanner.org/data/buildings/') as Promise, ]); const prices = Object.fromEntries(allPrices.filter((price) => price.ExchangeCode === 'IC1') .map((price) => [price.MaterialTicker, price])); const production: Production = {}; const analysisNodes: AnalysisNode[] = []; let cost = 0; for (const [mat, amount] of Object.entries(blueprint)) { const { cost: matCost, node } = analyzeMat(mat, amount, production, prices, recipes); cost += matCost; analysisNodes.push(node); } const expertiseGroups: Record = {}; for (const building of buildings) { if (!(building.building_ticker in production)) continue; if (!expertiseGroups[building.expertise]) expertiseGroups[building.expertise] = []; expertiseGroups[building.expertise].push(building.building_ticker); } main.innerHTML = ''; main.append( renderAnalysis(analysisNodes), element('p', {textContent: `total cost: ${formatWhole(cost)}`}), renderProduction(expertiseGroups, production, prices), ); } async function recipeForMats(): Promise> { const allRecipes: Recipe[] = await cachedFetchJSON('https://api.prunplanner.org/data/recipes/'); const matRecipes: Record = {}; // all ways to make a mat for (const recipe of allRecipes) for (const output of recipe.outputs) { const recipes = matRecipes[output.material_ticker]; if (recipes) recipes.push(recipe); else matRecipes[output.material_ticker] = [recipe]; } const matRecipe: Record = {}; // mats for which there's only one recipe to make for (const [mat, recipes] of Object.entries(matRecipes)) if (recipes.length === 1) matRecipe[mat] = recipes[0]; return matRecipe; } function analyzeMat(mat: string, amount: number, production: Production, prices: Record, recipes: Record): { cost: number, node: AnalysisNode } { const price = prices[mat]; if (!price) throw new Error(`missing price for ${mat}`); const traded = price.AverageTraded30D ?? 0; if (buy.has(mat)) { if (price.Ask == null) throw new Error(`missing ask price for ${mat}`); return { cost: price.Ask * amount, node: { text: `${formatAmount(amount)}x${mat} buy: ${formatAmount(price.Ask)}, daily traded ${formatFixed(traded, 1)}`, children: [] }, }; } const recipe = recipes[mat]; if (!recipe) return { cost: 0, node: { text: `${formatAmount(amount)}x${mat} make (unknown recipe)`, children: [] } }; 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: AnalysisNode[] = []; for (const inputMat of recipe.inputs) { const inputAmount = inputMat.material_amount * amount / recipe.outputs[0].material_amount; const { cost, node } = analyzeMat(inputMat.material_ticker, inputAmount, production, prices, recipes); totalCost += cost; children.push(node); } children.push({ text: `cost: ${formatWhole(totalCost)}`, children: [] }); return { cost: totalCost, node: { text: `${formatAmount(amount)}x${mat} make (${building}, ${liquid})`, children }, }; } function renderAnalysis(nodes: AnalysisNode[]): HTMLElement { const section = element('section'); for (const node of nodes) section.append(renderAnalysisNode(node)); return section; } function renderAnalysisNode(node: AnalysisNode, level = 0): HTMLElement { if (node.children.length === 0) { const div = element('div', {textContent: node.text, className: 'analysis-node'}); return div; } const details = element('details', {className: 'analysis-node'}); details.open = true; const summary = element('summary', {textContent: node.text}); details.append(summary); for (const child of node.children) details.append(renderAnalysisNode(child, level + 1)); return details; } function renderProduction(expertiseGroups: Record, production: Production, prices: Record): HTMLElement { const section = element('section'); section.append(element('h2', {textContent: 'production'})); for (const [expertise, buildings] of Object.entries(expertiseGroups)) { section.append(element('h3', {textContent: expertise})); for (const building of buildings) { const buildingRow = element('div', {className: 'building-row', textContent: building}); const buildingMats = element('div'); const mats = Object.entries(production[building]); for (const [index, [mat, amount]] of mats.entries()) { const traded = prices[mat]?.AverageTraded30D ?? 0; const span = element('span', { textContent: `${formatAmount(amount)}x${mat}`, }); span.style.color = traded > amount * 2 ? '#6c6' : '#c66'; buildingMats.append(span); if (index < mats.length - 1) buildingMats.append(document.createTextNode(' ')); } buildingRow.append(buildingMats); section.append(buildingRow); } } return section; } function element(tagName: K, properties: Partial = {}): HTMLElementTagNameMap[K] { const node = document.createElement(tagName); Object.assign(node, properties); return node; } function formatAmount(n: number): string { return n.toLocaleString(undefined, {maximumFractionDigits: 3}); } function formatFixed(n: number, digits: number): string { return n.toLocaleString(undefined, { minimumFractionDigits: digits, maximumFractionDigits: digits, }); } function formatWhole(n: number): string { return n.toLocaleString(undefined, {maximumFractionDigits: 0}); } interface AnalysisNode { text: string children: AnalysisNode[] } interface Recipe { recipe_name: string building_ticker: string inputs: RecipeMat[] outputs: RecipeMat[] time_ms: number } interface RecipeMat { material_ticker: string material_amount: number } interface RawPrice { MaterialTicker: string ExchangeCode: string Ask: number | null AverageTraded30D: number | null Supply: number } interface Building { building_ticker: string expertise: string } type Production = Record>;