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

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