// 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 `
| ${infra.Type} T${infra.CurrentLevel} |
${fill.numMats}/${infra.Upkeeps.length} |
`;
}).join("")}
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 `
| config |
projected happiness |
projected change |
cost/day |
unit cost |
${results.filter((result) => govProgramFilter === "" || govProgramFilter === (result.govProgram ?? "no program")).map((result) => {
let unitCost = "";
if (result.change !== 0)
unitCost = formatNum(result.cost / result.change);
return `
${[...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}
|
`;
}).join("")}
`;
}
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