Bladeren bron

shipbuilding: show shipbuilders buying instead of making

raylu 1 week geleden
bovenliggende
commit
3ee38fba12
1 gewijzigde bestanden met toevoegingen van 90 en 8 verwijderingen
  1. 90 8
      ts/shipbuilding.ts

+ 90 - 8
ts/shipbuilding.ts

@@ -1,6 +1,6 @@
 import {cachedFetchJSON} from './cache';
 
-const buy = new Set([
+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
@@ -27,6 +27,14 @@ const blueprint = {
 	'CQM': 1,
 }
 
+const shipbuilders = [
+	'CraftsmanThirteen',
+	'EvoV',
+	'neke86',
+	'SurvivorBob',
+	'TRUEnterprises',
+];
+
 render();
 
 async function render() {
@@ -35,23 +43,32 @@ async function render() {
 		throw new Error('missing shipbuilding container');
 	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[]>,
 		recipeForMats(),
 		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')
 			.map((price) => [price.MaterialTicker, price]));
 
 	const production: Production = {};
+	const buy: Record<string, number> = {};
 	const analysisNodes: AnalysisNode[] = [];
 	let cost = 0;
 	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;
 		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[]> = {};
 	for (const building of buildings) {
 		if (!(building.building_ticker in production))
@@ -66,11 +83,74 @@ async function render() {
 		renderAnalysis(analysisNodes),
 		element('p', {textContent: `total cost: ${formatWhole(cost)}`}),
 		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>> {
 	const allRecipes: Recipe[] = await cachedFetchJSON('https://api.prunplanner.org/data/recipes/');
 	const matRecipes: Record<string, Recipe[]> = {}; // all ways to make a mat
@@ -90,15 +170,16 @@ async function recipeForMats(): Promise<Record<string, Recipe>> {
 	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 } {
 	const price = prices[mat];
 	if (!price)
 		throw new Error(`missing price for ${mat}`);
 	const traded = price.AverageTraded30D ?? 0;
-	if (buy.has(mat)) {
+	if (BUY.has(mat)) {
 		if (price.Ask == null)
 			throw new Error(`missing ask price for ${mat}`);
+		buy[mat] = (buy[mat] ?? 0) + amount;
 		return {
 			cost: price.Ask * amount,
 			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[] = [];
 	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);
+		const {cost, node} = analyzeMat(inputMat.material_ticker, inputAmount, production, buy, prices, recipes);
 		totalCost += cost;
 		children.push(node);
 	}
@@ -235,4 +316,5 @@ interface Building {
 	expertise: string
 }
 
+type PMMGCompanyProduction = Record<string, Record<string, {amount: number}>>;
 type Production = Record<string, Record<string, number>>;