gov.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. import {cachedFetchJSON} from './cache';
  2. const renderTarget = document.querySelector('#gov')!;
  3. const planetInput = document.querySelector('#planet') as HTMLInputElement;
  4. const popSelect = document.querySelector('#pop') as HTMLSelectElement;
  5. function serializeToHash(planet: string, pop: Pop): void {
  6. const params = new URLSearchParams({'planet': planet, 'pop': pop});
  7. document.location.hash = params.toString();
  8. }
  9. function deserializeFromHash(): {planet: string, pop: Pop} | null {
  10. const params = new URLSearchParams(document.location.hash.substring(1));
  11. const planet = params.get('planet');
  12. const pop = params.get('pop');
  13. if (planet && pop && ['pio', 'set', 'tec', 'eng', 'sci'].includes(pop)) {
  14. planetInput.value = planet;
  15. popSelect.value = pop;
  16. return {planet, pop: pop as Pop};
  17. } else
  18. return null;
  19. }
  20. document.querySelector('form')!.addEventListener('submit', async (event) => {
  21. event.preventDefault();
  22. const planet = planetInput.value;
  23. const pop = popSelect.value as Pop;
  24. if (planet && pop) {
  25. await render(planet, pop);
  26. serializeToHash(planet, pop);
  27. }
  28. });
  29. {
  30. const deserialized = deserializeFromHash();
  31. if (deserialized)
  32. void render(deserialized.planet, deserialized.pop);
  33. }
  34. async function render(planetName: string, pop: Pop) {
  35. const loader = document.querySelector('#loader') as HTMLElement;
  36. loader.style.display = 'block';
  37. renderTarget.innerHTML = '';
  38. try {
  39. await _render(planetName, pop);
  40. } catch (e) {
  41. console.error(e);
  42. renderTarget.textContent = (e as Error).message;
  43. }
  44. loader.style.display = 'none';
  45. }
  46. async function _render(planetName: string, pop: Pop) {
  47. const encodedPlanetName = encodeURIComponent(planetName);
  48. const [planet, siteCounts, infras, allPrices]: [Planet, SiteCount[], Infrastructure[], Price[]] = await Promise.all([
  49. cachedFetchJSON(`https://api.fnar.net/planet/${encodedPlanetName}?include_population_reports=true`),
  50. cachedFetchJSON(`https://api.fnar.net/planet/sitecount?planet=${encodedPlanetName}`),
  51. cachedFetchJSON(`https://api.fnar.net/infrastructure?infrastructure=${encodedPlanetName}&include_upkeeps=true`),
  52. cachedFetchJSON('https://api.prunplanner.org/data/exchanges'),
  53. ]);
  54. let lastPOPR = null;
  55. for (const report of planet.PopulationReports) {
  56. if (!lastPOPR || report.SimulationPeriod > lastPOPR.SimulationPeriod)
  57. lastPOPR = report;
  58. }
  59. if (lastPOPR === null) {
  60. renderTarget.textContent = `no POPR for ${planetName}`;
  61. return;
  62. }
  63. const lastPOPRts = Math.floor(new Date(lastPOPR.ReportTimestamp).getTime() / 1000);
  64. const nextPOPRts = lastPOPRts + 7 * 24 * 60 * 60;
  65. let currentPop = 0, lowerPop = 0;
  66. if (pop == 'pio')
  67. currentPop = lastPOPR.NextPopulationPioneer;
  68. else if (pop == 'set') {
  69. currentPop = lastPOPR.NextPopulationSettler;
  70. lowerPop = lastPOPR.NextPopulationPioneer;
  71. }
  72. else if (pop == 'tec') {
  73. currentPop = lastPOPR.NextPopulationTechnician;
  74. lowerPop = lastPOPR.NextPopulationSettler;
  75. }
  76. else if (pop == 'eng') {
  77. currentPop = lastPOPR.NextPopulationEngineer;
  78. lowerPop = lastPOPR.NextPopulationTechnician;
  79. }
  80. else if (pop == 'sci') {
  81. currentPop = lastPOPR.NextPopulationScientist;
  82. lowerPop = lastPOPR.NextPopulationEngineer;
  83. }
  84. const siteCount = siteCounts[0].Count;
  85. const totalNeeds: Record<Need, number> = {
  86. 'safety': calcTotalNeeds(lastPOPR, 'safety'),
  87. 'health': calcTotalNeeds(lastPOPR, 'health'),
  88. 'comfort': calcTotalNeeds(lastPOPR, 'comfort'),
  89. 'culture': calcTotalNeeds(lastPOPR, 'culture'),
  90. 'education': calcTotalNeeds(lastPOPR, 'education'),
  91. };
  92. const totalPop = lastPOPR.NextPopulationPioneer + lastPOPR.NextPopulationSettler +
  93. lastPOPR.NextPopulationTechnician + lastPOPR.NextPopulationEngineer + lastPOPR.NextPopulationScientist;
  94. const currentPOPIFilled: POPIFill = new Map();
  95. for (const infra of infras) {
  96. const filled = calcPOPIFilled(infra);
  97. if (filled !== null)
  98. currentPOPIFilled.set(infra.Type, {tier: infra.CurrentLevel, numMats: filled});
  99. }
  100. const prices: Map<string, number> = new Map();
  101. for (const price of allPrices)
  102. if (price.exchange_code === 'IC1')
  103. prices.set(price.ticker, price.vwap_30d || price.ask);
  104. else if (price.exchange_code === 'UNIVERSE' && prices.get(price.ticker) === 0) // UNIVERSE always comes after IC1
  105. prices.set(price.ticker, price.vwap_30d);
  106. const results = paretoFront(pop, totalNeeds, siteCount, currentPOPIFilled, currentPop, lowerPop, totalPop, prices);
  107. renderTarget.innerHTML = `last POPR: ${lastPOPR.ReportTimestamp} (${lastPOPRts})
  108. <br>next POPR: ${new Date(nextPOPRts * 1000).toISOString()} (${nextPOPRts})
  109. <table>
  110. <tr>
  111. <th></th>
  112. <th>pio</th>
  113. <th>set</th>
  114. <th>tec</th>
  115. <th>eng</th>
  116. <th>sci</th>
  117. </tr>
  118. <tr>
  119. <th>population</th>
  120. <td>${lastPOPR.NextPopulationPioneer}<br>${formatDelta(lastPOPR.PopulationDifferencePioneer)}</td>
  121. <td>${lastPOPR.NextPopulationSettler}<br>${formatDelta(lastPOPR.PopulationDifferenceSettler)}</td>
  122. <td>${lastPOPR.NextPopulationTechnician}<br>${formatDelta(lastPOPR.PopulationDifferenceTechnician)}</td>
  123. <td>${lastPOPR.NextPopulationEngineer}<br>${formatDelta(lastPOPR.PopulationDifferenceEngineer)}</td>
  124. <td>${lastPOPR.NextPopulationScientist}<br>${formatDelta(lastPOPR.PopulationDifferenceScientist)}</td>
  125. </tr>
  126. <tr>
  127. <th>unemployed</th>
  128. <td>${unemployed(lastPOPR.NextPopulationPioneer, lastPOPR.PopulationDifferencePioneer, lastPOPR.OpenJobsPioneer, lastPOPR.UnemploymentRatePioneer)}</td>
  129. <td>${unemployed(lastPOPR.NextPopulationSettler, lastPOPR.PopulationDifferenceSettler, lastPOPR.OpenJobsSettler, lastPOPR.UnemploymentRateSettler)}</td>
  130. <td>${unemployed(lastPOPR.NextPopulationTechnician, lastPOPR.PopulationDifferenceTechnician, lastPOPR.OpenJobsTechnician, lastPOPR.UnemploymentRateTechnician)}</td>
  131. <td>${unemployed(lastPOPR.NextPopulationEngineer, lastPOPR.PopulationDifferenceEngineer, lastPOPR.OpenJobsEngineer, lastPOPR.UnemploymentRateEngineer)}</td>
  132. <td>${unemployed(lastPOPR.NextPopulationScientist, lastPOPR.PopulationDifferenceScientist, lastPOPR.OpenJobsScientist, lastPOPR.UnemploymentRateScientist)}</td>
  133. </tr>
  134. </table>
  135. <h2>needs</h2>
  136. <table>
  137. <tr>
  138. <th></th>
  139. <th>safety</th>
  140. <th>health</th>
  141. <th>comfort</th>
  142. <th>culture</th>
  143. <th>education</th>
  144. </tr>
  145. <tr>
  146. <td>last POPR</td>
  147. <td>${formatPct(lastPOPR.NeedFulfillmentSafety)}</td>
  148. <td>${formatPct(lastPOPR.NeedFulfillmentHealth)}</td>
  149. <td>${formatPct(lastPOPR.NeedFulfillmentComfort)}</td>
  150. <td>${formatPct(lastPOPR.NeedFulfillmentCulture)}</td>
  151. <td>${formatPct(lastPOPR.NeedFulfillmentEducation)}</td>
  152. </tr>
  153. <tr>
  154. <td>total needed</td>
  155. <td>${formatNum(totalNeeds['safety'])}</td>
  156. <td>${formatNum(totalNeeds['health'])}</td>
  157. <td>${formatNum(totalNeeds['comfort'])}</td>
  158. <td>${formatNum(totalNeeds['culture'])}</td>
  159. <td>${formatNum(totalNeeds['education'])}</td>
  160. </tr>
  161. </table>
  162. <h2>current POPI</h2>
  163. ${siteCount} bases
  164. <table>
  165. <tr>
  166. ${infras.map((infra) => {
  167. if (infra.CurrentLevel === 0)
  168. return '';
  169. const fill = currentPOPIFilled.get(infra.Type)!;
  170. return `<tr>
  171. <td>${infra.Type} T${infra.CurrentLevel}</td>
  172. <td>${fill.numMats}/${infra.Upkeeps.length}</td>
  173. </tr>`;
  174. }).join('')}
  175. </tr>
  176. </table>
  177. current projected ${pop.toUpperCase()} happiness:
  178. ${formatPct(projectedHappiness(pop, totalNeeds, siteCount, currentPOPIFilled, null))}
  179. <h2>options</h2>
  180. <select id="gov_program">
  181. <option value="">(all)</option>
  182. ${[...new Set(results.map((result) => result.govProgram))].map(
  183. (program) => `<option value="${program ?? 'no program'}">${program ?? 'no program'}</option>`).join('')}
  184. </select>
  185. ${renderOptions(results, currentPOPIFilled, '')}
  186. `;
  187. const govProgramSelect = renderTarget.querySelector('#gov_program') as HTMLSelectElement;
  188. govProgramSelect.addEventListener('change', (event) => {
  189. renderTarget.querySelector('.options')!.outerHTML = renderOptions(results, currentPOPIFilled, govProgramSelect.value);
  190. });
  191. }
  192. function renderOptions(results: ConfigResult[], currentPOPIFilled: POPIFill, govProgramFilter: string): string {
  193. const maxPop = Math.max(...results.map((result) => result.change));
  194. const bestUnitCost = Math.min(...results.filter((result) => result.change > 0).map((result) => result.cost / result.change));
  195. return `
  196. <table class="options">
  197. <tr>
  198. <th>config</th>
  199. <th>projected happiness</th>
  200. <th>projected change</th>
  201. <th>cost/day</th>
  202. <th>unit cost</th>
  203. </tr>
  204. ${results.filter((result) => govProgramFilter === '' || govProgramFilter === (result.govProgram ?? 'no program')).map((result) => {
  205. let unitCost = '';
  206. if (result.change !== 0)
  207. unitCost = formatNum(result.cost / result.change);
  208. return `<tr>
  209. <td>
  210. ${[...result.config.entries()].map(([building, fill]) => {
  211. let currentBuildingFill = currentPOPIFilled.get(building)!.numMats;
  212. let className = '';
  213. if (fill.numMats > currentBuildingFill) className = 'positive';
  214. else if (fill.numMats < currentBuildingFill) className = 'negative';
  215. return `${building}: <span class="${className}">${fill.numMats}</span>`;
  216. }).join('<br>')}
  217. ${result.govProgram !== null ? `<br>${result.govProgram}` : ''}
  218. </td>
  219. <td>${formatPct(result.happiness)}</td>
  220. <td style="color: ${color(result.change, 0, maxPop)}">${formatNum(result.change)}</td>
  221. <td>${formatNum(result.cost)}</td>
  222. <td style="color: ${result.change > 0 ? color(bestUnitCost - result.cost / result.change, -bestUnitCost, 0) : '#777'}">
  223. ${unitCost}
  224. </td>
  225. </tr>`;
  226. }).join('')}
  227. </table>
  228. `;
  229. }
  230. const formatNum = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2}).format;
  231. const formatPct = new Intl.NumberFormat(undefined, {style: 'percent', maximumFractionDigits: 2}).format;
  232. function formatDelta(n: number, withPlus: boolean = true): string {
  233. if (n > 0)
  234. return `<span class="positive">${withPlus ? '+' : ''}${formatNum(n)}</span>`;
  235. else if (n < 0)
  236. return `<span class="negative">${formatNum(n)}</span>`;
  237. else
  238. return n.toString();
  239. }
  240. function color(n: number, low: number, high: number): string {
  241. // scale n from low..high to 0..1 clamped
  242. const scale = Math.min(Math.max((n - low) / (high - low), 0), 1);
  243. return `color-mix(in xyz, #0aa ${scale * 100}%, #f80)`;
  244. }
  245. function unemployed(people: number, change: number, openJobs: number, unemploymentRate: number): string {
  246. let nextUnemployed;
  247. if (openJobs <= people + change) {
  248. let prevUnemploymentRate = unemploymentRate;
  249. if (openJobs > 0)
  250. if (people - change > 0)
  251. prevUnemploymentRate = -openJobs / (people - change);
  252. else
  253. prevUnemploymentRate = -1
  254. const prevUnemployed = prevUnemploymentRate * (people - change);
  255. nextUnemployed = prevUnemployed + change;
  256. } else
  257. nextUnemployed = -openJobs + change;
  258. return formatDelta(Math.round(nextUnemployed), false);
  259. }
  260. function calcTotalNeeds(popReport: POPR, need: Need): number {
  261. return popReport.NextPopulationPioneer * NEEDS.pio[need]
  262. + popReport.NextPopulationSettler * NEEDS.set[need]
  263. + popReport.NextPopulationTechnician * NEEDS.tec[need]
  264. + popReport.NextPopulationEngineer * NEEDS.eng[need]
  265. + popReport.NextPopulationScientist * NEEDS.sci[need];
  266. }
  267. function calcPOPIFilled(infra: Infrastructure): number | null {
  268. if (infra.CurrentLevel === 0)
  269. return null;
  270. let filled = 0;
  271. for (const upkeep of infra.Upkeeps) {
  272. const nextConsumptionAmount = upkeep.StoreCapacity / 30 * upkeep.Duration; // # capacity is always 30 days
  273. if (upkeep.Stored >= nextConsumptionAmount)
  274. filled++;
  275. }
  276. return filled;
  277. }
  278. function paretoFront(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, currentPOPIFilled: POPIFill,
  279. currentPop: number, lowerPop: number, totalPop: number, prices: Map<string, number>): ConfigResult[] {
  280. const results: ConfigResult[] = [];
  281. let education = 0;
  282. if (pop !== 'pio') {
  283. education = EDUCATION[pop];
  284. education += (currentPOPIFilled.get('PLANETARY_BROADCASTING_HUB')?.tier ?? 0) * 0.001;
  285. education += (currentPOPIFilled.get('LIBRARY')?.tier ?? 0) * 0.002;
  286. education += (currentPOPIFilled.get('UNIVERSITY')?.tier ?? 0) * 0.004;
  287. }
  288. const govPrograms: (GovProgram | null)[] = [null];
  289. govPrograms.push(...(Object.keys(GOV_PROGRAMS) as GovProgram[]));
  290. for (const config of popiFillCombinations(currentPOPIFilled)) {
  291. for (const govProgram of govPrograms) {
  292. const happiness = projectedHappiness(pop, totalNeeds, siteCount, config, govProgram);
  293. let change = 0;
  294. if (happiness > 0.7 && ['pio', 'set', 'tec'].includes(pop))
  295. change = currentPop * (happiness - 0.7);
  296. else if (happiness < 0.5)
  297. change = 0.8 * currentPop * (happiness - 0.5);
  298. if (govProgram === 'pio immigration' && pop === 'pio') change += 500;
  299. else if (govProgram === 'set immigration' && pop === 'set') change += 200;
  300. else if (govProgram === 'tec immigration' && pop === 'tec') change += 100;
  301. else if (govProgram === 'eng immigration' && pop === 'eng') change += 50;
  302. else if (govProgram === 'sci immigration' && pop === 'sci') change += 25;
  303. if (pop !== 'pio') {
  304. let eduIn = lowerPop * education * happiness;
  305. if (govProgram === 'education I') eduIn *= 1.5;
  306. else if (govProgram === 'education II') eduIn *= 1.75;
  307. else if (govProgram === 'education III') eduIn *= 2;
  308. change += eduIn;
  309. }
  310. // TODO: education out
  311. let cost = calcCost(config, prices);
  312. if (govProgram !== null) {
  313. const program = GOV_PROGRAMS[govProgram];
  314. cost += (program.baseCost + program.popCost * totalPop) / 7;
  315. }
  316. // is any result better than this one?
  317. if (results.some((result) => (result.change >= change && result.cost < cost) ||
  318. (result.change > change && result.cost <= cost)))
  319. continue;
  320. // are any results worse than this one?
  321. for (let i = results.length - 1; i >= 0; i--)
  322. if ((results[i].change <= change && results[i].cost > cost) ||
  323. (results[i].change < change && results[i].cost >= cost))
  324. results.splice(i, 1);
  325. results.push({config, govProgram, happiness, change, cost});
  326. }
  327. }
  328. results.sort((a, b) => b.change - a.change);
  329. return results;
  330. }
  331. function* popiFillCombinations(currentPOPIFilled: POPIFill): Generator<POPIFill> {
  332. const entries = [...currentPOPIFilled.keys()];
  333. function* helper(index: number, current: POPIFill): Generator<POPIFill> {
  334. if (index === entries.length) {
  335. yield new Map(current);
  336. return;
  337. }
  338. const building = entries[index];
  339. const tier = currentPOPIFilled.get(building)!.tier;
  340. const maxBuildingMats = Object.keys(POPI[building].mats).length;
  341. for (let i = 0; i <= maxBuildingMats; i++) {
  342. current.set(building, {tier, numMats: i});
  343. yield* helper(index + 1, current);
  344. }
  345. }
  346. yield* helper(0, new Map());
  347. }
  348. function projectedHappiness(pop: Pop, totalNeeds: Record<Need, number>, siteCount: number, popiFilled: POPIFill,
  349. govProgram: GovProgram | null): number {
  350. let totalProvided: Record<Need, number> = {'safety': 50 * siteCount, 'health': 50 * siteCount,
  351. 'comfort': 0, 'culture': 0, 'education': 0};
  352. for (const [building, fill] of popiFilled.entries()) {
  353. const {needs} = POPI[building];
  354. const maxBuildingMats = Object.keys(POPI[building].mats).length;
  355. for (const {need, supplied} of needs) {
  356. const provided = fill.numMats / maxBuildingMats * fill.tier * supplied;
  357. totalProvided[need] += provided;
  358. }
  359. }
  360. const satisfaction: Map<Need, number> = new Map(); // percentage of needs fulfilled
  361. for (const _need in totalProvided) {
  362. const need = _need as Need;
  363. satisfaction.set(need, Math.min(totalProvided[need] / totalNeeds[need], 1));
  364. }
  365. const safetyHealthCap = Math.min(satisfaction.get('safety')!, satisfaction.get('health')!);
  366. if (satisfaction.get('comfort')! > safetyHealthCap)
  367. satisfaction.set('comfort', safetyHealthCap);
  368. if (satisfaction.get('culture')! > safetyHealthCap)
  369. satisfaction.set('culture', safetyHealthCap);
  370. const comfortCultureCap = Math.min(satisfaction.get('comfort')!, satisfaction.get('culture')!);
  371. if (satisfaction.get('education')! > comfortCultureCap)
  372. satisfaction.set('education', comfortCultureCap);
  373. const weights = NEEDS[pop];
  374. let happiness = 1 - Object.values(weights).reduce((sum, weight) => sum + weight, 0); // assume 100% life support
  375. for (const [need, s] of satisfaction.entries())
  376. happiness += weights[need] * s;
  377. if (govProgram === 'festivities I')
  378. happiness += 0.05;
  379. else if (govProgram === 'festivities II')
  380. happiness += 0.1;
  381. else if (govProgram === 'festivities III')
  382. happiness += 0.2;
  383. return Math.min(happiness, 1);
  384. }
  385. function calcCost(config: POPIFill, prices: Map<string, number>): number {
  386. let cost = 0;
  387. for (const [building, fill] of config.entries()) {
  388. const {mats} = POPI[building];
  389. const matPrices: {ticker: string, costPerDay: number}[] = [];
  390. for (const [mat, amount] of Object.entries(mats)) {
  391. const matCost = prices.get(mat)!;
  392. if (!matCost)
  393. throw new Error('no price for ' + mat);
  394. matPrices.push({ticker: mat, costPerDay: matCost * amount});
  395. }
  396. matPrices.sort((a, b) => a.costPerDay - b.costPerDay);
  397. for (let i = 0; i < fill.numMats; i++)
  398. cost += matPrices[i].costPerDay * fill.tier;
  399. }
  400. return cost;
  401. }
  402. const NEEDS: Record<Pop, Record<Need, number>> = {
  403. 'pio': {'safety': 0.25, 'health': 0.15, 'comfort': 0.03, 'culture': 0.02, 'education': 0.01},
  404. 'set': {'safety': 0.30, 'health': 0.20, 'comfort': 0.03, 'culture': 0.03, 'education': 0.03},
  405. 'tec': {'safety': 0.20, 'health': 0.30, 'comfort': 0.20, 'culture': 0.10, 'education': 0.05},
  406. 'eng': {'safety': 0.10, 'health': 0.15, 'comfort': 0.35, 'culture': 0.20, 'education': 0.10},
  407. 'sci': {'safety': 0.10, 'health': 0.10, 'comfort': 0.20, 'culture': 0.25, 'education': 0.30},
  408. }
  409. const POPI: Record<POPIBuilding, {needs: {need: Need, supplied: number}[], mats: Record<string, number>}> = {
  410. 'SAFETY_STATION': {
  411. needs: [{need: 'safety', supplied: 2500}],
  412. mats: {'DW': 10, 'OFF': 10, 'SUN': 2},
  413. },
  414. 'SECURITY_DRONE_POST': {
  415. needs: [{need: 'safety', supplied: 5000}],
  416. mats: {'POW': 1, 'RAD': 0.47, 'CCD': 0.07, 'SUD': 0.07},
  417. },
  418. 'EMERGENCY_CENTER': {
  419. needs: [{need: 'safety', supplied: 1000}, {need: 'health', supplied: 1000}],
  420. mats: {'PK': 2, 'POW': 0.4, 'BND': 4, 'RED': 0.07, 'BSC': 0.07},
  421. },
  422. 'INFIRMARY': {
  423. needs: [{need: 'health', supplied: 2500}],
  424. mats: {'OFF': 10, 'TUB': 6.67, 'STR': 0.67},
  425. },
  426. 'HOSPITAL': {
  427. needs: [{need: 'health', supplied: 5000}],
  428. mats: {'PK': 2, 'SEQ': 0.4, 'BND': 4, 'SDR': 0.07, 'RED': 0.07, 'BSC': 0.13},
  429. },
  430. 'WELLNESS_CENTER': {
  431. needs: [{need: 'health', supplied: 1000}, {need: 'comfort', supplied: 1000}],
  432. mats: {'KOM': 4, 'OLF': 2, 'DW': 6, 'DEC': 0.67, 'PFE': 2.67, 'SOI': 6.67},
  433. },
  434. 'WILDLIFE_PARK': {
  435. needs: [{need: 'comfort', supplied: 2500}],
  436. mats: {'DW': 10, 'FOD': 6, 'PFE': 2, 'SOI': 3.33, 'DEC': 0.33},
  437. },
  438. 'ARCADES': {
  439. needs: [{need: 'comfort', supplied: 5000}],
  440. mats: {'POW': 2, 'MHP': 2, 'OLF': 4, 'BID': 0.2, 'HOG': 0.2, 'EDC': 0.2},
  441. },
  442. 'ART_CAFE': {
  443. needs: [{need: 'comfort', supplied: 1000}, {need: 'culture', supplied: 1000}],
  444. mats: {'MHP': 1, 'HOG': 1, 'UTS': 0.67, 'DEC': 0.67},
  445. },
  446. 'ART_GALLERY': {
  447. needs: [{need: 'culture', supplied: 2500}],
  448. mats: {'MHP': 1, 'HOG': 1, 'UTS': 0.67, 'DEC': 0.67},
  449. },
  450. 'THEATER': {
  451. needs: [{need: 'culture', supplied: 5000}],
  452. mats: {'POW': 1.4, 'MHP': 2, 'HOG': 1.4, 'OLF': 4, 'BID': 0.33, 'DEC': 0.67},
  453. },
  454. 'PLANETARY_BROADCASTING_HUB': {
  455. needs: [{need: 'culture', supplied: 1000}, {need: 'education', supplied: 1000}],
  456. mats: {'OFF': 10, 'MHP': 1, 'SP': 1.33, 'AAR': 0.67, 'EDC': 0.27, 'IDC': 0.13},
  457. },
  458. 'LIBRARY': {
  459. needs: [{need: 'education', supplied: 2500}],
  460. mats: {'MHP': 1, 'HOG': 1, 'CD': 0.33, 'DIS': 0.33, 'BID': 0.2},
  461. },
  462. 'UNIVERSITY': {
  463. needs: [{need: 'education', supplied: 5000}],
  464. mats: {'COF': 10, 'REA': 10, 'TUB': 10, 'BID': 0.33, 'HD': 0.67, 'IDC': 0.2},
  465. },
  466. };
  467. const EDUCATION: Record<Exclude<Pop, 'pio'>, number> = {
  468. 'set': 0.025,
  469. 'tec': 0.02,
  470. 'eng': 0.0125,
  471. 'sci': 0.0075,
  472. }
  473. const GOV_PROGRAMS: Record<GovProgram, {baseCost: number, popCost: number}> = {
  474. 'festivities I': {baseCost: 5000, popCost: 0.125},
  475. 'festivities II': {baseCost: 10000, popCost: 0.25},
  476. 'festivities III': {baseCost: 20000, popCost: 0.5},
  477. 'education I': {baseCost: 5000, popCost: 0.15},
  478. 'education II': {baseCost: 10000, popCost: 0.25},
  479. 'education III': {baseCost: 20000, popCost: 0.4},
  480. 'pio immigration': {baseCost: 10000, popCost: 0},
  481. 'set immigration': {baseCost: 25000, popCost: 0},
  482. 'tec immigration': {baseCost: 50000, popCost: 0},
  483. 'eng immigration': {baseCost: 80000, popCost: 0},
  484. 'sci immigration': {baseCost: 125000, popCost: 0},
  485. }
  486. type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci';
  487. type Need = 'safety' | 'health' | 'comfort' | 'culture' | 'education';
  488. type POPIBuilding = 'SAFETY_STATION' | 'SECURITY_DRONE_POST' | 'EMERGENCY_CENTER' | 'INFIRMARY' | 'HOSPITAL' |
  489. 'WELLNESS_CENTER' | 'WILDLIFE_PARK' | 'ARCADES' | 'ART_CAFE' | 'ART_GALLERY' | 'THEATER' |
  490. 'PLANETARY_BROADCASTING_HUB' | 'LIBRARY' | 'UNIVERSITY';
  491. type POPIFill = Map<POPIBuilding, {tier: number, numMats: number}>;
  492. type GovProgram = 'festivities I' | 'festivities II' | 'festivities III' | 'education I' | 'education II' | 'education III' |
  493. 'pio immigration' | 'set immigration' | 'tec immigration' | 'eng immigration' | 'sci immigration';
  494. type ConfigResult = {config: POPIFill, govProgram: GovProgram | null, happiness: number, change: number, cost: number};
  495. interface Planet {
  496. PopulationReports: POPR[]
  497. }
  498. interface POPR {
  499. SimulationPeriod: number;
  500. ReportTimestamp: string;
  501. NeedFulfillmentSafety: number;
  502. NeedFulfillmentHealth: number;
  503. NeedFulfillmentComfort: number;
  504. NeedFulfillmentCulture: number;
  505. NeedFulfillmentEducation: number;
  506. NextPopulationPioneer: number;
  507. NextPopulationSettler: number;
  508. NextPopulationTechnician: number;
  509. NextPopulationEngineer: number;
  510. NextPopulationScientist: number;
  511. PopulationDifferencePioneer: number;
  512. PopulationDifferenceSettler: number;
  513. PopulationDifferenceTechnician: number;
  514. PopulationDifferenceEngineer: number;
  515. PopulationDifferenceScientist: number;
  516. OpenJobsPioneer: number;
  517. OpenJobsSettler: number;
  518. OpenJobsTechnician: number;
  519. OpenJobsEngineer: number;
  520. OpenJobsScientist: number;
  521. UnemploymentRatePioneer: number;
  522. UnemploymentRateSettler: number;
  523. UnemploymentRateTechnician: number;
  524. UnemploymentRateEngineer: number;
  525. UnemploymentRateScientist: number;
  526. }
  527. interface SiteCount {
  528. Count: number;
  529. }
  530. interface Infrastructure {
  531. Type: POPIBuilding;
  532. CurrentLevel: number;
  533. Upkeeps: {Stored: number, StoreCapacity: number, 'Duration': number}[];
  534. }
  535. interface Price {
  536. ticker: string
  537. exchange_code: string
  538. vwap_30d: number
  539. ask: number
  540. }