@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
package/src/data/tiers.ts CHANGED
@@ -18,17 +18,20 @@ export function itemTypeCode(id: number): number {
18
18
  }
19
19
  }
20
20
 
21
+ const CRAFTED_BASE = 10000
22
+ const TIER_STRIDE = 1000
23
+
21
24
  export function itemTier(id: number): number {
22
- if (id < 10000) return 0
23
- return Math.floor(id / 10000)
25
+ if (id < CRAFTED_BASE) return 0
26
+ return Math.floor((id - CRAFTED_BASE) / TIER_STRIDE) + 1
24
27
  }
25
28
 
26
29
  export function itemOffset(id: number): number {
27
- return id % 10000
30
+ return id % TIER_STRIDE
28
31
  }
29
32
 
30
33
  export function itemCategory(id: number): CraftedItemCategory {
31
- if (id < 10000) return 'resource'
34
+ if (id < CRAFTED_BASE) return 'resource'
32
35
  const offset = itemOffset(id)
33
36
  if (offset >= 200) return 'entity'
34
37
  if (offset >= 100) return 'module'
@@ -36,10 +39,10 @@ export function itemCategory(id: number): CraftedItemCategory {
36
39
  }
37
40
 
38
41
  export function isRelatedItem(a: number, b: number): boolean {
39
- if (a < 10000 || b < 10000) return false
42
+ if (a < CRAFTED_BASE || b < CRAFTED_BASE) return false
40
43
  return itemOffset(a) === itemOffset(b)
41
44
  }
42
45
 
43
46
  export function isCraftedItem(id: number): boolean {
44
- return id >= 10000
47
+ return id >= CRAFTED_BASE
45
48
  }
@@ -0,0 +1,13 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {availableBuildMethods} from './build-methods'
3
+ import {ITEM_HUB_T1_PACKED, ITEM_WAREHOUSE_T1_PACKED} from '../data/item-ids'
4
+
5
+ describe('availableBuildMethods', () => {
6
+ test('orbital structures build via craft+deploy or plot', () => {
7
+ expect(availableBuildMethods(ITEM_WAREHOUSE_T1_PACKED)).toEqual(['craft+deploy', 'plot'])
8
+ })
9
+
10
+ test('hub is craft+deploy only — excluded from the plot path', () => {
11
+ expect(availableBuildMethods(ITEM_HUB_T1_PACKED)).toEqual(['craft+deploy'])
12
+ })
13
+ })
@@ -0,0 +1,48 @@
1
+ import {ENTITY_HUB, EntityClass, getKindMeta, getTemplateMeta} from '../data/kind-registry'
2
+ import {getRecipe} from '../data/recipes-runtime'
3
+ import {getItems} from '../data/catalog'
4
+ import type {Item} from '../types'
5
+
6
+ export type BuildMethod = 'craft+deploy' | 'plot'
7
+
8
+ export function availableBuildMethods(itemId: number): BuildMethod[] {
9
+ const recipe = getRecipe(itemId)
10
+ if (!recipe) return []
11
+
12
+ const template = getTemplateMeta(itemId)
13
+ if (!template) return ['craft+deploy']
14
+
15
+ const kindMeta = getKindMeta(template.kind)
16
+ if (!kindMeta) return ['craft+deploy']
17
+
18
+ if (
19
+ kindMeta.classification === EntityClass.OrbitalStructure &&
20
+ !kindMeta.kind.equals(ENTITY_HUB)
21
+ ) {
22
+ return ['craft+deploy', 'plot']
23
+ }
24
+ return ['craft+deploy']
25
+ }
26
+
27
+ export function isBuildable(itemId: number): boolean {
28
+ return availableBuildMethods(itemId).length > 0
29
+ }
30
+
31
+ export function isPlotBuildable(itemId: number): boolean {
32
+ return availableBuildMethods(itemId).includes('plot')
33
+ }
34
+
35
+ export function filterByBuildMethod<T extends {itemId: number}>(
36
+ items: T[],
37
+ method: BuildMethod
38
+ ): T[] {
39
+ return items.filter((i) => availableBuildMethods(i.itemId).includes(method))
40
+ }
41
+
42
+ export function allBuildableItems(): Item[] {
43
+ return getItems().filter((item) => isBuildable(item.id))
44
+ }
45
+
46
+ export function allPlotBuildableItems(): Item[] {
47
+ return getItems().filter((item) => isPlotBuildable(item.id))
48
+ }
@@ -0,0 +1,348 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {
3
+ computeEntityCapabilities,
4
+ computeEngineCapabilities,
5
+ computeGathererCapabilities,
6
+ computeGathererYield,
7
+ computeGeneratorCapabilities,
8
+ computeCrafterCapabilities,
9
+ computeBuilderCapabilities,
10
+ computeLoaderCapabilities,
11
+ computeBaseCapacity,
12
+ computeContainerCapabilities,
13
+ GATHERER_YIELD_TIER_TABLE,
14
+ ENGINE_THRUST_TIER_PCT,
15
+ GENERATOR_CAPACITY_TIER_PCT,
16
+ GENERATOR_RECHARGE_TIER_PCT,
17
+ CRAFTER_SPEED_TIER_PCT,
18
+ BUILDER_SPEED_TIER_PCT,
19
+ WARP_RANGE_TIER_PCT,
20
+ LOADER_THRUST_TIER_PCT,
21
+ moduleTierPct,
22
+ } from './capabilities'
23
+ import {
24
+ computeEngineThrust,
25
+ computeGeneratorCap,
26
+ computeGeneratorRech,
27
+ computeCrafterSpeed,
28
+ computeBuilderSpeed,
29
+ computeWarpRange,
30
+ computeLoaderThrust,
31
+ } from '../nft/description'
32
+ import {applySlotMultiplier, U16_MAX} from '../entities/slot-multiplier'
33
+ import {encodeStats} from './crafting'
34
+ import {
35
+ ITEM_EXTRACTOR_T1_PACKED,
36
+ ITEM_FACTORY_T1_PACKED,
37
+ ITEM_MASS_DRIVER_T1_PACKED,
38
+ ITEM_MASS_CATCHER_T1_PACKED,
39
+ ITEM_GATHERER_T1,
40
+ ITEM_CRAFTER_T1,
41
+ ITEM_LOADER_T1,
42
+ ITEM_SHIP_T1_PACKED,
43
+ ITEM_ROUSTABOUT_T1A_PACKED,
44
+ ITEM_PROSPECTOR_T1A_PACKED,
45
+ ITEM_TENDER_T1A_PACKED,
46
+ ITEM_TUG_T1A_PACKED,
47
+ ITEM_PORTER_T1A_PACKED,
48
+ ITEM_SMITH_T1A_PACKED,
49
+ ITEM_WRIGHT_T1A_PACKED,
50
+ ITEM_ENGINE_T1,
51
+ ITEM_GENERATOR_T1,
52
+ ITEM_BUILDER_T1,
53
+ } from '../data/item-ids'
54
+ import type {InstalledModule} from '../entities/slot-multiplier'
55
+ import type {EntitySlot} from '../data/recipes-runtime'
56
+ import {computeTravelDrain} from '../nft/description'
57
+ import {computeEffectiveModuleStat} from './stat-scaling'
58
+
59
+ function makeGathererStats(strength: number, tolerance: number, saturation: number): bigint {
60
+ return encodeStats([strength, tolerance, saturation, 0])
61
+ }
62
+
63
+ function makeCrafterStats(fineness: number, conductivity: number): bigint {
64
+ return encodeStats([fineness, conductivity])
65
+ }
66
+
67
+ function makeLoaderStats(insulation: number, plasticity: number): bigint {
68
+ return encodeStats([insulation, plasticity])
69
+ }
70
+
71
+ function makeBuilderStats(resonance: number, fineness: number): bigint {
72
+ return encodeStats([resonance, fineness])
73
+ }
74
+
75
+ test('computeBaseCapacity uses container formula for all container-class entities', () => {
76
+ const stats = {strength: 300, hardness: 400, density: 100}
77
+ const expected = computeContainerCapabilities(stats).capacity
78
+ for (const itemId of [
79
+ ITEM_EXTRACTOR_T1_PACKED,
80
+ ITEM_FACTORY_T1_PACKED,
81
+ ITEM_MASS_DRIVER_T1_PACKED,
82
+ ITEM_MASS_CATCHER_T1_PACKED,
83
+ ]) {
84
+ expect(computeBaseCapacity(itemId, stats)).toBe(expected)
85
+ }
86
+ })
87
+
88
+ test('computeBaseCapacity uses ship-hull formula for every ship-class entity', () => {
89
+ const stats = {strength: 300, hardness: 400, density: 100}
90
+ const expected = computeBaseCapacity(ITEM_SHIP_T1_PACKED, stats)
91
+ expect(expected).toBeGreaterThan(0)
92
+ for (const itemId of [
93
+ ITEM_ROUSTABOUT_T1A_PACKED,
94
+ ITEM_PROSPECTOR_T1A_PACKED,
95
+ ITEM_TENDER_T1A_PACKED,
96
+ ITEM_TUG_T1A_PACKED,
97
+ ITEM_PORTER_T1A_PACKED,
98
+ ITEM_SMITH_T1A_PACKED,
99
+ ITEM_WRIGHT_T1A_PACKED,
100
+ ]) {
101
+ expect(computeBaseCapacity(itemId, stats)).toBe(expected)
102
+ }
103
+ })
104
+
105
+ test('computeEntityCapabilities emits gathererLanes alongside legacy gatherer sum', () => {
106
+ // Two gatherers with distinct stats in separate slots, amp=100 for both
107
+ const gathStats1 = makeGathererStats(300, 200, 400)
108
+ const gathStats2 = makeGathererStats(500, 100, 300)
109
+
110
+ const modules: InstalledModule[] = [
111
+ {slotIndex: 0, itemId: ITEM_GATHERER_T1, stats: gathStats1},
112
+ {slotIndex: 1, itemId: ITEM_GATHERER_T1, stats: gathStats2},
113
+ ]
114
+
115
+ const layout: EntitySlot[] = [
116
+ {type: 'gatherer', outputPct: 100, maxTier: 1},
117
+ {type: 'gatherer', outputPct: 100, maxTier: 1},
118
+ ]
119
+
120
+ const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
121
+
122
+ // Lane lists must exist
123
+ expect(result.gathererLanes).toBeDefined()
124
+ expect(result.gathererLanes!.length).toBe(2)
125
+
126
+ // Each lane has the right slotIndex
127
+ expect(result.gathererLanes![0].slotIndex).toBe(0)
128
+ expect(result.gathererLanes![1].slotIndex).toBe(1)
129
+
130
+ // Yields are amp-scaled and distinct
131
+ const caps1 = computeGathererCapabilities({strength: 300, tolerance: 200, saturation: 400}, 1)
132
+ const caps2 = computeGathererCapabilities({strength: 500, tolerance: 100, saturation: 300}, 1)
133
+ const expectedYield1 = applySlotMultiplier(caps1.yield, 100)
134
+ const expectedYield2 = applySlotMultiplier(caps2.yield, 100)
135
+ expect(result.gathererLanes![0].yield).toBe(expectedYield1)
136
+ expect(result.gathererLanes![1].yield).toBe(expectedYield2)
137
+ expect(result.gathererLanes![0].yield).not.toBe(result.gathererLanes![1].yield)
138
+
139
+ // Unscaled per-module drain and depth carried verbatim from the compute helper
140
+ expect(result.gathererLanes![0].drain).toBe(caps1.drain)
141
+ expect(result.gathererLanes![1].drain).toBe(caps2.drain)
142
+ expect(result.gathererLanes![0].depth).toBe(caps1.depth)
143
+ expect(result.gathererLanes![1].depth).toBe(caps2.depth)
144
+
145
+ // outputPct reflects the slot amp
146
+ expect(result.gathererLanes![0].outputPct).toBe(100)
147
+ expect(result.gathererLanes![1].outputPct).toBe(100)
148
+
149
+ // Legacy sum still equals sum of both lane yields
150
+ expect(result.gatherer).toBeDefined()
151
+ expect(result.gatherer!.yield).toBe(expectedYield1 + expectedYield2)
152
+ })
153
+
154
+ test('computeEntityCapabilities emits crafterLanes alongside legacy crafter sum', () => {
155
+ const crafterStats = makeCrafterStats(400, 300)
156
+
157
+ const modules: InstalledModule[] = [
158
+ {slotIndex: 0, itemId: ITEM_CRAFTER_T1, stats: crafterStats},
159
+ ]
160
+
161
+ const layout: EntitySlot[] = [{type: 'crafter', outputPct: 120, maxTier: 1}]
162
+
163
+ const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
164
+
165
+ expect(result.crafterLanes).toBeDefined()
166
+ expect(result.crafterLanes!.length).toBe(1)
167
+ expect(result.crafterLanes![0].slotIndex).toBe(0)
168
+
169
+ const caps = computeCrafterCapabilities({fineness: 400, conductivity: 300}, 1)
170
+ const expectedSpeed = applySlotMultiplier(caps.speed, 120)
171
+ expect(result.crafterLanes![0].speed).toBe(expectedSpeed)
172
+ expect(result.crafterLanes![0].drain).toBe(caps.drain)
173
+ expect(result.crafterLanes![0].outputPct).toBe(120)
174
+
175
+ // Legacy crafter speed equals single-lane speed
176
+ expect(result.crafter).toBeDefined()
177
+ expect(result.crafter!.speed).toBe(expectedSpeed)
178
+ })
179
+
180
+ test('builder capabilities read canonical resonance and fineness slots', () => {
181
+ expect(computeBuilderCapabilities({resonance: 500, fineness: 330}, 1)).toEqual({
182
+ speed: 500,
183
+ drain: 20_000,
184
+ })
185
+ })
186
+
187
+ test('computeEntityCapabilities emits a Builder lane from resonance and fineness', () => {
188
+ const modules: InstalledModule[] = [
189
+ {slotIndex: 0, itemId: ITEM_BUILDER_T1, stats: makeBuilderStats(500, 330)},
190
+ ]
191
+ const layout: EntitySlot[] = [{type: 'builder', outputPct: 80, maxTier: 1}]
192
+ const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
193
+
194
+ expect(result.builderLanes).toEqual([{slotIndex: 0, speed: 400, drain: 20_000, outputPct: 80}])
195
+ expect(result.builder).toEqual({speed: 400, drain: 20_000})
196
+ })
197
+
198
+ test('computeEntityCapabilities emits loaderLanes alongside legacy loaders sum', () => {
199
+ const loaderStats = makeLoaderStats(600, 500)
200
+
201
+ const modules: InstalledModule[] = [{slotIndex: 0, itemId: ITEM_LOADER_T1, stats: loaderStats}]
202
+
203
+ const layout: EntitySlot[] = [{type: 'loader', outputPct: 80, maxTier: 1}]
204
+
205
+ const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
206
+
207
+ expect(result.loaderLanes).toBeDefined()
208
+ expect(result.loaderLanes!.length).toBe(1)
209
+ expect(result.loaderLanes![0].slotIndex).toBe(0)
210
+
211
+ const caps = computeLoaderCapabilities({insulation: 600, plasticity: 500}, 1)
212
+ // mass is unscaled (raw); thrust is amp-scaled
213
+ expect(result.loaderLanes![0].mass).toBe(caps.mass)
214
+ expect(result.loaderLanes![0].thrust).toBe(applySlotMultiplier(caps.thrust, 80))
215
+ expect(result.loaderLanes![0].outputPct).toBe(80)
216
+
217
+ // Legacy loaders.mass is total (same as single-lane raw mass here)
218
+ expect(result.loaders).toBeDefined()
219
+ expect(result.loaders!.mass).toBe(caps.mass)
220
+ })
221
+
222
+ test('per-lane amp-scaled stats clamp to UInt16, matching the contract clamp_to_uint16', () => {
223
+ expect(applySlotMultiplier(60000, 200)).toBe(U16_MAX)
224
+ expect(applySlotMultiplier(1000, 150)).toBe(1500)
225
+ })
226
+
227
+ test('generator capacity and recharge are denominated to milli-energy', () => {
228
+ const caps = computeGeneratorCapabilities({resonance: 213, reflectivity: 213}, 1)
229
+ expect(caps.capacity).toBe(1_406_500)
230
+ expect(caps.recharge).toBe(3_278)
231
+ })
232
+
233
+ test('engine and generator capabilities use tapered quality consistently', () => {
234
+ const modules: InstalledModule[] = [
235
+ {slotIndex: 0, itemId: ITEM_ENGINE_T1, stats: encodeStats([500, 500])},
236
+ {slotIndex: 1, itemId: ITEM_GENERATOR_T1, stats: encodeStats([500, 500])},
237
+ ]
238
+ const layout: EntitySlot[] = [
239
+ {type: 'engine', outputPct: 100, maxTier: 1},
240
+ {type: 'generator', outputPct: 100, maxTier: 1},
241
+ ]
242
+
243
+ const result = computeEntityCapabilities({}, ITEM_ROUSTABOUT_T1A_PACKED, modules, layout)
244
+ const engines = computeEngineCapabilities({volatility: 500, thermal: 500}, 1)
245
+
246
+ expect(result.engines).toEqual({
247
+ thrust: engines.thrust,
248
+ drain: computeTravelDrain(engines.thrust, computeEffectiveModuleStat(500)),
249
+ })
250
+ expect(result.generator).toEqual(
251
+ computeGeneratorCapabilities({resonance: 500, reflectivity: 500}, 1)
252
+ )
253
+ })
254
+
255
+ test('gatherer/crafter/hauler drains are denominated', () => {
256
+ expect(computeGathererCapabilities({strength: 0, hardness: 0, saturation: 213}, 1).drain).toBe(
257
+ 1_967_500
258
+ )
259
+ expect(computeCrafterCapabilities({fineness: 0, conductivity: 213}, 1).drain).toBe(23_546)
260
+ })
261
+
262
+ test('gatherer depth accepts canonical tolerance and legacy recipe-labelled hardness', () => {
263
+ expect(computeGathererCapabilities({strength: 0, tolerance: 213, saturation: 0}, 2).depth).toBe(
264
+ 4_343
265
+ )
266
+ expect(computeGathererCapabilities({strength: 0, hardness: 213, saturation: 0}, 1).depth).toBe(
267
+ 1_565
268
+ )
269
+ })
270
+
271
+ describe('computeGathererYield', () => {
272
+ test('table is the k=0.2 integer-percent curve T1..T10', () => {
273
+ expect([...GATHERER_YIELD_TIER_TABLE]).toEqual([
274
+ 100, 120, 140, 160, 180, 200, 220, 240, 260, 280,
275
+ ])
276
+ })
277
+
278
+ test('T1 equals the base yield 200 + str (no change)', () => {
279
+ expect(computeGathererYield(500, 1)).toBe(700)
280
+ expect(computeGathererYield(0, 1)).toBe(200)
281
+ })
282
+
283
+ test('T2 scales base yield by 1.2, floored', () => {
284
+ expect(computeGathererYield(500, 2)).toBe(840) // floor(700 * 120 / 100)
285
+ expect(computeGathererYield(213, 2)).toBe(495) // floor(413 * 120 / 100) = floor(495.6)
286
+ })
287
+
288
+ test('T10 scales base yield by 2.8, floored', () => {
289
+ expect(computeGathererYield(500, 10)).toBe(1960) // 700 * 280 / 100
290
+ })
291
+
292
+ test('tier is clamped to [1, 10]', () => {
293
+ expect(computeGathererYield(500, 0)).toBe(700)
294
+ expect(computeGathererYield(500, 99)).toBe(1960)
295
+ })
296
+ })
297
+
298
+ describe('module tier tables', () => {
299
+ const ALL_TABLES = [
300
+ ENGINE_THRUST_TIER_PCT,
301
+ GENERATOR_CAPACITY_TIER_PCT,
302
+ GENERATOR_RECHARGE_TIER_PCT,
303
+ CRAFTER_SPEED_TIER_PCT,
304
+ BUILDER_SPEED_TIER_PCT,
305
+ WARP_RANGE_TIER_PCT,
306
+ LOADER_THRUST_TIER_PCT,
307
+ ]
308
+ test('every table has 10 rows and T1 = 100', () => {
309
+ for (const table of ALL_TABLES) {
310
+ expect(table.length).toBe(10)
311
+ expect(table[0]).toBe(100)
312
+ }
313
+ })
314
+ test('moduleTierPct clamps out-of-range tiers', () => {
315
+ expect(moduleTierPct(ENGINE_THRUST_TIER_PCT, 0)).toBe(100)
316
+ expect(moduleTierPct(ENGINE_THRUST_TIER_PCT, 11)).toBe(280)
317
+ })
318
+ test('engine thrust tiers (stat 500 pre-effective)', () => {
319
+ expect(computeEngineThrust(500, 1)).toBe(775)
320
+ expect(computeEngineThrust(500, 2)).toBe(930)
321
+ expect(computeEngineThrust(500, 10)).toBe(2170)
322
+ })
323
+ test('generator capacity k=0.1, recharge k=0.2', () => {
324
+ expect(computeGeneratorCap(500, 1)).toBe(1_550_000)
325
+ expect(computeGeneratorCap(500, 2)).toBe(1_705_000)
326
+ expect(computeGeneratorCap(500, 10)).toBe(2_945_000)
327
+ expect(computeGeneratorRech(500, 1)).toBe(5000)
328
+ expect(computeGeneratorRech(500, 2)).toBe(6000)
329
+ expect(computeGeneratorRech(500, 10)).toBe(14000)
330
+ })
331
+ test('crafter/builder speed tiers', () => {
332
+ expect(computeCrafterSpeed(500, 1)).toBe(500)
333
+ expect(computeCrafterSpeed(500, 2)).toBe(600)
334
+ expect(computeBuilderSpeed(500, 2)).toBe(600)
335
+ expect(computeBuilderSpeed(500, 10)).toBe(1400)
336
+ })
337
+ test('warp range tiers', () => {
338
+ expect(computeWarpRange(500, 1)).toBe(1600)
339
+ expect(computeWarpRange(500, 2)).toBe(1920)
340
+ expect(computeWarpRange(500, 10)).toBe(4480)
341
+ })
342
+ test('loader thrust tiers floor correctly', () => {
343
+ expect(computeLoaderThrust(500, 1)).toBe(26)
344
+ expect(computeLoaderThrust(500, 2)).toBe(31)
345
+ expect(computeLoaderThrust(500, 10)).toBe(72)
346
+ expect(computeLoaderThrust(0, 2)).toBe(1)
347
+ })
348
+ })