gov.js 19 KB

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