@shipload/sdk 1.0.0-next.0 → 1.0.0-next.10

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 +512 -320
  2. package/lib/shipload.js +1960 -1060
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +1920 -1056
  5. package/lib/shipload.m.js.map +1 -1
  6. package/package.json +8 -3
  7. package/src/capabilities/modules.ts +3 -0
  8. package/src/capabilities/storage.ts +1 -1
  9. package/src/contracts/platform.ts +13 -1
  10. package/src/contracts/server.ts +227 -282
  11. package/src/data/capabilities.ts +5 -330
  12. package/src/data/capability-formulas.ts +70 -0
  13. package/src/data/catalog.ts +0 -5
  14. package/src/data/colors.ts +2 -16
  15. package/src/data/entities.json +33 -10
  16. package/src/data/item-ids.ts +3 -1
  17. package/src/data/items.json +258 -0
  18. package/src/data/metadata.ts +57 -1
  19. package/src/data/recipes-runtime.ts +1 -0
  20. package/src/data/recipes.json +82 -11
  21. package/src/derivation/capability-mappings.ts +122 -0
  22. package/src/derivation/index.ts +1 -0
  23. package/src/derivation/resources.ts +116 -37
  24. package/src/derivation/stats.ts +1 -2
  25. package/src/entities/container.ts +25 -10
  26. package/src/entities/extractor.ts +144 -0
  27. package/src/entities/gamestate.ts +0 -9
  28. package/src/entities/makers.ts +71 -20
  29. package/src/entities/ship-deploy.ts +114 -56
  30. package/src/entities/ship.ts +17 -0
  31. package/src/entities/slot-multiplier.ts +21 -0
  32. package/src/entities/warehouse.ts +20 -3
  33. package/src/index-module.ts +67 -26
  34. package/src/managers/actions.ts +53 -80
  35. package/src/managers/entities.ts +31 -17
  36. package/src/managers/locations.ts +2 -20
  37. package/src/nft/atomicdata.ts +125 -0
  38. package/src/nft/description.ts +41 -7
  39. package/src/nft/index.ts +1 -0
  40. package/src/resolution/resolve-item.ts +17 -9
  41. package/src/scheduling/accessor.ts +4 -0
  42. package/src/scheduling/projection.ts +8 -0
  43. package/src/scheduling/schedule.ts +15 -1
  44. package/src/scheduling/task-cargo.ts +47 -0
  45. package/src/subscriptions/connection.ts +50 -2
  46. package/src/subscriptions/manager.ts +81 -2
  47. package/src/travel/travel.ts +61 -2
  48. package/src/types/entity-traits.ts +64 -1
  49. package/src/types.ts +11 -1
  50. package/src/utils/cargo.ts +27 -0
  51. package/src/utils/system.ts +25 -24
@@ -20,6 +20,55 @@
20
20
  "tier": 3,
21
21
  "category": "ore"
22
22
  },
23
+ {
24
+ "id": 104,
25
+ "mass": 71000,
26
+ "type": "resource",
27
+ "tier": 4,
28
+ "category": "ore"
29
+ },
30
+ {
31
+ "id": 105,
32
+ "mass": 78000,
33
+ "type": "resource",
34
+ "tier": 5,
35
+ "category": "ore"
36
+ },
37
+ {
38
+ "id": 106,
39
+ "mass": 87000,
40
+ "type": "resource",
41
+ "tier": 6,
42
+ "category": "ore"
43
+ },
44
+ {
45
+ "id": 107,
46
+ "mass": 96000,
47
+ "type": "resource",
48
+ "tier": 7,
49
+ "category": "ore"
50
+ },
51
+ {
52
+ "id": 108,
53
+ "mass": 107000,
54
+ "type": "resource",
55
+ "tier": 8,
56
+ "category": "ore"
57
+ },
58
+ {
59
+ "id": 109,
60
+ "mass": 118000,
61
+ "type": "resource",
62
+ "tier": 9,
63
+ "category": "ore"
64
+ },
65
+ {
66
+ "id": 110,
67
+ "mass": 130000,
68
+ "type": "resource",
69
+ "tier": 10,
70
+ "category": "ore"
71
+ },
23
72
  {
24
73
  "id": 201,
25
74
  "mass": 35000,
@@ -41,6 +90,55 @@
41
90
  "tier": 3,
42
91
  "category": "crystal"
43
92
  },
93
+ {
94
+ "id": 204,
95
+ "mass": 35000,
96
+ "type": "resource",
97
+ "tier": 4,
98
+ "category": "crystal"
99
+ },
100
+ {
101
+ "id": 205,
102
+ "mass": 35000,
103
+ "type": "resource",
104
+ "tier": 5,
105
+ "category": "crystal"
106
+ },
107
+ {
108
+ "id": 206,
109
+ "mass": 35000,
110
+ "type": "resource",
111
+ "tier": 6,
112
+ "category": "crystal"
113
+ },
114
+ {
115
+ "id": 207,
116
+ "mass": 35000,
117
+ "type": "resource",
118
+ "tier": 7,
119
+ "category": "crystal"
120
+ },
121
+ {
122
+ "id": 208,
123
+ "mass": 35000,
124
+ "type": "resource",
125
+ "tier": 8,
126
+ "category": "crystal"
127
+ },
128
+ {
129
+ "id": 209,
130
+ "mass": 35000,
131
+ "type": "resource",
132
+ "tier": 9,
133
+ "category": "crystal"
134
+ },
135
+ {
136
+ "id": 210,
137
+ "mass": 35000,
138
+ "type": "resource",
139
+ "tier": 10,
140
+ "category": "crystal"
141
+ },
44
142
  {
45
143
  "id": 301,
46
144
  "mass": 15000,
@@ -62,6 +160,55 @@
62
160
  "tier": 3,
63
161
  "category": "gas"
64
162
  },
163
+ {
164
+ "id": 304,
165
+ "mass": 11000,
166
+ "type": "resource",
167
+ "tier": 4,
168
+ "category": "gas"
169
+ },
170
+ {
171
+ "id": 305,
172
+ "mass": 10000,
173
+ "type": "resource",
174
+ "tier": 5,
175
+ "category": "gas"
176
+ },
177
+ {
178
+ "id": 306,
179
+ "mass": 9000,
180
+ "type": "resource",
181
+ "tier": 6,
182
+ "category": "gas"
183
+ },
184
+ {
185
+ "id": 307,
186
+ "mass": 8000,
187
+ "type": "resource",
188
+ "tier": 7,
189
+ "category": "gas"
190
+ },
191
+ {
192
+ "id": 308,
193
+ "mass": 7500,
194
+ "type": "resource",
195
+ "tier": 8,
196
+ "category": "gas"
197
+ },
198
+ {
199
+ "id": 309,
200
+ "mass": 6500,
201
+ "type": "resource",
202
+ "tier": 9,
203
+ "category": "gas"
204
+ },
205
+ {
206
+ "id": 310,
207
+ "mass": 6000,
208
+ "type": "resource",
209
+ "tier": 10,
210
+ "category": "gas"
211
+ },
65
212
  {
66
213
  "id": 401,
67
214
  "mass": 22000,
@@ -83,6 +230,55 @@
83
230
  "tier": 3,
84
231
  "category": "regolith"
85
232
  },
233
+ {
234
+ "id": 404,
235
+ "mass": 32000,
236
+ "type": "resource",
237
+ "tier": 4,
238
+ "category": "regolith"
239
+ },
240
+ {
241
+ "id": 405,
242
+ "mass": 36000,
243
+ "type": "resource",
244
+ "tier": 5,
245
+ "category": "regolith"
246
+ },
247
+ {
248
+ "id": 406,
249
+ "mass": 40500,
250
+ "type": "resource",
251
+ "tier": 6,
252
+ "category": "regolith"
253
+ },
254
+ {
255
+ "id": 407,
256
+ "mass": 46000,
257
+ "type": "resource",
258
+ "tier": 7,
259
+ "category": "regolith"
260
+ },
261
+ {
262
+ "id": 408,
263
+ "mass": 52000,
264
+ "type": "resource",
265
+ "tier": 8,
266
+ "category": "regolith"
267
+ },
268
+ {
269
+ "id": 409,
270
+ "mass": 58500,
271
+ "type": "resource",
272
+ "tier": 9,
273
+ "category": "regolith"
274
+ },
275
+ {
276
+ "id": 410,
277
+ "mass": 66000,
278
+ "type": "resource",
279
+ "tier": 10,
280
+ "category": "regolith"
281
+ },
86
282
  {
87
283
  "id": 501,
88
284
  "mass": 42000,
@@ -104,6 +300,55 @@
104
300
  "tier": 3,
105
301
  "category": "biomass"
106
302
  },
303
+ {
304
+ "id": 504,
305
+ "mass": 29000,
306
+ "type": "resource",
307
+ "tier": 4,
308
+ "category": "biomass"
309
+ },
310
+ {
311
+ "id": 505,
312
+ "mass": 26000,
313
+ "type": "resource",
314
+ "tier": 5,
315
+ "category": "biomass"
316
+ },
317
+ {
318
+ "id": 506,
319
+ "mass": 23000,
320
+ "type": "resource",
321
+ "tier": 6,
322
+ "category": "biomass"
323
+ },
324
+ {
325
+ "id": 507,
326
+ "mass": 20000,
327
+ "type": "resource",
328
+ "tier": 7,
329
+ "category": "biomass"
330
+ },
331
+ {
332
+ "id": 508,
333
+ "mass": 18000,
334
+ "type": "resource",
335
+ "tier": 8,
336
+ "category": "biomass"
337
+ },
338
+ {
339
+ "id": 509,
340
+ "mass": 16000,
341
+ "type": "resource",
342
+ "tier": 9,
343
+ "category": "biomass"
344
+ },
345
+ {
346
+ "id": 510,
347
+ "mass": 14000,
348
+ "type": "resource",
349
+ "tier": 10,
350
+ "category": "biomass"
351
+ },
107
352
  {
108
353
  "id": 10001,
109
354
  "mass": 50000,
@@ -213,6 +458,13 @@
213
458
  "tier": 1,
214
459
  "subtype": "hauler"
215
460
  },
461
+ {
462
+ "id": 10107,
463
+ "mass": 180000,
464
+ "type": "module",
465
+ "tier": 1,
466
+ "subtype": "warp"
467
+ },
216
468
  {
217
469
  "id": 10200,
218
470
  "mass": 80000,
@@ -231,6 +483,12 @@
231
483
  "type": "entity",
232
484
  "tier": 1
233
485
  },
486
+ {
487
+ "id": 10203,
488
+ "mass": 800000,
489
+ "type": "entity",
490
+ "tier": 1
491
+ },
234
492
  {
235
493
  "id": 20001,
236
494
  "mass": 50000,
@@ -11,7 +11,7 @@ export interface EntityMetadata {
11
11
  }
12
12
 
13
13
  export const itemMetadata: Record<number, ItemMetadata> = {
14
- // === Resources (raw) ===
14
+ // === Resources / Ore ===
15
15
  101: {name: 'Ore', description: 'Crude metallic ore.', color: '#C26D3F'},
16
16
  102: {name: 'Ore', description: 'Refined metallic ore with improved purity.', color: '#C26D3F'},
17
17
  103: {
@@ -19,6 +19,15 @@ export const itemMetadata: Record<number, ItemMetadata> = {
19
19
  description: 'High-grade metallic ore with exceptional density.',
20
20
  color: '#C26D3F',
21
21
  },
22
+ 104: {name: 'Ore', description: '', color: '#C26D3F'},
23
+ 105: {name: 'Ore', description: '', color: '#C26D3F'},
24
+ 106: {name: 'Ore', description: '', color: '#C26D3F'},
25
+ 107: {name: 'Ore', description: '', color: '#C26D3F'},
26
+ 108: {name: 'Ore', description: '', color: '#C26D3F'},
27
+ 109: {name: 'Ore', description: '', color: '#C26D3F'},
28
+ 110: {name: 'Ore', description: '', color: '#C26D3F'},
29
+
30
+ // === Resources / Crystal ===
22
31
  201: {name: 'Crystal', description: 'Raw resonant crystal.', color: '#4ADBFF'},
23
32
  202: {
24
33
  name: 'Crystal',
@@ -30,6 +39,15 @@ export const itemMetadata: Record<number, ItemMetadata> = {
30
39
  description: 'High-grade resonant crystal with exceptional purity.',
31
40
  color: '#4ADBFF',
32
41
  },
42
+ 204: {name: 'Crystal', description: '', color: '#4ADBFF'},
43
+ 205: {name: 'Crystal', description: '', color: '#4ADBFF'},
44
+ 206: {name: 'Crystal', description: '', color: '#4ADBFF'},
45
+ 207: {name: 'Crystal', description: '', color: '#4ADBFF'},
46
+ 208: {name: 'Crystal', description: '', color: '#4ADBFF'},
47
+ 209: {name: 'Crystal', description: '', color: '#4ADBFF'},
48
+ 210: {name: 'Crystal', description: '', color: '#4ADBFF'},
49
+
50
+ // === Resources / Gas ===
33
51
  301: {name: 'Gas', description: 'Raw volatile gas.', color: '#B8E4A0'},
34
52
  302: {
35
53
  name: 'Gas',
@@ -41,6 +59,15 @@ export const itemMetadata: Record<number, ItemMetadata> = {
41
59
  description: 'High-grade volatile gas with exceptional energy density.',
42
60
  color: '#B8E4A0',
43
61
  },
62
+ 304: {name: 'Gas', description: '', color: '#B8E4A0'},
63
+ 305: {name: 'Gas', description: '', color: '#B8E4A0'},
64
+ 306: {name: 'Gas', description: '', color: '#B8E4A0'},
65
+ 307: {name: 'Gas', description: '', color: '#B8E4A0'},
66
+ 308: {name: 'Gas', description: '', color: '#B8E4A0'},
67
+ 309: {name: 'Gas', description: '', color: '#B8E4A0'},
68
+ 310: {name: 'Gas', description: '', color: '#B8E4A0'},
69
+
70
+ // === Resources / Regolith ===
44
71
  401: {name: 'Regolith', description: 'Crude regolith dust.', color: '#C4A57B'},
45
72
  402: {
46
73
  name: 'Regolith',
@@ -52,6 +79,15 @@ export const itemMetadata: Record<number, ItemMetadata> = {
52
79
  description: 'High-grade regolith with exceptional uniformity.',
53
80
  color: '#C4A57B',
54
81
  },
82
+ 404: {name: 'Regolith', description: '', color: '#C4A57B'},
83
+ 405: {name: 'Regolith', description: '', color: '#C4A57B'},
84
+ 406: {name: 'Regolith', description: '', color: '#C4A57B'},
85
+ 407: {name: 'Regolith', description: '', color: '#C4A57B'},
86
+ 408: {name: 'Regolith', description: '', color: '#C4A57B'},
87
+ 409: {name: 'Regolith', description: '', color: '#C4A57B'},
88
+ 410: {name: 'Regolith', description: '', color: '#C4A57B'},
89
+
90
+ // === Resources / Biomass ===
55
91
  501: {name: 'Biomass', description: 'Crude organic biomass.', color: '#5A8B3E'},
56
92
  502: {
57
93
  name: 'Biomass',
@@ -63,6 +99,13 @@ export const itemMetadata: Record<number, ItemMetadata> = {
63
99
  description: 'High-grade biomass with exceptional saturation.',
64
100
  color: '#5A8B3E',
65
101
  },
102
+ 504: {name: 'Biomass', description: '', color: '#5A8B3E'},
103
+ 505: {name: 'Biomass', description: '', color: '#5A8B3E'},
104
+ 506: {name: 'Biomass', description: '', color: '#5A8B3E'},
105
+ 507: {name: 'Biomass', description: '', color: '#5A8B3E'},
106
+ 508: {name: 'Biomass', description: '', color: '#5A8B3E'},
107
+ 509: {name: 'Biomass', description: '', color: '#5A8B3E'},
108
+ 510: {name: 'Biomass', description: '', color: '#5A8B3E'},
66
109
 
67
110
  // === Components (T1) ===
68
111
  10001: {
@@ -157,6 +200,12 @@ export const itemMetadata: Record<number, ItemMetadata> = {
157
200
  'Projects a haul beam to lock onto and transport containers or warehouses through group travel.',
158
201
  color: '#4ADBFF',
159
202
  },
203
+ 10107: {
204
+ name: 'Warp',
205
+ description:
206
+ 'Folds local space-time around the hull, projecting the ship across vast distances in a single discharge of the entire energy reserve.',
207
+ color: '#9be4ff',
208
+ },
160
209
 
161
210
  // === Entities (packed, T1) ===
162
211
  10200: {
@@ -174,6 +223,12 @@ export const itemMetadata: Record<number, ItemMetadata> = {
174
223
  description: 'Massive stationary storage facility with a single loader module slot.',
175
224
  color: '#EAB308',
176
225
  },
226
+ 10203: {
227
+ name: 'Extractor',
228
+ description:
229
+ 'Planetary resource extraction facility with generator and gatherer module slots.',
230
+ color: '#D4726F',
231
+ },
177
232
 
178
233
  // === Components (T2) ===
179
234
  20001: {
@@ -199,6 +254,7 @@ export const itemMetadata: Record<number, ItemMetadata> = {
199
254
  export const entityMetadata: Record<number, EntityMetadata> = {
200
255
  10201: {moduleSlotLabels: ['Engine', 'Generator', 'Gatherer', 'Loader', 'Storage']},
201
256
  10202: {moduleSlotLabels: ['Loader', 'Storage', 'Storage', 'Storage', 'Storage']},
257
+ 10203: {moduleSlotLabels: ['Generator', 'Gatherer']},
202
258
  }
203
259
 
204
260
  for (const item of items as Array<{id: number}>) {
@@ -30,6 +30,7 @@ export interface Recipe {
30
30
 
31
31
  export interface EntitySlot {
32
32
  type: ModuleType
33
+ outputPct: number
33
34
  }
34
35
 
35
36
  export interface EntityLayout {
@@ -393,12 +393,7 @@
393
393
  ]
394
394
  },
395
395
  {
396
- "sources": [
397
- {
398
- "inputIndex": 1,
399
- "statIndex": 1
400
- }
401
- ]
396
+ "sources": []
402
397
  },
403
398
  {
404
399
  "sources": [
@@ -436,8 +431,8 @@
436
431
  {
437
432
  "sources": [
438
433
  {
439
- "inputIndex": 0,
440
- "statIndex": 0
434
+ "inputIndex": 1,
435
+ "statIndex": 1
441
436
  }
442
437
  ]
443
438
  },
@@ -552,7 +547,7 @@
552
547
  "sources": [
553
548
  {
554
549
  "inputIndex": 0,
555
- "statIndex": 0
550
+ "statIndex": 1
556
551
  }
557
552
  ]
558
553
  },
@@ -568,12 +563,36 @@
568
563
  "sources": [
569
564
  {
570
565
  "inputIndex": 0,
571
- "statIndex": 1
566
+ "statIndex": 0
572
567
  }
573
568
  ]
574
569
  },
570
+ {
571
+ "sources": []
572
+ }
573
+ ],
574
+ "blendWeights": []
575
+ },
576
+ {
577
+ "outputItemId": 10107,
578
+ "outputMass": 180000,
579
+ "inputs": [
580
+ {
581
+ "itemId": 10010,
582
+ "quantity": 6
583
+ },
584
+ {
585
+ "itemId": 10009,
586
+ "quantity": 4
587
+ }
588
+ ],
589
+ "statSlots": [
575
590
  {
576
591
  "sources": [
592
+ {
593
+ "inputIndex": 0,
594
+ "statIndex": 1
595
+ },
577
596
  {
578
597
  "inputIndex": 1,
579
598
  "statIndex": 1
@@ -581,7 +600,10 @@
581
600
  ]
582
601
  }
583
602
  ],
584
- "blendWeights": []
603
+ "blendWeights": [
604
+ 1,
605
+ 1
606
+ ]
585
607
  },
586
608
  {
587
609
  "outputItemId": 10200,
@@ -730,6 +752,55 @@
730
752
  ],
731
753
  "blendWeights": []
732
754
  },
755
+ {
756
+ "outputItemId": 10203,
757
+ "outputMass": 800000,
758
+ "inputs": [
759
+ {
760
+ "itemId": 10001,
761
+ "quantity": 15
762
+ },
763
+ {
764
+ "itemId": 10002,
765
+ "quantity": 8
766
+ }
767
+ ],
768
+ "statSlots": [
769
+ {
770
+ "sources": [
771
+ {
772
+ "inputIndex": 0,
773
+ "statIndex": 0
774
+ }
775
+ ]
776
+ },
777
+ {
778
+ "sources": [
779
+ {
780
+ "inputIndex": 0,
781
+ "statIndex": 1
782
+ }
783
+ ]
784
+ },
785
+ {
786
+ "sources": [
787
+ {
788
+ "inputIndex": 1,
789
+ "statIndex": 0
790
+ }
791
+ ]
792
+ },
793
+ {
794
+ "sources": [
795
+ {
796
+ "inputIndex": 1,
797
+ "statIndex": 1
798
+ }
799
+ ]
800
+ }
801
+ ],
802
+ "blendWeights": []
803
+ },
733
804
  {
734
805
  "outputItemId": 20001,
735
806
  "outputMass": 50000,
@@ -0,0 +1,122 @@
1
+ import {SLOT_FORMULAS, type SlotConsumerKind} from '../data/capability-formulas'
2
+ import {getStatDefinitions, type StatDefinition} from './stats'
3
+ import {
4
+ getRecipe,
5
+ type Recipe,
6
+ type RecipeInput,
7
+ type RecipeInputCategory,
8
+ } from '../data/recipes-runtime'
9
+ import {
10
+ ITEM_ENGINE_T1,
11
+ ITEM_EXTRACTOR_T1_PACKED,
12
+ ITEM_GENERATOR_T1,
13
+ ITEM_GATHERER_T1,
14
+ ITEM_LOADER_T1,
15
+ ITEM_CRAFTER_T1,
16
+ ITEM_STORAGE_T1,
17
+ ITEM_HAULER_T1,
18
+ ITEM_WARP_T1,
19
+ ITEM_SHIP_T1_PACKED,
20
+ ITEM_CONTAINER_T1_PACKED,
21
+ ITEM_WAREHOUSE_T1_PACKED,
22
+ ITEM_CONTAINER_T2_PACKED,
23
+ } from '../data/item-ids'
24
+ import type {StatMapping} from '../data/capabilities'
25
+
26
+ export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
27
+ engine: ITEM_ENGINE_T1,
28
+ generator: ITEM_GENERATOR_T1,
29
+ gatherer: ITEM_GATHERER_T1,
30
+ loader: ITEM_LOADER_T1,
31
+ crafter: ITEM_CRAFTER_T1,
32
+ storage: ITEM_STORAGE_T1,
33
+ hauler: ITEM_HAULER_T1,
34
+ warp: ITEM_WARP_T1,
35
+ 'ship-t1': ITEM_SHIP_T1_PACKED,
36
+ 'container-t1': ITEM_CONTAINER_T1_PACKED,
37
+ 'warehouse-t1': ITEM_WAREHOUSE_T1_PACKED,
38
+ 'extractor-t1': ITEM_EXTRACTOR_T1_PACKED,
39
+ 'container-t2': ITEM_CONTAINER_T2_PACKED,
40
+ }
41
+
42
+ function isCategoryInput(input: RecipeInput): input is RecipeInputCategory {
43
+ return 'category' in input
44
+ }
45
+
46
+ /**
47
+ * Walk a recipe's slot source down to the raw category stat that ultimately
48
+ * lands in that slot. Returns the StatDefinition or undefined if the trace
49
+ * dead-ends (unknown sub-component, missing slot, etc.).
50
+ *
51
+ * Multi-source sub-slots collapse to `sources[0]`; top-level multi-source slots
52
+ * are expanded by the caller (`deriveStatMappings`).
53
+ */
54
+ function traceToRawCategoryStat(
55
+ recipe: Recipe,
56
+ source: {inputIndex: number; statIndex: number},
57
+ visited: Set<number> = new Set()
58
+ ): StatDefinition | undefined {
59
+ const input = recipe.inputs[source.inputIndex]
60
+ if (!input) return undefined
61
+ if (isCategoryInput(input)) {
62
+ const defs = getStatDefinitions(input.category)
63
+ return defs[source.statIndex]
64
+ }
65
+ if (visited.has(input.itemId)) return undefined
66
+ const subRecipe = getRecipe(input.itemId)
67
+ if (!subRecipe) return undefined
68
+ const subSlot = subRecipe.statSlots[source.statIndex]
69
+ if (!subSlot) return undefined
70
+ const subSource = subSlot.sources[0]
71
+ if (!subSource) return undefined
72
+ const nextVisited = new Set(visited)
73
+ nextVisited.add(input.itemId)
74
+ return traceToRawCategoryStat(subRecipe, subSource, nextVisited)
75
+ }
76
+
77
+ let cached: StatMapping[] | undefined
78
+
79
+ export function deriveStatMappings(): StatMapping[] {
80
+ if (cached) return cached
81
+ const out: StatMapping[] = []
82
+ const seen = new Set<string>()
83
+ for (const [kind, slots] of Object.entries(SLOT_FORMULAS) as [
84
+ SlotConsumerKind,
85
+ Record<number, {capability: string; attribute: string}>,
86
+ ][]) {
87
+ const itemId = KIND_TO_ITEM_ID[kind]
88
+ const recipe = getRecipe(itemId)
89
+ if (!recipe) continue
90
+ for (const [slotIdxStr, consumer] of Object.entries(slots)) {
91
+ const slotIdx = Number(slotIdxStr)
92
+ const slot = recipe.statSlots[slotIdx]
93
+ if (!slot) continue
94
+ for (const source of slot.sources) {
95
+ const stat = traceToRawCategoryStat(recipe, source)
96
+ if (!stat) continue
97
+ const key = `${stat.label}|${consumer.capability}|${consumer.attribute}`
98
+ if (seen.has(key)) continue
99
+ seen.add(key)
100
+ out.push({
101
+ stat: stat.label,
102
+ capability: consumer.capability,
103
+ attribute: consumer.attribute,
104
+ })
105
+ }
106
+ }
107
+ }
108
+ cached = out
109
+ return out
110
+ }
111
+
112
+ export function getStatMappings(): StatMapping[] {
113
+ return deriveStatMappings()
114
+ }
115
+
116
+ export function getStatMappingsForStat(stat: string): StatMapping[] {
117
+ return deriveStatMappings().filter((m) => m.stat === stat)
118
+ }
119
+
120
+ export function getStatMappingsForCapability(capability: string): StatMapping[] {
121
+ return deriveStatMappings().filter((m) => m.capability === capability)
122
+ }
@@ -7,6 +7,7 @@ export {
7
7
  getEligibleResources,
8
8
  getResourceWeight,
9
9
  getLocationCandidates,
10
+ getLocationProfile,
10
11
  getDepthThreshold,
11
12
  getResourceTier,
12
13
  DEPTH_THRESHOLD_T1,