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>;