@shipload/sdk 1.0.0-next.6 → 1.0.0-next.61

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 (156) 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 +3601 -1364
  7. package/lib/shipload.js +15671 -5261
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +15392 -5209
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1173 -0
  12. package/lib/testing.js +4971 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +4965 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +35 -0
  20. package/src/capabilities/crafting.ts +34 -7
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +37 -19
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.ts +16 -1
  28. package/src/contracts/platform.ts +231 -3
  29. package/src/contracts/server.ts +1518 -501
  30. package/src/coordinates/address.ts +88 -0
  31. package/src/coordinates/constants.test.ts +15 -0
  32. package/src/coordinates/constants.ts +23 -0
  33. package/src/coordinates/index.ts +15 -0
  34. package/src/coordinates/memo.test.ts +47 -0
  35. package/src/coordinates/memo.ts +20 -0
  36. package/src/coordinates/permutation.ts +77 -0
  37. package/src/coordinates/regions.ts +48 -0
  38. package/src/coordinates/sectors.ts +115 -0
  39. package/src/data/capabilities.ts +49 -18
  40. package/src/data/capability-formulas.ts +26 -14
  41. package/src/data/catalog.ts +7 -8
  42. package/src/data/colors.ts +14 -48
  43. package/src/data/entities.json +380 -15
  44. package/src/data/item-ids.ts +51 -13
  45. package/src/data/items.json +318 -77
  46. package/src/data/kind-registry.json +195 -0
  47. package/src/data/kind-registry.ts +182 -0
  48. package/src/data/metadata.ts +291 -51
  49. package/src/data/recipes-runtime.ts +6 -23
  50. package/src/data/recipes.json +2260 -173
  51. package/src/data/tiers.ts +9 -6
  52. package/src/derivation/build-methods.test.ts +13 -0
  53. package/src/derivation/build-methods.ts +48 -0
  54. package/src/derivation/capabilities.test.ts +348 -0
  55. package/src/derivation/capabilities.ts +693 -0
  56. package/src/derivation/capability-mappings.ts +60 -16
  57. package/src/derivation/crafting.test.ts +17 -0
  58. package/src/derivation/crafting.ts +57 -34
  59. package/src/derivation/index.ts +27 -2
  60. package/src/derivation/recipe-usage.test.ts +90 -0
  61. package/src/derivation/recipe-usage.ts +141 -0
  62. package/src/derivation/reserve-regen.ts +34 -0
  63. package/src/derivation/resources.ts +9 -1
  64. package/src/derivation/rollups.test.ts +55 -0
  65. package/src/derivation/rollups.ts +72 -0
  66. package/src/derivation/stars.test.ts +67 -0
  67. package/src/derivation/stars.ts +25 -0
  68. package/src/derivation/stat-scaling.ts +12 -0
  69. package/src/derivation/stats.ts +6 -6
  70. package/src/derivation/stratum.ts +26 -22
  71. package/src/derivation/tiers.ts +40 -7
  72. package/src/derivation/upgrades.ts +14 -0
  73. package/src/derivation/wormhole.ts +154 -0
  74. package/src/entities/entity.ts +102 -0
  75. package/src/entities/gamestate.ts +3 -28
  76. package/src/entities/makers.ts +141 -135
  77. package/src/entities/slot-multiplier.ts +43 -0
  78. package/src/errors.ts +12 -16
  79. package/src/format.ts +26 -4
  80. package/src/index-module.ts +333 -35
  81. package/src/managers/actions.ts +668 -103
  82. package/src/managers/base.ts +6 -2
  83. package/src/managers/cluster.test.ts +39 -0
  84. package/src/managers/cluster.ts +53 -0
  85. package/src/managers/construction-types.ts +80 -0
  86. package/src/managers/construction.ts +443 -0
  87. package/src/managers/context.ts +29 -1
  88. package/src/managers/coordinates.ts +14 -0
  89. package/src/managers/entities.ts +27 -66
  90. package/src/managers/epochs.ts +40 -0
  91. package/src/managers/index.ts +20 -1
  92. package/src/managers/locations.ts +25 -29
  93. package/src/managers/nft.test.ts +14 -0
  94. package/src/managers/nft.ts +70 -0
  95. package/src/managers/players.ts +25 -0
  96. package/src/managers/plot.ts +122 -0
  97. package/src/nft/atomicassets.abi.json +1342 -0
  98. package/src/nft/atomicassets.ts +237 -0
  99. package/src/nft/atomicdata.ts +130 -0
  100. package/src/nft/buildImmutableData.ts +388 -0
  101. package/src/nft/description.ts +215 -99
  102. package/src/nft/index.ts +3 -0
  103. package/src/planner/index.ts +242 -0
  104. package/src/planner/planner.test.ts +334 -0
  105. package/src/resolution/describe-module.ts +43 -25
  106. package/src/resolution/display-name.ts +38 -10
  107. package/src/resolution/resolve-item.test.ts +48 -0
  108. package/src/resolution/resolve-item.ts +112 -58
  109. package/src/scan/index.ts +244 -0
  110. package/src/scan/scan-wasm.base64.ts +2 -0
  111. package/src/scheduling/accessor.ts +65 -23
  112. package/src/scheduling/availability.test.ts +204 -0
  113. package/src/scheduling/availability.ts +211 -0
  114. package/src/scheduling/cancel.test.ts +542 -0
  115. package/src/scheduling/cancel.ts +254 -0
  116. package/src/scheduling/cluster-stock.test.ts +31 -0
  117. package/src/scheduling/cluster-stock.ts +15 -0
  118. package/src/scheduling/energy.ts +47 -0
  119. package/src/scheduling/idle-resolve.ts +45 -0
  120. package/src/scheduling/jobs.ts +71 -0
  121. package/src/scheduling/lane-core.ts +128 -0
  122. package/src/scheduling/lanes.test.ts +250 -0
  123. package/src/scheduling/lanes.ts +227 -0
  124. package/src/scheduling/projection.ts +257 -133
  125. package/src/scheduling/schedule.test.ts +119 -0
  126. package/src/scheduling/schedule.ts +257 -117
  127. package/src/scheduling/task-cargo.test.ts +44 -0
  128. package/src/scheduling/task-cargo.ts +46 -0
  129. package/src/scheduling/unwrap.test.ts +86 -0
  130. package/src/scheduling/unwrap.ts +190 -0
  131. package/src/shipload.ts +26 -1
  132. package/src/subscriptions/manager.cluster.test.ts +51 -0
  133. package/src/subscriptions/manager.ts +245 -172
  134. package/src/subscriptions/mappers.ts +5 -8
  135. package/src/subscriptions/types.ts +28 -3
  136. package/src/testing/catalog-hash.ts +19 -0
  137. package/src/testing/index.ts +2 -0
  138. package/src/testing/projection-parity.ts +167 -0
  139. package/src/travel/reach.ts +21 -0
  140. package/src/travel/route-planner.ts +288 -0
  141. package/src/travel/route-simulator.ts +170 -0
  142. package/src/travel/travel.ts +188 -122
  143. package/src/types/capabilities.ts +37 -8
  144. package/src/types/entity.ts +3 -3
  145. package/src/types/index.ts +0 -1
  146. package/src/types.ts +51 -16
  147. package/src/utils/cargo.test.ts +14 -0
  148. package/src/utils/cargo.ts +29 -0
  149. package/src/utils/display-name.ts +70 -0
  150. package/src/utils/system.ts +15 -16
  151. package/src/capabilities/loading.ts +0 -8
  152. package/src/entities/container.ts +0 -123
  153. package/src/entities/ship-deploy.ts +0 -295
  154. package/src/entities/ship.ts +0 -221
  155. package/src/entities/warehouse.ts +0 -127
  156. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,693 @@
1
+ import {getEntityLayout} from '../data/recipes-runtime'
2
+ import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
3
+ import {computeEffectiveModuleStat} from './stat-scaling'
4
+
5
+ export const DEFAULT_BASE_HULLMASS = 100_000
6
+
7
+ export function getBaseHullmassFor(itemId: number): number {
8
+ return getEntityLayout(itemId)?.baseHullmass ?? DEFAULT_BASE_HULLMASS
9
+ }
10
+
11
+ function isShipHull(itemId: number): boolean {
12
+ return getPackedEntityType(itemId)?.equals(ENTITY_SHIP) ?? false
13
+ }
14
+
15
+ function sumHullStats(stats: Record<string, number>): number {
16
+ return Object.values(stats).reduce((sum, value) => sum + value, 0)
17
+ }
18
+
19
+ export function computeBaseHullmass(itemId: number, stats: Record<string, number>): number {
20
+ if (isShipHull(itemId)) {
21
+ return Math.floor((getBaseHullmassFor(itemId) * (10_000 - sumHullStats(stats))) / 10_000)
22
+ }
23
+ const lightness = stats.density ?? stats.hardness ?? 0
24
+ return Math.floor((getBaseHullmassFor(itemId) * (2000 - lightness)) / 2000)
25
+ }
26
+
27
+ export function computeShipHullCapabilities(
28
+ stats: Record<string, number>,
29
+ itemId: number = ITEM_SHIP_T1_PACKED
30
+ ): {
31
+ hullmass: number
32
+ capacity: number
33
+ } {
34
+ const statSum = sumHullStats(stats)
35
+ const exponent = statSum / 4995.0
36
+ return {
37
+ hullmass: computeBaseHullmass(itemId, stats),
38
+ capacity: Math.floor(5000000 * 6 ** exponent),
39
+ }
40
+ }
41
+
42
+ export function computeEngineCapabilities(
43
+ stats: Record<string, number>,
44
+ tier: number
45
+ ): {thrust: number; drain: number} {
46
+ const vol = computeEffectiveModuleStat(stats.volatility)
47
+ const thm = computeEffectiveModuleStat(stats.thermal)
48
+ return {
49
+ thrust: Math.floor(
50
+ ((400 + Math.floor((vol * 3) / 4)) * moduleTierPct(ENGINE_THRUST_TIER_PCT, tier)) / 100
51
+ ),
52
+ drain: 2 * Math.max(30, 50 - Math.floor(thm / 70)),
53
+ }
54
+ }
55
+
56
+ export function computeGeneratorCapabilities(
57
+ stats: Record<string, number>,
58
+ tier: number
59
+ ): {capacity: number; recharge: number} {
60
+ const res = computeEffectiveModuleStat(stats.resonance)
61
+ const ref = computeEffectiveModuleStat(stats.reflectivity)
62
+ return {
63
+ capacity: Math.floor(
64
+ ((1_300_000 + res * 500) * moduleTierPct(GENERATOR_CAPACITY_TIER_PCT, tier)) / 100
65
+ ),
66
+ recharge: Math.floor(
67
+ ((2000 + ref * 6) * moduleTierPct(GENERATOR_RECHARGE_TIER_PCT, tier)) / 100
68
+ ),
69
+ }
70
+ }
71
+
72
+ export interface GathererDepthParams {
73
+ readonly floor: number
74
+ readonly slope: number
75
+ }
76
+
77
+ export const GATHERER_DEPTH_TABLE: readonly GathererDepthParams[] = [
78
+ {floor: 500, slope: 5},
79
+ {floor: 2000, slope: 11},
80
+ {floor: 7000, slope: 16},
81
+ {floor: 15000, slope: 18},
82
+ {floor: 25000, slope: 19},
83
+ {floor: 35000, slope: 16},
84
+ {floor: 46000, slope: 12},
85
+ {floor: 53500, slope: 10},
86
+ {floor: 60000, slope: 5},
87
+ {floor: 63537, slope: 2},
88
+ ]
89
+
90
+ export const GATHERER_DEPTH_MAX_TIER = 10
91
+
92
+ export function gathererDepthForTier(tol: number, tier: number): number {
93
+ if (tier < 1 || tier > GATHERER_DEPTH_MAX_TIER) {
94
+ throw new Error(`gatherer tier out of range: ${tier}`)
95
+ }
96
+ const p = GATHERER_DEPTH_TABLE[tier - 1]
97
+ return p.floor + tol * p.slope
98
+ }
99
+
100
+ export function computeGathererCapabilities(
101
+ stats: Record<string, number>,
102
+ tier: number
103
+ ): {
104
+ yield: number
105
+ drain: number
106
+ depth: number
107
+ } {
108
+ const str = stats.strength
109
+ const con = stats.saturation
110
+ // Gatherer capabilities are positional in the contract: packed slot 1 is tolerance.
111
+ // T1 recipe presentation historically labels that blended slot as hardness, so retain
112
+ // that fallback for callers that only have recipe-decoded named stats.
113
+ const tol = stats.tolerance ?? stats.hardness
114
+
115
+ return {
116
+ yield: computeGathererYield(str, tier),
117
+ drain: 2 * Math.max(250_000, 1_250_000 - con * 1250),
118
+ depth: gathererDepthForTier(tol, tier),
119
+ }
120
+ }
121
+
122
+ export function computeLoaderCapabilities(
123
+ stats: Record<string, number>,
124
+ tier: number
125
+ ): {mass: number; thrust: number; quantity: number} {
126
+ const insulation = stats.insulation
127
+ const plasticity = stats.plasticity
128
+ return {
129
+ mass: Math.max(200, 2000 - Math.floor(insulation * 2)),
130
+ thrust: Math.floor(
131
+ ((1 + Math.floor((plasticity * plasticity) / 10000)) *
132
+ moduleTierPct(LOADER_THRUST_TIER_PCT, tier)) /
133
+ 100
134
+ ),
135
+ quantity: 1,
136
+ }
137
+ }
138
+
139
+ export function computeCrafterCapabilities(
140
+ stats: Record<string, number>,
141
+ tier: number
142
+ ): {speed: number; drain: number} {
143
+ const fin = stats.fineness
144
+ const con = stats.conductivity
145
+ return {
146
+ speed: Math.floor(
147
+ ((100 + Math.floor((fin * 4) / 5)) * moduleTierPct(CRAFTER_SPEED_TIER_PCT, tier)) / 100
148
+ ),
149
+ drain: Math.max(5000, 30000 - Math.floor((con * 1000) / 33)),
150
+ }
151
+ }
152
+
153
+ export function computeBuilderCapabilities(
154
+ stats: Record<string, number>,
155
+ tier: number
156
+ ): {speed: number; drain: number} {
157
+ const resonance = stats.resonance
158
+ const fineness = stats.fineness
159
+ return {
160
+ speed: Math.floor(
161
+ ((100 + Math.floor((resonance * 4) / 5)) *
162
+ moduleTierPct(BUILDER_SPEED_TIER_PCT, tier)) /
163
+ 100
164
+ ),
165
+ drain: Math.max(5000, 30000 - Math.floor((fineness * 1000) / 33)),
166
+ }
167
+ }
168
+
169
+ export function computeHaulerCapabilities(
170
+ stats: Record<string, number>,
171
+ tier: number
172
+ ): {
173
+ capacity: number
174
+ efficiency: number
175
+ drain: number
176
+ } {
177
+ const resonance = stats.resonance
178
+ const plasticity = stats.plasticity
179
+ const conductivity = stats.conductivity
180
+
181
+ return {
182
+ capacity: computeHaulerCapacity(resonance, tier),
183
+ efficiency: 2000 + plasticity * 6,
184
+ drain: computeHaulerDrain(conductivity, tier),
185
+ }
186
+ }
187
+
188
+ export function computeLauncherCapabilities(
189
+ stats: {charge_rate: number; velocity: number; drain: number},
190
+ amp = 100
191
+ ): {chargeRate: number; velocity: number; drain: number} {
192
+ return {
193
+ chargeRate: Math.floor((stats.charge_rate * amp) / 100),
194
+ velocity: Math.floor((stats.velocity * amp) / 100),
195
+ drain: stats.drain,
196
+ }
197
+ }
198
+
199
+ export function computeStorageCapabilities(
200
+ stats: Record<string, number>,
201
+ tier: number
202
+ ): {
203
+ capacity: number
204
+ drain: number
205
+ } {
206
+ return {
207
+ capacity: computeCargoBayCapacity(
208
+ stats.strength ?? 0,
209
+ stats.density ?? 0,
210
+ stats.hardness ?? 0,
211
+ tier
212
+ ),
213
+ drain: computeCargoBayDrain(stats.cohesion ?? 0, tier),
214
+ }
215
+ }
216
+
217
+ export function computeBatteryCapabilities(
218
+ stats: Record<string, number>,
219
+ tier: number
220
+ ): {
221
+ capacity: number
222
+ } {
223
+ return {
224
+ capacity: computeBatteryBankCapacity(
225
+ stats.volatility ?? 0,
226
+ stats.thermal ?? 0,
227
+ stats.plasticity ?? 0,
228
+ stats.insulation ?? 0,
229
+ tier
230
+ ),
231
+ }
232
+ }
233
+
234
+ import {
235
+ ITEM_CONTAINER_T1_PACKED,
236
+ ITEM_SHIP_T1_PACKED,
237
+ ITEM_WAREHOUSE_T1_PACKED,
238
+ ITEM_WORKSHOP_T1_PACKED,
239
+ } from '../data/item-ids'
240
+ import {
241
+ getModuleCapabilityType,
242
+ MODULE_BATTERY,
243
+ MODULE_ENGINE,
244
+ MODULE_GENERATOR,
245
+ MODULE_GATHERER,
246
+ MODULE_LOADER,
247
+ MODULE_STORAGE,
248
+ MODULE_CRAFTER,
249
+ MODULE_BUILDER,
250
+ MODULE_HAULER,
251
+ MODULE_WARP,
252
+ MODULE_LAUNCHER,
253
+ } from '../capabilities/modules'
254
+ import {getItem} from '../data/catalog'
255
+ import {decodeCraftedItemStats, decodeStat} from './crafting'
256
+ import {
257
+ applySlotMultiplier,
258
+ applySlotMultiplierUint32,
259
+ clampUint16,
260
+ clampUint32,
261
+ getSlotAmp,
262
+ type InstalledModule,
263
+ } from '../entities/slot-multiplier'
264
+ import type {EntitySlot} from '../data/recipes-runtime'
265
+ import {
266
+ computeCargoBayCapacity,
267
+ computeCargoBayDrain,
268
+ computeBatteryBankCapacity,
269
+ computeHaulerCapacity,
270
+ computeHaulerDrain,
271
+ computeTravelDrain,
272
+ } from '../nft/description'
273
+
274
+ 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]
275
+
276
+ export function capacityTierMultiplier(tier: number): number {
277
+ const clampedTier = tier >= 1 && tier <= 10 ? tier : 1
278
+ return CAPACITY_TIER_TABLE[clampedTier - 1]
279
+ }
280
+
281
+ export function applyCapacityTier(baseCapacity: number, tier: number): number {
282
+ return clampUint32(Math.floor(baseCapacity * capacityTierMultiplier(tier)))
283
+ }
284
+
285
+ export const GATHERER_YIELD_TIER_TABLE = [100, 120, 140, 160, 180, 200, 220, 240, 260, 280] as const
286
+
287
+ export const MODULE_MAX_TIER = 10
288
+
289
+ export const ENGINE_THRUST_TIER_PCT = [100, 120, 140, 160, 180, 200, 220, 240, 260, 280] as const
290
+ export const GENERATOR_CAPACITY_TIER_PCT = [
291
+ 100, 110, 120, 130, 140, 150, 160, 170, 180, 190,
292
+ ] as const
293
+ export const GENERATOR_RECHARGE_TIER_PCT = [
294
+ 100, 120, 140, 160, 180, 200, 220, 240, 260, 280,
295
+ ] as const
296
+ export const CRAFTER_SPEED_TIER_PCT = [100, 120, 140, 160, 180, 200, 220, 240, 260, 280] as const
297
+ export const BUILDER_SPEED_TIER_PCT = [100, 120, 140, 160, 180, 200, 220, 240, 260, 280] as const
298
+ export const WARP_RANGE_TIER_PCT = [100, 120, 140, 160, 180, 200, 220, 240, 260, 280] as const
299
+ export const LOADER_THRUST_TIER_PCT = [100, 120, 140, 160, 180, 200, 220, 240, 260, 280] as const
300
+ export const CARGO_BAY_CAPACITY_TIER_PCT = [
301
+ 100, 110, 120, 130, 140, 150, 160, 170, 180, 190,
302
+ ] as const
303
+ export const BATTERY_CAPACITY_TIER_PCT = [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] as const
304
+
305
+ export function moduleTierPct(table: readonly number[], tier: number): number {
306
+ const clamped = Math.min(Math.max(tier, 1), MODULE_MAX_TIER)
307
+ return table[clamped - 1]
308
+ }
309
+
310
+ export function computeGathererYield(str: number, tier: number): number {
311
+ const clampedTier = Math.min(Math.max(tier, 1), GATHERER_YIELD_TIER_TABLE.length)
312
+ const pct = GATHERER_YIELD_TIER_TABLE[clampedTier - 1]
313
+ return Math.floor(((200 + str) * pct) / 100)
314
+ }
315
+
316
+ const BASE_CAPACITY_FN_BY_KIND: Record<string, (stats: Record<string, number>) => number> = {
317
+ warehouse: (stats) => computeWarehouseHullCapabilities(stats).capacity,
318
+ workshop: (stats) => computeWorkshopHullCapabilities(stats).capacity,
319
+ extractor: (stats) => computeContainerCapabilities(stats).capacity,
320
+ factory: (stats) => computeContainerCapabilities(stats).capacity,
321
+ builddock: (stats) => computeContainerCapabilities(stats).capacity,
322
+ mdriver: (stats) => computeContainerCapabilities(stats).capacity,
323
+ mcatcher: (stats) => computeContainerCapabilities(stats).capacity,
324
+ container: (stats) => computeContainerCapabilities(stats).capacity,
325
+ }
326
+
327
+ export function computeBaseCapacity(itemId: number, stats: Record<string, number>): number {
328
+ let base: number
329
+ if (isShipHull(itemId)) {
330
+ base = computeShipHullCapabilities(stats, itemId).capacity
331
+ } else {
332
+ const kind = getPackedEntityType(itemId)
333
+ const capacityFn = kind ? BASE_CAPACITY_FN_BY_KIND[kind.toString()] : undefined
334
+ if (!capacityFn) return 0
335
+ base = capacityFn(stats)
336
+ }
337
+ return applyCapacityTier(base, getItem(itemId).tier)
338
+ }
339
+
340
+ export function computeWarpCapabilities(
341
+ stats: Record<string, number>,
342
+ tier: number
343
+ ): {range: number} {
344
+ const reflectivity = stats.reflectivity
345
+ return {
346
+ range: Math.floor(
347
+ ((100 + reflectivity * 3) * moduleTierPct(WARP_RANGE_TIER_PCT, tier)) / 100
348
+ ),
349
+ }
350
+ }
351
+
352
+ export function computeWarehouseHullCapabilities(stats: Record<string, number>): {
353
+ hullmass: number
354
+ capacity: number
355
+ } {
356
+ const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
357
+ const exponent = statSum / 1998.0
358
+ return {
359
+ hullmass: computeBaseHullmass(ITEM_WAREHOUSE_T1_PACKED, stats),
360
+ capacity: Math.floor(100000000 * 6 ** exponent),
361
+ }
362
+ }
363
+
364
+ export function computeWorkshopHullCapabilities(stats: Record<string, number>): {
365
+ hullmass: number
366
+ capacity: number
367
+ } {
368
+ const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
369
+ const exponent = statSum / 1998.0
370
+ return {
371
+ hullmass: computeBaseHullmass(ITEM_WORKSHOP_T1_PACKED, stats),
372
+ capacity: Math.floor(5000000 * 6 ** exponent),
373
+ }
374
+ }
375
+
376
+ export interface GathererLaneEntry {
377
+ slotIndex: number
378
+ yield: number
379
+ drain: number
380
+ depth: number
381
+ outputPct: number
382
+ }
383
+
384
+ export interface CrafterLaneEntry {
385
+ slotIndex: number
386
+ speed: number
387
+ drain: number
388
+ outputPct: number
389
+ }
390
+
391
+ export interface BuilderLaneEntry {
392
+ slotIndex: number
393
+ speed: number
394
+ drain: number
395
+ outputPct: number
396
+ }
397
+
398
+ export interface LoaderLaneEntry {
399
+ slotIndex: number
400
+ mass: number
401
+ thrust: number
402
+ outputPct: number
403
+ }
404
+
405
+ export interface TravelDrainBreakdown {
406
+ engine: number
407
+ cargoHolds: number
408
+ tractorBeams: number
409
+ total: number
410
+ }
411
+
412
+ export interface ComputedCapabilities {
413
+ hullmass: number
414
+ capacity: number
415
+ engines?: {thrust: number; drain: number}
416
+ travelDrain?: TravelDrainBreakdown
417
+ generator?: {capacity: number; recharge: number}
418
+ gatherer?: {yield: number; drain: number; depth: number}
419
+ gathererLanes?: GathererLaneEntry[]
420
+ loaders?: {mass: number; thrust: number; quantity: number}
421
+ loaderLanes?: LoaderLaneEntry[]
422
+ crafter?: {speed: number; drain: number}
423
+ crafterLanes?: CrafterLaneEntry[]
424
+ builder?: {speed: number; drain: number}
425
+ builderLanes?: BuilderLaneEntry[]
426
+ hauler?: {
427
+ capacity: number
428
+ efficiency: number
429
+ drain: number
430
+ capacityByTier: {tier: number; capacity: number}[]
431
+ }
432
+ warp?: {range: number}
433
+ launcher?: {chargeRate: number; velocity: number; drain: number}
434
+ }
435
+
436
+ export function computeEntityCapabilities(
437
+ stats: Record<string, number>,
438
+ itemId: number,
439
+ modules: InstalledModule[],
440
+ layout: EntitySlot[]
441
+ ): ComputedCapabilities {
442
+ let totalThrust = 0
443
+ let totalEngineThm = 0
444
+ let engineCount = 0
445
+ let hasEngine = false
446
+
447
+ let totalGenCapacity = 0
448
+ let totalGenRecharge = 0
449
+ let hasGenerator = false
450
+
451
+ let totalLoaderMass = 0
452
+ let totalLoaderThrust = 0
453
+ let totalLoaderQuantity = 0
454
+ let hasLoader = false
455
+
456
+ let totalGathYield = 0
457
+ let totalGathDrain = 0
458
+ let maxGathDepth = 0
459
+ let hasGatherer = false
460
+
461
+ let totalStorageCapacity = 0
462
+ let totalCargoHoldDrain = 0
463
+ const baseCapacity = computeBaseCapacity(itemId, stats)
464
+ let installedModuleMass = 0
465
+
466
+ let totalCrafterSpeed = 0
467
+ let totalCrafterDrain = 0
468
+ let hasCrafter = false
469
+
470
+ let totalBuilderSpeed = 0
471
+ let totalBuilderDrain = 0
472
+ let hasBuilder = false
473
+
474
+ let totalHaulerCapacity = 0
475
+ let weightedHaulerEffNum = 0n
476
+ let totalHaulerDrain = 0
477
+ let hasHauler = false
478
+ const haulerCapByTier = new Map<number, number>()
479
+
480
+ let totalWarpRange = 0
481
+ let hasWarp = false
482
+
483
+ let totalLauncherChargeRate = 0
484
+ let totalLauncherVelocity = 0
485
+ let totalLauncherDrain = 0
486
+ let hasLauncher = false
487
+
488
+ let totalBatteryCapacity = 0
489
+
490
+ const gathererLanes: GathererLaneEntry[] = []
491
+ const crafterLanes: CrafterLaneEntry[] = []
492
+ const builderLanes: BuilderLaneEntry[] = []
493
+ const loaderLanes: LoaderLaneEntry[] = []
494
+
495
+ for (const mod of modules) {
496
+ const item = getItem(mod.itemId)
497
+ const modType = getModuleCapabilityType(mod.itemId)
498
+ const amp = getSlotAmp(layout, mod.slotIndex)
499
+ const decodedStats = decodeCraftedItemStats(mod.itemId, mod.stats)
500
+ installedModuleMass += item.mass
501
+
502
+ if (modType === MODULE_ENGINE) {
503
+ hasEngine = true
504
+ const caps = computeEngineCapabilities(decodedStats, item.tier)
505
+ totalThrust += applySlotMultiplier(caps.thrust, amp)
506
+ totalEngineThm += computeEffectiveModuleStat(decodedStats.thermal ?? 0)
507
+ engineCount += 1
508
+ } else if (modType === MODULE_GENERATOR) {
509
+ hasGenerator = true
510
+ const caps = computeGeneratorCapabilities(decodedStats, item.tier)
511
+ totalGenCapacity += applySlotMultiplierUint32(caps.capacity, amp)
512
+ totalGenRecharge += applySlotMultiplierUint32(caps.recharge, amp)
513
+ } else if (modType === MODULE_GATHERER) {
514
+ hasGatherer = true
515
+ const tier = item.tier
516
+ const caps = computeGathererCapabilities(
517
+ {
518
+ strength: decodeStat(mod.stats, 0),
519
+ tolerance: decodeStat(mod.stats, 1),
520
+ saturation: decodeStat(mod.stats, 2),
521
+ },
522
+ tier
523
+ )
524
+ const scaledYield = applySlotMultiplier(caps.yield, amp)
525
+ totalGathYield += scaledYield
526
+ totalGathDrain += caps.drain
527
+ if (caps.depth > maxGathDepth) maxGathDepth = caps.depth
528
+ gathererLanes.push({
529
+ slotIndex: mod.slotIndex,
530
+ yield: scaledYield,
531
+ drain: caps.drain,
532
+ depth: caps.depth,
533
+ outputPct: amp,
534
+ })
535
+ } else if (modType === MODULE_LOADER) {
536
+ hasLoader = true
537
+ const caps = computeLoaderCapabilities(decodedStats, item.tier)
538
+ totalLoaderMass += caps.mass
539
+ totalLoaderThrust += applySlotMultiplier(caps.thrust, amp)
540
+ totalLoaderQuantity += caps.quantity
541
+ loaderLanes.push({
542
+ slotIndex: mod.slotIndex,
543
+ mass: caps.mass,
544
+ thrust: applySlotMultiplier(caps.thrust, amp),
545
+ outputPct: amp,
546
+ })
547
+ } else if (modType === MODULE_STORAGE) {
548
+ const caps = computeStorageCapabilities(decodedStats, item.tier)
549
+ totalStorageCapacity += applySlotMultiplierUint32(caps.capacity, amp)
550
+ totalCargoHoldDrain += caps.drain
551
+ } else if (modType === MODULE_CRAFTER) {
552
+ hasCrafter = true
553
+ const caps = computeCrafterCapabilities(decodedStats, item.tier)
554
+ const scaledSpeed = applySlotMultiplier(caps.speed, amp)
555
+ totalCrafterSpeed += scaledSpeed
556
+ totalCrafterDrain += caps.drain
557
+ crafterLanes.push({
558
+ slotIndex: mod.slotIndex,
559
+ speed: scaledSpeed,
560
+ drain: caps.drain,
561
+ outputPct: amp,
562
+ })
563
+ } else if (modType === MODULE_BUILDER) {
564
+ hasBuilder = true
565
+ const caps = computeBuilderCapabilities(decodedStats, item.tier)
566
+ const scaledSpeed = applySlotMultiplier(caps.speed, amp)
567
+ totalBuilderSpeed += scaledSpeed
568
+ totalBuilderDrain += caps.drain
569
+ builderLanes.push({
570
+ slotIndex: mod.slotIndex,
571
+ speed: scaledSpeed,
572
+ drain: caps.drain,
573
+ outputPct: amp,
574
+ })
575
+ } else if (modType === MODULE_HAULER) {
576
+ hasHauler = true
577
+ const caps = computeHaulerCapabilities(decodedStats, item.tier)
578
+ const eff = applySlotMultiplier(caps.efficiency, amp)
579
+ totalHaulerCapacity += caps.capacity
580
+ haulerCapByTier.set(item.tier, (haulerCapByTier.get(item.tier) ?? 0) + caps.capacity)
581
+ weightedHaulerEffNum += BigInt(eff) * BigInt(caps.capacity)
582
+ totalHaulerDrain += caps.drain
583
+ } else if (modType === MODULE_WARP) {
584
+ hasWarp = true
585
+ const caps = computeWarpCapabilities(decodedStats, item.tier)
586
+ totalWarpRange += applySlotMultiplier(caps.range, amp)
587
+ } else if (modType === MODULE_LAUNCHER) {
588
+ hasLauncher = true
589
+ const caps = computeLauncherCapabilities(
590
+ {
591
+ charge_rate: decodedStats.charge_rate ?? decodeStat(mod.stats, 0),
592
+ velocity: decodedStats.velocity ?? decodeStat(mod.stats, 1),
593
+ drain: decodedStats.drain ?? decodeStat(mod.stats, 2),
594
+ },
595
+ amp
596
+ )
597
+ totalLauncherChargeRate = clampUint16(totalLauncherChargeRate + caps.chargeRate)
598
+ totalLauncherVelocity = clampUint16(totalLauncherVelocity + caps.velocity)
599
+ totalLauncherDrain = clampUint16(totalLauncherDrain + caps.drain)
600
+ } else if (modType === MODULE_BATTERY) {
601
+ const caps = computeBatteryCapabilities(decodedStats, item.tier)
602
+ totalBatteryCapacity += applySlotMultiplierUint32(caps.capacity, amp)
603
+ }
604
+ }
605
+
606
+ if (hasGenerator && totalBatteryCapacity > 0) {
607
+ totalGenCapacity += totalBatteryCapacity
608
+ }
609
+
610
+ const result: ComputedCapabilities = {
611
+ hullmass: computeBaseHullmass(itemId, stats) + installedModuleMass,
612
+ capacity: clampUint32(baseCapacity + totalStorageCapacity),
613
+ }
614
+
615
+ if (hasEngine) {
616
+ const avgThm = engineCount > 0 ? Math.trunc(totalEngineThm / engineCount) : 0
617
+ const engineDrain = computeTravelDrain(totalThrust, avgThm)
618
+ const totalDrain = clampUint32(engineDrain + totalCargoHoldDrain + totalHaulerDrain)
619
+ result.engines = {thrust: totalThrust, drain: totalDrain}
620
+ result.travelDrain = {
621
+ engine: engineDrain,
622
+ cargoHolds: totalCargoHoldDrain,
623
+ tractorBeams: totalHaulerDrain,
624
+ total: totalDrain,
625
+ }
626
+ }
627
+ if (hasGenerator) {
628
+ result.generator = {
629
+ capacity: clampUint32(totalGenCapacity),
630
+ recharge: clampUint32(totalGenRecharge),
631
+ }
632
+ }
633
+ if (hasGatherer) {
634
+ result.gatherer = {
635
+ yield: clampUint16(totalGathYield),
636
+ drain: totalGathDrain,
637
+ depth: maxGathDepth,
638
+ }
639
+ result.gathererLanes = gathererLanes
640
+ }
641
+ if (hasLoader) {
642
+ result.loaders = {
643
+ mass: totalLoaderMass,
644
+ thrust: clampUint16(totalLoaderThrust),
645
+ quantity: totalLoaderQuantity,
646
+ }
647
+ result.loaderLanes = loaderLanes
648
+ }
649
+ if (hasCrafter) {
650
+ result.crafter = {speed: clampUint16(totalCrafterSpeed), drain: totalCrafterDrain}
651
+ result.crafterLanes = crafterLanes
652
+ }
653
+ if (hasBuilder) {
654
+ result.builder = {speed: clampUint16(totalBuilderSpeed), drain: totalBuilderDrain}
655
+ result.builderLanes = builderLanes
656
+ }
657
+ if (hasHauler) {
658
+ const efficiency =
659
+ totalHaulerCapacity > 0 ? Number(weightedHaulerEffNum / BigInt(totalHaulerCapacity)) : 0
660
+ result.hauler = {
661
+ capacity: totalHaulerCapacity,
662
+ efficiency: clampUint16(efficiency),
663
+ drain: totalHaulerDrain,
664
+ capacityByTier: [...haulerCapByTier.entries()]
665
+ .sort((a, b) => a[0] - b[0])
666
+ .map(([tier, capacity]) => ({tier, capacity})),
667
+ }
668
+ }
669
+ if (hasWarp) {
670
+ result.warp = {range: totalWarpRange}
671
+ }
672
+ if (hasLauncher) {
673
+ result.launcher = {
674
+ chargeRate: totalLauncherChargeRate,
675
+ velocity: totalLauncherVelocity,
676
+ drain: totalLauncherDrain,
677
+ }
678
+ }
679
+
680
+ return result
681
+ }
682
+
683
+ export function computeContainerCapabilities(stats: Record<string, number>): {
684
+ hullmass: number
685
+ capacity: number
686
+ } {
687
+ const statSum = (stats.strength ?? 0) + (stats.hardness ?? 0)
688
+ const exponent = statSum / 1998.0
689
+ return {
690
+ hullmass: computeBaseHullmass(ITEM_CONTAINER_T1_PACKED, stats),
691
+ capacity: Math.floor(22000000 * 6 ** exponent),
692
+ }
693
+ }