@shipload/sdk 1.0.0-next.45 → 1.0.0-next.47

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 (43) hide show
  1. package/lib/shipload.d.ts +236 -39
  2. package/lib/shipload.js +1465 -191
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +1450 -192
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +81 -15
  7. package/lib/testing.js +377 -44
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +378 -45
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/capabilities/gathering.test.ts +14 -1
  13. package/src/capabilities/gathering.ts +6 -5
  14. package/src/capabilities/modules.ts +13 -0
  15. package/src/contracts/server.ts +242 -28
  16. package/src/data/capabilities.ts +1 -1
  17. package/src/data/entities.json +93 -19
  18. package/src/data/item-ids.ts +12 -0
  19. package/src/data/items.json +76 -2
  20. package/src/data/kind-registry.json +10 -0
  21. package/src/data/metadata.ts +75 -7
  22. package/src/data/recipes-runtime.ts +1 -0
  23. package/src/data/recipes.json +736 -0
  24. package/src/derivation/capabilities.test.ts +11 -11
  25. package/src/derivation/capabilities.ts +42 -27
  26. package/src/derivation/crafting.ts +13 -9
  27. package/src/derivation/index.ts +2 -0
  28. package/src/derivation/recipe-usage.test.ts +11 -7
  29. package/src/derivation/stars.test.ts +16 -0
  30. package/src/derivation/stars.ts +10 -0
  31. package/src/derivation/stratum.ts +11 -4
  32. package/src/entities/makers.ts +8 -1
  33. package/src/index-module.ts +5 -1
  34. package/src/managers/actions.ts +10 -0
  35. package/src/nft/buildImmutableData.ts +1 -1
  36. package/src/nft/description.ts +25 -12
  37. package/src/resolution/describe-module.ts +21 -8
  38. package/src/resolution/resolve-item.ts +31 -33
  39. package/src/scheduling/projection.ts +8 -1
  40. package/src/subscriptions/manager.cluster.test.ts +7 -2
  41. package/src/subscriptions/types.ts +1 -0
  42. package/src/utils/cargo.test.ts +14 -0
  43. package/src/utils/cargo.ts +2 -0
@@ -21,12 +21,12 @@ import {
21
21
  import type {InstalledModule} from '../entities/slot-multiplier'
22
22
  import type {EntitySlot} from '../data/recipes-runtime'
23
23
 
24
- function makeGathererStats(strength: number, tolerance: number, saturation: number): bigint {
25
- return encodeStats([strength, tolerance, saturation, 0])
24
+ function makeGathererStats(strength: number, hardness: number, saturation: number): bigint {
25
+ return encodeStats([strength, hardness, saturation, 0])
26
26
  }
27
27
 
28
- function makeCrafterStats(reactivity: number, conductivity: number): bigint {
29
- return encodeStats([reactivity, conductivity])
28
+ function makeCrafterStats(fineness: number, conductivity: number): bigint {
29
+ return encodeStats([fineness, conductivity])
30
30
  }
31
31
 
32
32
  function makeLoaderStats(insulation: number, plasticity: number): bigint {
@@ -57,8 +57,8 @@ test('computeEntityCapabilities emits gathererLanes alongside legacy gatherer su
57
57
  ]
58
58
 
59
59
  const layout: EntitySlot[] = [
60
- {type: 'gatherer', outputPct: 100},
61
- {type: 'gatherer', outputPct: 100},
60
+ {type: 'gatherer', outputPct: 100, maxTier: 1},
61
+ {type: 'gatherer', outputPct: 100, maxTier: 1},
62
62
  ]
63
63
 
64
64
  const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
@@ -72,8 +72,8 @@ test('computeEntityCapabilities emits gathererLanes alongside legacy gatherer su
72
72
  expect(result.gathererLanes![1].slotIndex).toBe(1)
73
73
 
74
74
  // Yields are amp-scaled and distinct
75
- const caps1 = computeGathererCapabilities({strength: 300, tolerance: 200, saturation: 400}, 1)
76
- const caps2 = computeGathererCapabilities({strength: 500, tolerance: 100, saturation: 300}, 1)
75
+ const caps1 = computeGathererCapabilities({strength: 300, hardness: 200, saturation: 400}, 1)
76
+ const caps2 = computeGathererCapabilities({strength: 500, hardness: 100, saturation: 300}, 1)
77
77
  const expectedYield1 = applySlotMultiplier(caps1.yield, 100)
78
78
  const expectedYield2 = applySlotMultiplier(caps2.yield, 100)
79
79
  expect(result.gathererLanes![0].yield).toBe(expectedYield1)
@@ -102,7 +102,7 @@ test('computeEntityCapabilities emits crafterLanes alongside legacy crafter sum'
102
102
  {slotIndex: 0, itemId: ITEM_CRAFTER_T1, stats: crafterStats},
103
103
  ]
104
104
 
105
- const layout: EntitySlot[] = [{type: 'crafter', outputPct: 120}]
105
+ const layout: EntitySlot[] = [{type: 'crafter', outputPct: 120, maxTier: 1}]
106
106
 
107
107
  const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
108
108
 
@@ -110,7 +110,7 @@ test('computeEntityCapabilities emits crafterLanes alongside legacy crafter sum'
110
110
  expect(result.crafterLanes!.length).toBe(1)
111
111
  expect(result.crafterLanes![0].slotIndex).toBe(0)
112
112
 
113
- const caps = computeCrafterCapabilities({reactivity: 400, conductivity: 300})
113
+ const caps = computeCrafterCapabilities({fineness: 400, conductivity: 300})
114
114
  const expectedSpeed = applySlotMultiplier(caps.speed, 120)
115
115
  expect(result.crafterLanes![0].speed).toBe(expectedSpeed)
116
116
  expect(result.crafterLanes![0].drain).toBe(caps.drain)
@@ -126,7 +126,7 @@ test('computeEntityCapabilities emits loaderLanes alongside legacy loaders sum',
126
126
 
127
127
  const modules: InstalledModule[] = [{slotIndex: 0, itemId: ITEM_LOADER_T1, stats: loaderStats}]
128
128
 
129
- const layout: EntitySlot[] = [{type: 'loader', outputPct: 80}]
129
+ const layout: EntitySlot[] = [{type: 'loader', outputPct: 80, maxTier: 1}]
130
130
 
131
131
  const result = computeEntityCapabilities({}, ITEM_EXTRACTOR_T1_PACKED, modules, layout)
132
132
 
@@ -115,7 +115,10 @@ export function computeCrafterCapabilities(stats: Record<string, number>): {
115
115
  }
116
116
  }
117
117
 
118
- export function computeHaulerCapabilities(stats: Record<string, number>): {
118
+ export function computeHaulerCapabilities(
119
+ stats: Record<string, number>,
120
+ tier: number
121
+ ): {
119
122
  capacity: number
120
123
  efficiency: number
121
124
  drain: number
@@ -125,7 +128,7 @@ export function computeHaulerCapabilities(stats: Record<string, number>): {
125
128
  const conductivity = stats.conductivity
126
129
 
127
130
  return {
128
- capacity: Math.max(1, 1 + Math.floor(resonance / 400)),
131
+ capacity: computeHaulerCapacity(resonance, tier),
129
132
  efficiency: 2000 + plasticity * 6,
130
133
  drain: Math.max(3, 15 - Math.floor(conductivity / 80)),
131
134
  }
@@ -171,8 +174,10 @@ import {
171
174
  ITEM_CONTAINER_T2_PACKED,
172
175
  ITEM_EXTRACTOR_T1_PACKED,
173
176
  ITEM_FACTORY_T1_PACKED,
177
+ ITEM_HAULER_SHIP_T2_PACKED,
174
178
  ITEM_MASS_CATCHER_T1_PACKED,
175
179
  ITEM_MASS_DRIVER_T1_PACKED,
180
+ ITEM_PROSPECTOR_T2_PACKED,
176
181
  ITEM_SHIP_T1_PACKED,
177
182
  ITEM_WAREHOUSE_T1_PACKED,
178
183
  } from '../data/item-ids'
@@ -200,25 +205,42 @@ import {
200
205
  type InstalledModule,
201
206
  } from '../entities/slot-multiplier'
202
207
  import type {EntitySlot} from '../data/recipes-runtime'
203
- import {computeTravelDrain} from '../nft/description'
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
+ }
204
220
 
205
221
  export function computeBaseCapacity(itemId: number, stats: Record<string, number>): number {
222
+ let base: number
206
223
  switch (itemId) {
207
224
  case ITEM_SHIP_T1_PACKED:
208
- return computeShipHullCapabilities(stats).capacity
225
+ case ITEM_PROSPECTOR_T2_PACKED:
226
+ case ITEM_HAULER_SHIP_T2_PACKED:
227
+ base = computeShipHullCapabilities(stats).capacity
228
+ break
209
229
  case ITEM_EXTRACTOR_T1_PACKED:
210
230
  case ITEM_FACTORY_T1_PACKED:
211
231
  case ITEM_MASS_DRIVER_T1_PACKED:
212
232
  case ITEM_MASS_CATCHER_T1_PACKED:
213
233
  case ITEM_CONTAINER_T1_PACKED:
214
- return computeContainerCapabilities(stats).capacity
215
- case ITEM_WAREHOUSE_T1_PACKED:
216
- return computeWarehouseHullCapabilities(stats).capacity
217
234
  case ITEM_CONTAINER_T2_PACKED:
218
- return computeContainerT2Capabilities(stats).capacity
235
+ base = computeContainerCapabilities(stats).capacity
236
+ break
237
+ case ITEM_WAREHOUSE_T1_PACKED:
238
+ base = computeWarehouseHullCapabilities(stats).capacity
239
+ break
219
240
  default:
220
241
  return 0
221
242
  }
243
+ return applyCapacityTier(base, getItem(itemId).tier)
222
244
  }
223
245
 
224
246
  export function computeWarpCapabilities(stats: Record<string, number>): {
@@ -273,7 +295,12 @@ export interface ComputedCapabilities {
273
295
  loaderLanes?: LoaderLaneEntry[]
274
296
  crafter?: {speed: number; drain: number}
275
297
  crafterLanes?: CrafterLaneEntry[]
276
- hauler?: {capacity: number; efficiency: number; drain: number}
298
+ hauler?: {
299
+ capacity: number
300
+ efficiency: number
301
+ drain: number
302
+ capacityByTier: {tier: number; capacity: number}[]
303
+ }
277
304
  warp?: {range: number}
278
305
  launcher?: {chargeRate: number; velocity: number; drain: number}
279
306
  }
@@ -315,6 +342,7 @@ export function computeEntityCapabilities(
315
342
  let weightedHaulerEffNum = 0n
316
343
  let totalHaulerDrain = 0
317
344
  let hasHauler = false
345
+ const haulerCapByTier = new Map<number, number>()
318
346
 
319
347
  let totalWarpRange = 0
320
348
  let hasWarp = false
@@ -392,9 +420,10 @@ export function computeEntityCapabilities(
392
420
  })
393
421
  } else if (modType === MODULE_HAULER) {
394
422
  hasHauler = true
395
- const caps = computeHaulerCapabilities(decodedStats)
423
+ const caps = computeHaulerCapabilities(decodedStats, item.tier)
396
424
  const eff = applySlotMultiplier(caps.efficiency, amp)
397
425
  totalHaulerCapacity += caps.capacity
426
+ haulerCapByTier.set(item.tier, (haulerCapByTier.get(item.tier) ?? 0) + caps.capacity)
398
427
  weightedHaulerEffNum += BigInt(eff) * BigInt(caps.capacity)
399
428
  totalHaulerDrain += caps.drain
400
429
  } else if (modType === MODULE_WARP) {
@@ -466,6 +495,9 @@ export function computeEntityCapabilities(
466
495
  capacity: totalHaulerCapacity,
467
496
  efficiency: clampUint16(efficiency),
468
497
  drain: totalHaulerDrain,
498
+ capacityByTier: [...haulerCapByTier.entries()]
499
+ .sort((a, b) => a[0] - b[0])
500
+ .map(([tier, capacity]) => ({tier, capacity})),
469
501
  }
470
502
  }
471
503
  if (hasWarp) {
@@ -493,20 +525,3 @@ export function computeContainerCapabilities(stats: Record<string, number>): {
493
525
  capacity: Math.floor(22000000 * 6 ** exponent),
494
526
  }
495
527
  }
496
-
497
- export function computeContainerT2Capabilities(stats: Record<string, number>): {
498
- hullmass: number
499
- capacity: number
500
- } {
501
- const strength = stats.strength ?? 0
502
- const density = stats.density ?? 0
503
- const hardness = stats.hardness ?? 0
504
-
505
- const hullmass = 70000 - 50 * density
506
-
507
- const statSum = strength + hardness
508
- const exponent = statSum / 2947
509
- const capacity = Math.floor(24000000 * 6 ** exponent)
510
-
511
- return {hullmass, capacity}
512
- }
@@ -141,16 +141,20 @@ export function computeEntityStats(
141
141
  }
142
142
 
143
143
  return recipe.statSlots.map((slot) => {
144
- const src = slot.sources[0]
145
- const key = keyForStatSlot(recipe, slot)
146
- if (!src) return {key, value: 1}
147
- const input = recipe.inputs[src.inputIndex]
148
- if (!input) {
149
- return {key, value: 1}
144
+ const slotKey = keyForStatSlot(recipe, slot)
145
+ if (slot.sources.length === 0) return {key: slotKey, value: 1}
146
+ let weightedSum = 0
147
+ let totalWeight = 0
148
+ for (const src of slot.sources) {
149
+ const key = keyForRecipeInputStat(recipe, src.inputIndex, src.statIndex)
150
+ const input = recipe.inputs[src.inputIndex]
151
+ if (!input) continue
152
+ const weight = recipe.blendWeights?.[src.inputIndex] ?? 1
153
+ weightedSum += (blendedByComponent[input.itemId]?.[key] ?? 0) * weight
154
+ totalWeight += weight
150
155
  }
151
- const blended = blendedByComponent[input.itemId] ?? {}
152
- const value = blended[key] ?? 0
153
- return {key, value: Math.max(1, Math.min(999, value))}
156
+ const value = totalWeight > 0 ? Math.floor(weightedSum / totalWeight) : 0
157
+ return {key: slotKey, value: Math.max(1, Math.min(999, value))}
154
158
  })
155
159
  }
156
160
 
@@ -52,4 +52,6 @@ export {
52
52
  starsForStat,
53
53
  starRating,
54
54
  statMagnitude,
55
+ compareByStars,
55
56
  } from './stars'
57
+ export type {StarSortable} from './stars'
@@ -7,7 +7,9 @@ import {
7
7
  } from './recipe-usage'
8
8
  import {
9
9
  ITEM_SENSOR,
10
+ ITEM_SENSOR_T2,
10
11
  ITEM_RESIN,
12
+ ITEM_FRAME,
11
13
  ITEM_PLATE,
12
14
  ITEM_BEAM,
13
15
  ITEM_GATHERER_T1,
@@ -26,12 +28,14 @@ test('getRecipeConsumers lists every recipe that consumes Sensor', () => {
26
28
  const consumers = getRecipeConsumers(ITEM_SENSOR)
27
29
  const ids = consumers.map((c) => c.outputItemId).sort((a, b) => a - b)
28
30
  expect(ids).toEqual(
29
- [ITEM_CRAFTER_T1, ITEM_SHIP_T1_PACKED, ITEM_EXTRACTOR_T1_PACKED].sort((a, b) => a - b)
31
+ [ITEM_CRAFTER_T1, ITEM_SHIP_T1_PACKED, ITEM_EXTRACTOR_T1_PACKED, ITEM_SENSOR_T2].sort(
32
+ (a, b) => a - b
33
+ )
30
34
  )
31
35
  })
32
36
 
33
- test('Resin feeds the gatherer drain stat', () => {
34
- const consumers = getRecipeConsumers(ITEM_RESIN)
37
+ test('Frame feeds the gatherer drain stat', () => {
38
+ const consumers = getRecipeConsumers(ITEM_FRAME)
35
39
  const gatherer = consumers.find((c) => c.outputItemId === ITEM_GATHERER_T1)
36
40
  expect(gatherer).toBeDefined()
37
41
  const drain = gatherer?.statFlows.find(
@@ -56,12 +60,12 @@ test('getResourceDemand traces a dual-resource component to both resources', ()
56
60
  })
57
61
 
58
62
  test('getResourceDemand recurses through a module to raw resources', () => {
59
- // Gatherer = 300 Beam (5 ore + 5 gas each) + 300 Resin (5 biomass + 5 crystal each)
63
+ // Gatherer = 300 Beam (5 ore + 5 gas each) + 300 Frame (5 regolith + 5 biomass each)
60
64
  expect(getResourceDemand(ITEM_GATHERER_T1)).toEqual({
61
65
  ore: 1500,
62
66
  gas: 1500,
67
+ regolith: 1500,
63
68
  biomass: 1500,
64
- crystal: 1500,
65
69
  })
66
70
  })
67
71
 
@@ -69,9 +73,9 @@ test('getResourceDemand scales by quantity', () => {
69
73
  expect(getResourceDemand(ITEM_PLATE, 3)).toEqual({ore: 30})
70
74
  })
71
75
 
72
- test('getComponentDemand reports Resin as consumed by two recipes', () => {
76
+ test('getComponentDemand reports Resin as consumed by three recipes', () => {
73
77
  const demand = getComponentDemand()
74
78
  const resin = demand.find((d) => d.itemId === ITEM_RESIN)
75
79
  expect(resin).toBeDefined()
76
- expect(resin?.consumerCount).toBe(2)
80
+ expect(resin?.consumerCount).toBe(3)
77
81
  })
@@ -1,5 +1,6 @@
1
1
  import {expect, test} from 'bun:test'
2
2
  import {
3
+ compareByStars,
3
4
  MAX_STARS_PER_STAT,
4
5
  MAX_STAR_RATING,
5
6
  starRating,
@@ -44,6 +45,21 @@ test('statMagnitude sums raw values for tiebreaking', () => {
44
45
  expect(statMagnitude(599, 599, 599)).toBeGreaterThan(statMagnitude(251, 251, 251))
45
46
  })
46
47
 
48
+ test('compareByStars orders by rating desc, then magnitude desc', () => {
49
+ const rows = [
50
+ {key: 'low-rating', rating: 3, magnitude: 9000},
51
+ {key: 'high-rating', rating: 6, magnitude: 300},
52
+ {key: 'tie-small', rating: 6, magnitude: 600},
53
+ {key: 'tie-big', rating: 6, magnitude: 900},
54
+ ]
55
+ const ordered = [...rows].sort(compareByStars).map((r) => r.key)
56
+ expect(ordered).toEqual(['tie-big', 'tie-small', 'high-rating', 'low-rating'])
57
+ })
58
+
59
+ test('compareByStars returns 0 when rating and magnitude are equal', () => {
60
+ expect(compareByStars({rating: 4, magnitude: 500}, {rating: 4, magnitude: 500})).toBe(0)
61
+ })
62
+
47
63
  test('constants hold their documented values', () => {
48
64
  expect(STAR_STEP).toBe(250)
49
65
  expect(MAX_STARS_PER_STAT).toBe(3)
@@ -13,3 +13,13 @@ export function starRating(stat1: number, stat2: number, stat3: number): number
13
13
  export function statMagnitude(stat1: number, stat2: number, stat3: number): number {
14
14
  return stat1 + stat2 + stat3
15
15
  }
16
+
17
+ export interface StarSortable {
18
+ rating: number
19
+ magnitude: number
20
+ }
21
+
22
+ // Star-quality ordering shared by every "sort by stars" surface: rating desc, then total sum of stats desc.
23
+ export function compareByStars(a: StarSortable, b: StarSortable): number {
24
+ return b.rating - a.rating || b.magnitude - a.magnitude
25
+ }
@@ -3,13 +3,20 @@ import {hash512} from '../utils/hash'
3
3
  import {Coordinates, type CoordinatesType} from '../types'
4
4
  import {getItem} from '../data/catalog'
5
5
  import {getEligibleResources, getResourceWeight, yieldThresholdAt} from './resources'
6
- import {RESERVE_TIERS, rollTier, rollWithinTier, applyResourceTierMultiplier} from './tiers'
6
+ import {
7
+ RESERVE_TIERS,
8
+ rollTier,
9
+ rollWithinTier,
10
+ applyResourceTierMultiplier,
11
+ type ReserveTier,
12
+ } from './tiers'
7
13
 
8
14
  export interface StratumInfo {
9
15
  itemId: number
10
16
  seed: bigint
11
17
  richness: number
12
18
  reserve: number
19
+ tier: ReserveTier
13
20
  }
14
21
 
15
22
  export interface ResourceStats {
@@ -35,10 +42,10 @@ export function deriveStratum(
35
42
  const rawReserve = ((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]) >>> 0
36
43
 
37
44
  if (rawReserve > yieldThresholdAt(stratum))
38
- return {itemId: 0, seed: 0n, richness: 0, reserve: 0}
45
+ return {itemId: 0, seed: 0n, richness: 0, reserve: 0, tier: 'small'}
39
46
 
40
47
  const eligible = getEligibleResources(locationType, subtype, stratum)
41
- if (eligible.length === 0) return {itemId: 0, seed: 0n, richness: 0, reserve: 0}
48
+ if (eligible.length === 0) return {itemId: 0, seed: 0n, richness: 0, reserve: 0, tier: 'small'}
42
49
 
43
50
  const resourceRoll = ((bytes[4] << 24) | (bytes[5] << 16) | (bytes[6] << 8) | bytes[7]) >>> 0
44
51
 
@@ -83,7 +90,7 @@ export function deriveStratum(
83
90
  const roll = 500 + 100 * z
84
91
  const richness = Math.max(1, Math.min(999, Math.round(roll)))
85
92
 
86
- return {itemId: selectedItemId, seed: seedBigInt, richness, reserve}
93
+ return {itemId: selectedItemId, seed: seedBigInt, richness, reserve, tier}
87
94
  }
88
95
 
89
96
  /**
@@ -140,7 +140,14 @@ export function makeEntity(packedItemId: number, state: EntityStateInput): Entit
140
140
 
141
141
  if (caps.engines) info.engines = caps.engines
142
142
  if (caps.generator) info.generator = caps.generator
143
- if (caps.hauler) info.hauler = caps.hauler
143
+ if (caps.hauler) {
144
+ info.hauler = ServerContract.Types.hauler_stats.from({
145
+ capacity: caps.hauler.capacity,
146
+ efficiency: caps.hauler.efficiency,
147
+ drain: caps.hauler.drain,
148
+ capacity_by_tier: caps.hauler.capacityByTier,
149
+ })
150
+ }
144
151
  if (caps.warp) info.warp = caps.warp
145
152
  if (caps.launcher) {
146
153
  info.launcher = ServerContract.Types.launcher_stats.from({
@@ -160,7 +160,9 @@ export {
160
160
  starsForStat,
161
161
  starRating,
162
162
  statMagnitude,
163
+ compareByStars,
163
164
  } from './derivation'
165
+ export type {StarSortable} from './derivation'
164
166
 
165
167
  export {hash, hash512} from './utils/hash'
166
168
  export {validateDisplayName, normalizeDisplayName} from './utils/display-name'
@@ -424,11 +426,13 @@ export {
424
426
  computeStorageCapabilities,
425
427
  computeBatteryCapabilities,
426
428
  computeContainerCapabilities,
427
- computeContainerT2Capabilities,
428
429
  computeWarpCapabilities,
429
430
  computeLauncherCapabilities,
430
431
  computeBaseCapacity,
431
432
  computeEntityCapabilities,
433
+ CAPACITY_TIER_TABLE,
434
+ capacityTierMultiplier,
435
+ applyCapacityTier,
432
436
  GATHERER_DEPTH_TABLE,
433
437
  GATHERER_DEPTH_MAX_TIER,
434
438
  gathererDepthForTier,
@@ -471,6 +471,16 @@ export class ActionsManager extends BaseManager {
471
471
  })
472
472
  }
473
473
 
474
+ swaptile(hubId: UInt64Type, aGx: number, aGy: number, bGx: number, bGy: number): Action {
475
+ return this.server.action('swaptile', {
476
+ hub_id: UInt64.from(hubId),
477
+ a_gx: aGx,
478
+ a_gy: aGy,
479
+ b_gx: bGx,
480
+ b_gy: bGy,
481
+ })
482
+ }
483
+
474
484
  buildplot(entityId: UInt64Type, plotId: UInt64Type): Action {
475
485
  return this.server.action('buildplot', {
476
486
  builder_id: UInt64.from(entityId),
@@ -269,7 +269,7 @@ export function buildModuleImmutable(
269
269
  base.push({first: 'resonance', second: ['uint16', res]})
270
270
  base.push({first: 'plasticity', second: ['uint16', pla]})
271
271
  base.push({first: 'conductivity', second: ['uint16', con]})
272
- base.push({first: 'capacity', second: ['uint8', computeHaulerCapacity(res)]})
272
+ base.push({first: 'capacity', second: ['uint8', computeHaulerCapacity(res, item.tier)]})
273
273
  base.push({first: 'efficiency', second: ['uint16', computeHaulerEfficiency(pla)]})
274
274
  base.push({first: 'drain', second: ['uint16', computeHaulerDrain(con)]})
275
275
  break
@@ -18,10 +18,15 @@ import {
18
18
  ITEM_EXTRACTOR_T1_PACKED,
19
19
  ITEM_FACTORY_T1_PACKED,
20
20
  ITEM_GATHERER_T1,
21
+ ITEM_GATHERER_T2,
21
22
  ITEM_GENERATOR_T1,
23
+ ITEM_HAULER_SHIP_T2_PACKED,
22
24
  ITEM_HAULER_T1,
25
+ ITEM_HAULER_T2,
23
26
  ITEM_BATTERY_T1,
27
+ ITEM_LAUNCHER_T1,
24
28
  ITEM_LOADER_T1,
29
+ ITEM_PROSPECTOR_T2_PACKED,
25
30
  ITEM_SHIP_T1_PACKED,
26
31
  ITEM_STORAGE_T1,
27
32
  ITEM_WAREHOUSE_T1_PACKED,
@@ -50,11 +55,6 @@ export function computeBaseCapacityContainer(stats: bigint): number {
50
55
  return Math.floor(22_000_000 * 6 ** (s / 1998))
51
56
  }
52
57
 
53
- export function computeBaseCapacityContainerT2(stats: bigint): number {
54
- const s = decodeStat(stats, 0) + decodeStat(stats, 2)
55
- return Math.floor(24_000_000 * 6 ** (s / 2947))
56
- }
57
-
58
58
  export function computeBaseCapacityWarehouse(stats: bigint): number {
59
59
  const s = decodeStat(stats, 0) + decodeStat(stats, 2)
60
60
  return Math.floor(100_000_000 * 6 ** (s / 1998))
@@ -83,7 +83,8 @@ export const computeLoaderMass = (ins: number): number => Math.max(200, 2000 - i
83
83
  export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla * pla, 10000)
84
84
  export const computeCrafterSpeed = (rea: number): number => 100 + idiv(rea * 4, 5)
85
85
  export const computeCrafterDrain = (fin: number): number => Math.max(5, 30 - idiv(fin, 33))
86
- export const computeHaulerCapacity = (fin: number): number => Math.max(1, 1 + idiv(fin, 400))
86
+ export const computeHaulerCapacity = (fin: number, tier: number): number =>
87
+ Math.max(tier, tier + idiv(fin, 400))
87
88
  export const computeHaulerEfficiency = (con: number): number => 2000 + con * 6
88
89
  export const computeHaulerDrain = (com: number): number => Math.max(3, 15 - idiv(com, 80))
89
90
  export const computeWarpRange = (stat: number): number => 100 + stat * 3
@@ -114,6 +115,10 @@ export function entityDisplayName(itemId: number): string {
114
115
  return 'Container'
115
116
  case ITEM_CONTAINER_T2_PACKED:
116
117
  return 'Container'
118
+ case ITEM_PROSPECTOR_T2_PACKED:
119
+ return 'Prospector'
120
+ case ITEM_HAULER_SHIP_T2_PACKED:
121
+ return 'Hauler'
117
122
  default:
118
123
  return 'Entity'
119
124
  }
@@ -124,8 +129,9 @@ export function moduleDisplayName(itemId: number): string {
124
129
  case ITEM_ENGINE_T1:
125
130
  return 'Engine'
126
131
  case ITEM_GENERATOR_T1:
127
- return 'Reactor'
132
+ return 'Power Core'
128
133
  case ITEM_GATHERER_T1:
134
+ case ITEM_GATHERER_T2:
129
135
  return 'Limpet Bay'
130
136
  case ITEM_LOADER_T1:
131
137
  return 'Shuttle Bay'
@@ -134,11 +140,14 @@ export function moduleDisplayName(itemId: number): string {
134
140
  case ITEM_STORAGE_T1:
135
141
  return 'Cargo Hold'
136
142
  case ITEM_HAULER_T1:
143
+ case ITEM_HAULER_T2:
137
144
  return 'Tractor Beam'
138
145
  case ITEM_WARP_T1:
139
146
  return 'Warp Drive'
140
147
  case ITEM_BATTERY_T1:
141
148
  return 'Battery Bank'
149
+ case ITEM_LAUNCHER_T1:
150
+ return 'Drive Coil'
142
151
  default:
143
152
  return 'Module'
144
153
  }
@@ -202,7 +211,8 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
202
211
  const res = decodeStat(stats, 0)
203
212
  const pla = decodeStat(stats, 1)
204
213
  const ref = decodeStat(stats, 2)
205
- out += ` Capacity ${computeHaulerCapacity(res)} Efficiency ${computeHaulerEfficiency(pla)} Drain ${computeHaulerDrain(ref)}`
214
+ const tier = getItem(itemId).tier
215
+ out += ` Capacity ${computeHaulerCapacity(res, tier)} Efficiency ${computeHaulerEfficiency(pla)} Drain ${computeHaulerDrain(ref)}`
206
216
  break
207
217
  }
208
218
  case MODULE_WARP: {
@@ -230,18 +240,21 @@ export function buildEntityDescription(
230
240
  ): string {
231
241
  const hullMass = computeBaseHullmass(hullStats)
232
242
  let baseCapacity = 0
233
- if (itemId === ITEM_SHIP_T1_PACKED) {
243
+ if (
244
+ itemId === ITEM_SHIP_T1_PACKED ||
245
+ itemId === ITEM_PROSPECTOR_T2_PACKED ||
246
+ itemId === ITEM_HAULER_SHIP_T2_PACKED
247
+ ) {
234
248
  baseCapacity = computeBaseCapacityShip(hullStats)
235
249
  } else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
236
250
  baseCapacity = computeBaseCapacityWarehouse(hullStats)
237
251
  } else if (
238
252
  itemId === ITEM_EXTRACTOR_T1_PACKED ||
239
253
  itemId === ITEM_FACTORY_T1_PACKED ||
240
- itemId === ITEM_CONTAINER_T1_PACKED
254
+ itemId === ITEM_CONTAINER_T1_PACKED ||
255
+ itemId === ITEM_CONTAINER_T2_PACKED
241
256
  ) {
242
257
  baseCapacity = computeBaseCapacityContainer(hullStats)
243
- } else if (itemId === ITEM_CONTAINER_T2_PACKED) {
244
- baseCapacity = computeBaseCapacityContainerT2(hullStats)
245
258
  }
246
259
 
247
260
  let out = entityDisplayName(itemId)
@@ -20,6 +20,7 @@ export interface ModuleDescription {
20
20
  export interface RenderDescriptionOptions {
21
21
  translate?: (id: string, fallback: string) => string
22
22
  formatNumber?: (n: number) => string
23
+ formatParam?: (paramName: string, value: number, descId: string) => string | undefined
23
24
  }
24
25
 
25
26
  interface TemplateSpec {
@@ -27,6 +28,7 @@ interface TemplateSpec {
27
28
  template: string
28
29
  params: readonly [string, string][]
29
30
  highlightKeys: readonly string[]
31
+ derive?: Record<string, (get: (label: string) => number) => number>
30
32
  }
31
33
 
32
34
  const TEMPLATES: Record<string, TemplateSpec> = {
@@ -53,13 +55,15 @@ const TEMPLATES: Record<string, TemplateSpec> = {
53
55
  gatherer: {
54
56
  id: 'module.gatherer.description',
55
57
  template:
56
- 'mines resources at {yield} yield to a max depth of {depth} while draining {drain} energy per second',
58
+ 'mines resources at {yield} yield to a max depth of {depth} while draining {drain} energy per minute',
57
59
  params: [
58
60
  ['yield', 'Yield'],
59
61
  ['depth', 'Depth'],
60
- ['drain', 'Drain'],
61
62
  ],
62
63
  highlightKeys: ['yield', 'depth', 'drain'],
64
+ derive: {
65
+ drain: (get) => Math.round((get('Drain') / 10000) * 60 * 10) / 10,
66
+ },
63
67
  },
64
68
  loading: {
65
69
  id: 'module.loading.description',
@@ -72,12 +76,12 @@ const TEMPLATES: Record<string, TemplateSpec> = {
72
76
  },
73
77
  crafting: {
74
78
  id: 'module.crafting.description',
75
- template: 'manufactures items at {speed} speed while draining {drain} energy per second',
76
- params: [
77
- ['speed', 'Speed'],
78
- ['drain', 'Drain'],
79
- ],
79
+ template: 'manufactures items at {speed} speed while draining {drain} energy per minute',
80
+ params: [['speed', 'Speed']],
80
81
  highlightKeys: ['speed', 'drain'],
82
+ derive: {
83
+ drain: (get) => Math.round(((get('Drain') * get('Speed')) / 150000) * 60 * 10) / 10,
84
+ },
81
85
  },
82
86
  storage: {
83
87
  id: 'module.storage.description',
@@ -114,6 +118,12 @@ export function describeModule(input: CapabilityInput): ModuleDescription | null
114
118
  const attr = input.attributes.find((a) => a.label === attrLabel)
115
119
  if (attr) params[paramName] = attr.value
116
120
  }
121
+ if (spec.derive) {
122
+ const get = (label: string) => input.attributes.find((a) => a.label === label)?.value ?? 0
123
+ for (const [paramName, fn] of Object.entries(spec.derive)) {
124
+ params[paramName] = fn(get)
125
+ }
126
+ }
117
127
  return {
118
128
  id: spec.id,
119
129
  template: spec.template,
@@ -158,7 +168,10 @@ export function renderDescription(
158
168
  if (raw === undefined) {
159
169
  spans.push({text: `{${paramName}}`})
160
170
  } else {
161
- const formatted = typeof raw === 'number' ? formatNumber(raw) : raw
171
+ const formatted =
172
+ typeof raw === 'number'
173
+ ? (options?.formatParam?.(paramName, raw, desc.id) ?? formatNumber(raw))
174
+ : raw
162
175
  const highlight = (desc.highlightKeys as readonly string[]).includes(paramName)
163
176
  spans.push(highlight ? {text: formatted, highlight: true} : {text: formatted})
164
177
  }