gov.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. const encodedPlanetName = encodeURIComponent(planetName);
  39. const [planet, siteCounts, infras]: [Planet, SiteCount[], Infrastructure[]] = await Promise.all([
  40. cachedFetchJSON(`https://api.fnar.net/planet/${encodedPlanetName}?include_population_reports=true`),
  41. cachedFetchJSON(`https://api.fnar.net/planet/sitecount?planet=${encodedPlanetName}`),
  42. cachedFetchJSON(`https://api.fnar.net/infrastructure?infrastructure=${encodedPlanetName}&include_upkeeps=true`)
  43. ]);
  44. let lastPOPR = null;
  45. for (const report of planet.PopulationReports) {
  46. if (!lastPOPR || report.SimulationPeriod > lastPOPR.SimulationPeriod)
  47. lastPOPR = report;
  48. }
  49. if (lastPOPR === null) {
  50. renderTarget.textContent = `no POPR for ${planetName}`;
  51. return;
  52. }
  53. const lastPOPRts = Math.floor(new Date(lastPOPR.ReportTimestamp).getTime() / 1000);
  54. const nextPOPRts = lastPOPRts + 7 * 24 * 60 * 60;
  55. const siteCount = siteCounts[0].Count;
  56. const totalNeeds: Record<Need, number> = {
  57. 'safety': calcTotalNeeds(lastPOPR, 'safety'),
  58. 'health': calcTotalNeeds(lastPOPR, 'health'),
  59. 'comfort': calcTotalNeeds(lastPOPR, 'comfort'),
  60. 'culture': calcTotalNeeds(lastPOPR, 'culture'),
  61. 'education': calcTotalNeeds(lastPOPR, 'education'),
  62. };
  63. renderTarget.innerHTML = `last POPR: ${lastPOPR.ReportTimestamp} (${lastPOPRts})
  64. <br>next POPR: ${new Date(nextPOPRts * 1000).toISOString()} (${nextPOPRts})
  65. <table>
  66. <tr>
  67. <th></th>
  68. <th>pio</th>
  69. <th>set</th>
  70. <th>tec</th>
  71. <th>eng</th>
  72. <th>sci</th>
  73. </tr>
  74. <tr>
  75. <th>population</th>
  76. <td>${lastPOPR.NextPopulationPioneer}<br>${formatDelta(lastPOPR.PopulationDifferencePioneer)}</td>
  77. <td>${lastPOPR.NextPopulationSettler}<br>${formatDelta(lastPOPR.PopulationDifferenceSettler)}</td>
  78. <td>${lastPOPR.NextPopulationTechnician}<br>${formatDelta(lastPOPR.PopulationDifferenceTechnician)}</td>
  79. <td>${lastPOPR.NextPopulationEngineer}<br>${formatDelta(lastPOPR.PopulationDifferenceEngineer)}</td>
  80. <td>${lastPOPR.NextPopulationScientist}<br>${formatDelta(lastPOPR.PopulationDifferenceScientist)}</td>
  81. </tr>
  82. <tr>
  83. <th>unemployed</th>
  84. <td>${unemployed(lastPOPR.NextPopulationPioneer, lastPOPR.PopulationDifferencePioneer, lastPOPR.OpenJobsPioneer, lastPOPR.UnemploymentRatePioneer)}</td>
  85. <td>${unemployed(lastPOPR.NextPopulationSettler, lastPOPR.PopulationDifferenceSettler, lastPOPR.OpenJobsSettler, lastPOPR.UnemploymentRateSettler)}</td>
  86. <td>${unemployed(lastPOPR.NextPopulationTechnician, lastPOPR.PopulationDifferenceTechnician, lastPOPR.OpenJobsTechnician, lastPOPR.UnemploymentRateTechnician)}</td>
  87. <td>${unemployed(lastPOPR.NextPopulationEngineer, lastPOPR.PopulationDifferenceEngineer, lastPOPR.OpenJobsEngineer, lastPOPR.UnemploymentRateEngineer)}</td>
  88. <td>${unemployed(lastPOPR.NextPopulationScientist, lastPOPR.PopulationDifferenceScientist, lastPOPR.OpenJobsScientist, lastPOPR.UnemploymentRateScientist)}</td>
  89. </tr>
  90. </table>
  91. <h2>needs</h2>
  92. ${siteCount} bases
  93. <table>
  94. <tr>
  95. <th></th>
  96. <th>safety</th>
  97. <th>health</th>
  98. <th>comfort</th>
  99. <th>culture</th>
  100. <th>education</th>
  101. </tr>
  102. <tr>
  103. <td>last POPR</td>
  104. <td>${formatPct(lastPOPR.NeedFulfillmentSafety)}</td>
  105. <td>${formatPct(lastPOPR.NeedFulfillmentHealth)}</td>
  106. <td>${formatPct(lastPOPR.NeedFulfillmentComfort)}</td>
  107. <td>${formatPct(lastPOPR.NeedFulfillmentCulture)}</td>
  108. <td>${formatPct(lastPOPR.NeedFulfillmentEducation)}</td>
  109. </tr>
  110. <tr>
  111. <td>total needed</td>
  112. <td>${formatNum(totalNeeds['safety'])}</td>
  113. <td>${formatNum(totalNeeds['health'])}</td>
  114. <td>${formatNum(totalNeeds['comfort'])}</td>
  115. <td>${formatNum(totalNeeds['culture'])}</td>
  116. <td>${formatNum(totalNeeds['education'])}</td>
  117. </tr>
  118. </table>
  119. <h2>POPI</h2>
  120. <table>
  121. <tr>
  122. ${infras.map(infra => popiFilled(infra)).join('')}
  123. </tr>
  124. </table>
  125. `;
  126. loader.style.display = 'none';
  127. }
  128. const formatNum = new Intl.NumberFormat(undefined, {maximumFractionDigits: 2}).format;
  129. const formatPct = new Intl.NumberFormat(undefined, {style: 'percent', maximumFractionDigits: 2}).format;
  130. function formatDelta(n: number, withPlus: boolean = true): string {
  131. if (n > 0)
  132. return `<span class="positive">${withPlus ? '+' : ''}${n}</span>`;
  133. else if (n < 0)
  134. return `<span class="negative">${n}</span>`;
  135. else
  136. return n.toString();
  137. }
  138. function unemployed(people: number, change: number, openJobs: number, unemploymentRate: number): string {
  139. let nextUnemployed;
  140. if (openJobs <= people + change) {
  141. let prevUnemploymentRate = unemploymentRate;
  142. if (openJobs > 0)
  143. if (people - change > 0)
  144. prevUnemploymentRate = -openJobs / (people - change);
  145. else
  146. prevUnemploymentRate = -1
  147. const prevUnemployed = prevUnemploymentRate * (people - change);
  148. nextUnemployed = prevUnemployed + change;
  149. } else
  150. nextUnemployed = -openJobs + change;
  151. return formatDelta(Math.round(nextUnemployed), false);
  152. }
  153. function calcTotalNeeds(popReport: POPR, need: Need): number {
  154. return popReport.NextPopulationPioneer * NEEDS.pio[need]
  155. + popReport.NextPopulationSettler * NEEDS.set[need]
  156. + popReport.NextPopulationTechnician * NEEDS.tec[need]
  157. + popReport.NextPopulationEngineer * NEEDS.eng[need]
  158. + popReport.NextPopulationScientist * NEEDS.sci[need];
  159. }
  160. function popiFilled(infra: Infrastructure): string {
  161. if (infra.CurrentLevel === 0)
  162. return '';
  163. let filled = 0;
  164. for (const upkeep of infra.Upkeeps) {
  165. const nextConsumptionAmount = upkeep.StoreCapacity / 30 * upkeep.Duration; // # capacity is always 30 days
  166. if (upkeep.Stored >= nextConsumptionAmount)
  167. filled++;
  168. }
  169. return `<tr>
  170. <td>${infra.Type} T${infra.CurrentLevel}</td>
  171. <td>${filled}/${infra.Upkeeps.length}</td>
  172. </tr>`;
  173. }
  174. const NEEDS: Record<Pop, Record<Need, number>> = {
  175. 'pio': {'safety': 0.25, 'health': 0.15, 'comfort': 0.03, 'culture': 0.02, 'education': 0.01},
  176. 'set': {'safety': 0.30, 'health': 0.20, 'comfort': 0.03, 'culture': 0.03, 'education': 0.03},
  177. 'tec': {'safety': 0.20, 'health': 0.30, 'comfort': 0.20, 'culture': 0.10, 'education': 0.05},
  178. 'eng': {'safety': 0.10, 'health': 0.15, 'comfort': 0.35, 'culture': 0.20, 'education': 0.10},
  179. 'sci': {'safety': 0.10, 'health': 0.10, 'comfort': 0.20, 'culture': 0.25, 'education': 0.30},
  180. }
  181. type Pop = 'pio' | 'set' | 'tec' | 'eng' | 'sci';
  182. type Need = 'safety' | 'health' | 'comfort' | 'culture' | 'education';
  183. interface Planet {
  184. PopulationReports: POPR[]
  185. }
  186. interface POPR {
  187. SimulationPeriod: number;
  188. ReportTimestamp: string;
  189. NeedFulfillmentSafety: number;
  190. NeedFulfillmentHealth: number;
  191. NeedFulfillmentComfort: number;
  192. NeedFulfillmentCulture: number;
  193. NeedFulfillmentEducation: number;
  194. NextPopulationPioneer: number;
  195. NextPopulationSettler: number;
  196. NextPopulationTechnician: number;
  197. NextPopulationEngineer: number;
  198. NextPopulationScientist: number;
  199. PopulationDifferencePioneer: number;
  200. PopulationDifferenceSettler: number;
  201. PopulationDifferenceTechnician: number;
  202. PopulationDifferenceEngineer: number;
  203. PopulationDifferenceScientist: number;
  204. OpenJobsPioneer: number;
  205. OpenJobsSettler: number;
  206. OpenJobsTechnician: number;
  207. OpenJobsEngineer: number;
  208. OpenJobsScientist: number;
  209. UnemploymentRatePioneer: number;
  210. UnemploymentRateSettler: number;
  211. UnemploymentRateTechnician: number;
  212. UnemploymentRateEngineer: number;
  213. UnemploymentRateScientist: number;
  214. }
  215. interface SiteCount {
  216. Count: number;
  217. }
  218. interface Infrastructure {
  219. Type: string;
  220. CurrentLevel: number;
  221. Upkeeps: {Stored: number, StoreCapacity: number, 'Duration': number}[];
  222. }