@shipload/sdk 1.0.0-next.5 → 1.0.0-next.51

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 (145) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +3167 -1353
  7. package/lib/shipload.js +13319 -5640
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +13091 -5589
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1037 -0
  12. package/lib/testing.js +4363 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +4357 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.ts +51 -0
  18. package/src/capabilities/crafting.test.ts +7 -0
  19. package/src/capabilities/crafting.ts +6 -7
  20. package/src/capabilities/gathering.test.ts +29 -0
  21. package/src/capabilities/gathering.ts +37 -19
  22. package/src/capabilities/index.ts +0 -1
  23. package/src/capabilities/modules.ts +22 -0
  24. package/src/capabilities/storage.ts +16 -1
  25. package/src/contracts/platform.ts +231 -3
  26. package/src/contracts/server.ts +1178 -511
  27. package/src/coordinates/address.ts +88 -0
  28. package/src/coordinates/constants.test.ts +15 -0
  29. package/src/coordinates/constants.ts +23 -0
  30. package/src/coordinates/index.ts +15 -0
  31. package/src/coordinates/memo.test.ts +47 -0
  32. package/src/coordinates/memo.ts +20 -0
  33. package/src/coordinates/permutation.ts +77 -0
  34. package/src/coordinates/regions.ts +48 -0
  35. package/src/coordinates/sectors.ts +115 -0
  36. package/src/data/capabilities.ts +31 -16
  37. package/src/data/capability-formulas.ts +21 -14
  38. package/src/data/catalog.ts +0 -5
  39. package/src/data/colors.ts +13 -47
  40. package/src/data/entities.json +154 -10
  41. package/src/data/item-ids.ts +31 -12
  42. package/src/data/items.json +192 -74
  43. package/src/data/kind-registry.json +131 -0
  44. package/src/data/kind-registry.ts +172 -0
  45. package/src/data/metadata.ts +166 -48
  46. package/src/data/recipes-runtime.ts +5 -23
  47. package/src/data/recipes.json +1003 -163
  48. package/src/derivation/build-methods.test.ts +13 -0
  49. package/src/derivation/build-methods.ts +48 -0
  50. package/src/derivation/capabilities.test.ts +151 -0
  51. package/src/derivation/capabilities.ts +527 -0
  52. package/src/derivation/capability-mappings.ts +58 -16
  53. package/src/derivation/crafting.ts +36 -33
  54. package/src/derivation/index.ts +28 -2
  55. package/src/derivation/recipe-usage.test.ts +81 -0
  56. package/src/derivation/recipe-usage.ts +141 -0
  57. package/src/derivation/reserve-regen.ts +34 -0
  58. package/src/derivation/resources.ts +98 -19
  59. package/src/derivation/rollups.test.ts +55 -0
  60. package/src/derivation/rollups.ts +56 -0
  61. package/src/derivation/stars.test.ts +67 -0
  62. package/src/derivation/stars.ts +25 -0
  63. package/src/derivation/stats.ts +6 -6
  64. package/src/derivation/stratum.ts +26 -22
  65. package/src/derivation/tiers.ts +40 -7
  66. package/src/derivation/upgrades.ts +14 -0
  67. package/src/derivation/wormhole.ts +141 -0
  68. package/src/entities/entity.ts +98 -0
  69. package/src/entities/gamestate.ts +3 -28
  70. package/src/entities/makers.ts +131 -134
  71. package/src/entities/slot-multiplier.ts +43 -0
  72. package/src/errors.ts +12 -16
  73. package/src/format.ts +26 -4
  74. package/src/index-module.ts +294 -34
  75. package/src/managers/actions.ts +616 -105
  76. package/src/managers/base.ts +6 -2
  77. package/src/managers/cluster.test.ts +39 -0
  78. package/src/managers/cluster.ts +53 -0
  79. package/src/managers/construction-types.ts +80 -0
  80. package/src/managers/construction.ts +412 -0
  81. package/src/managers/context.ts +29 -1
  82. package/src/managers/coordinates.ts +14 -0
  83. package/src/managers/entities.ts +27 -66
  84. package/src/managers/epochs.ts +40 -0
  85. package/src/managers/flatten-gather-plan.test.ts +80 -0
  86. package/src/managers/index.ts +20 -1
  87. package/src/managers/locations.ts +25 -29
  88. package/src/managers/nft.test.ts +14 -0
  89. package/src/managers/nft.ts +70 -0
  90. package/src/managers/players.ts +25 -0
  91. package/src/managers/plot.ts +122 -0
  92. package/src/nft/atomicassets.abi.json +1342 -0
  93. package/src/nft/atomicassets.ts +237 -0
  94. package/src/nft/atomicdata.ts +130 -0
  95. package/src/nft/buildImmutableData.ts +336 -0
  96. package/src/nft/description.ts +104 -36
  97. package/src/nft/index.ts +3 -0
  98. package/src/planner/index.ts +242 -0
  99. package/src/planner/planner.test.ts +325 -0
  100. package/src/resolution/describe-module.ts +43 -25
  101. package/src/resolution/display-name.ts +38 -10
  102. package/src/resolution/resolve-item.test.ts +48 -0
  103. package/src/resolution/resolve-item.ts +74 -47
  104. package/src/scan/index.ts +244 -0
  105. package/src/scan/scan-wasm.base64.ts +2 -0
  106. package/src/scheduling/accessor.ts +65 -23
  107. package/src/scheduling/availability.ts +108 -0
  108. package/src/scheduling/cancel.test.ts +348 -0
  109. package/src/scheduling/cancel.ts +209 -0
  110. package/src/scheduling/energy.ts +47 -0
  111. package/src/scheduling/idle-resolve.ts +45 -0
  112. package/src/scheduling/lane-core.ts +128 -0
  113. package/src/scheduling/lanes.test.ts +249 -0
  114. package/src/scheduling/lanes.ts +198 -0
  115. package/src/scheduling/projection.ts +210 -107
  116. package/src/scheduling/schedule.ts +240 -117
  117. package/src/scheduling/task-cargo.ts +46 -0
  118. package/src/scheduling/unwrap.test.ts +86 -0
  119. package/src/scheduling/unwrap.ts +190 -0
  120. package/src/shipload.ts +26 -1
  121. package/src/subscriptions/manager.cluster.test.ts +51 -0
  122. package/src/subscriptions/manager.ts +245 -172
  123. package/src/subscriptions/mappers.ts +5 -8
  124. package/src/subscriptions/types.ts +28 -3
  125. package/src/testing/catalog-hash.ts +19 -0
  126. package/src/testing/index.ts +2 -0
  127. package/src/testing/projection-parity.ts +167 -0
  128. package/src/travel/reach.ts +23 -0
  129. package/src/travel/route-planner.ts +277 -0
  130. package/src/travel/route-simulator.ts +174 -0
  131. package/src/travel/travel.ts +175 -118
  132. package/src/types/capabilities.ts +29 -6
  133. package/src/types/entity.ts +3 -3
  134. package/src/types/index.ts +0 -1
  135. package/src/types.ts +39 -14
  136. package/src/utils/cargo.test.ts +14 -0
  137. package/src/utils/cargo.ts +29 -0
  138. package/src/utils/display-name.ts +70 -0
  139. package/src/utils/system.ts +36 -24
  140. package/src/capabilities/loading.ts +0 -8
  141. package/src/entities/container.ts +0 -123
  142. package/src/entities/ship-deploy.ts +0 -295
  143. package/src/entities/ship.ts +0 -221
  144. package/src/entities/warehouse.ts +0 -127
  145. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,527 @@
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 ?? 0) + (stats.hardness ?? 0)
10
+ const exponent = statSum / 1998.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: 1300 + 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.saturation
81
+ const hrd = stats.hardness
82
+
83
+ return {
84
+ yield: 200 + str,
85
+ drain: 2 * Math.max(250, 1250 - Math.floor((con * 25) / 20)),
86
+ depth: gathererDepthForTier(hrd, 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 * plasticity) / 10000),
101
+ quantity: 1,
102
+ }
103
+ }
104
+
105
+ export function computeCrafterCapabilities(stats: Record<string, number>): {
106
+ speed: number
107
+ drain: number
108
+ } {
109
+ const fin = stats.fineness
110
+ const con = stats.conductivity
111
+
112
+ return {
113
+ speed: 100 + Math.floor((fin * 4) / 5),
114
+ drain: Math.max(5, 30 - Math.floor(con / 33)),
115
+ }
116
+ }
117
+
118
+ export function computeHaulerCapabilities(
119
+ stats: Record<string, number>,
120
+ tier: number
121
+ ): {
122
+ capacity: number
123
+ efficiency: number
124
+ drain: number
125
+ } {
126
+ const resonance = stats.resonance
127
+ const plasticity = stats.plasticity
128
+ const conductivity = stats.conductivity
129
+
130
+ return {
131
+ capacity: computeHaulerCapacity(resonance, tier),
132
+ efficiency: 2000 + plasticity * 6,
133
+ drain: Math.max(3, 15 - Math.floor(conductivity / 80)),
134
+ }
135
+ }
136
+
137
+ export function computeLauncherCapabilities(
138
+ stats: {charge_rate: number; velocity: number; drain: number},
139
+ amp = 100
140
+ ): {chargeRate: number; velocity: number; drain: number} {
141
+ return {
142
+ chargeRate: Math.floor((stats.charge_rate * amp) / 100),
143
+ velocity: Math.floor((stats.velocity * amp) / 100),
144
+ drain: stats.drain,
145
+ }
146
+ }
147
+
148
+ export function computeStorageCapabilities(stats: Record<string, number>): {
149
+ capacity: number
150
+ } {
151
+ const strength = stats.strength ?? 0
152
+ const density = stats.density ?? 0
153
+ const hardness = stats.hardness ?? 0
154
+ const cohesion = stats.cohesion ?? 0
155
+
156
+ const statSum = strength + density + hardness + cohesion
157
+ return {capacity: 10_000_000 + Math.floor((statSum * 50_000_000) / 3996)}
158
+ }
159
+
160
+ export function computeBatteryCapabilities(stats: Record<string, number>): {
161
+ capacity: number
162
+ } {
163
+ const volatility = stats.volatility ?? 0
164
+ const thermal = stats.thermal ?? 0
165
+ const plasticity = stats.plasticity ?? 0
166
+ const insulation = stats.insulation ?? 0
167
+
168
+ const statSum = volatility + thermal + plasticity + insulation
169
+ return {capacity: 2_500 + Math.floor((statSum * 7_500) / 3996)}
170
+ }
171
+
172
+ import {
173
+ ITEM_CONTAINER_T1_PACKED,
174
+ ITEM_CONTAINER_T2_PACKED,
175
+ ITEM_EXTRACTOR_T1_PACKED,
176
+ ITEM_FACTORY_T1_PACKED,
177
+ ITEM_HAULER_SHIP_T2_PACKED,
178
+ ITEM_MASS_CATCHER_T1_PACKED,
179
+ ITEM_MASS_DRIVER_T1_PACKED,
180
+ ITEM_PROSPECTOR_T2_PACKED,
181
+ ITEM_SHIP_T1_PACKED,
182
+ ITEM_WAREHOUSE_T1_PACKED,
183
+ } from '../data/item-ids'
184
+ import {
185
+ getModuleCapabilityType,
186
+ MODULE_BATTERY,
187
+ MODULE_ENGINE,
188
+ MODULE_GENERATOR,
189
+ MODULE_GATHERER,
190
+ MODULE_LOADER,
191
+ MODULE_STORAGE,
192
+ MODULE_CRAFTER,
193
+ MODULE_HAULER,
194
+ MODULE_WARP,
195
+ MODULE_LAUNCHER,
196
+ } from '../capabilities/modules'
197
+ import {getItem} from '../data/catalog'
198
+ import {decodeCraftedItemStats, decodeStat} from './crafting'
199
+ import {
200
+ applySlotMultiplier,
201
+ applySlotMultiplierUint32,
202
+ clampUint16,
203
+ clampUint32,
204
+ getSlotAmp,
205
+ type InstalledModule,
206
+ } from '../entities/slot-multiplier'
207
+ import type {EntitySlot} from '../data/recipes-runtime'
208
+ import {computeHaulerCapacity, computeTravelDrain} from '../nft/description'
209
+
210
+ export const CAPACITY_TIER_TABLE = [1.0, 1.4, 1.8, 2.2, 2.6, 3.0, 3.4, 3.8, 4.2, 4.6]
211
+
212
+ export function capacityTierMultiplier(tier: number): number {
213
+ const clampedTier = tier >= 1 && tier <= 10 ? tier : 1
214
+ return CAPACITY_TIER_TABLE[clampedTier - 1]
215
+ }
216
+
217
+ export function applyCapacityTier(baseCapacity: number, tier: number): number {
218
+ return clampUint32(Math.floor(baseCapacity * capacityTierMultiplier(tier)))
219
+ }
220
+
221
+ export function computeBaseCapacity(itemId: number, stats: Record<string, number>): number {
222
+ let base: number
223
+ switch (itemId) {
224
+ case ITEM_SHIP_T1_PACKED:
225
+ case ITEM_PROSPECTOR_T2_PACKED:
226
+ case ITEM_HAULER_SHIP_T2_PACKED:
227
+ base = computeShipHullCapabilities(stats).capacity
228
+ break
229
+ case ITEM_EXTRACTOR_T1_PACKED:
230
+ case ITEM_FACTORY_T1_PACKED:
231
+ case ITEM_MASS_DRIVER_T1_PACKED:
232
+ case ITEM_MASS_CATCHER_T1_PACKED:
233
+ case ITEM_CONTAINER_T1_PACKED:
234
+ case ITEM_CONTAINER_T2_PACKED:
235
+ base = computeContainerCapabilities(stats).capacity
236
+ break
237
+ case ITEM_WAREHOUSE_T1_PACKED:
238
+ base = computeWarehouseHullCapabilities(stats).capacity
239
+ break
240
+ default:
241
+ return 0
242
+ }
243
+ return applyCapacityTier(base, getItem(itemId).tier)
244
+ }
245
+
246
+ export function computeWarpCapabilities(stats: Record<string, number>): {
247
+ range: number
248
+ } {
249
+ const reflectivity = stats.reflectivity
250
+ return {range: 100 + reflectivity * 3}
251
+ }
252
+
253
+ export function computeWarehouseHullCapabilities(stats: Record<string, number>): {
254
+ hullmass: number
255
+ capacity: number
256
+ } {
257
+ const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
258
+ const exponent = statSum / 1998.0
259
+ return {
260
+ hullmass: computeBaseHullmass(stats),
261
+ capacity: Math.floor(100000000 * 6 ** exponent),
262
+ }
263
+ }
264
+
265
+ export interface GathererLaneEntry {
266
+ slotIndex: number
267
+ yield: number
268
+ drain: number
269
+ depth: number
270
+ outputPct: number
271
+ }
272
+
273
+ export interface CrafterLaneEntry {
274
+ slotIndex: number
275
+ speed: number
276
+ drain: number
277
+ outputPct: number
278
+ }
279
+
280
+ export interface LoaderLaneEntry {
281
+ slotIndex: number
282
+ mass: number
283
+ thrust: number
284
+ outputPct: number
285
+ }
286
+
287
+ export interface ComputedCapabilities {
288
+ hullmass: number
289
+ capacity: number
290
+ engines?: {thrust: number; drain: number}
291
+ generator?: {capacity: number; recharge: number}
292
+ gatherer?: {yield: number; drain: number; depth: number}
293
+ gathererLanes?: GathererLaneEntry[]
294
+ loaders?: {mass: number; thrust: number; quantity: number}
295
+ loaderLanes?: LoaderLaneEntry[]
296
+ crafter?: {speed: number; drain: number}
297
+ crafterLanes?: CrafterLaneEntry[]
298
+ hauler?: {
299
+ capacity: number
300
+ efficiency: number
301
+ drain: number
302
+ capacityByTier: {tier: number; capacity: number}[]
303
+ }
304
+ warp?: {range: number}
305
+ launcher?: {chargeRate: number; velocity: number; drain: number}
306
+ }
307
+
308
+ export function computeEntityCapabilities(
309
+ stats: Record<string, number>,
310
+ itemId: number,
311
+ modules: InstalledModule[],
312
+ layout: EntitySlot[]
313
+ ): ComputedCapabilities {
314
+ let totalThrust = 0
315
+ let totalEngineThm = 0
316
+ let engineCount = 0
317
+ let hasEngine = false
318
+
319
+ let totalGenCapacity = 0
320
+ let totalGenRecharge = 0
321
+ let hasGenerator = false
322
+
323
+ let totalLoaderMass = 0
324
+ let totalLoaderThrust = 0
325
+ let totalLoaderQuantity = 0
326
+ let hasLoader = false
327
+
328
+ let totalGathYield = 0
329
+ let totalGathDrain = 0
330
+ let maxGathDepth = 0
331
+ let hasGatherer = false
332
+
333
+ let totalStorageCapacity = 0
334
+ const baseCapacity = computeBaseCapacity(itemId, stats)
335
+ let installedModuleMass = 0
336
+
337
+ let totalCrafterSpeed = 0
338
+ let totalCrafterDrain = 0
339
+ let hasCrafter = false
340
+
341
+ let totalHaulerCapacity = 0
342
+ let weightedHaulerEffNum = 0n
343
+ let totalHaulerDrain = 0
344
+ let hasHauler = false
345
+ const haulerCapByTier = new Map<number, number>()
346
+
347
+ let totalWarpRange = 0
348
+ let hasWarp = false
349
+
350
+ let totalLauncherChargeRate = 0
351
+ let totalLauncherVelocity = 0
352
+ let totalLauncherDrain = 0
353
+ let hasLauncher = false
354
+
355
+ let totalBatteryCapacity = 0
356
+
357
+ const gathererLanes: GathererLaneEntry[] = []
358
+ const crafterLanes: CrafterLaneEntry[] = []
359
+ const loaderLanes: LoaderLaneEntry[] = []
360
+
361
+ for (const mod of modules) {
362
+ const item = getItem(mod.itemId)
363
+ const modType = getModuleCapabilityType(mod.itemId)
364
+ const amp = getSlotAmp(layout, mod.slotIndex)
365
+ const decodedStats = decodeCraftedItemStats(mod.itemId, mod.stats)
366
+ installedModuleMass += item.mass
367
+
368
+ if (modType === MODULE_ENGINE) {
369
+ hasEngine = true
370
+ const caps = computeEngineCapabilities(decodedStats)
371
+ totalThrust += applySlotMultiplier(caps.thrust, amp)
372
+ totalEngineThm += decodedStats.thermal ?? 0
373
+ engineCount += 1
374
+ } else if (modType === MODULE_GENERATOR) {
375
+ hasGenerator = true
376
+ const caps = computeGeneratorCapabilities(decodedStats)
377
+ totalGenCapacity += applySlotMultiplier(caps.capacity, amp)
378
+ totalGenRecharge += applySlotMultiplier(caps.recharge, amp)
379
+ } else if (modType === MODULE_GATHERER) {
380
+ hasGatherer = true
381
+ const tier = item.tier
382
+ const caps = computeGathererCapabilities(decodedStats, tier)
383
+ const scaledYield = applySlotMultiplier(caps.yield, amp)
384
+ totalGathYield += scaledYield
385
+ totalGathDrain += caps.drain
386
+ if (caps.depth > maxGathDepth) maxGathDepth = caps.depth
387
+ gathererLanes.push({
388
+ slotIndex: mod.slotIndex,
389
+ yield: scaledYield,
390
+ drain: caps.drain,
391
+ depth: caps.depth,
392
+ outputPct: amp,
393
+ })
394
+ } else if (modType === MODULE_LOADER) {
395
+ hasLoader = true
396
+ const caps = computeLoaderCapabilities(decodedStats)
397
+ totalLoaderMass += caps.mass
398
+ totalLoaderThrust += applySlotMultiplier(caps.thrust, amp)
399
+ totalLoaderQuantity += caps.quantity
400
+ loaderLanes.push({
401
+ slotIndex: mod.slotIndex,
402
+ mass: caps.mass,
403
+ thrust: applySlotMultiplier(caps.thrust, amp),
404
+ outputPct: amp,
405
+ })
406
+ } else if (modType === MODULE_STORAGE) {
407
+ const caps = computeStorageCapabilities(decodedStats)
408
+ totalStorageCapacity += applySlotMultiplierUint32(caps.capacity, amp)
409
+ } else if (modType === MODULE_CRAFTER) {
410
+ hasCrafter = true
411
+ const caps = computeCrafterCapabilities(decodedStats)
412
+ const scaledSpeed = applySlotMultiplier(caps.speed, amp)
413
+ totalCrafterSpeed += scaledSpeed
414
+ totalCrafterDrain += caps.drain
415
+ crafterLanes.push({
416
+ slotIndex: mod.slotIndex,
417
+ speed: scaledSpeed,
418
+ drain: caps.drain,
419
+ outputPct: amp,
420
+ })
421
+ } else if (modType === MODULE_HAULER) {
422
+ hasHauler = true
423
+ const caps = computeHaulerCapabilities(decodedStats, item.tier)
424
+ const eff = applySlotMultiplier(caps.efficiency, amp)
425
+ totalHaulerCapacity += caps.capacity
426
+ haulerCapByTier.set(item.tier, (haulerCapByTier.get(item.tier) ?? 0) + caps.capacity)
427
+ weightedHaulerEffNum += BigInt(eff) * BigInt(caps.capacity)
428
+ totalHaulerDrain += caps.drain
429
+ } else if (modType === MODULE_WARP) {
430
+ hasWarp = true
431
+ const caps = computeWarpCapabilities(decodedStats)
432
+ totalWarpRange += applySlotMultiplier(caps.range, amp)
433
+ } else if (modType === MODULE_LAUNCHER) {
434
+ hasLauncher = true
435
+ const caps = computeLauncherCapabilities(
436
+ {
437
+ charge_rate: decodedStats.charge_rate ?? decodeStat(mod.stats, 0),
438
+ velocity: decodedStats.velocity ?? decodeStat(mod.stats, 1),
439
+ drain: decodedStats.drain ?? decodeStat(mod.stats, 2),
440
+ },
441
+ amp
442
+ )
443
+ totalLauncherChargeRate = clampUint16(totalLauncherChargeRate + caps.chargeRate)
444
+ totalLauncherVelocity = clampUint16(totalLauncherVelocity + caps.velocity)
445
+ totalLauncherDrain = clampUint16(totalLauncherDrain + caps.drain)
446
+ } else if (modType === MODULE_BATTERY) {
447
+ const caps = computeBatteryCapabilities(decodedStats)
448
+ totalBatteryCapacity += applySlotMultiplierUint32(caps.capacity, amp)
449
+ }
450
+ }
451
+
452
+ if (hasGenerator && totalBatteryCapacity > 0) {
453
+ totalGenCapacity += totalBatteryCapacity
454
+ }
455
+
456
+ const result: ComputedCapabilities = {
457
+ hullmass: computeBaseHullmass(stats) + installedModuleMass,
458
+ capacity: clampUint32(baseCapacity + totalStorageCapacity),
459
+ }
460
+
461
+ if (hasEngine) {
462
+ const avgThm = engineCount > 0 ? Math.trunc(totalEngineThm / engineCount) : 0
463
+ result.engines = {thrust: totalThrust, drain: computeTravelDrain(totalThrust, avgThm)}
464
+ }
465
+ if (hasGenerator) {
466
+ result.generator = {
467
+ capacity: clampUint32(totalGenCapacity),
468
+ recharge: clampUint32(totalGenRecharge),
469
+ }
470
+ }
471
+ if (hasGatherer) {
472
+ result.gatherer = {
473
+ yield: clampUint16(totalGathYield),
474
+ drain: totalGathDrain,
475
+ depth: maxGathDepth,
476
+ }
477
+ result.gathererLanes = gathererLanes
478
+ }
479
+ if (hasLoader) {
480
+ result.loaders = {
481
+ mass: totalLoaderMass,
482
+ thrust: clampUint16(totalLoaderThrust),
483
+ quantity: totalLoaderQuantity,
484
+ }
485
+ result.loaderLanes = loaderLanes
486
+ }
487
+ if (hasCrafter) {
488
+ result.crafter = {speed: clampUint16(totalCrafterSpeed), drain: totalCrafterDrain}
489
+ result.crafterLanes = crafterLanes
490
+ }
491
+ if (hasHauler) {
492
+ const efficiency =
493
+ totalHaulerCapacity > 0 ? Number(weightedHaulerEffNum / BigInt(totalHaulerCapacity)) : 0
494
+ result.hauler = {
495
+ capacity: totalHaulerCapacity,
496
+ efficiency: clampUint16(efficiency),
497
+ drain: totalHaulerDrain,
498
+ capacityByTier: [...haulerCapByTier.entries()]
499
+ .sort((a, b) => a[0] - b[0])
500
+ .map(([tier, capacity]) => ({tier, capacity})),
501
+ }
502
+ }
503
+ if (hasWarp) {
504
+ result.warp = {range: totalWarpRange}
505
+ }
506
+ if (hasLauncher) {
507
+ result.launcher = {
508
+ chargeRate: totalLauncherChargeRate,
509
+ velocity: totalLauncherVelocity,
510
+ drain: totalLauncherDrain,
511
+ }
512
+ }
513
+
514
+ return result
515
+ }
516
+
517
+ export function computeContainerCapabilities(stats: Record<string, number>): {
518
+ hullmass: number
519
+ capacity: number
520
+ } {
521
+ const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
522
+ const exponent = statSum / 1998.0
523
+ return {
524
+ hullmass: computeBaseHullmass(stats),
525
+ capacity: Math.floor(22000000 * 6 ** exponent),
526
+ }
527
+ }
@@ -1,13 +1,10 @@
1
1
  import {SLOT_FORMULAS, type SlotConsumerKind} from '../data/capability-formulas'
2
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'
3
+ import {getRecipe, type Recipe} from '../data/recipes-runtime'
4
+ import {getItem} from '../data/catalog'
9
5
  import {
10
6
  ITEM_ENGINE_T1,
7
+ ITEM_EXTRACTOR_T1_PACKED,
11
8
  ITEM_GENERATOR_T1,
12
9
  ITEM_GATHERER_T1,
13
10
  ITEM_LOADER_T1,
@@ -15,12 +12,14 @@ import {
15
12
  ITEM_STORAGE_T1,
16
13
  ITEM_HAULER_T1,
17
14
  ITEM_WARP_T1,
15
+ ITEM_BATTERY_T1,
18
16
  ITEM_SHIP_T1_PACKED,
19
17
  ITEM_CONTAINER_T1_PACKED,
20
18
  ITEM_WAREHOUSE_T1_PACKED,
21
19
  ITEM_CONTAINER_T2_PACKED,
22
20
  } from '../data/item-ids'
23
- import type {StatMapping} from '../data/capabilities'
21
+ import type {StatMapping, CapabilityAttributeRow} from '../data/capabilities'
22
+ import {capabilityAttributes} from '../data/capabilities'
24
23
 
25
24
  export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
26
25
  engine: ITEM_ENGINE_T1,
@@ -31,16 +30,14 @@ export const KIND_TO_ITEM_ID: Record<SlotConsumerKind, number> = {
31
30
  storage: ITEM_STORAGE_T1,
32
31
  hauler: ITEM_HAULER_T1,
33
32
  warp: ITEM_WARP_T1,
33
+ battery: ITEM_BATTERY_T1,
34
34
  'ship-t1': ITEM_SHIP_T1_PACKED,
35
35
  'container-t1': ITEM_CONTAINER_T1_PACKED,
36
36
  'warehouse-t1': ITEM_WAREHOUSE_T1_PACKED,
37
+ 'extractor-t1': ITEM_EXTRACTOR_T1_PACKED,
37
38
  'container-t2': ITEM_CONTAINER_T2_PACKED,
38
39
  }
39
40
 
40
- function isCategoryInput(input: RecipeInput): input is RecipeInputCategory {
41
- return 'category' in input
42
- }
43
-
44
41
  /**
45
42
  * Walk a recipe's slot source down to the raw category stat that ultimately
46
43
  * lands in that slot. Returns the StatDefinition or undefined if the trace
@@ -56,8 +53,9 @@ function traceToRawCategoryStat(
56
53
  ): StatDefinition | undefined {
57
54
  const input = recipe.inputs[source.inputIndex]
58
55
  if (!input) return undefined
59
- if (isCategoryInput(input)) {
60
- const defs = getStatDefinitions(input.category)
56
+ const inputItem = getItem(input.itemId)
57
+ if (inputItem.type === 'resource' && inputItem.category) {
58
+ const defs = getStatDefinitions(inputItem.category)
61
59
  return defs[source.statIndex]
62
60
  }
63
61
  if (visited.has(input.itemId)) return undefined
@@ -72,6 +70,12 @@ function traceToRawCategoryStat(
72
70
  return traceToRawCategoryStat(subRecipe, subSource, nextVisited)
73
71
  }
74
72
 
73
+ // Producing role for a capability·attribute: entity hull slots all roll up to "Hull"; modules use their own name.
74
+ export function sourceLabelForOutput(itemId: number): string {
75
+ const item = getItem(itemId)
76
+ return item.type === 'entity' ? 'Hull' : item.name
77
+ }
78
+
75
79
  let cached: StatMapping[] | undefined
76
80
 
77
81
  export function deriveStatMappings(): StatMapping[] {
@@ -85,20 +89,22 @@ export function deriveStatMappings(): StatMapping[] {
85
89
  const itemId = KIND_TO_ITEM_ID[kind]
86
90
  const recipe = getRecipe(itemId)
87
91
  if (!recipe) continue
92
+ const source = sourceLabelForOutput(itemId)
88
93
  for (const [slotIdxStr, consumer] of Object.entries(slots)) {
89
94
  const slotIdx = Number(slotIdxStr)
90
95
  const slot = recipe.statSlots[slotIdx]
91
96
  if (!slot) continue
92
- for (const source of slot.sources) {
93
- const stat = traceToRawCategoryStat(recipe, source)
97
+ for (const src of slot.sources) {
98
+ const stat = traceToRawCategoryStat(recipe, src)
94
99
  if (!stat) continue
95
- const key = `${stat.label}|${consumer.capability}|${consumer.attribute}`
100
+ const key = `${stat.label}|${consumer.capability}|${consumer.attribute}|${source}`
96
101
  if (seen.has(key)) continue
97
102
  seen.add(key)
98
103
  out.push({
99
104
  stat: stat.label,
100
105
  capability: consumer.capability,
101
106
  attribute: consumer.attribute,
107
+ source,
102
108
  })
103
109
  }
104
110
  }
@@ -118,3 +124,39 @@ export function getStatMappingsForStat(stat: string): StatMapping[] {
118
124
  export function getStatMappingsForCapability(capability: string): StatMapping[] {
119
125
  return deriveStatMappings().filter((m) => m.capability === capability)
120
126
  }
127
+
128
+ export function getProducersForAttribute(capability: string, attribute: string): string[] {
129
+ const seen = new Set<string>()
130
+ const out: string[] = []
131
+ for (const m of deriveStatMappings()) {
132
+ if (m.capability !== capability || m.attribute !== attribute) continue
133
+ if (seen.has(m.source)) continue
134
+ seen.add(m.source)
135
+ out.push(m.source)
136
+ }
137
+ return out
138
+ }
139
+
140
+ export function getCapabilityAttributeRows(): CapabilityAttributeRow[] {
141
+ const rows: CapabilityAttributeRow[] = []
142
+ for (const ca of capabilityAttributes) {
143
+ const producers = getProducersForAttribute(ca.capability, ca.attribute)
144
+ if (producers.length === 0) {
145
+ rows.push({
146
+ capability: ca.capability,
147
+ attribute: ca.attribute,
148
+ description: ca.description,
149
+ })
150
+ continue
151
+ }
152
+ for (const source of producers) {
153
+ rows.push({
154
+ capability: ca.capability,
155
+ attribute: ca.attribute,
156
+ description: ca.description,
157
+ source,
158
+ })
159
+ }
160
+ }
161
+ return rows
162
+ }