import {cachedFetchJSON} from './cache'; const renderTarget = document.querySelector('#gov')!; const planetInput = document.querySelector('#planet') as HTMLInputElement; const popSelect = document.querySelector('#pop') as HTMLSelectElement; function serializeToHash(planet: string, pop: Pop): void { const params = new URLSearchParams({'planet': planet, 'pop': pop}); document.location.hash = params.toString(); } function deserializeFromHash(): {planet: string, pop: Pop} | null { const params = new URLSearchParams(document.location.hash.substring(1)); const planet = params.get('planet'); const pop = params.get('pop'); if (planet && pop && ['pio', 'set', 'tec', 'eng', 'sci'].includes(pop)) { planetInput.value = planet; popSelect.value = pop; return {planet, pop: pop as Pop}; } else return null; } document.querySelector('form')!.addEventListener('submit', async (event) => { event.preventDefault(); const planet = planetInput.value; const pop = popSelect.value as Pop; if (planet && pop) { await render(planet, pop); serializeToHash(planet, pop); } }); { const deserialized = deserializeFromHash(); if (deserialized) void render(deserialized.planet, deserialized.pop); } async function render(planetName: string, pop: Pop) { const loader = document.querySelector('#loader') as HTMLElement; loader.style.display = 'block'; renderTarget.innerHTML = ''; const encodedPlanetName = encodeURIComponent(planetName); const [planet, siteCounts, infras]: [Planet, SiteCount[], Infrastructure[]] = await Promise.all([ cachedFetchJSON(`https://api.fnar.net/planet/${encodedPlanetName}?include_population_reports=true`), cachedFetchJSON(`https://api.fnar.net/planet/sitecount?planet=${encodedPlanetName}`), cachedFetchJSON(`https://api.fnar.net/infrastructure?infrastructure=${encodedPlanetName}&include_upkeeps=true`) ]); let lastPOPR = null; for (const report of planet.PopulationReports) { if (!lastPOPR || report.SimulationPeriod > lastPOPR.SimulationPeriod) lastPOPR = report; } if (lastPOPR === null) { renderTarget.textContent = `no POPR for ${planetName}`; return; } const lastPOPRts = Math.floor(new Date(lastPOPR.ReportTimestamp).getTime() / 1000); const nextPOPRts = lastPOPRts + 7 * 24 * 60 * 60; const siteCount = siteCounts[0].Count; const totalNeeds: Record = { 'safety': calcTotalNeeds(lastPOPR, 'safety'), 'health': calcTotalNeeds(lastPOPR, 'health'), 'comfort': calcTotalNeeds(lastPOPR, 'comfort'), 'culture': calcTotalNeeds(lastPOPR, 'culture'), 'education': calcTotalNeeds(lastPOPR, 'education'), }; renderTarget.innerHTML = `last POPR: ${lastPOPR.ReportTimestamp} (${lastPOPRts})
next POPR: ${new Date(nextPOPRts * 1000).toISOString()} (${nextPOPRts})
pio set tec eng sci
population ${lastPOPR.NextPopulationPioneer}
${formatDelta(lastPOPR.PopulationDifferencePioneer)}
${lastPOPR.NextPopulationSettler}
${formatDelta(lastPOPR.PopulationDifferenceSettler)}
${lastPOPR.NextPopulationTechnician}
${formatDelta(lastPOPR.PopulationDifferenceTechnician)}
${lastPOPR.NextPopulationEngineer}
${formatDelta(lastPOPR.PopulationDifferenceEngineer)}
${lastPOPR.NextPopulationScientist}
${formatDelta(lastPOPR.PopulationDifferenceScientist)}
unemployed ${unemployed(lastPOPR.NextPopulationPioneer, lastPOPR.PopulationDifferencePioneer, lastPOPR.OpenJobsPioneer, lastPOPR.UnemploymentRatePioneer)} ${unemployed(lastPOPR.NextPopulationSettler, lastPOPR.PopulationDifferenceSettler, lastPOPR.OpenJobsSettler, lastPOPR.UnemploymentRateSettler)} ${unemployed(lastPOPR.NextPopulationTechnician, lastPOPR.PopulationDifferenceTechnician, lastPOPR.OpenJobsTechnician, lastPOPR.UnemploymentRateTechnician)} ${unemployed(lastPOPR.NextPopulationEngineer, lastPOPR.PopulationDifferenceEngineer, lastPOPR.OpenJobsEngineer, lastPOPR.UnemploymentRateEngineer)} ${unemployed(lastPOPR.NextPopulationScientist, lastPOPR.PopulationDifferenceScientist, lastPOPR.OpenJobsScientist, lastPOPR.UnemploymentRateScientist)}

needs

${siteCount} bases
safety health comfort culture education
last POPR ${formatPct(lastPOPR.NeedFulfillmentSafety)} ${formatPct(lastPOPR.NeedFulfillmentHealth)} ${formatPct(lastPOPR.NeedFulfillmentComfort)} ${formatPct(lastPOPR.NeedFulfillmentCulture)} ${formatPct(lastPOPR.NeedFulfillmentEducation)}
total needed ${formatNum(totalNeeds['safety'])} ${formatNum(totalNeeds['health'])} ${formatNum(totalNeeds['comfort'])} ${formatNum(totalNeeds['culture'])} ${formatNum(totalNeeds['education'])}

POPI

${infras.map(infra => popiFilled(infra)).join('')}
`; loader.style.display = 'none'; } const formatNum = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2}).format; const formatPct = new Intl.NumberFormat(undefined, {style: 'percent', maximumFractionDigits: 2}).format; function formatDelta(n: number, withPlus: boolean = true): string { if (n > 0) return `${withPlus ? '+' : ''}${n}`; else if (n < 0) return `${n}`; else return n.toString(); } function unemployed(people: number, change: number, openJobs: number, unemploymentRate: number): string { let nextUnemployed; if (openJobs <= people + change) { let prevUnemploymentRate = unemploymentRate; if (openJobs > 0) if (people - change > 0) prevUnemploymentRate = -openJobs / (people - change); else prevUnemploymentRate = -1 const prevUnemployed = prevUnemploymentRate * (people - change); nextUnemployed = prevUnemployed + change; } else nextUnemployed = -openJobs + change; return formatDelta(Math.round(nextUnemployed), false); } function calcTotalNeeds(popReport: POPR, need: Need): number { return popReport.NextPopulationPioneer * NEEDS.pio[need] + popReport.NextPopulationSettler * NEEDS.set[need] + popReport.NextPopulationTechnician * NEEDS.tec[need] + popReport.NextPopulationEngineer * NEEDS.eng[need] + popReport.NextPopulationScientist * NEEDS.sci[need]; } function popiFilled(infra: Infrastructure): string { if (infra.CurrentLevel === 0) return ''; let filled = 0; for (const upkeep of infra.Upkeeps) { const nextConsumptionAmount = upkeep.StoreCapacity / 30 * upkeep.Duration; // # capacity is always 30 days if (upkeep.Stored >= nextConsumptionAmount) filled++; } return ` ${infra.Type} T${infra.CurrentLevel} ${filled}/${infra.Upkeeps.length} `; } const NEEDS: Record> = { 'pio': {'safety': 0.25, 'health': 0.15, 'comfort': 0.03, 'culture': 0.02, 'education': 0.01}, 'set': {'safety': 0.30, 'health': 0.20, 'comfort': 0.03, 'culture': 0.03, 'education': 0.03}, 'tec': {'safety': 0.20, 'health': 0.30, 'comfort': 0.20, 'culture': 0.10, 'education': 0.05}, 'eng': {'safety': 0.10, 'health': 0.15, 'comfort': 0.35, 'culture': 0.20, 'education': 0.10}, 'sci': {'safety': 0.10, 'health': 0.10, 'comfort': 0.20, 'culture': 0.25, 'education': 0.30}, } type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci'; type Need = 'safety' | 'health' | 'comfort' | 'culture' | 'education'; interface Planet { PopulationReports: POPR[] } interface POPR { SimulationPeriod: number; ReportTimestamp: string; NeedFulfillmentSafety: number; NeedFulfillmentHealth: number; NeedFulfillmentComfort: number; NeedFulfillmentCulture: number; NeedFulfillmentEducation: number; NextPopulationPioneer: number; NextPopulationSettler: number; NextPopulationTechnician: number; NextPopulationEngineer: number; NextPopulationScientist: number; PopulationDifferencePioneer: number; PopulationDifferenceSettler: number; PopulationDifferenceTechnician: number; PopulationDifferenceEngineer: number; PopulationDifferenceScientist: number; OpenJobsPioneer: number; OpenJobsSettler: number; OpenJobsTechnician: number; OpenJobsEngineer: number; OpenJobsScientist: number; UnemploymentRatePioneer: number; UnemploymentRateSettler: number; UnemploymentRateTechnician: number; UnemploymentRateEngineer: number; UnemploymentRateScientist: number; } interface SiteCount { Count: number; } interface Infrastructure { Type: string; CurrentLevel: number; Upkeeps: {Stored: number, StoreCapacity: number, 'Duration': number}[]; }