@shipload/sdk 1.0.0-next.3 → 1.0.0-next.30

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 (99) hide show
  1. package/lib/shipload.d.ts +1847 -962
  2. package/lib/shipload.js +9088 -4854
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +8957 -4805
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +856 -0
  7. package/lib/testing.js +3739 -0
  8. package/lib/testing.js.map +1 -0
  9. package/lib/testing.m.js +3733 -0
  10. package/lib/testing.m.js.map +1 -0
  11. package/package.json +15 -2
  12. package/src/capabilities/craftable.ts +51 -0
  13. package/src/capabilities/crafting.test.ts +7 -0
  14. package/src/capabilities/crafting.ts +3 -3
  15. package/src/capabilities/gathering.ts +17 -7
  16. package/src/capabilities/index.ts +0 -1
  17. package/src/capabilities/modules.ts +6 -0
  18. package/src/capabilities/storage.ts +16 -1
  19. package/src/contracts/platform.ts +231 -3
  20. package/src/contracts/server.ts +816 -471
  21. package/src/data/capabilities.ts +14 -329
  22. package/src/data/capability-formulas.ts +76 -0
  23. package/src/data/catalog.ts +0 -5
  24. package/src/data/colors.ts +14 -47
  25. package/src/data/entities.json +46 -10
  26. package/src/data/item-ids.ts +15 -12
  27. package/src/data/items.json +302 -38
  28. package/src/data/kind-registry.json +85 -0
  29. package/src/data/kind-registry.ts +150 -0
  30. package/src/data/metadata.ts +100 -31
  31. package/src/data/recipes-runtime.ts +3 -23
  32. package/src/data/recipes.json +250 -113
  33. package/src/derivation/build-methods.ts +45 -0
  34. package/src/derivation/capabilities.ts +415 -0
  35. package/src/derivation/capability-mappings.ts +117 -0
  36. package/src/derivation/crafting.ts +23 -24
  37. package/src/derivation/index.ts +17 -2
  38. package/src/derivation/reserve-regen.ts +34 -0
  39. package/src/derivation/resources.ts +125 -38
  40. package/src/derivation/stars.test.ts +51 -0
  41. package/src/derivation/stars.ts +15 -0
  42. package/src/derivation/stats.ts +6 -6
  43. package/src/derivation/stratum.ts +15 -19
  44. package/src/derivation/tiers.ts +28 -7
  45. package/src/entities/entity.ts +98 -0
  46. package/src/entities/gamestate.ts +3 -28
  47. package/src/entities/makers.ts +91 -136
  48. package/src/entities/slot-multiplier.ts +39 -0
  49. package/src/errors.ts +10 -15
  50. package/src/format.ts +26 -4
  51. package/src/index-module.ts +189 -47
  52. package/src/managers/actions.ts +252 -83
  53. package/src/managers/base.ts +6 -2
  54. package/src/managers/construction-types.ts +79 -0
  55. package/src/managers/construction.ts +396 -0
  56. package/src/managers/context.ts +11 -1
  57. package/src/managers/entities.ts +18 -66
  58. package/src/managers/epochs.ts +40 -0
  59. package/src/managers/index.ts +17 -1
  60. package/src/managers/locations.ts +25 -29
  61. package/src/managers/nft.ts +28 -0
  62. package/src/managers/plot.ts +127 -0
  63. package/src/nft/atomicassets.abi.json +1342 -0
  64. package/src/nft/atomicassets.ts +237 -0
  65. package/src/nft/atomicdata.ts +130 -0
  66. package/src/nft/buildImmutableData.ts +321 -0
  67. package/src/nft/description.ts +37 -15
  68. package/src/nft/index.ts +3 -0
  69. package/src/resolution/describe-module.ts +5 -8
  70. package/src/resolution/display-name.ts +38 -10
  71. package/src/resolution/resolve-item.ts +22 -20
  72. package/src/scheduling/accessor.ts +68 -22
  73. package/src/scheduling/availability.ts +108 -0
  74. package/src/scheduling/energy.ts +48 -0
  75. package/src/scheduling/lane-core.ts +130 -0
  76. package/src/scheduling/lanes.ts +60 -0
  77. package/src/scheduling/projection.ts +121 -94
  78. package/src/scheduling/schedule.ts +237 -103
  79. package/src/scheduling/task-cargo.ts +46 -0
  80. package/src/shipload.ts +16 -1
  81. package/src/subscriptions/manager.ts +40 -6
  82. package/src/subscriptions/mappers.ts +3 -8
  83. package/src/subscriptions/types.ts +3 -2
  84. package/src/testing/catalog-hash.ts +19 -0
  85. package/src/testing/index.ts +2 -0
  86. package/src/testing/projection-parity.ts +143 -0
  87. package/src/travel/travel.ts +90 -13
  88. package/src/types/capabilities.ts +1 -0
  89. package/src/types/index.ts +0 -1
  90. package/src/types.ts +19 -12
  91. package/src/utils/cargo.ts +27 -0
  92. package/src/utils/display-name.ts +61 -0
  93. package/src/utils/system.ts +25 -24
  94. package/src/capabilities/loading.ts +0 -8
  95. package/src/entities/container.ts +0 -108
  96. package/src/entities/ship-deploy.ts +0 -258
  97. package/src/entities/ship.ts +0 -204
  98. package/src/entities/warehouse.ts +0 -119
  99. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,45 @@
1
+ import {EntityClass, getKindMeta, getTemplateMeta} from '../data/kind-registry'
2
+ import {getRecipe} from '../data/recipes-runtime'
3
+ import {getItems} from '../data/catalog'
4
+ import type {Item} from '../types'
5
+
6
+ export type BuildMethod = 'craft+deploy' | 'plot'
7
+
8
+ export function availableBuildMethods(itemId: number): BuildMethod[] {
9
+ const recipe = getRecipe(itemId)
10
+ if (!recipe) return []
11
+
12
+ const template = getTemplateMeta(itemId)
13
+ if (!template) return ['craft+deploy']
14
+
15
+ const kindMeta = getKindMeta(template.kind)
16
+ if (!kindMeta) return ['craft+deploy']
17
+
18
+ if (kindMeta.classification === EntityClass.PlanetaryStructure) {
19
+ return ['craft+deploy', 'plot']
20
+ }
21
+ return ['craft+deploy']
22
+ }
23
+
24
+ export function isBuildable(itemId: number): boolean {
25
+ return availableBuildMethods(itemId).length > 0
26
+ }
27
+
28
+ export function isPlotBuildable(itemId: number): boolean {
29
+ return availableBuildMethods(itemId).includes('plot')
30
+ }
31
+
32
+ export function filterByBuildMethod<T extends {itemId: number}>(
33
+ items: T[],
34
+ method: BuildMethod
35
+ ): T[] {
36
+ return items.filter((i) => availableBuildMethods(i.itemId).includes(method))
37
+ }
38
+
39
+ export function allBuildableItems(): Item[] {
40
+ return getItems().filter((item) => isBuildable(item.id))
41
+ }
42
+
43
+ export function allPlotBuildableItems(): Item[] {
44
+ return getItems().filter((item) => isPlotBuildable(item.id))
45
+ }
@@ -0,0 +1,415 @@
1
+ export function computeBaseHullmass(stats: Record<string, number>): number {
2
+ return 100000 - 75 * stats.density
3
+ }
4
+
5
+ export function computeShipHullCapabilities(stats: Record<string, number>): {
6
+ hullmass: number
7
+ capacity: number
8
+ } {
9
+ const statSum = stats.strength + stats.hardness + stats.cohesion
10
+ const exponent = statSum / 2997.0
11
+ return {
12
+ hullmass: computeBaseHullmass(stats),
13
+ capacity: Math.floor(5000000 * 6 ** exponent),
14
+ }
15
+ }
16
+
17
+ export function computeEngineCapabilities(stats: Record<string, number>): {
18
+ thrust: number
19
+ drain: number
20
+ } {
21
+ const vol = stats.volatility
22
+ const thm = stats.thermal
23
+
24
+ return {
25
+ thrust: 400 + Math.floor((vol * 3) / 4),
26
+ drain: 2 * Math.max(30, 50 - Math.floor(thm / 70)),
27
+ }
28
+ }
29
+
30
+ export function computeGeneratorCapabilities(stats: Record<string, number>): {
31
+ capacity: number
32
+ recharge: number
33
+ } {
34
+ const res = stats.resonance
35
+ const ref = stats.reflectivity
36
+
37
+ return {
38
+ capacity: 950 + Math.floor(res / 2),
39
+ recharge: 2 * (1 + Math.floor((ref * 3) / 1000)),
40
+ }
41
+ }
42
+
43
+ export interface GathererDepthParams {
44
+ readonly floor: number
45
+ readonly slope: number
46
+ }
47
+
48
+ export const GATHERER_DEPTH_TABLE: readonly GathererDepthParams[] = [
49
+ {floor: 500, slope: 5},
50
+ {floor: 2000, slope: 11},
51
+ {floor: 7000, slope: 16},
52
+ {floor: 15000, slope: 18},
53
+ {floor: 25000, slope: 19},
54
+ {floor: 35000, slope: 16},
55
+ {floor: 46000, slope: 12},
56
+ {floor: 53500, slope: 10},
57
+ {floor: 60000, slope: 5},
58
+ {floor: 63537, slope: 2},
59
+ ]
60
+
61
+ export const GATHERER_DEPTH_MAX_TIER = 10
62
+
63
+ export function gathererDepthForTier(tol: number, tier: number): number {
64
+ if (tier < 1 || tier > GATHERER_DEPTH_MAX_TIER) {
65
+ throw new Error(`gatherer tier out of range: ${tier}`)
66
+ }
67
+ const p = GATHERER_DEPTH_TABLE[tier - 1]
68
+ return p.floor + tol * p.slope
69
+ }
70
+
71
+ export function computeGathererCapabilities(
72
+ stats: Record<string, number>,
73
+ tier: number
74
+ ): {
75
+ yield: number
76
+ drain: number
77
+ depth: number
78
+ } {
79
+ const str = stats.strength
80
+ const con = stats.conductivity
81
+ const tol = stats.tolerance
82
+
83
+ return {
84
+ yield: 200 + str,
85
+ drain: 2 * Math.max(250, 1250 - Math.floor((con * 25) / 20)),
86
+ depth: gathererDepthForTier(tol, tier),
87
+ }
88
+ }
89
+
90
+ export function computeLoaderCapabilities(stats: Record<string, number>): {
91
+ mass: number
92
+ thrust: number
93
+ quantity: number
94
+ } {
95
+ const insulation = stats.insulation
96
+ const plasticity = stats.plasticity
97
+
98
+ return {
99
+ mass: Math.max(200, 2000 - Math.floor(insulation * 2)),
100
+ thrust: 1 + Math.floor(plasticity / 500),
101
+ quantity: 1,
102
+ }
103
+ }
104
+
105
+ export function computeCrafterCapabilities(stats: Record<string, number>): {
106
+ speed: number
107
+ drain: number
108
+ } {
109
+ const rea = stats.reactivity
110
+ const fin = stats.fineness
111
+
112
+ return {
113
+ speed: 100 + Math.floor((rea * 4) / 5),
114
+ drain: Math.max(5, 30 - Math.floor(fin / 33)),
115
+ }
116
+ }
117
+
118
+ export function computeHaulerCapabilities(stats: Record<string, number>): {
119
+ capacity: number
120
+ efficiency: number
121
+ drain: number
122
+ } {
123
+ const resonance = stats.resonance
124
+ const plasticity = stats.plasticity
125
+ const reflectivity = stats.reflectivity
126
+
127
+ return {
128
+ capacity: Math.max(1, 1 + Math.floor(resonance / 400)),
129
+ efficiency: 2000 + plasticity * 6,
130
+ drain: Math.max(3, 15 - Math.floor(reflectivity / 80)),
131
+ }
132
+ }
133
+
134
+ export function computeStorageCapabilities(
135
+ stats: Record<string, number>,
136
+ baseCapacity: number
137
+ ): {
138
+ capacityBonus: number
139
+ } {
140
+ const strength = stats.strength
141
+ const density = stats.density
142
+ const hardness = stats.hardness
143
+ const cohesion = stats.cohesion
144
+
145
+ const statSum = strength + density + hardness + cohesion
146
+ const capacityBonus = Math.floor(
147
+ (baseCapacity * (10 + Math.floor((statSum * 10) / 2997))) / 100
148
+ )
149
+
150
+ return {capacityBonus}
151
+ }
152
+
153
+ import {
154
+ ITEM_CONTAINER_T1_PACKED,
155
+ ITEM_CONTAINER_T2_PACKED,
156
+ ITEM_EXTRACTOR_T1_PACKED,
157
+ ITEM_FACTORY_T1_PACKED,
158
+ ITEM_SHIP_T1_PACKED,
159
+ ITEM_WAREHOUSE_T1_PACKED,
160
+ } from '../data/item-ids'
161
+ import {
162
+ getModuleCapabilityType,
163
+ MODULE_BATTERY,
164
+ MODULE_ENGINE,
165
+ MODULE_GENERATOR,
166
+ MODULE_GATHERER,
167
+ MODULE_LOADER,
168
+ MODULE_STORAGE,
169
+ MODULE_CRAFTER,
170
+ MODULE_HAULER,
171
+ MODULE_WARP,
172
+ } from '../capabilities/modules'
173
+ import {getItem} from '../data/catalog'
174
+ import {decodeCraftedItemStats} from './crafting'
175
+ import {
176
+ applySlotMultiplier,
177
+ clampUint16,
178
+ clampUint32,
179
+ getSlotAmp,
180
+ type InstalledModule,
181
+ } from '../entities/slot-multiplier'
182
+ import type {EntitySlot} from '../data/recipes-runtime'
183
+
184
+ export function computeBaseCapacity(itemId: number, stats: Record<string, number>): number {
185
+ switch (itemId) {
186
+ case ITEM_SHIP_T1_PACKED:
187
+ case ITEM_EXTRACTOR_T1_PACKED:
188
+ case ITEM_FACTORY_T1_PACKED:
189
+ return computeShipHullCapabilities(stats).capacity
190
+ case ITEM_CONTAINER_T1_PACKED:
191
+ return computeContainerCapabilities(stats).capacity
192
+ case ITEM_WAREHOUSE_T1_PACKED:
193
+ return computeWarehouseHullCapabilities(stats).capacity
194
+ case ITEM_CONTAINER_T2_PACKED:
195
+ return computeContainerT2Capabilities(stats).capacity
196
+ default:
197
+ return 0
198
+ }
199
+ }
200
+
201
+ export function computeWarpCapabilities(stats: Record<string, number>): {
202
+ range: number
203
+ } {
204
+ const resonance = stats.resonance
205
+ return {range: 100 + resonance * 3}
206
+ }
207
+
208
+ export function computeWarehouseHullCapabilities(stats: Record<string, number>): {
209
+ hullmass: number
210
+ capacity: number
211
+ } {
212
+ const statSum = stats.strength + stats.hardness + stats.cohesion
213
+ const exponent = statSum / 2997.0
214
+ return {
215
+ hullmass: computeBaseHullmass(stats),
216
+ capacity: Math.floor(100000000 * 6 ** exponent),
217
+ }
218
+ }
219
+
220
+ export interface ComputedCapabilities {
221
+ hullmass: number
222
+ capacity: number
223
+ engines?: {thrust: number; drain: number}
224
+ generator?: {capacity: number; recharge: number}
225
+ gatherer?: {yield: number; drain: number; depth: number}
226
+ loaders?: {mass: number; thrust: number; quantity: number}
227
+ crafter?: {speed: number; drain: number}
228
+ hauler?: {capacity: number; efficiency: number; drain: number}
229
+ warp?: {range: number}
230
+ }
231
+
232
+ export function computeEntityCapabilities(
233
+ stats: Record<string, number>,
234
+ itemId: number,
235
+ modules: InstalledModule[],
236
+ layout: EntitySlot[]
237
+ ): ComputedCapabilities {
238
+ let totalThrust = 0
239
+ let totalEngineDrain = 0
240
+ let hasEngine = false
241
+
242
+ let totalGenCapacity = 0
243
+ let totalGenRecharge = 0
244
+ let hasGenerator = false
245
+
246
+ let totalLoaderMass = 0
247
+ let totalLoaderThrust = 0
248
+ let totalLoaderQuantity = 0
249
+ let hasLoader = false
250
+
251
+ let totalGathYield = 0
252
+ let totalGathDrain = 0
253
+ let maxGathDepth = 0
254
+ let hasGatherer = false
255
+
256
+ let totalStorageBonus = 0
257
+ const baseCapacity = computeBaseCapacity(itemId, stats)
258
+ let installedModuleMass = 0
259
+
260
+ let totalCrafterSpeed = 0
261
+ let totalCrafterDrain = 0
262
+ let hasCrafter = false
263
+
264
+ let totalHaulerCapacity = 0
265
+ let weightedHaulerEffNum = 0n
266
+ let totalHaulerDrain = 0
267
+ let hasHauler = false
268
+
269
+ let totalWarpRange = 0
270
+ let hasWarp = false
271
+
272
+ let totalBatteryStatSum = 0
273
+ let batteryCount = 0
274
+
275
+ for (const mod of modules) {
276
+ const item = getItem(mod.itemId)
277
+ const modType = getModuleCapabilityType(mod.itemId)
278
+ const amp = getSlotAmp(layout, mod.slotIndex)
279
+ const decodedStats = decodeCraftedItemStats(mod.itemId, mod.stats)
280
+ installedModuleMass += item.mass
281
+
282
+ if (modType === MODULE_ENGINE) {
283
+ hasEngine = true
284
+ const caps = computeEngineCapabilities(decodedStats)
285
+ totalThrust += applySlotMultiplier(caps.thrust, amp)
286
+ totalEngineDrain += caps.drain
287
+ } else if (modType === MODULE_GENERATOR) {
288
+ hasGenerator = true
289
+ const caps = computeGeneratorCapabilities(decodedStats)
290
+ totalGenCapacity += applySlotMultiplier(caps.capacity, amp)
291
+ totalGenRecharge += applySlotMultiplier(caps.recharge, amp)
292
+ } else if (modType === MODULE_GATHERER) {
293
+ hasGatherer = true
294
+ const tier = item.tier
295
+ const caps = computeGathererCapabilities(decodedStats, tier)
296
+ totalGathYield += applySlotMultiplier(caps.yield, amp)
297
+ totalGathDrain += caps.drain
298
+ if (caps.depth > maxGathDepth) maxGathDepth = caps.depth
299
+ } else if (modType === MODULE_LOADER) {
300
+ hasLoader = true
301
+ const caps = computeLoaderCapabilities(decodedStats)
302
+ totalLoaderMass += caps.mass
303
+ totalLoaderThrust += applySlotMultiplier(caps.thrust, amp)
304
+ totalLoaderQuantity += caps.quantity
305
+ } else if (modType === MODULE_STORAGE) {
306
+ const caps = computeStorageCapabilities(decodedStats, baseCapacity)
307
+ totalStorageBonus += caps.capacityBonus
308
+ } else if (modType === MODULE_CRAFTER) {
309
+ hasCrafter = true
310
+ const caps = computeCrafterCapabilities(decodedStats)
311
+ totalCrafterSpeed += applySlotMultiplier(caps.speed, amp)
312
+ totalCrafterDrain += caps.drain
313
+ } else if (modType === MODULE_HAULER) {
314
+ hasHauler = true
315
+ const caps = computeHaulerCapabilities(decodedStats)
316
+ const eff = applySlotMultiplier(caps.efficiency, amp)
317
+ totalHaulerCapacity += caps.capacity
318
+ weightedHaulerEffNum += BigInt(eff) * BigInt(caps.capacity)
319
+ totalHaulerDrain += caps.drain
320
+ } else if (modType === MODULE_WARP) {
321
+ hasWarp = true
322
+ const caps = computeWarpCapabilities(decodedStats)
323
+ totalWarpRange += applySlotMultiplier(caps.range, amp)
324
+ } else if (modType === MODULE_BATTERY) {
325
+ batteryCount++
326
+ const vol = decodedStats.volatility ?? 0
327
+ const thm = decodedStats.thermal ?? 0
328
+ const pla = decodedStats.plasticity ?? 0
329
+ const ins = decodedStats.insulation ?? 0
330
+ totalBatteryStatSum += vol + thm + pla + ins
331
+ }
332
+ }
333
+
334
+ if (hasGenerator && batteryCount > 0) {
335
+ const genCapBase = totalGenCapacity
336
+ const bonusPctNum = 10 * batteryCount + Math.floor((totalBatteryStatSum * 10) / 2997)
337
+ totalGenCapacity += Math.floor((genCapBase * bonusPctNum) / 100)
338
+ }
339
+
340
+ const result: ComputedCapabilities = {
341
+ hullmass: computeBaseHullmass(stats) + installedModuleMass,
342
+ capacity: baseCapacity + totalStorageBonus,
343
+ }
344
+
345
+ if (hasEngine) {
346
+ result.engines = {thrust: totalThrust, drain: totalEngineDrain}
347
+ }
348
+ if (hasGenerator) {
349
+ result.generator = {
350
+ capacity: clampUint32(totalGenCapacity),
351
+ recharge: clampUint32(totalGenRecharge),
352
+ }
353
+ }
354
+ if (hasGatherer) {
355
+ result.gatherer = {
356
+ yield: clampUint16(totalGathYield),
357
+ drain: totalGathDrain,
358
+ depth: maxGathDepth,
359
+ }
360
+ }
361
+ if (hasLoader) {
362
+ result.loaders = {
363
+ mass: totalLoaderMass,
364
+ thrust: clampUint16(totalLoaderThrust),
365
+ quantity: totalLoaderQuantity,
366
+ }
367
+ }
368
+ if (hasCrafter) {
369
+ result.crafter = {speed: clampUint16(totalCrafterSpeed), drain: totalCrafterDrain}
370
+ }
371
+ if (hasHauler) {
372
+ const efficiency =
373
+ totalHaulerCapacity > 0 ? Number(weightedHaulerEffNum / BigInt(totalHaulerCapacity)) : 0
374
+ result.hauler = {
375
+ capacity: totalHaulerCapacity,
376
+ efficiency: clampUint16(efficiency),
377
+ drain: totalHaulerDrain,
378
+ }
379
+ }
380
+ if (hasWarp) {
381
+ result.warp = {range: totalWarpRange}
382
+ }
383
+
384
+ return result
385
+ }
386
+
387
+ export function computeContainerCapabilities(stats: Record<string, number>): {
388
+ hullmass: number
389
+ capacity: number
390
+ } {
391
+ const statSum = stats.strength + stats.hardness + stats.cohesion
392
+ const exponent = statSum / 2997.0
393
+ return {
394
+ hullmass: computeBaseHullmass(stats),
395
+ capacity: Math.floor(22000000 * 6 ** exponent),
396
+ }
397
+ }
398
+
399
+ export function computeContainerT2Capabilities(stats: Record<string, number>): {
400
+ hullmass: number
401
+ capacity: number
402
+ } {
403
+ const strength = stats.strength
404
+ const density = stats.density
405
+ const hardness = stats.hardness
406
+ const cohesion = stats.cohesion
407
+
408
+ const hullmass = 70000 - 50 * density
409
+
410
+ const statSum = strength + hardness + cohesion
411
+ const exponent = statSum / 2947
412
+ const capacity = Math.floor(24000000 * 6 ** exponent)
413
+
414
+ return {hullmass, capacity}
415
+ }
@@ -0,0 +1,117 @@
1
+ import {SLOT_FORMULAS, type SlotConsumerKind} from '../data/capability-formulas'
2
+ import {getStatDefinitions, type StatDefinition} from './stats'
3
+ import {getRecipe, type Recipe} from '../data/recipes-runtime'
4
+ import {getItem} from '../data/catalog'
5
+ import {
6
+ ITEM_ENGINE_T1,
7
+ ITEM_EXTRACTOR_T1_PACKED,
8
+ ITEM_GENERATOR_T1,
9
+ ITEM_GATHERER_T1,
10
+ ITEM_LOADER_T1,
11
+ ITEM_CRAFTER_T1,
12
+ ITEM_STORAGE_T1,
13
+ ITEM_HAULER_T1,
14
+ ITEM_WARP_T1,
15
+ ITEM_BATTERY_T1,
16
+ ITEM_SHIP_T1_PACKED,
17
+ ITEM_CONTAINER_T1_PACKED,
18
+ ITEM_WAREHOUSE_T1_PACKED,
19
+ ITEM_CONTAINER_T2_PACKED,
20
+ } from '../data/item-ids'
21
+ import type {StatMapping} from '../data/capabilities'
22
+
23
+ export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
24
+ engine: ITEM_ENGINE_T1,
25
+ generator: ITEM_GENERATOR_T1,
26
+ gatherer: ITEM_GATHERER_T1,
27
+ loader: ITEM_LOADER_T1,
28
+ crafter: ITEM_CRAFTER_T1,
29
+ storage: ITEM_STORAGE_T1,
30
+ hauler: ITEM_HAULER_T1,
31
+ warp: ITEM_WARP_T1,
32
+ battery: ITEM_BATTERY_T1,
33
+ 'ship-t1': ITEM_SHIP_T1_PACKED,
34
+ 'container-t1': ITEM_CONTAINER_T1_PACKED,
35
+ 'warehouse-t1': ITEM_WAREHOUSE_T1_PACKED,
36
+ 'extractor-t1': ITEM_EXTRACTOR_T1_PACKED,
37
+ 'container-t2': ITEM_CONTAINER_T2_PACKED,
38
+ }
39
+
40
+ /**
41
+ * Walk a recipe's slot source down to the raw category stat that ultimately
42
+ * lands in that slot. Returns the StatDefinition or undefined if the trace
43
+ * dead-ends (unknown sub-component, missing slot, etc.).
44
+ *
45
+ * Multi-source sub-slots collapse to `sources[0]`; top-level multi-source slots
46
+ * are expanded by the caller (`deriveStatMappings`).
47
+ */
48
+ function traceToRawCategoryStat(
49
+ recipe: Recipe,
50
+ source: {inputIndex: number; statIndex: number},
51
+ visited: Set<number> = new Set()
52
+ ): StatDefinition | undefined {
53
+ const input = recipe.inputs[source.inputIndex]
54
+ if (!input) return undefined
55
+ const inputItem = getItem(input.itemId)
56
+ if (inputItem.type === 'resource' && inputItem.category) {
57
+ const defs = getStatDefinitions(inputItem.category)
58
+ return defs[source.statIndex]
59
+ }
60
+ if (visited.has(input.itemId)) return undefined
61
+ const subRecipe = getRecipe(input.itemId)
62
+ if (!subRecipe) return undefined
63
+ const subSlot = subRecipe.statSlots[source.statIndex]
64
+ if (!subSlot) return undefined
65
+ const subSource = subSlot.sources[0]
66
+ if (!subSource) return undefined
67
+ const nextVisited = new Set(visited)
68
+ nextVisited.add(input.itemId)
69
+ return traceToRawCategoryStat(subRecipe, subSource, nextVisited)
70
+ }
71
+
72
+ let cached: StatMapping[] | undefined
73
+
74
+ export function deriveStatMappings(): StatMapping[] {
75
+ if (cached) return cached
76
+ const out: StatMapping[] = []
77
+ const seen = new Set<string>()
78
+ for (const [kind, slots] of Object.entries(SLOT_FORMULAS) as [
79
+ SlotConsumerKind,
80
+ Record<number, {capability: string; attribute: string}>,
81
+ ][]) {
82
+ const itemId = KIND_TO_ITEM_ID[kind]
83
+ const recipe = getRecipe(itemId)
84
+ if (!recipe) continue
85
+ for (const [slotIdxStr, consumer] of Object.entries(slots)) {
86
+ const slotIdx = Number(slotIdxStr)
87
+ const slot = recipe.statSlots[slotIdx]
88
+ if (!slot) continue
89
+ for (const source of slot.sources) {
90
+ const stat = traceToRawCategoryStat(recipe, source)
91
+ if (!stat) continue
92
+ const key = `${stat.label}|${consumer.capability}|${consumer.attribute}`
93
+ if (seen.has(key)) continue
94
+ seen.add(key)
95
+ out.push({
96
+ stat: stat.label,
97
+ capability: consumer.capability,
98
+ attribute: consumer.attribute,
99
+ })
100
+ }
101
+ }
102
+ }
103
+ cached = out
104
+ return out
105
+ }
106
+
107
+ export function getStatMappings(): StatMapping[] {
108
+ return deriveStatMappings()
109
+ }
110
+
111
+ export function getStatMappingsForStat(stat: string): StatMapping[] {
112
+ return deriveStatMappings().filter((m) => m.stat === stat)
113
+ }
114
+
115
+ export function getStatMappingsForCapability(capability: string): StatMapping[] {
116
+ return deriveStatMappings().filter((m) => m.capability === capability)
117
+ }
@@ -1,6 +1,6 @@
1
1
  import {UInt64} from '@wharfkit/antelope'
2
2
  import type {ResourceCategory} from '../types'
3
- import {findItemByCategoryAndTier, getRecipe, type Recipe} from '../data/recipes-runtime'
3
+ import {getRecipe, type Recipe} from '../data/recipes-runtime'
4
4
  import {getItem} from '../data/catalog'
5
5
  import {getStatDefinitions} from './stats'
6
6
  import {deriveResourceStats} from './stratum'
@@ -58,11 +58,8 @@ function keyForStatSlot(
58
58
  function keyForRecipeInputStat(recipe: Recipe, inputIndex: number, statIndex: number): string {
59
59
  const input = recipe.inputs[inputIndex]
60
60
  if (!input) return ''
61
- if ('category' in input) {
62
- const defs = getStatDefinitions(input.category)
63
- return defs[statIndex]?.key ?? ''
64
- }
65
- // itemId-typed input — its stats follow that item's own statSlots layout.
61
+ // Every input names an item by id; its stats follow that item's own layout
62
+ // (resource stat definitions for resources, statSlots for crafted items).
66
63
  const innerKeys = getItemStatKeys(input.itemId)
67
64
  return innerKeys[statIndex] ?? ''
68
65
  }
@@ -117,10 +114,11 @@ export function computeComponentStats(
117
114
  const src = slot.sources[0]
118
115
  const key = keyForStatSlot(recipe, slot)
119
116
  const input = src ? recipe.inputs[src.inputIndex] : undefined
120
- if (!input || !('category' in input)) {
117
+ const inputItem = input ? getItem(input.itemId) : undefined
118
+ if (!inputItem || inputItem.type !== 'resource' || !inputItem.category) {
121
119
  return {key, value: Math.max(1, Math.min(999, 0))}
122
120
  }
123
- const matching = categoryStacks.find((cs) => cs.category === input.category)
121
+ const matching = categoryStacks.find((cs) => cs.category === inputItem.category)
124
122
  const value = matching ? blendStacks(matching.stacks, key) : 0
125
123
  return {key, value: Math.max(1, Math.min(999, value))}
126
124
  })
@@ -147,7 +145,7 @@ export function computeEntityStats(
147
145
  const key = keyForStatSlot(recipe, slot)
148
146
  if (!src) return {key, value: 1}
149
147
  const input = recipe.inputs[src.inputIndex]
150
- if (!input || 'category' in input) {
148
+ if (!input) {
151
149
  return {key, value: 1}
152
150
  }
153
151
  const blended = blendedByComponent[input.itemId] ?? {}
@@ -185,12 +183,7 @@ export function computeInputMass(itemId: number): number {
185
183
 
186
184
  let total = 0
187
185
  for (const input of recipe.inputs) {
188
- if ('itemId' in input) {
189
- total += getItem(input.itemId).mass * input.quantity
190
- } else {
191
- const item = findItemByCategoryAndTier(input.category, input.tier)
192
- total += item.mass * input.quantity
193
- }
186
+ total += getItem(input.itemId).mass * input.quantity
194
187
  }
195
188
  return total
196
189
  }
@@ -305,10 +298,13 @@ export function computeCraftedOutputStats(
305
298
  const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
306
299
  const input = recipe.inputs[src.inputIndex]
307
300
  let value = 0
308
- if (input && 'category' in input) {
309
- value = blendStacks(decodedByCategory[input.category] ?? [], key)
310
- } else if (input) {
311
- value = blendedByItem[input.itemId]?.[key] ?? 0
301
+ if (input) {
302
+ const inputItem = getItem(input.itemId)
303
+ if (inputItem.type === 'resource' && inputItem.category) {
304
+ value = blendStacks(decodedByCategory[inputItem.category] ?? [], key)
305
+ } else {
306
+ value = blendedByItem[input.itemId]?.[key] ?? 0
307
+ }
312
308
  }
313
309
  out.push(Math.max(1, Math.min(999, value)))
314
310
  } else {
@@ -319,10 +315,13 @@ export function computeCraftedOutputStats(
319
315
  const input = recipe.inputs[src.inputIndex]
320
316
  const weight = recipe.blendWeights[src.inputIndex] ?? 1
321
317
  let value = 0
322
- if (input && 'category' in input) {
323
- value = blendStacks(decodedByCategory[input.category] ?? [], key)
324
- } else if (input) {
325
- value = blendedByItem[input.itemId]?.[key] ?? 0
318
+ if (input) {
319
+ const inputItem = getItem(input.itemId)
320
+ if (inputItem.type === 'resource' && inputItem.category) {
321
+ value = blendStacks(decodedByCategory[inputItem.category] ?? [], key)
322
+ } else {
323
+ value = blendedByItem[input.itemId]?.[key] ?? 0
324
+ }
326
325
  }
327
326
  weightedSum += value * weight
328
327
  totalWeight += weight
@@ -340,7 +339,7 @@ export function computeCraftedOutputStats(
340
339
  * returns a UInt64 whose bit-packed form matches what the contract writes
341
340
  * to cargo_item.stats on gather.
342
341
  *
343
- * Use this whenever off-chain code simulates a gather (testmap, player
342
+ * Use this whenever off-chain code simulates a gather (webapp, player
344
343
  * scanners that project cargo outcomes) and needs a value that matches
345
344
  * what on-chain cargo would carry.
346
345
  */