@shipload/sdk 1.0.0-next.62 → 1.0.0-next.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/lib/shipload.d.ts +1172 -29
  2. package/lib/shipload.js +3877 -1294
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +3781 -1298
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +391 -13
  7. package/lib/testing.js +1554 -114
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +1555 -115
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +2 -2
  12. package/src/contracts/fund.ts +201 -0
  13. package/src/contracts/platform.ts +73 -1
  14. package/src/contracts/server.ts +946 -26
  15. package/src/contracts.ts +1 -0
  16. package/src/data/charters.json +198 -0
  17. package/src/data/entities.json +36 -0
  18. package/src/data/item-ids.ts +2 -0
  19. package/src/data/items.json +12 -0
  20. package/src/data/kind-registry.json +20 -3
  21. package/src/data/kind-registry.ts +5 -0
  22. package/src/data/metadata.ts +14 -0
  23. package/src/data/recipes.json +0 -48
  24. package/src/derivation/capabilities.ts +14 -0
  25. package/src/derivation/recipe-usage.test.ts +0 -2
  26. package/src/format.ts +9 -0
  27. package/src/index-module.ts +24 -1
  28. package/src/influence/categories.ts +32 -0
  29. package/src/influence/charters.test.ts +126 -0
  30. package/src/influence/charters.ts +120 -0
  31. package/src/influence/citizenry.ts +31 -0
  32. package/src/influence/constants.ts +63 -0
  33. package/src/influence/decay.ts +20 -0
  34. package/src/influence/decomp.ts +95 -0
  35. package/src/influence/demand.ts +106 -0
  36. package/src/influence/duration.ts +36 -0
  37. package/src/influence/index.ts +63 -0
  38. package/src/influence/influence.test.ts +291 -0
  39. package/src/influence/quality.ts +25 -0
  40. package/src/influence/valuation.ts +154 -0
  41. package/src/managers/actions.ts +135 -0
  42. package/src/managers/construction.ts +11 -1
  43. package/src/managers/context.ts +9 -0
  44. package/src/managers/entities.ts +11 -0
  45. package/src/managers/index.ts +12 -0
  46. package/src/managers/influence.test.ts +58 -0
  47. package/src/managers/influence.ts +347 -0
  48. package/src/nft/description.ts +7 -0
  49. package/src/scheduling/task-cargo.ts +1 -0
  50. package/src/shipload.ts +5 -0
  51. package/src/types.ts +23 -4
package/src/contracts.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * as FundContract from './contracts/fund'
1
2
  export * as PlatformContract from './contracts/platform'
2
3
  export * as ServerContract from './contracts/server'
@@ -0,0 +1,198 @@
1
+ {
2
+ "nodes": [
3
+ {
4
+ "nodeId": 1,
5
+ "cost": "2000000000",
6
+ "prereqs": [],
7
+ "effect": {
8
+ "kind": 1,
9
+ "itemId": 10208,
10
+ "targetItemId": 0,
11
+ "slotMask": 0,
12
+ "stat": 213
13
+ }
14
+ },
15
+ {
16
+ "nodeId": 2,
17
+ "cost": "4000000000",
18
+ "prereqs": [
19
+ 1
20
+ ],
21
+ "effect": {
22
+ "kind": 1,
23
+ "itemId": 10216,
24
+ "targetItemId": 0,
25
+ "slotMask": 0,
26
+ "stat": 213
27
+ }
28
+ },
29
+ {
30
+ "nodeId": 3,
31
+ "cost": "3000000000",
32
+ "prereqs": [
33
+ 1
34
+ ],
35
+ "effect": {
36
+ "kind": 1,
37
+ "itemId": 10209,
38
+ "targetItemId": 0,
39
+ "slotMask": 0,
40
+ "stat": 213
41
+ }
42
+ },
43
+ {
44
+ "nodeId": 4,
45
+ "cost": "2000000000",
46
+ "prereqs": [
47
+ 1
48
+ ],
49
+ "effect": {
50
+ "kind": 2,
51
+ "itemId": 10104,
52
+ "targetItemId": 10208,
53
+ "slotMask": 0,
54
+ "stat": 400
55
+ }
56
+ },
57
+ {
58
+ "nodeId": 5,
59
+ "cost": "3000000000",
60
+ "prereqs": [
61
+ 3
62
+ ],
63
+ "effect": {
64
+ "kind": 2,
65
+ "itemId": 10110,
66
+ "targetItemId": 10209,
67
+ "slotMask": 2,
68
+ "stat": 400
69
+ }
70
+ },
71
+ {
72
+ "nodeId": 6,
73
+ "cost": "2500000000",
74
+ "prereqs": [
75
+ 1
76
+ ],
77
+ "effect": {
78
+ "kind": 1,
79
+ "itemId": 10219,
80
+ "targetItemId": 0,
81
+ "slotMask": 1,
82
+ "stat": 0
83
+ }
84
+ },
85
+ {
86
+ "nodeId": 7,
87
+ "cost": "1000000000",
88
+ "prereqs": [
89
+ 6
90
+ ],
91
+ "effect": {
92
+ "kind": 2,
93
+ "itemId": 10105,
94
+ "targetItemId": 10219,
95
+ "slotMask": 2,
96
+ "stat": 213
97
+ }
98
+ },
99
+ {
100
+ "nodeId": 8,
101
+ "cost": "1500000000",
102
+ "prereqs": [
103
+ 7
104
+ ],
105
+ "effect": {
106
+ "kind": 2,
107
+ "itemId": 10105,
108
+ "targetItemId": 10219,
109
+ "slotMask": 4,
110
+ "stat": 213
111
+ }
112
+ },
113
+ {
114
+ "nodeId": 9,
115
+ "cost": "2000000000",
116
+ "prereqs": [
117
+ 8
118
+ ],
119
+ "effect": {
120
+ "kind": 2,
121
+ "itemId": 10105,
122
+ "targetItemId": 10219,
123
+ "slotMask": 8,
124
+ "stat": 213
125
+ }
126
+ },
127
+ {
128
+ "nodeId": 10,
129
+ "cost": "2500000000",
130
+ "prereqs": [
131
+ 9
132
+ ],
133
+ "effect": {
134
+ "kind": 2,
135
+ "itemId": 10105,
136
+ "targetItemId": 10219,
137
+ "slotMask": 16,
138
+ "stat": 213
139
+ }
140
+ },
141
+ {
142
+ "nodeId": 11,
143
+ "cost": "800000000",
144
+ "prereqs": [
145
+ 6
146
+ ],
147
+ "effect": {
148
+ "kind": 2,
149
+ "itemId": 10103,
150
+ "targetItemId": 10219,
151
+ "slotMask": 1,
152
+ "stat": 100
153
+ }
154
+ },
155
+ {
156
+ "nodeId": 12,
157
+ "cost": "1200000000",
158
+ "prereqs": [
159
+ 11
160
+ ],
161
+ "effect": {
162
+ "kind": 2,
163
+ "itemId": 10103,
164
+ "targetItemId": 10219,
165
+ "slotMask": 1,
166
+ "stat": 200
167
+ }
168
+ },
169
+ {
170
+ "nodeId": 13,
171
+ "cost": "1600000000",
172
+ "prereqs": [
173
+ 12
174
+ ],
175
+ "effect": {
176
+ "kind": 2,
177
+ "itemId": 10103,
178
+ "targetItemId": 10219,
179
+ "slotMask": 1,
180
+ "stat": 300
181
+ }
182
+ },
183
+ {
184
+ "nodeId": 14,
185
+ "cost": "2000000000",
186
+ "prereqs": [
187
+ 13
188
+ ],
189
+ "effect": {
190
+ "kind": 2,
191
+ "itemId": 10103,
192
+ "targetItemId": 10219,
193
+ "slotMask": 1,
194
+ "stat": 400
195
+ }
196
+ }
197
+ ]
198
+ }
@@ -313,6 +313,11 @@
313
313
  ],
314
314
  "baseHullmass": 4000000
315
315
  },
316
+ {
317
+ "entityItemId": 10216,
318
+ "slots": [],
319
+ "baseHullmass": 100000
320
+ },
316
321
  {
317
322
  "entityItemId": 10218,
318
323
  "slots": [
@@ -334,6 +339,37 @@
334
339
  ],
335
340
  "baseHullmass": 4000000
336
341
  },
342
+ {
343
+ "entityItemId": 10219,
344
+ "slots": [
345
+ {
346
+ "type": "loader",
347
+ "outputPct": 100,
348
+ "maxTier": 1
349
+ },
350
+ {
351
+ "type": "storage",
352
+ "outputPct": 100,
353
+ "maxTier": 1
354
+ },
355
+ {
356
+ "type": "storage",
357
+ "outputPct": 100,
358
+ "maxTier": 1
359
+ },
360
+ {
361
+ "type": "storage",
362
+ "outputPct": 100,
363
+ "maxTier": 1
364
+ },
365
+ {
366
+ "type": "storage",
367
+ "outputPct": 100,
368
+ "maxTier": 1
369
+ }
370
+ ],
371
+ "baseHullmass": 100000
372
+ },
337
373
  {
338
374
  "entityItemId": 11200,
339
375
  "slots": [],
@@ -87,7 +87,9 @@ export const ITEM_TENDER_T1A_PACKED = 10212
87
87
  export const ITEM_WRIGHT_T1A_PACKED = 10213
88
88
  export const ITEM_TUG_T1A_PACKED = 10214
89
89
  export const ITEM_PORTER_T1A_PACKED = 10215
90
+ export const ITEM_NEXUS_T1_PACKED = 10216
90
91
  export const ITEM_SMITH_T1A_PACKED = 10218
92
+ export const ITEM_DEPOT_T1_PACKED = 10219
91
93
  export const ITEM_PLATE_T2 = 11001
92
94
  export const ITEM_FRAME_T2 = 11002
93
95
  export const ITEM_PLASMA_CELL_T2 = 11003
@@ -582,12 +582,24 @@
582
582
  "type": "entity",
583
583
  "tier": 1
584
584
  },
585
+ {
586
+ "id": 10216,
587
+ "mass": 4800000,
588
+ "type": "entity",
589
+ "tier": 1
590
+ },
585
591
  {
586
592
  "id": 10218,
587
593
  "mass": 2400000,
588
594
  "type": "entity",
589
595
  "tier": 1
590
596
  },
597
+ {
598
+ "id": 10219,
599
+ "mass": 4800000,
600
+ "type": "entity",
601
+ "tier": 1
602
+ },
591
603
  {
592
604
  "id": 11001,
593
605
  "mass": 4800,
@@ -51,9 +51,9 @@
51
51
  },
52
52
  {
53
53
  "kind": "nexus",
54
- "classification": "OrbitalVessel",
54
+ "classification": "OrbitalStructure",
55
55
  "capabilityFlags": 0,
56
- "zCoord": 800,
56
+ "zCoord": 0,
57
57
  "defaultLabel": "Nexus"
58
58
  },
59
59
  {
@@ -72,7 +72,7 @@
72
72
  },
73
73
  {
74
74
  "kind": "workshop",
75
- "classification": "OrbitalStructure",
75
+ "classification": "PlanetaryStructure",
76
76
  "capabilityFlags": 20,
77
77
  "zCoord": 0,
78
78
  "defaultLabel": "Workshop"
@@ -83,6 +83,13 @@
83
83
  "capabilityFlags": 20,
84
84
  "zCoord": 0,
85
85
  "defaultLabel": "Construction Dock"
86
+ },
87
+ {
88
+ "kind": "depot",
89
+ "classification": "PlanetaryStructure",
90
+ "capabilityFlags": 0,
91
+ "zCoord": 0,
92
+ "defaultLabel": "Depot"
86
93
  }
87
94
  ],
88
95
  "templates": [
@@ -176,6 +183,11 @@
176
183
  "kind": "builddock",
177
184
  "displayLabel": ""
178
185
  },
186
+ {
187
+ "itemId": 10216,
188
+ "kind": "nexus",
189
+ "displayLabel": ""
190
+ },
179
191
  {
180
192
  "itemId": 11212,
181
193
  "kind": "ship",
@@ -190,6 +202,11 @@
190
202
  "itemId": 11214,
191
203
  "kind": "ship",
192
204
  "displayLabel": "Dredger"
205
+ },
206
+ {
207
+ "itemId": 10219,
208
+ "kind": "depot",
209
+ "displayLabel": ""
193
210
  }
194
211
  ]
195
212
  }
@@ -34,6 +34,7 @@ export type EntityTypeName =
34
34
  | 'mdriver'
35
35
  | 'mcatcher'
36
36
  | 'hub'
37
+ | 'depot'
37
38
 
38
39
  export interface KindMeta {
39
40
  kind: Name
@@ -143,6 +144,7 @@ export const ENTITY_PLOT = Name.from('plot')
143
144
  export const ENTITY_MASS_DRIVER = Name.from('mdriver')
144
145
  export const ENTITY_MASS_CATCHER = Name.from('mcatcher')
145
146
  export const ENTITY_HUB = Name.from('hub')
147
+ export const ENTITY_DEPOT = Name.from('depot')
146
148
 
147
149
  export function isShip(entity: {type?: Name}): boolean {
148
150
  return entity.type?.equals(ENTITY_SHIP) ?? false
@@ -180,3 +182,6 @@ export function isMassCatcher(entity: {type?: Name}): boolean {
180
182
  export function isHub(entity: {type?: Name}): boolean {
181
183
  return entity.type?.equals(ENTITY_HUB) ?? false
182
184
  }
185
+ export function isDepot(entity: {type?: Name}): boolean {
186
+ return entity.type?.equals(ENTITY_DEPOT) ?? false
187
+ }
@@ -320,12 +320,23 @@ export const itemMetadata: Record<number, ItemMetadata> = {
320
320
  'A cargo ship built around a large storage hold. The extra mass makes it slower than a Roustabout.',
321
321
  color: '#4AE898',
322
322
  },
323
+ 10216: {
324
+ name: 'Nexus',
325
+ description:
326
+ 'A permanent installation at a world where NFT-backed assets are wrapped, unwrapped, and deployed.',
327
+ color: '#FFD24C',
328
+ },
323
329
  10218: {
324
330
  name: 'Smith',
325
331
  description:
326
332
  'A crafting ship. One engine, one power core, and a fabricator for crafting away from home.',
327
333
  color: '#4AE898',
328
334
  },
335
+ 10219: {
336
+ name: 'Depot',
337
+ description: 'Planetary storage facility with a loader module slot and four cargo slots.',
338
+ color: '#EAB308',
339
+ },
329
340
 
330
341
  // === Components (T2) ===
331
342
  11001: {
@@ -485,6 +496,9 @@ export const entityMetadata: Record<number, EntityMetadata> = {
485
496
  moduleSlotLabels: ['Fabricator', 'Fabricator', 'Fabricator', 'Fabricator', 'Fabricator'],
486
497
  },
487
498
  10209: {moduleSlotLabels: ['Power Core', 'Assembly Arm']},
499
+ 10219: {
500
+ moduleSlotLabels: ['Shuttle Bay', 'Cargo Hold', 'Cargo Hold', 'Cargo Hold', 'Cargo Hold'],
501
+ },
488
502
  11212: {moduleSlotLabels: ['Power Core', 'Engine', 'Limpet Bay']},
489
503
  11213: {moduleSlotLabels: ['Power Core', 'Engine', 'Auxiliary System', 'Limpet Bay']},
490
504
  11214: {moduleSlotLabels: ['Power Core', 'Engine', 'Limpet Bay', 'Cargo Hold']},
@@ -1016,54 +1016,6 @@
1016
1016
  ],
1017
1017
  "blendWeights": []
1018
1018
  },
1019
- {
1020
- "outputItemId": 10208,
1021
- "outputMass": 4800000,
1022
- "inputs": [
1023
- {
1024
- "itemId": 10006,
1025
- "quantity": 1000
1026
- },
1027
- {
1028
- "itemId": 10003,
1029
- "quantity": 1000
1030
- },
1031
- {
1032
- "itemId": 10007,
1033
- "quantity": 1000
1034
- }
1035
- ],
1036
- "statSlots": [
1037
- {
1038
- "sources": []
1039
- },
1040
- {
1041
- "sources": [
1042
- {
1043
- "inputIndex": 1,
1044
- "statIndex": 0
1045
- }
1046
- ]
1047
- },
1048
- {
1049
- "sources": [
1050
- {
1051
- "inputIndex": 0,
1052
- "statIndex": 0
1053
- }
1054
- ]
1055
- },
1056
- {
1057
- "sources": [
1058
- {
1059
- "inputIndex": 2,
1060
- "statIndex": 0
1061
- }
1062
- ]
1063
- }
1064
- ],
1065
- "blendWeights": []
1066
- },
1067
1019
  {
1068
1020
  "outputItemId": 10209,
1069
1021
  "outputMass": 1900000,
@@ -233,6 +233,7 @@ export function computeBatteryCapabilities(
233
233
 
234
234
  import {
235
235
  ITEM_CONTAINER_T1_PACKED,
236
+ ITEM_DEPOT_T1_PACKED,
236
237
  ITEM_SHIP_T1_PACKED,
237
238
  ITEM_WAREHOUSE_T1_PACKED,
238
239
  ITEM_WORKSHOP_T1_PACKED,
@@ -315,6 +316,7 @@ export function computeGathererYield(str: number, tier: number): number {
315
316
 
316
317
  const BASE_CAPACITY_FN_BY_KIND: Record<string, (stats: Record<string, number>) => number> = {
317
318
  warehouse: (stats) => computeWarehouseHullCapabilities(stats).capacity,
319
+ depot: (stats) => computeDepotHullCapabilities(stats).capacity,
318
320
  workshop: (stats) => computeWorkshopHullCapabilities(stats).capacity,
319
321
  extractor: (stats) => computeContainerCapabilities(stats).capacity,
320
322
  factory: (stats) => computeContainerCapabilities(stats).capacity,
@@ -361,6 +363,18 @@ export function computeWarehouseHullCapabilities(stats: Record<string, number>):
361
363
  }
362
364
  }
363
365
 
366
+ export function computeDepotHullCapabilities(stats: Record<string, number>): {
367
+ hullmass: number
368
+ capacity: number
369
+ } {
370
+ const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
371
+ const exponent = statSum / 1998.0
372
+ return {
373
+ hullmass: computeBaseHullmass(ITEM_DEPOT_T1_PACKED, stats),
374
+ capacity: Math.floor(50000000 * 6 ** exponent),
375
+ }
376
+ }
377
+
364
378
  export function computeWorkshopHullCapabilities(stats: Record<string, number>): {
365
379
  hullmass: number
366
380
  capacity: number
@@ -15,7 +15,6 @@ import {
15
15
  ITEM_CRAFTER_T1,
16
16
  ITEM_BUILDER_T1,
17
17
  ITEM_EXTRACTOR_T1_PACKED,
18
- ITEM_WORKSHOP_T1_PACKED,
19
18
  ITEM_ROUSTABOUT_T1A_PACKED,
20
19
  ITEM_PROSPECTOR_T1A_PACKED,
21
20
  ITEM_TENDER_T1A_PACKED,
@@ -37,7 +36,6 @@ test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
37
36
  ITEM_CRAFTER_T1,
38
37
  ITEM_BUILDER_T1,
39
38
  ITEM_EXTRACTOR_T1_PACKED,
40
- ITEM_WORKSHOP_T1_PACKED,
41
39
  ITEM_ROUSTABOUT_T1A_PACKED,
42
40
  ].sort((a, b) => a - b)
43
41
  )
package/src/format.ts CHANGED
@@ -32,3 +32,12 @@ export function formatMassScaled(kg: number): string {
32
32
  if (tonnes >= 1_000) return `${sign}${trim(tonnes / 1_000)}k t`
33
33
  return formatMass(kg)
34
34
  }
35
+
36
+ export function formatInfluence(atomic: bigint | number | string): string {
37
+ const points = BigInt(String(atomic)) / 10_000n
38
+ return `${points.toLocaleString('en-US')} pts`
39
+ }
40
+
41
+ export function formatNeedMultiplier(needFp: bigint | number | string): string {
42
+ return `×${(Number(BigInt(String(needFp))) / 10_000).toFixed(1)}`
43
+ }
@@ -52,9 +52,19 @@ export {
52
52
  NftManager,
53
53
  ConstructionManager,
54
54
  JobsManager,
55
+ InfluenceManager,
55
56
  } from './managers'
56
57
  export type {GridCell, ClusterCell, Cluster} from './managers'
57
58
  export type {
59
+ CharterProgress,
60
+ ContributePreview,
61
+ ContributePreviewRow,
62
+ FoundedWorld,
63
+ InfluenceStanding,
64
+ PendingBallot,
65
+ VoteCast,
66
+ VoteOption,
67
+ VoteStandings,
58
68
  PlayerRosterEntry,
59
69
  LocationStratum,
60
70
  NftConfigForItem,
@@ -356,6 +366,7 @@ export {
356
366
  ENTITY_CONTAINER,
357
367
  ENTITY_NEXUS,
358
368
  ENTITY_HUB,
369
+ ENTITY_DEPOT,
359
370
  getEntityClass,
360
371
  getPackedEntityType,
361
372
  getKindMeta,
@@ -376,6 +387,7 @@ export {
376
387
  isNexus,
377
388
  isPlot,
378
389
  isHub,
390
+ isDepot,
379
391
  } from './data/kind-registry'
380
392
  export type {EntityTypeName, KindMeta, TemplateMeta} from './data/kind-registry'
381
393
  export * from './capabilities'
@@ -462,6 +474,7 @@ export {
462
474
  computeCrafterCapabilities,
463
475
  computeBuilderCapabilities,
464
476
  computeWarehouseHullCapabilities,
477
+ computeDepotHullCapabilities,
465
478
  computeStorageCapabilities,
466
479
  computeBatteryCapabilities,
467
480
  computeContainerCapabilities,
@@ -583,6 +596,7 @@ export {
583
596
  computeBaseHullmass,
584
597
  computeBaseCapacityShip,
585
598
  computeBaseCapacityWarehouse,
599
+ computeBaseCapacityDepot,
586
600
  computeBaseCapacityContainer,
587
601
  computeEngineThrust,
588
602
  computeEngineDrain,
@@ -613,7 +627,14 @@ export {
613
627
  itemTypeCode,
614
628
  } from './data/tiers'
615
629
 
616
- export {formatMass, formatMassDelta, formatMassScaled, formatLocation} from './format'
630
+ export {
631
+ formatInfluence,
632
+ formatLocation,
633
+ formatMass,
634
+ formatMassDelta,
635
+ formatMassScaled,
636
+ formatNeedMultiplier,
637
+ } from './format'
617
638
  export * from './coordinates'
618
639
 
619
640
  export {displayName, baseName, describeItem} from './resolution/display-name'
@@ -639,3 +660,5 @@ export type {
639
660
  LanePlanEntry,
640
661
  PlanTarget,
641
662
  } from './planner'
663
+
664
+ export * from './influence'
@@ -0,0 +1,32 @@
1
+ import {categoryFromIndex} from '../data/catalog'
2
+ import type {ResourceCategory} from '../types'
3
+ import {
4
+ RESOURCE_BIOMASS,
5
+ RESOURCE_CATEGORY_COUNT,
6
+ RESOURCE_CRYSTAL,
7
+ RESOURCE_GAS,
8
+ RESOURCE_ORE,
9
+ RESOURCE_REGOLITH,
10
+ } from './constants'
11
+
12
+ const INDEX_BY_CATEGORY: Record<ResourceCategory, number> = {
13
+ ore: RESOURCE_ORE,
14
+ gas: RESOURCE_GAS,
15
+ regolith: RESOURCE_REGOLITH,
16
+ biomass: RESOURCE_BIOMASS,
17
+ crystal: RESOURCE_CRYSTAL,
18
+ }
19
+
20
+ export function categoryIndex(category: ResourceCategory): number {
21
+ return INDEX_BY_CATEGORY[category]
22
+ }
23
+
24
+ export function categoryAtIndex(index: number): ResourceCategory {
25
+ const category = categoryFromIndex(index)
26
+ if (!category) throw new Error(`influence: invalid resource category index ${index}`)
27
+ return category
28
+ }
29
+
30
+ export function allCategoryIndexes(): number[] {
31
+ return Array.from({length: RESOURCE_CATEGORY_COUNT}, (_, i) => i)
32
+ }