Răsfoiți Sursa

shipbuilding: show shipbuilders buying instead of making

raylu 1 săptămână în urmă
părinte
comite
3ee38fba12
1 a modificat fișierele cu 90 adăugiri și 8 ștergeri
  1. 90 8
      ts/shipbuilding.ts

+ 90 - 8
ts/shipbuilding.ts

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