Quellcode durchsuchen

Merge branch 'main' of https://git.raylu.net/raylu/pruncalc into updatesjun2

Thomas Knott vor 1 Woche
Ursprung
Commit
1f83560dac
5 geänderte Dateien mit 145 neuen und 26 gelöschten Zeilen
  1. 3 0
      cache.py
  2. 4 1
      planet_bases.py
  3. 84 25
      ts/production.ts
  4. 53 0
      www/aen.html
  5. 1 0
      www/index.html

+ 3 - 0
cache.py

@@ -55,6 +55,9 @@ def get(url: str, *, json=True, headers=None, expiry=datetime.timedelta(minutes=
 		pass # fall through
 
 	r = get_with_retries(url, headers)
+	if r.status_code == 204:
+		raise NoContent
+
 	cache_path.parent.mkdir(parents=True, exist_ok=True)
 	with lzma.open(cache_path, 'wb') as f:
 		if json:

+ 4 - 1
planet_bases.py

@@ -69,7 +69,10 @@ def get_bases(planet: str) -> typing.Sequence[Site]:
 	return cache.get('https://rest.fnar.net/planet/sites/' + planet, expiry=cache.ONE_DAY)
 
 def get_company(code: str) -> typing.Mapping[str, typing.Any]:
-	return cache.get('https://rest.fnar.net/company/code/' + code, expiry=cache.ONE_DAY)
+	try:
+		return cache.get('https://rest.fnar.net/company/code/' + code, expiry=cache.ONE_DAY)
+	except cache.NoContent:
+		return {'CorporationCode': None, 'CompanyCode': code, 'CompanyName': None, 'UserName': None}
 
 class System(typing.TypedDict):
 	SystemId: str

+ 84 - 25
ts/production.ts

@@ -38,14 +38,13 @@ async function render() {
 }
 
 async function _render() {
-	const [allPrices, {recipes, extractables}, buildingList, storage] = await Promise.all([
-		cachedFetchJSON('https://refined-prun.github.io/refined-prices/all.json') as Promise<RawPrice[]>,
+	const [prices, {recipes, extractables}, buildingList, buildingPlanets, storage] = await Promise.all([
+		fetchPrices(),
 		recipeForMats(),
 		cachedFetchJSON('https://api.prunplanner.org/data/buildings/') as Promise<Building[]>,
+		fetchSites(),
 		fetchStorage(),
 	]);
-	const prices = Object.fromEntries(allPrices.filter((price) => price.ExchangeCode === cx)
-			.map((price) => [price.MaterialTicker, price]));
 	const buildings = Object.fromEntries(buildingList.map((b) => [b.building_ticker, b]));
 
 	const production: Production = {};
@@ -54,7 +53,7 @@ async function _render() {
 	const analysisNodes: AnalysisNode[] = [];
 	let cost = 0;
 	for (const [mat, amount] of Object.entries(blueprint)) {
-		const node = analyzeMat(mat, amount, production, extract, buy, prices, recipes, extractables, storage);
+		const node = analyzeMat(mat, amount, production, extract, buy, prices, recipes, extractables, storage.allItems);
 		cost += node.cost;
 		analysisNodes.push(node);
 	}
@@ -78,9 +77,9 @@ async function _render() {
 	renderTarget.append(
 		renderAnalysis(analysisNodes),
 		element('p', {textContent: `total cost: ${formatWhole(cost)}`}),
-		renderProduction(expertiseGroups, production, storage, prices, recipes, buildings),
-		renderMatList('extract', extract, storage),
-		renderMatList('buy', buy, storage),
+		renderProduction(expertiseGroups, production, buildingPlanets, storage, prices, recipes, buildings),
+		renderMatList('extract', extract, storage.allItems),
+		renderMatList('buy', buy, storage.allItems),
 	);
 }
 
@@ -112,8 +111,10 @@ async function recipeForMats(): Promise<{recipes: Record<string, Recipe>, extrac
 		'AL': '6xALO 1xO 1xC 1xFLX=>4xAL',
 		'C': '4xHCP=>4xC',
 		'DRF': '50xNFI 1xDCS=>1xDRF',
+		'DW': '10xH2O 1xPG=>10xDW',
 		'FE': '6xFEO 1xC 1xO 1xFLX=>4xFE',
 		'GL': '2xSIO 1xNA 1xSEN=>10xGL',
+		'GRA': '30xH2O 1xDDT 3xSOI=>6xGRA',
 		'HCP': '14xH2O 1xNS=>8xHCP',
 		'RE': '8xREO 1xC 1xO 1xFLX=>5xRE',
 		'RG': '10xGL 15xPG 1xSEN=>10xRG',
@@ -208,7 +209,7 @@ function renderAnalysisNode(node: AnalysisNode, level = 0): HTMLElement {
 	const amountText = element('span', {textContent: `${formatAmount(node.amount)}x${node.ticker} `});
 	const storageText = element('span', {textContent: `(${formatAmount(node.inStorage)})`});
 	const percent = Math.min(node.inStorage / node.amount, 1);
-	storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80 )`;
+	storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80)`;
 	const acquisitionText = element('span', {textContent: ' ' + node.acquisition});
 
 	let el;
@@ -250,8 +251,9 @@ function buildingDailyCost(building: Building, prices: Record<string, RawPrice>)
 	return cost;
 }
 
-function renderProduction(expertiseGroups: Record<string, string[]>, production: Production, storage: Record<string, number>,
-	prices: Record<string, RawPrice>, recipes: Record<string, Recipe>, buildings: Record<string, Building>): HTMLElement {
+function renderProduction(expertiseGroups: Record<string, string[]>, production: Production,
+	buildingPlanets: Record<string, Set<string>>, storage: Storage, prices: Record<string, RawPrice>,
+	recipes: Record<string, Recipe>, buildings: Record<string, Building>): HTMLElement {
 	const section = element('section');
 	section.append(element('h2', {textContent: 'production'}));
 
@@ -284,6 +286,7 @@ function renderProduction(expertiseGroups: Record<string, string[]>, production:
 		section.append(element('h3', {textContent: expertise.toLocaleLowerCase()}));
 		const imports: Record<string, number> = {};
 		const exportTo: Record<string, Record<string, number>> = {};
+		const planets = new Set<string>();
 		for (const building of productionBuildings) {
 			const buildingRow = element('div', {className: 'building-row'});
 			const mats = Object.entries(production[building]);
@@ -303,6 +306,9 @@ function renderProduction(expertiseGroups: Record<string, string[]>, production:
 			buildingRow.prepend(document.createTextNode(
 					`${formatFixed(numBuildings, 1)}x${building} (${formatWhole(consumablesCost)}/d)`));
 			section.append(buildingRow);
+
+			for (const planet of buildingPlanets[building] ?? [])
+				planets.add(planet);
 		}
 
 		const importDetails = element('details');
@@ -310,7 +316,20 @@ function renderProduction(expertiseGroups: Record<string, string[]>, production:
 		for (const [mat, amount] of Object.entries(imports)) {
 			if (recipes[mat] && buildings[recipes[mat].building_ticker].expertise == expertise)
 				continue;
-			importDetails.append(element('div', {textContent: `${formatAmount(amount)}x${mat}`}));
+			let onSite = 0;
+			for (const planet of planets)
+				onSite += storage.planetItems[planet][mat] ?? 0;
+			const offSite = (storage.allItems[mat] ?? 0) - onSite;
+			const importDetail = element('div', {
+					textContent: `${formatAmount(amount)}x${mat} (${formatAmount(onSite)} on-site, ${formatAmount(offSite)} off-site)`});
+			if (onSite >= amount * 2)
+				importDetail.style.color = '#777';
+			else {
+				const shouldBring = amount - onSite;
+				if (shouldBring > 0)
+					importDetail.style.color = `color-mix(in xyz, #0aa ${Math.min(offSite / shouldBring, 1) * 100}%, #f80)`;
+			}
+			importDetails.append(importDetail);
 		}
 		section.append(importDetails);
 
@@ -329,11 +348,11 @@ function renderProduction(expertiseGroups: Record<string, string[]>, production:
 	return section;
 }
 
-function renderProductionBuildingMat(expertise: string, mat: string, amount: number, storage: Record<string, number>,
+function renderProductionBuildingMat(expertise: string, mat: string, amount: number, storage: Storage,
 		matInputs: Record<string, {upstreamMat: string, amount: number}[]>,
 		matConsumers: Record<string, {downstreamMat: string, expertise: string, amount: number}[]>,
 		imports: Record<string, number>, exportTo: Record<string, Record<string, number>>): HTMLElement {
-	const inStorage = storage[mat] ?? 0;
+	const inStorage = storage.allItems[mat] ?? 0;
 	const wrapper = element('span');
 
 	const amountText = element('span', {textContent: `${formatAmount(amount)}x${mat}`});
@@ -343,18 +362,18 @@ function renderProductionBuildingMat(expertise: string, mat: string, amount: num
 	wrapper.append(storageText);
 
 	const percent = Math.min(inStorage / (amount * 2), 1);
-	storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80 )`;
+	storageText.style.color = `color-mix(in xyz, #0aa ${percent * 100}%, #f80)`;
 	if (percent >= 1)
 		amountText.style.color = '#777';
 	else {
-		const produceable = Object.values(matInputs[mat]).every((input) => storage[input.upstreamMat] ?? 0 >= input.amount);
+		const produceable = Object.values(matInputs[mat]).every((input) => storage.allItems[input.upstreamMat] ?? 0 >= input.amount);
 		amountText.style.color = produceable ? '#0aa' : '#f80';
 	}
 
 	let tooltip = '';
 	const inputs = matInputs[mat];
 	if (inputs) {
-		tooltip += inputs.map((i) => `${formatAmount(i.amount)}x${i.upstreamMat} (${storage[i.upstreamMat] ?? 0}) → ` +
+		tooltip += inputs.map((i) => `${formatAmount(i.amount)}x${i.upstreamMat} (${storage.allItems[i.upstreamMat] ?? 0}) → ` +
 				`${formatAmount(amount)}x${mat}`).join('\n') + '\n';
 		for (const input of inputs)
 			imports[input.upstreamMat] = (imports[input.upstreamMat] ?? 0) + input.amount;
@@ -376,17 +395,48 @@ function renderProductionBuildingMat(expertise: string, mat: string, amount: num
 	return wrapper;
 }
 
-async function fetchStorage(): Promise<Record<string, number>> {
+async function fetchPrices(): Promise<Record<string, RawPrice>> {
+	const allPrices: RawPrice[] = await cachedFetchJSON('https://refined-prun.github.io/refined-prices/all.json');
+	const prices = Object.fromEntries(allPrices.filter((price) => price.ExchangeCode === cx)
+			.map((price) => [price.MaterialTicker, price]));
+	if (prices['AFP'].Ask === null)
+		prices['AFP'] = {MaterialTicker: 'AFP', ExchangeCode: cx, Ask: 30000, AverageTraded30D: null, VWAP30D: 30000, Supply: 24};
+	return prices;
+}
+
+async function fetchSites(): Promise<Record<string, Set<string>>> {
 	if (!apiKey.value)
 		return {};
-	const users = await fetch('https://api.punoted.net/v1/storages/', {headers: {'X-Data-Token': apiKey.value}})
-			.then((r) => r.json()) as PUNUserStore[];
-	const items: Record<string, number> = {};
-	for (const user of users)
+	const userSites: PUNUserSite[] = await fetch('https://api.punoted.net/v1/sites/?include_buildings=true',
+			{headers: {'X-Data-Token': apiKey.value}}).then((r) => r.json());
+
+	const buildingPlanets: Record<string, Set<string>> = {};
+	for (const user of userSites)
+		for (const site of user.Sites)
+			for (const building of site.Buildings) {
+				if (!buildingPlanets[building.BuildingTicker])
+					buildingPlanets[building.BuildingTicker] = new Set();
+				buildingPlanets[building.BuildingTicker].add(site.PlanetName);
+			}
+	return buildingPlanets;
+}
+
+async function fetchStorage(): Promise<Storage> {
+	if (!apiKey.value)
+		return {allItems: {}, planetItems: {}};
+	const userStores: PUNUserStore[] = await fetch('https://api.punoted.net/v1/storages/',
+			{headers: {'X-Data-Token': apiKey.value}}).then((r) => r.json());
+
+	const allItems: Record<string, number> = {};
+	const planetItems: Record<string, Record<string, number>> = {};
+	for (const user of userStores)
 		for (const storage of user.Storages)
-			for (const item of storage.StorageItems)
-				items[item.MaterialTicker] = (items[item.MaterialTicker] ?? 0) + item.MaterialAmount;
-	return items;
+			for (const item of storage.StorageItems) {
+				allItems[item.MaterialTicker] = (allItems[item.MaterialTicker] ?? 0) + item.MaterialAmount;
+				const planetStorage = planetItems[storage.Location] ??= {};
+				planetStorage[item.MaterialTicker] = (planetStorage[item.MaterialTicker] ?? 0) + item.MaterialAmount;
+			}
+	return {allItems, planetItems};
 }
 
 function element<K extends keyof HTMLElementTagNameMap>(tagName: K,
@@ -459,6 +509,7 @@ interface Building {
 
 interface PUNUserStore {
 	Storages: Array<{
+		Location: string
 		StorageItems: Array<{
 			MaterialTicker: string
 			MaterialAmount: number
@@ -466,4 +517,12 @@ interface PUNUserStore {
 	}>
 }
 
+interface PUNUserSite {
+	Sites: Array<{
+		PlanetName: string
+		Buildings: Array<{BuildingTicker: string}>
+	}>
+}
+
 type Production = Record<string, Record<string, number>>;
+type Storage = {allItems: Record<string, number>, planetItems: Record<string, Record<string, number>>};

+ 53 - 0
www/aen.html

@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="UTF-8">
+	<title>PrUn AEN</title>
+	<link rel="stylesheet" type="text/css" href="style.css">
+	<link rel="icon" href="https://www.raylu.net/hammer-man.svg" />
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+	<meta name="theme-color" content="#222">
+</head>
+<body>
+	<a href="/">← back</a>
+	<main class="production">
+		<form>
+			<label>PUNoted API key: <input type="password" size="30" id="api-key"></label>
+			<input type="button" value="fetch" id="fetch">
+		</form>
+		<section id="loader"></section>
+		<div id="aen"></div>
+	</main>
+	<div id="popover" popover="hint"></div>
+	<script type="module">
+		import {setupProduction} from './production.js';
+		const blueprint = {
+			'FFC': 1,
+			'AEN': 1,
+			'HPR': 1,
+			'LFE': 2,
+			'MFE': 2,
+			'SFE': 2,
+			'LCB': 1,
+			'LFL': 1,
+			'MSL': 1,
+			'LHP': 97,
+			'SSC': 134,
+			'BR2': 1,
+			'CQL': 1,
+		};
+		const buy = new Set([
+			// definitely buy
+			'AFP', 'FIR', 'C', 'FLX', 'H', 'H2O', 'HAL', 'HCP', 'HE', 'LST', 'MG', 'N', 'NA', 'NCS', 'NS', 'O', 'PE', 'PG', 'S', 'TCL', 'THF',
+			// maybe buy
+			'AIR', 'AU', 'BE', 'BRM', 'BOR', 'BTS', 'CU', 'FAN', 'FC', 'FE', 'HCC', 'HD', 'LDI', 'LI', 'MFK', 'MWF',
+			'REA', 'RG', 'RGO', 'ROM', 'SFK', 'SI', 'STL', 'TCO', 'TPU',
+			// import
+			'AAR', 'AWF', 'CAP', 'CF', 'RAD',
+			// skip
+			'LFE', 'LHP', 'MFE', 'SFE', 'SSC',
+		])
+		setupProduction(blueprint, buy, 'IC1', 5, document.querySelector('#aen'));
+	</script>
+</body>
+</html>

+ 1 - 0
www/index.html

@@ -13,6 +13,7 @@
 		<p><a href="roi.html">RoI</a></p>
 		<p><a href="buy.html">buy</a></p>
 		<p><a href="mat.html">material</a></p>
+		<p><a href="corps.html">corporations</a></p>
 		<p><a href="gov.html">government</a></p>
 	</main>
 </body>