// ts/cache.ts var fetchCache = new Map; async function cachedFetchJSON(url, options) { if (fetchCache.has(url)) return fetchCache.get(url); const response = await fetch(url, options).then((r) => r.json()); fetchCache.set(url, response); return response; } // ts/gov.ts var renderTarget = document.querySelector("#gov"); var planetInput = document.querySelector("#planet"); var popSelect = document.querySelector("#pop"); function serializeToHash(planet, pop) { const params = new URLSearchParams({ planet, pop }); document.location.hash = params.toString(); } function deserializeFromHash() { 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 }; } else return null; } document.querySelector("form").addEventListener("submit", async (event) => { event.preventDefault(); const planet = planetInput.value; const pop = popSelect.value; if (planet && pop) { await render(planet, pop); serializeToHash(planet, pop); } }); { const deserialized = deserializeFromHash(); if (deserialized) render(deserialized.planet, deserialized.pop); } async function render(planetName, pop) { const loader = document.querySelector("#loader"); loader.style.display = "block"; renderTarget.innerHTML = ""; try { await _render(planetName, pop); } catch (e) { console.error(e); renderTarget.textContent = e.message; } loader.style.display = "none"; } async function _render(planetName, pop) { const encodedPlanetName = encodeURIComponent(planetName); const [planet, siteCounts, infras, allPrices] = 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`), cachedFetchJSON("https://api.prunplanner.org/data/exchanges") ]); 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; let currentPop = 0, lowerPop = 0; if (pop == "pio") currentPop = lastPOPR.NextPopulationPioneer; else if (pop == "set") { currentPop = lastPOPR.NextPopulationSettler; lowerPop = lastPOPR.NextPopulationPioneer; } else if (pop == "tec") { currentPop = lastPOPR.NextPopulationTechnician; lowerPop = lastPOPR.NextPopulationSettler; } else if (pop == "eng") { currentPop = lastPOPR.NextPopulationEngineer; lowerPop = lastPOPR.NextPopulationTechnician; } else if (pop == "sci") { currentPop = lastPOPR.NextPopulationScientist; lowerPop = lastPOPR.NextPopulationEngineer; } const siteCount = siteCounts[0].Count; const totalNeeds = { safety: calcTotalNeeds(lastPOPR, "safety"), health: calcTotalNeeds(lastPOPR, "health"), comfort: calcTotalNeeds(lastPOPR, "comfort"), culture: calcTotalNeeds(lastPOPR, "culture"), education: calcTotalNeeds(lastPOPR, "education") }; const totalPop = lastPOPR.NextPopulationPioneer + lastPOPR.NextPopulationSettler + lastPOPR.NextPopulationTechnician + lastPOPR.NextPopulationEngineer + lastPOPR.NextPopulationScientist; const currentPOPIFilled = new Map; for (const infra of infras) { const filled = calcPOPIFilled(infra); if (filled !== null) currentPOPIFilled.set(infra.Type, { tier: infra.CurrentLevel, numMats: filled }); } const prices = new Map; for (const price of allPrices) if (price.exchange_code === "IC1") prices.set(price.ticker, price.vwap_30d || price.ask); else if (price.exchange_code === "UNIVERSE" && prices.get(price.ticker) === 0) prices.set(price.ticker, price.vwap_30d); const results = paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled, currentPop, lowerPop, totalPop, prices); 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

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"])}

current POPI

${siteCount} bases ${infras.map((infra) => { if (infra.CurrentLevel === 0) return ""; const fill = currentPOPIFilled.get(infra.Type); return ``; }).join("")}
${infra.Type} T${infra.CurrentLevel} ${fill.numMats}/${infra.Upkeeps.length}
current projected ${pop.toUpperCase()} happiness: ${formatPct(projectedHappiness(pop, totalNeeds, siteCount, currentPOPIFilled, null))}

options

${renderOptions(results, currentPOPIFilled, "")} `; const govProgramSelect = renderTarget.querySelector("#gov_program"); govProgramSelect.addEventListener("change", (event) => { renderTarget.querySelector(".options").outerHTML = renderOptions(results, currentPOPIFilled, govProgramSelect.value); }); } function renderOptions(results, currentPOPIFilled, govProgramFilter) { const maxPop = Math.max(...results.map((result) => result.change)); const bestUnitCost = Math.min(...results.filter((result) => result.change > 0).map((result) => result.cost / result.change)); return ` ${results.filter((result) => govProgramFilter === "" || govProgramFilter === (result.govProgram ?? "no program")).map((result) => { let unitCost = ""; if (result.change !== 0) unitCost = formatNum(result.cost / result.change); return ``; }).join("")}
config projected happiness projected change cost/day unit cost
${[...result.config.entries()].map(([building, fill]) => { let currentBuildingFill = currentPOPIFilled.get(building).numMats; let className = ""; if (fill.numMats > currentBuildingFill) className = "positive"; else if (fill.numMats < currentBuildingFill) className = "negative"; return `${building}: ${fill.numMats}`; }).join("
")} ${result.govProgram !== null ? `
${result.govProgram}` : ""}
${formatPct(result.happiness)} ${formatNum(result.change)} ${formatNum(result.cost)} ${unitCost}
`; } var formatNum = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format; var formatPct = new Intl.NumberFormat(undefined, { style: "percent", maximumFractionDigits: 2 }).format; function formatDelta(n, withPlus = true) { if (n > 0) return `${withPlus ? "+" : ""}${formatNum(n)}`; else if (n < 0) return `${formatNum(n)}`; else return n.toString(); } function color(n, low, high) { const scale = Math.min(Math.max((n - low) / (high - low), 0), 1); return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`; } function unemployed(people, change, openJobs, unemploymentRate) { 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, need) { 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 calcPOPIFilled(infra) { if (infra.CurrentLevel === 0) return null; let filled = 0; for (const upkeep of infra.Upkeeps) { const nextConsumptionAmount = upkeep.StoreCapacity / 30 * upkeep.Duration; if (upkeep.Stored >= nextConsumptionAmount) filled++; } return filled; } function paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled, currentPop, lowerPop, totalPop, prices) { const results = []; let education = 0; if (pop !== "pio") { education = EDUCATION[pop]; education += (currentPOPIFilled.get("PLANETARY_BROADCASTING_HUB")?.tier ?? 0) * 0.001; education += (currentPOPIFilled.get("LIBRARY")?.tier ?? 0) * 0.002; education += (currentPOPIFilled.get("UNIVERSITY")?.tier ?? 0) * 0.004; } const govPrograms = [null]; govPrograms.push(...Object.keys(GOV_PROGRAMS)); for (const config of popiFillCombinations(currentPOPIFilled)) { for (const govProgram of govPrograms) { const happiness = projectedHappiness(pop, totalNeeds, siteCount, config, govProgram); let change = 0; if (happiness > 0.7 && ["pio", "set", "tec"].includes(pop)) change = currentPop * (happiness - 0.7); else if (happiness < 0.5) change = 0.8 * currentPop * (happiness - 0.5); if (govProgram === "pio immigration" && pop === "pio") change += 500; else if (govProgram === "set immigration" && pop === "set") change += 200; else if (govProgram === "tec immigration" && pop === "tec") change += 100; else if (govProgram === "eng immigration" && pop === "eng") change += 50; else if (govProgram === "sci immigration" && pop === "sci") change += 25; if (pop !== "pio") { let eduIn = lowerPop * education * happiness; if (govProgram === "education I") eduIn *= 1.5; else if (govProgram === "education II") eduIn *= 1.75; else if (govProgram === "education III") eduIn *= 2; change += eduIn; } let cost = calcCost(config, prices); if (govProgram !== null) { const program = GOV_PROGRAMS[govProgram]; cost += (program.baseCost + program.popCost * totalPop) / 7; } if (results.some((result) => result.change >= change && result.cost < cost || result.change > change && result.cost <= cost)) continue; for (let i = results.length - 1;i >= 0; i--) if (results[i].change <= change && results[i].cost > cost || results[i].change < change && results[i].cost >= cost) results.splice(i, 1); results.push({ config, govProgram, happiness, change, cost }); } } results.sort((a, b) => b.change - a.change); return results; } function* popiFillCombinations(currentPOPIFilled) { const entries = [...currentPOPIFilled.keys()]; function* helper(index, current) { if (index === entries.length) { yield new Map(current); return; } const building = entries[index]; const tier = currentPOPIFilled.get(building).tier; const maxBuildingMats = Object.keys(POPI[building].mats).length; for (let i = 0;i <= maxBuildingMats; i++) { current.set(building, { tier, numMats: i }); yield* helper(index + 1, current); } } yield* helper(0, new Map); } function projectedHappiness(pop, totalNeeds, siteCount, popiFilled, govProgram) { let totalProvided = { safety: 50 * siteCount, health: 50 * siteCount, comfort: 0, culture: 0, education: 0 }; for (const [building, fill] of popiFilled.entries()) { const { needs } = POPI[building]; const maxBuildingMats = Object.keys(POPI[building].mats).length; for (const { need, supplied } of needs) { const provided = fill.numMats / maxBuildingMats * fill.tier * supplied; totalProvided[need] += provided; } } const satisfaction = new Map; for (const _need in totalProvided) { const need = _need; satisfaction.set(need, Math.min(totalProvided[need] / totalNeeds[need], 1)); } const safetyHealthCap = Math.min(satisfaction.get("safety"), satisfaction.get("health")); if (satisfaction.get("comfort") > safetyHealthCap) satisfaction.set("comfort", safetyHealthCap); if (satisfaction.get("culture") > safetyHealthCap) satisfaction.set("culture", safetyHealthCap); const comfortCultureCap = Math.min(satisfaction.get("comfort"), satisfaction.get("culture")); if (satisfaction.get("education") > comfortCultureCap) satisfaction.set("education", comfortCultureCap); const weights = NEEDS[pop]; let happiness = 1 - Object.values(weights).reduce((sum, weight) => sum + weight, 0); for (const [need, s] of satisfaction.entries()) happiness += weights[need] * s; if (govProgram === "festivities I") happiness += 0.05; else if (govProgram === "festivities II") happiness += 0.1; else if (govProgram === "festivities III") happiness += 0.2; return Math.min(happiness, 1); } function calcCost(config, prices) { let cost = 0; for (const [building, fill] of config.entries()) { const { mats } = POPI[building]; const matPrices = []; for (const [mat, amount] of Object.entries(mats)) { const matCost = prices.get(mat); if (!matCost) throw new Error("no price for " + mat); matPrices.push({ ticker: mat, costPerDay: matCost * amount }); } matPrices.sort((a, b) => a.costPerDay - b.costPerDay); for (let i = 0;i < fill.numMats; i++) cost += matPrices[i].costPerDay * fill.tier; } return cost; } var NEEDS = { pio: { safety: 0.25, health: 0.15, comfort: 0.03, culture: 0.02, education: 0.01 }, set: { safety: 0.3, health: 0.2, comfort: 0.03, culture: 0.03, education: 0.03 }, tec: { safety: 0.2, health: 0.3, comfort: 0.2, culture: 0.1, education: 0.05 }, eng: { safety: 0.1, health: 0.15, comfort: 0.35, culture: 0.2, education: 0.1 }, sci: { safety: 0.1, health: 0.1, comfort: 0.2, culture: 0.25, education: 0.3 } }; var POPI = { SAFETY_STATION: { needs: [{ need: "safety", supplied: 2500 }], mats: { DW: 10, OFF: 10, SUN: 2 } }, SECURITY_DRONE_POST: { needs: [{ need: "safety", supplied: 5000 }], mats: { POW: 1, RAD: 0.47, CCD: 0.07, SUD: 0.07 } }, EMERGENCY_CENTER: { needs: [{ need: "safety", supplied: 1000 }, { need: "health", supplied: 1000 }], mats: { PK: 2, POW: 0.4, BND: 4, RED: 0.07, BSC: 0.07 } }, INFIRMARY: { needs: [{ need: "health", supplied: 2500 }], mats: { OFF: 10, TUB: 6.67, STR: 0.67 } }, HOSPITAL: { needs: [{ need: "health", supplied: 5000 }], mats: { PK: 2, SEQ: 0.4, BND: 4, SDR: 0.07, RED: 0.07, BSC: 0.13 } }, WELLNESS_CENTER: { needs: [{ need: "health", supplied: 1000 }, { need: "comfort", supplied: 1000 }], mats: { KOM: 4, OLF: 2, DW: 6, DEC: 0.67, PFE: 2.67, SOI: 6.67 } }, WILDLIFE_PARK: { needs: [{ need: "comfort", supplied: 2500 }], mats: { DW: 10, FOD: 6, PFE: 2, SOI: 3.33, DEC: 0.33 } }, ARCADES: { needs: [{ need: "comfort", supplied: 5000 }], mats: { POW: 2, MHP: 2, OLF: 4, BID: 0.2, HOG: 0.2, EDC: 0.2 } }, ART_CAFE: { needs: [{ need: "comfort", supplied: 1000 }, { need: "culture", supplied: 1000 }], mats: { MHP: 1, HOG: 1, UTS: 0.67, DEC: 0.67 } }, ART_GALLERY: { needs: [{ need: "culture", supplied: 2500 }], mats: { MHP: 1, HOG: 1, UTS: 0.67, DEC: 0.67 } }, THEATER: { needs: [{ need: "culture", supplied: 5000 }], mats: { POW: 1.4, MHP: 2, HOG: 1.4, OLF: 4, BID: 0.33, DEC: 0.67 } }, PLANETARY_BROADCASTING_HUB: { needs: [{ need: "culture", supplied: 1000 }, { need: "education", supplied: 1000 }], mats: { OFF: 10, MHP: 1, SP: 1.33, AAR: 0.67, EDC: 0.27, IDC: 0.13 } }, LIBRARY: { needs: [{ need: "education", supplied: 2500 }], mats: { MHP: 1, HOG: 1, CD: 0.33, DIS: 0.33, BID: 0.2 } }, UNIVERSITY: { needs: [{ need: "education", supplied: 5000 }], mats: { COF: 10, REA: 10, TUB: 10, BID: 0.33, HD: 0.67, IDC: 0.2 } } }; var EDUCATION = { set: 0.025, tec: 0.02, eng: 0.0125, sci: 0.0075 }; var GOV_PROGRAMS = { "festivities I": { baseCost: 5000, popCost: 0.125 }, "festivities II": { baseCost: 1e4, popCost: 0.25 }, "festivities III": { baseCost: 20000, popCost: 0.5 }, "education I": { baseCost: 5000, popCost: 0.15 }, "education II": { baseCost: 1e4, popCost: 0.25 }, "education III": { baseCost: 20000, popCost: 0.4 }, "pio immigration": { baseCost: 1e4, popCost: 0 }, "set immigration": { baseCost: 25000, popCost: 0 }, "tec immigration": { baseCost: 50000, popCost: 0 }, "eng immigration": { baseCost: 80000, popCost: 0 }, "sci immigration": { baseCost: 125000, popCost: 0 } }; //# debugId=C8AE7A4D6970DFAA64756E2164756E21