| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- import {cachedFetchJSON} from './cache';
- import {Counter} from './counter';
- const renderTarget = document.querySelector('#plan')!;
- const shareURL = document.querySelector('#share_url') as HTMLInputElement;
- const cxSelect = document.querySelector('#cx') as HTMLSelectElement;
- function serializeToHash(shared: string, cx: string): void {
- const params = new URLSearchParams({shared, cx});
- document.location.hash = params.toString();
- }
- function deserializeFromHash(): {shareUUID: string, cx: string} | null {
- const params = new URLSearchParams(document.location.hash.substring(1));
- const shareUUID = params.get('shared');
- const cx = params.get('cx');
- if (shareUUID === null || cx === null)
- return null;
- shareURL.value = 'https://prunplanner.org/shared/' + shareUUID;
- cxSelect.value = cx;
- return {shareUUID, cx};
- }
- document.querySelector('form')!.addEventListener('submit', async (event) => {
- event.preventDefault();
- let shareUUID = shareURL.value;
- if (shareUUID) {
- if (shareUUID.startsWith('https://prunplanner.org/shared/'))
- shareUUID = shareUUID.substring('https://prunplanner.org/shared/'.length);
- const cx = cxSelect.value;
- await render(shareUUID, cx);
- serializeToHash(shareUUID, cx);
- }
- });
- {
- const hash = deserializeFromHash();
- if (hash !== null)
- void render(hash.shareUUID, hash.cx);
- }
- async function render(shareUUID: string, cx: string) {
- const loader = document.querySelector('#loader') as HTMLElement;
- loader.style.display = 'block';
- renderTarget.innerHTML = '';
- try {
- await _render(shareUUID, cx);
- } catch (e) {
- console.error(e);
- renderTarget.textContent = (e as Error).message;
- }
- loader.style.display = 'none';
- }
- async function _render(shareUUID: string, cx: string) {
- const [plan, buildings, recipeList, exchanges]: [Plan, Building[], Recipe[], Exchange[]] = await Promise.all([
- cachedFetchJSON('https://api.prunplanner.org/planning/shared/' + shareUUID),
- cachedFetchJSON('https://api.prunplanner.org/data/buildings/'),
- cachedFetchJSON('https://api.prunplanner.org/data/recipes/'),
- cachedFetchJSON('https://api.prunplanner.org/data/exchanges/'),
- ]);
- const planet: Planet = await cachedFetchJSON(`https://api.prunplanner.org/data/planet/${plan.plan_details.planet_natural_id}/`);
- const planExperts = new Map<Expertise, number>(plan.plan_details.plan_data.experts.map(
- e => [e.type.toUpperCase() as Expertise, e.amount]));
- const buildingExpertise = new Map<string, Expertise>(buildings.map(b => [b.building_ticker, b.expertise]));
- const recipes = new Map<string, Recipe>(recipeList.map(r => [r.recipe_id, r]));
- const dailyTraded = new Map<string, number>(exchanges.filter((ex) => ex.exchange_code === cx)
- .map((ex) => [ex.ticker, ex.avg_traded_7d]));
- const planInput = new Counter();
- const planOutput = new Counter();
- for (const building of plan.plan_details.plan_data.buildings) {
- const expertise = buildingExpertise.get(building.name)!;
- const experts = planExperts.get(expertise)!;
- const {input, output} = calcBuilding(recipes, building, plan.plan_details.plan_cogc === expertise, experts, planet);
- planInput.update(input);
- planOutput.update(output);
- }
- const net = new Counter();
- net.update(planOutput);
- for (const [key, amount] of planInput.entries())
- net.add(key, -amount);
- renderTarget.innerHTML = `
- <table>
- <tr>
- <th></th>
- <th>in</th>
- <th>out</th>
- <th>net</th>
- <th>daily traded</th>
- </tr>
- ${[...net.entries()].map(([mat, netAmount]) => {
- const traded = dailyTraded.get(mat)!;
- let tradedDisplay = wholeFmt.format(traded) + ' ';
- if (netAmount > 0)
- tradedDisplay += ' ';
- tradedDisplay += pctFmt.format(netAmount / traded);
- const colorPct = Math.min(Math.abs(netAmount) / traded * 500, 100);
- const color = `color-mix(in xyz, #f80 ${colorPct}%, #0aa)`;
- return `<tr>
- <td>${mat}</td>
- <td>${formatNumber(planInput.get(mat))}</td>
- <td>${formatNumber(planOutput.get(mat))}</td>
- <td>${formatNumber(netAmount)}</td>
- <td style="color: ${color}">${tradedDisplay}</td>
- </tr>`;
- }).join('')}
- </table>
- `;
- }
- function calcBuilding(recipes: Map<string, Recipe>, building: PlanBuilding, cogc: boolean, experts: number,
- planet: Planet): {input: Counter, output: Counter} {
- let efficiency = expertBonus[experts];
- if (cogc)
- efficiency *= 1.25;
- const input = new Counter();
- const output = new Counter();
- if (['COL', 'EXT', 'RIG'].includes(building.name)) {
- const total = building.active_recipes.reduce((sum, recipe) => sum + recipe.amount, 0);
- for (const recipe of building.active_recipes) {
- const [buildingName, recipeName] = recipe.recipeid.split('#');
- if (buildingName != building.name)
- throw new Error(`recipe ${recipe.recipeid} doesn't match building ${building.name}`);
- const resource = planet.resources.find((r) => r.material_ticker === recipeName);
- if (resource === undefined)
- throw new Error(`resource ${recipeName} not found on planet ${planet.planet_natural_id}`);
- const dailyOutput = resource.daily_extraction * efficiency * building.amount * recipe.amount / total;
- output.add(recipeName, dailyOutput);
- }
- } else {
- const totalMs = building.active_recipes.reduce((sum, recipe) => {
- const recipeData = recipes.get(recipe.recipeid);
- if (recipeData === undefined)
- throw new Error(`recipe ${recipe.recipeid} not found`);
- return sum + recipeData.time_ms * recipe.amount;
- }, 0);
- for (const recipe of building.active_recipes) {
- const recipeData = recipes.get(recipe.recipeid)!;
- let runsPerDay = 24 * 60 * 60 * 1000 / totalMs * efficiency * building.amount * recipe.amount;
- if (['FRM', 'ORC'].includes(building.name))
- runsPerDay *= 1 + planet.fertility * (10 / 33);
- for (const inputMaterial of recipeData.inputs)
- input.add(inputMaterial.material_ticker, inputMaterial.material_amount * runsPerDay);
- for (const outputMaterial of recipeData.outputs)
- output.add(outputMaterial.material_ticker, outputMaterial.material_amount * runsPerDay);
- }
- }
- return {input, output};
- }
- const numberFmt = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2});
- const wholeFmt = new Intl.NumberFormat(undefined, {maximumFractionDigits: 0});
- const pctFmt = new Intl.NumberFormat(undefined, {style: 'percent', maximumFractionDigits: 2});
- function formatNumber(num: number): string {
- if (num === 0)
- return '';
- return numberFmt.format(num);
- }
- const expertBonus: Record<number, number> = {
- 0: 1,
- 1: 1.0306,
- 2: 1.0696,
- 3: 1.1248,
- 4: 1.1974,
- 5: 1.284,
- } as const;
- interface Plan {
- plan_details: {
- planet_natural_id: string
- plan_cogc: Expertise
- plan_data: {
- buildings: Array<PlanBuilding>
- experts: Array<{type: string; amount: number}>
- }
- }
- }
- interface PlanBuilding {
- name: string
- active_recipes: Array<{amount: number; recipeid: string}>
- amount: number
- }
- interface Building {
- building_ticker: string
- expertise: Expertise
- }
- type Expertise = 'AGRICULTURE' | 'CHEMISTRY' | 'CONSTRUCTION' | 'ELECTRONICS' | 'FOOD_INDUSTRIES' | 'FUEL_REFINING' |
- 'MANUFACTURING' | 'METALLURGY' | 'RESOURCE_EXTRACTION';
- interface Recipe {
- recipe_id: string
- inputs: Array<{material_ticker: string; material_amount: number}>
- outputs: Array<{material_ticker: string; material_amount: number}>
- time_ms: number
- }
- interface Exchange {
- ticker: string
- exchange_code: string
- avg_traded_7d: number
- }
- interface Planet {
- planet_natural_id: string
- fertility: number
- resources: Array<{
- material_ticker: string
- daily_extraction: number
- }>
- }
|