|
@@ -1,6 +1,6 @@
|
|
|
import {cachedFetchJSON} from './cache';
|
|
import {cachedFetchJSON} from './cache';
|
|
|
|
|
|
|
|
-const buy = new Set([
|
|
|
|
|
|
|
+const BUY = new Set([
|
|
|
// definitely buy
|
|
// definitely buy
|
|
|
'C', 'FLX', 'H', 'H2O', 'HAL', 'HCP', 'HE', 'LST', 'MG', 'N', 'NA', 'NCS', 'NS', 'O', 'PE', 'PG', 'S', 'TCL', 'THF',
|
|
'C', 'FLX', 'H', 'H2O', 'HAL', 'HCP', 'HE', 'LST', 'MG', 'N', 'NA', 'NCS', 'NS', 'O', 'PE', 'PG', 'S', 'TCL', 'THF',
|
|
|
// maybe buy
|
|
// maybe buy
|
|
@@ -27,6 +27,14 @@ const blueprint = {
|
|
|
'CQM': 1,
|
|
'CQM': 1,
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const shipbuilders = [
|
|
|
|
|
+ 'CraftsmanThirteen',
|
|
|
|
|
+ 'EvoV',
|
|
|
|
|
+ 'neke86',
|
|
|
|
|
+ 'SurvivorBob',
|
|
|
|
|
+ 'TRUEnterprises',
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
render();
|
|
render();
|
|
|
|
|
|
|
|
async function render() {
|
|
async function render() {
|
|
@@ -35,23 +43,32 @@ async function render() {
|
|
|
throw new Error('missing shipbuilding container');
|
|
throw new Error('missing shipbuilding container');
|
|
|
main.innerHTML = '<p>Loading...</p>';
|
|
main.innerHTML = '<p>Loading...</p>';
|
|
|
|
|
|
|
|
- const [allPrices, recipes, buildings] = await Promise.all([
|
|
|
|
|
|
|
+ const [allPrices, recipes, buildings, knownCompanies, companyProductionById] = await Promise.all([
|
|
|
cachedFetchJSON('https://refined-prun.github.io/refined-prices/all.json') as Promise<RawPrice[]>,
|
|
cachedFetchJSON('https://refined-prun.github.io/refined-prices/all.json') as Promise<RawPrice[]>,
|
|
|
recipeForMats(),
|
|
recipeForMats(),
|
|
|
cachedFetchJSON('https://api.prunplanner.org/data/buildings/') as Promise<Building[]>,
|
|
cachedFetchJSON('https://api.prunplanner.org/data/buildings/') as Promise<Building[]>,
|
|
|
|
|
+ cachedFetchJSON('https://pmmg-products.github.io/reports/data/knownCompanies.json') as Promise<Record<string, {Username: string}>>,
|
|
|
|
|
+ pmmgMonthlyReport(),
|
|
|
]);
|
|
]);
|
|
|
const prices = Object.fromEntries(allPrices.filter((price) => price.ExchangeCode === 'IC1')
|
|
const prices = Object.fromEntries(allPrices.filter((price) => price.ExchangeCode === 'IC1')
|
|
|
.map((price) => [price.MaterialTicker, price]));
|
|
.map((price) => [price.MaterialTicker, price]));
|
|
|
|
|
|
|
|
const production: Production = {};
|
|
const production: Production = {};
|
|
|
|
|
+ const buy: Record<string, number> = {};
|
|
|
const analysisNodes: AnalysisNode[] = [];
|
|
const analysisNodes: AnalysisNode[] = [];
|
|
|
let cost = 0;
|
|
let cost = 0;
|
|
|
for (const [mat, amount] of Object.entries(blueprint)) {
|
|
for (const [mat, amount] of Object.entries(blueprint)) {
|
|
|
- const { cost: matCost, node } = analyzeMat(mat, amount, production, prices, recipes);
|
|
|
|
|
|
|
+ const { cost: matCost, node } = analyzeMat(mat, amount, production, buy, prices, recipes);
|
|
|
cost += matCost;
|
|
cost += matCost;
|
|
|
analysisNodes.push(node);
|
|
analysisNodes.push(node);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // requiredMats = buy + production
|
|
|
|
|
+ const requiredMats: Record<string, number> = {...buy};
|
|
|
|
|
+ for (const buildingProduction of Object.values(production))
|
|
|
|
|
+ for (const [mat, amount] of Object.entries(buildingProduction))
|
|
|
|
|
+ requiredMats[mat] = (requiredMats[mat] ?? 0) + amount;
|
|
|
|
|
+
|
|
|
const expertiseGroups: Record<string, string[]> = {};
|
|
const expertiseGroups: Record<string, string[]> = {};
|
|
|
for (const building of buildings) {
|
|
for (const building of buildings) {
|
|
|
if (!(building.building_ticker in production))
|
|
if (!(building.building_ticker in production))
|
|
@@ -66,11 +83,74 @@ async function render() {
|
|
|
renderAnalysis(analysisNodes),
|
|
renderAnalysis(analysisNodes),
|
|
|
element('p', {textContent: `total cost: ${formatWhole(cost)}`}),
|
|
element('p', {textContent: `total cost: ${formatWhole(cost)}`}),
|
|
|
renderProduction(expertiseGroups, production, prices),
|
|
renderProduction(expertiseGroups, production, prices),
|
|
|
- element('h2', {textContent: 'buy'}),
|
|
|
|
|
- element('p', {textContent: Array.from(buy).join(', ')}),
|
|
|
|
|
|
|
+ renderBuyMaterials(buy),
|
|
|
|
|
+ renderShipbuilders(requiredMats, knownCompanies, companyProductionById),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function renderBuyMaterials(buy: Record<string, number>): HTMLElement {
|
|
|
|
|
+ const section = element('section');
|
|
|
|
|
+ section.append(element('h2', {textContent: 'buy'}));
|
|
|
|
|
+ const mats = Object.entries(buy).sort(([a], [b]) => a.localeCompare(b));
|
|
|
|
|
+ section.append(element('p', {
|
|
|
|
|
+ textContent: mats.map(([mat, amount]) => `${formatAmount(amount)}x${mat}`).join(', '),
|
|
|
|
|
+ }));
|
|
|
|
|
+ return section;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function pmmgMonthlyReport(): Promise<PMMGCompanyProduction> {
|
|
|
|
|
+ const constants = await fetch('https://raw.githubusercontent.com/PMMG-Products/pmmg-products.github.io/main/reports/src/staticData/constants.ts')
|
|
|
|
|
+ .then((res) => res.text());
|
|
|
|
|
+ const match = constants.match(/export const months = \[(.*?)\];/s);
|
|
|
|
|
+ if (!match)
|
|
|
|
|
+ throw new Error('failed to parse PMMG months');
|
|
|
|
|
+ const months = match[1].split(',').map((m) => m.trim().replace(/^"|"$/g, ''));
|
|
|
|
|
+ const latestMonth = months.at(-1);
|
|
|
|
|
+ if (!latestMonth)
|
|
|
|
|
+ throw new Error('missing PMMG month');
|
|
|
|
|
+ const report = await cachedFetchJSON(`https://pmmg-products.github.io/reports/data/company-data-${latestMonth}.json`);
|
|
|
|
|
+ return report['individual'];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderShipbuilders(requiredMats: Record<string, number>,
|
|
|
|
|
+ knownCompanies: Record<string, {Username: string}>, companyProductionById: PMMGCompanyProduction): HTMLElement {
|
|
|
|
|
+ const matTickers = Object.keys(requiredMats).sort();
|
|
|
|
|
+ const shipbuilderSet = new Set(shipbuilders);
|
|
|
|
|
+ const shipbuildersById: Record<string, string> = {};
|
|
|
|
|
+ for (const [companyId, company] of Object.entries(knownCompanies))
|
|
|
|
|
+ if (shipbuilderSet.has(company.Username))
|
|
|
|
|
+ shipbuildersById[companyId] = company.Username;
|
|
|
|
|
+
|
|
|
|
|
+ const missingShipbuilders = shipbuilders
|
|
|
|
|
+ .filter((username) => !Object.values(shipbuildersById).includes(username));
|
|
|
|
|
+ if (missingShipbuilders.length > 0)
|
|
|
|
|
+ return element('h3', {textContent: `missing shipbuilders in PMMG report: ${missingShipbuilders.join(', ')}`});
|
|
|
|
|
+
|
|
|
|
|
+ const section = element('section');
|
|
|
|
|
+ section.append(element('h2', {textContent: 'shipbuilders'}));
|
|
|
|
|
+
|
|
|
|
|
+ const table = element('table');
|
|
|
|
|
+ const header = element('tr');
|
|
|
|
|
+ header.append(element('th', {textContent: 'mat'}));
|
|
|
|
|
+ header.append(element('th', {textContent: 'amount'}));
|
|
|
|
|
+ for (const username of Object.values(shipbuildersById))
|
|
|
|
|
+ header.append(element('th', {textContent: username}));
|
|
|
|
|
+ table.append(header);
|
|
|
|
|
+
|
|
|
|
|
+ for (const mat of matTickers) {
|
|
|
|
|
+ const row = element('tr');
|
|
|
|
|
+ row.append(element('td', {textContent: mat}));
|
|
|
|
|
+ row.append(element('td', {textContent: formatAmount(requiredMats[mat])}));
|
|
|
|
|
+ for (const companyId of Object.keys(shipbuildersById)) {
|
|
|
|
|
+ const makes = mat in companyProductionById[companyId];
|
|
|
|
|
+ row.append(element('td', {textContent: makes ? '' : 'x'}));
|
|
|
|
|
+ }
|
|
|
|
|
+ table.append(row);
|
|
|
|
|
+ }
|
|
|
|
|
+ section.append(table);
|
|
|
|
|
+ return section;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function recipeForMats(): Promise<Record<string, Recipe>> {
|
|
async function recipeForMats(): Promise<Record<string, Recipe>> {
|
|
|
const allRecipes: Recipe[] = await cachedFetchJSON('https://api.prunplanner.org/data/recipes/');
|
|
const allRecipes: Recipe[] = await cachedFetchJSON('https://api.prunplanner.org/data/recipes/');
|
|
|
const matRecipes: Record<string, Recipe[]> = {}; // all ways to make a mat
|
|
const matRecipes: Record<string, Recipe[]> = {}; // all ways to make a mat
|
|
@@ -90,15 +170,16 @@ async function recipeForMats(): Promise<Record<string, Recipe>> {
|
|
|
return matRecipe;
|
|
return matRecipe;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function analyzeMat(mat: string, amount: number, production: Production,
|
|
|
|
|
|
|
+function analyzeMat(mat: string, amount: number, production: Production, buy: Record<string, number>,
|
|
|
prices: Record<string, RawPrice>, recipes: Record<string, Recipe>): { cost: number, node: AnalysisNode } {
|
|
prices: Record<string, RawPrice>, recipes: Record<string, Recipe>): { cost: number, node: AnalysisNode } {
|
|
|
const price = prices[mat];
|
|
const price = prices[mat];
|
|
|
if (!price)
|
|
if (!price)
|
|
|
throw new Error(`missing price for ${mat}`);
|
|
throw new Error(`missing price for ${mat}`);
|
|
|
const traded = price.AverageTraded30D ?? 0;
|
|
const traded = price.AverageTraded30D ?? 0;
|
|
|
- if (buy.has(mat)) {
|
|
|
|
|
|
|
+ if (BUY.has(mat)) {
|
|
|
if (price.Ask == null)
|
|
if (price.Ask == null)
|
|
|
throw new Error(`missing ask price for ${mat}`);
|
|
throw new Error(`missing ask price for ${mat}`);
|
|
|
|
|
+ buy[mat] = (buy[mat] ?? 0) + amount;
|
|
|
return {
|
|
return {
|
|
|
cost: price.Ask * amount,
|
|
cost: price.Ask * amount,
|
|
|
node: { text: `${formatAmount(amount)}x${mat} buy: ${formatAmount(price.Ask)}, daily traded ${formatFixed(traded, 1)}`, children: [] },
|
|
node: { text: `${formatAmount(amount)}x${mat} buy: ${formatAmount(price.Ask)}, daily traded ${formatFixed(traded, 1)}`, children: [] },
|
|
@@ -120,7 +201,7 @@ function analyzeMat(mat: string, amount: number, production: Production,
|
|
|
const children: AnalysisNode[] = [];
|
|
const children: AnalysisNode[] = [];
|
|
|
for (const inputMat of recipe.inputs) {
|
|
for (const inputMat of recipe.inputs) {
|
|
|
const inputAmount = inputMat.material_amount * amount / recipe.outputs[0].material_amount;
|
|
const inputAmount = inputMat.material_amount * amount / recipe.outputs[0].material_amount;
|
|
|
- const { cost, node } = analyzeMat(inputMat.material_ticker, inputAmount, production, prices, recipes);
|
|
|
|
|
|
|
+ const {cost, node} = analyzeMat(inputMat.material_ticker, inputAmount, production, buy, prices, recipes);
|
|
|
totalCost += cost;
|
|
totalCost += cost;
|
|
|
children.push(node);
|
|
children.push(node);
|
|
|
}
|
|
}
|
|
@@ -235,4 +316,5 @@ interface Building {
|
|
|
expertise: string
|
|
expertise: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+type PMMGCompanyProduction = Record<string, Record<string, {amount: number}>>;
|
|
|
type Production = Record<string, Record<string, number>>;
|
|
type Production = Record<string, Record<string, number>>;
|