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

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 (60) hide show
  1. package/lib/shipload.d.ts +471 -158
  2. package/lib/shipload.js +2320 -485
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +2290 -484
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +123 -8
  7. package/lib/testing.js +450 -19
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +451 -20
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/capabilities/crafting.ts +10 -1
  13. package/src/capabilities/gathering.ts +1 -1
  14. package/src/capabilities/hauling.ts +0 -5
  15. package/src/capabilities/modules.ts +6 -0
  16. package/src/capabilities/movement.ts +1 -1
  17. package/src/contracts/server.ts +293 -13
  18. package/src/data/capabilities.ts +18 -2
  19. package/src/data/capability-formulas.ts +5 -0
  20. package/src/data/entities.json +235 -20
  21. package/src/data/item-ids.ts +10 -0
  22. package/src/data/items.json +61 -0
  23. package/src/data/kind-registry.json +53 -1
  24. package/src/data/kind-registry.ts +5 -0
  25. package/src/data/metadata.ts +74 -5
  26. package/src/data/recipes-runtime.ts +1 -0
  27. package/src/data/recipes.json +895 -119
  28. package/src/derivation/capabilities.test.ts +105 -4
  29. package/src/derivation/capabilities.ts +162 -57
  30. package/src/derivation/capability-mappings.ts +2 -0
  31. package/src/derivation/crafting.ts +2 -0
  32. package/src/derivation/recipe-usage.test.ts +10 -7
  33. package/src/derivation/rollups.ts +16 -0
  34. package/src/derivation/stat-scaling.ts +12 -0
  35. package/src/entities/makers.ts +10 -1
  36. package/src/index-module.ts +29 -3
  37. package/src/managers/actions.ts +77 -46
  38. package/src/managers/construction-types.ts +2 -2
  39. package/src/managers/construction.ts +15 -15
  40. package/src/managers/plot.ts +1 -1
  41. package/src/nft/buildImmutableData.ts +55 -9
  42. package/src/nft/description.ts +99 -36
  43. package/src/planner/planner.test.ts +50 -41
  44. package/src/resolution/resolve-item.test.ts +1 -1
  45. package/src/resolution/resolve-item.ts +26 -11
  46. package/src/scheduling/availability.ts +7 -6
  47. package/src/scheduling/cancel.test.ts +40 -6
  48. package/src/scheduling/cancel.ts +21 -29
  49. package/src/scheduling/jobs.ts +71 -0
  50. package/src/scheduling/lanes.ts +29 -0
  51. package/src/scheduling/projection.ts +42 -25
  52. package/src/scheduling/task-cargo.ts +1 -1
  53. package/src/testing/projection-parity.ts +2 -2
  54. package/src/travel/reach.ts +4 -6
  55. package/src/travel/route-planner.ts +60 -49
  56. package/src/travel/route-simulator.ts +3 -7
  57. package/src/travel/travel.ts +14 -5
  58. package/src/types/capabilities.ts +9 -3
  59. package/src/types.ts +5 -3
  60. package/src/managers/flatten-gather-plan.test.ts +0 -80
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  getModuleCapabilityType,
3
+ MODULE_BUILDER,
3
4
  MODULE_CRAFTER,
4
5
  MODULE_ENGINE,
5
6
  MODULE_GATHERER,
@@ -13,6 +14,7 @@ import {
13
14
  import {
14
15
  ITEM_CONTAINER_T1_PACKED,
15
16
  ITEM_CONTAINER_T2_PACKED,
17
+ ITEM_BUILDER_T1,
16
18
  ITEM_CRAFTER_T1,
17
19
  ITEM_ENGINE_T1,
18
20
  ITEM_EXTRACTOR_T1_PACKED,
@@ -26,28 +28,56 @@ import {
26
28
  ITEM_BATTERY_T1,
27
29
  ITEM_LAUNCHER_T1,
28
30
  ITEM_LOADER_T1,
31
+ ITEM_PROSPECTOR_T1_PACKED,
29
32
  ITEM_PROSPECTOR_T2_PACKED,
33
+ ITEM_ROUSTABOUT_T1_PACKED,
30
34
  ITEM_SHIP_T1_PACKED,
31
35
  ITEM_STORAGE_T1,
36
+ ITEM_TENDER_T1_PACKED,
37
+ ITEM_TUG_T1_PACKED,
38
+ ITEM_PORTER_T1_PACKED,
39
+ ITEM_WRANGLER_T1_PACKED,
40
+ ITEM_DREDGER_T1_PACKED,
32
41
  ITEM_WAREHOUSE_T1_PACKED,
33
42
  ITEM_WARP_T1,
34
43
  } from '../data/item-ids'
35
44
  import {decodeStat} from '../derivation/crafting'
36
45
  import {gathererDepthForTier} from '../derivation/capabilities'
37
46
  import {getItem} from '../data/catalog'
47
+ import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
48
+ import {getBaseHullmassFor} from '../derivation/capabilities'
49
+ import {computeEffectiveModuleStat} from '../derivation/stat-scaling'
38
50
 
39
51
  function idiv(a: number, b: number): number {
40
52
  return Math.floor(a / b)
41
53
  }
42
54
 
43
- export function computeBaseHullmass(stats: bigint): number {
55
+ export function toWholeEnergy(milli: number): number {
56
+ return idiv(milli + 500, 1000)
57
+ }
58
+
59
+ function isShipHull(itemId: number): boolean {
60
+ return getPackedEntityType(itemId)?.equals(ENTITY_SHIP) ?? false
61
+ }
62
+
63
+ function sumPackedShipChannels(stats: bigint): number {
64
+ let sum = 0
65
+ for (let slot = 0; slot < 5; slot++) sum += decodeStat(stats, slot)
66
+ return sum
67
+ }
68
+
69
+ export function computeBaseHullmass(itemId: number, stats: bigint): number {
70
+ if (isShipHull(itemId)) {
71
+ return Math.floor(
72
+ (getBaseHullmassFor(itemId) * (10_000 - sumPackedShipChannels(stats))) / 10_000
73
+ )
74
+ }
44
75
  const density = decodeStat(stats, 1)
45
- return 100000 - 75 * density
76
+ return Math.floor((getBaseHullmassFor(itemId) * (2000 - density)) / 2000)
46
77
  }
47
78
 
48
79
  export function computeBaseCapacityShip(stats: bigint): number {
49
- const s = decodeStat(stats, 0) + decodeStat(stats, 2)
50
- return Math.floor(5_000_000 * 6 ** (s / 1998))
80
+ return Math.floor(5_000_000 * 6 ** (sumPackedShipChannels(stats) / 4995))
51
81
  }
52
82
 
53
83
  export function computeBaseCapacityContainer(stats: bigint): number {
@@ -62,49 +92,75 @@ export function computeBaseCapacityWarehouse(stats: bigint): number {
62
92
 
63
93
  export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
64
94
  export const computeEngineDrain = (thm: number): number => 2 * Math.max(30, 50 - idiv(thm, 70))
65
- export const ENGINE_DRAIN_BASE = 118
95
+ export const ENGINE_DRAIN_BASE = 156
66
96
  export const ENGINE_DRAIN_REF_THRUST = 775
67
97
  export const ENGINE_DRAIN_REF_THM = 500
68
98
 
69
99
  export const computeTravelDrain = (totalThrust: number, avgThm: number): number => {
70
100
  if (totalThrust <= 0) return 0
71
- const num = ENGINE_DRAIN_BASE * ENGINE_DRAIN_REF_THRUST * computeEngineDrain(avgThm)
72
- const den = totalThrust * computeEngineDrain(ENGINE_DRAIN_REF_THM)
73
- return idiv(num, den)
101
+ const thermalFactor = computeEngineDrain(avgThm) / computeEngineDrain(ENGINE_DRAIN_REF_THM)
102
+ const thrustFactor = Math.sqrt(ENGINE_DRAIN_REF_THRUST / totalThrust)
103
+ return Math.floor(ENGINE_DRAIN_BASE * 1000 * thermalFactor * thrustFactor)
74
104
  }
75
- export const computeGeneratorCap = (com: number): number => 1300 + idiv(com, 2)
76
- export const computeGeneratorRech = (fin: number): number => 2 * (1 + idiv(fin * 3, 1000))
105
+ export const computeGeneratorCap = (com: number): number => 1_300_000 + com * 500
106
+ export const computeGeneratorRech = (fin: number): number => 2000 + fin * 6
77
107
  export const computeGathererYield = (str: number): number => 200 + str
78
108
  export const computeGathererDrain = (con: number): number =>
79
- 2 * Math.max(250, 1250 - idiv(con * 25, 20))
109
+ 2 * Math.max(250_000, 1_250_000 - con * 1250)
80
110
  export const computeGathererDepth = (tol: number, tier: number): number =>
81
111
  gathererDepthForTier(tol, tier)
82
112
  export const computeLoaderMass = (ins: number): number => Math.max(200, 2000 - ins * 2)
83
113
  export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla * pla, 10000)
84
114
  export const computeCrafterSpeed = (rea: number): number => 100 + idiv(rea * 4, 5)
85
- export const computeCrafterDrain = (fin: number): number => Math.max(5, 30 - idiv(fin, 33))
115
+ export const computeCrafterDrain = (fin: number): number =>
116
+ Math.max(5000, 30000 - idiv(fin * 1000, 33))
117
+ export const computeBuilderSpeed = (resonance: number): number => 100 + idiv(resonance * 4, 5)
118
+ export const computeBuilderDrain = (fineness: number): number =>
119
+ Math.max(5000, 30000 - idiv(fineness * 1000, 33))
86
120
  export const computeHaulerCapacity = (fin: number, tier: number): number =>
87
121
  Math.max(tier, tier + idiv(fin, 400))
88
122
  export const computeHaulerEfficiency = (con: number): number => 2000 + con * 6
89
- export const computeHaulerDrain = (com: number): number => Math.max(3, 15 - idiv(com, 80))
123
+ export const supportDrainTierPercent = (tier: number): number => {
124
+ const clampedTier = Math.min(10, Math.max(1, Math.trunc(tier)))
125
+ return Math.max(50, 110 - clampedTier * 10)
126
+ }
127
+ const computeT1LogisticsDrain = (stat: number): number =>
128
+ Math.max(3000, 15000 - Math.min(12000, idiv(stat * 1000, 80)))
129
+ export const computeHaulerDrain = (conductivity: number, tier: number): number =>
130
+ idiv(computeT1LogisticsDrain(conductivity) * supportDrainTierPercent(tier), 100)
131
+ export const computeCargoBayDrain = (cohesion: number, tier: number): number =>
132
+ idiv(computeT1LogisticsDrain(cohesion) * 3 * supportDrainTierPercent(tier), 400)
90
133
  export const computeWarpRange = (stat: number): number => 100 + stat * 3
91
134
  export const computeCargoBayCapacity = (
92
135
  strength: number,
93
136
  density: number,
94
- hardness: number,
95
- cohesion: number
96
- ): number => 10_000_000 + idiv((strength + density + hardness + cohesion) * 50_000_000, 3996)
137
+ hardness: number
138
+ ): number => 10_000_000 + idiv((strength + density + hardness) * 50_000_000, 2997)
97
139
  export const computeBatteryBankCapacity = (
98
140
  volatility: number,
99
141
  thermal: number,
100
142
  plasticity: number,
101
143
  insulation: number
102
- ): number => 2_500 + idiv((volatility + thermal + plasticity + insulation) * 7_500, 3996)
144
+ ): number => 2_500_000 + idiv((volatility + thermal + plasticity + insulation) * 7_500_000, 3996)
103
145
 
104
146
  export function entityDisplayName(itemId: number): string {
105
147
  switch (itemId) {
106
148
  case ITEM_SHIP_T1_PACKED:
149
+ return 'Ship'
150
+ case ITEM_ROUSTABOUT_T1_PACKED:
107
151
  return 'Roustabout'
152
+ case ITEM_PROSPECTOR_T1_PACKED:
153
+ return 'Prospector'
154
+ case ITEM_TENDER_T1_PACKED:
155
+ return 'Tender'
156
+ case ITEM_TUG_T1_PACKED:
157
+ return 'Tug'
158
+ case ITEM_PORTER_T1_PACKED:
159
+ return 'Porter'
160
+ case ITEM_WRANGLER_T1_PACKED:
161
+ return 'Wrangler'
162
+ case ITEM_DREDGER_T1_PACKED:
163
+ return 'Dredger'
108
164
  case ITEM_WAREHOUSE_T1_PACKED:
109
165
  return 'Warehouse'
110
166
  case ITEM_EXTRACTOR_T1_PACKED:
@@ -118,7 +174,7 @@ export function entityDisplayName(itemId: number): string {
118
174
  case ITEM_PROSPECTOR_T2_PACKED:
119
175
  return 'Prospector'
120
176
  case ITEM_HAULER_SHIP_T2_PACKED:
121
- return 'Hauler'
177
+ return 'Tug'
122
178
  default:
123
179
  return 'Entity'
124
180
  }
@@ -137,6 +193,8 @@ export function moduleDisplayName(itemId: number): string {
137
193
  return 'Shuttle Bay'
138
194
  case ITEM_CRAFTER_T1:
139
195
  return 'Fabricator'
196
+ case ITEM_BUILDER_T1:
197
+ return 'Assembly Arm'
140
198
  case ITEM_STORAGE_T1:
141
199
  return 'Cargo Hold'
142
200
  case ITEM_HAULER_T1:
@@ -165,15 +223,17 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
165
223
 
166
224
  switch (subtype) {
167
225
  case MODULE_ENGINE: {
168
- const vol = decodeStat(stats, 0)
169
- const thm = decodeStat(stats, 1)
226
+ const vol = computeEffectiveModuleStat(decodeStat(stats, 0))
227
+ const thm = computeEffectiveModuleStat(decodeStat(stats, 1))
170
228
  out += ` Thrust ${computeEngineThrust(vol)} Drain ${computeEngineDrain(thm)}`
171
229
  break
172
230
  }
173
231
  case MODULE_GENERATOR: {
174
- const res = decodeStat(stats, 0)
175
- const ref = decodeStat(stats, 1)
176
- out += ` Capacity ${computeGeneratorCap(res)} Recharge ${computeGeneratorRech(ref)}`
232
+ const res = computeEffectiveModuleStat(decodeStat(stats, 0))
233
+ const ref = computeEffectiveModuleStat(decodeStat(stats, 1))
234
+ out += ` Capacity ${toWholeEnergy(computeGeneratorCap(res))} Recharge ${toWholeEnergy(
235
+ computeGeneratorRech(ref)
236
+ )}`
177
237
  break
178
238
  }
179
239
  case MODULE_GATHERER: {
@@ -184,7 +244,7 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
184
244
  out += ` Yield ${computeGathererYield(str)} Depth ${computeGathererDepth(
185
245
  tol,
186
246
  tier
187
- )} Drain ${computeGathererDrain(con)}`
247
+ )} Drain ${toWholeEnergy(computeGathererDrain(con))}`
188
248
  break
189
249
  }
190
250
  case MODULE_LOADER: {
@@ -196,23 +256,30 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
196
256
  case MODULE_CRAFTER: {
197
257
  const rea = decodeStat(stats, 0)
198
258
  const con = decodeStat(stats, 1)
199
- out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(con)}`
259
+ out += ` Speed ${computeCrafterSpeed(rea)} Drain ${toWholeEnergy(computeCrafterDrain(con))}`
260
+ break
261
+ }
262
+ case MODULE_BUILDER: {
263
+ const res = decodeStat(stats, 0)
264
+ const fin = decodeStat(stats, 1)
265
+ out += ` Speed ${computeBuilderSpeed(res)} Drain ${toWholeEnergy(computeBuilderDrain(fin))}`
200
266
  break
201
267
  }
202
268
  case MODULE_STORAGE: {
203
269
  const str = decodeStat(stats, 0)
204
270
  const den = decodeStat(stats, 1)
205
271
  const hrd = decodeStat(stats, 2)
206
- const com = decodeStat(stats, 3)
207
- out += ` Cargo Capacity ${computeCargoBayCapacity(str, den, hrd, com)}`
272
+ const coh = decodeStat(stats, 3)
273
+ const tier = getItem(itemId).tier
274
+ out += ` Cargo Capacity ${computeCargoBayCapacity(str, den, hrd)} Drain ${toWholeEnergy(computeCargoBayDrain(coh, tier))}`
208
275
  break
209
276
  }
210
277
  case MODULE_HAULER: {
211
278
  const res = decodeStat(stats, 0)
212
279
  const pla = decodeStat(stats, 1)
213
- const ref = decodeStat(stats, 2)
280
+ const con = decodeStat(stats, 2)
214
281
  const tier = getItem(itemId).tier
215
- out += ` Capacity ${computeHaulerCapacity(res, tier)} Efficiency ${computeHaulerEfficiency(pla)} Drain ${computeHaulerDrain(ref)}`
282
+ out += ` Capacity ${computeHaulerCapacity(res, tier)} Efficiency ${computeHaulerEfficiency(pla)} Drain ${toWholeEnergy(computeHaulerDrain(con, tier))}`
216
283
  break
217
284
  }
218
285
  case MODULE_WARP: {
@@ -225,7 +292,7 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
225
292
  const thm = decodeStat(stats, 1)
226
293
  const pla = decodeStat(stats, 2)
227
294
  const ins = decodeStat(stats, 3)
228
- out += ` Energy Capacity ${computeBatteryBankCapacity(vol, thm, pla, ins)}`
295
+ out += ` Energy Capacity ${toWholeEnergy(computeBatteryBankCapacity(vol, thm, pla, ins))}`
229
296
  break
230
297
  }
231
298
  }
@@ -238,13 +305,9 @@ export function buildEntityDescription(
238
305
  moduleItems: number[],
239
306
  moduleStats: bigint[]
240
307
  ): string {
241
- const hullMass = computeBaseHullmass(hullStats)
308
+ const hullMass = computeBaseHullmass(itemId, hullStats)
242
309
  let baseCapacity = 0
243
- if (
244
- itemId === ITEM_SHIP_T1_PACKED ||
245
- itemId === ITEM_PROSPECTOR_T2_PACKED ||
246
- itemId === ITEM_HAULER_SHIP_T2_PACKED
247
- ) {
310
+ if (isShipHull(itemId)) {
248
311
  baseCapacity = computeBaseCapacityShip(hullStats)
249
312
  } else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
250
313
  baseCapacity = computeBaseCapacityWarehouse(hullStats)
@@ -58,7 +58,7 @@ function entity(overrides: EntityOverrides = {}): GatherPlanEntity {
58
58
  gatherer_lanes: overrides.gatherer_lanes ?? [],
59
59
  loader_lanes: overrides.loader_lanes ?? [],
60
60
  generator: overrides.generator,
61
- energy: overrides.energy !== undefined ? UInt16.from(overrides.energy) : undefined,
61
+ energy: overrides.energy !== undefined ? UInt32.from(overrides.energy) : undefined,
62
62
  lanes: overrides.lanes ?? [],
63
63
  coordinates: ServerContract.Types.coordinates.from({x: 0, y: 0}),
64
64
  cargo: [],
@@ -118,27 +118,30 @@ describe('cost helpers', () => {
118
118
  const RICHNESS = 500
119
119
 
120
120
  test('gatherEnergyCost is 0 for non-positive quantity and positive otherwise', () => {
121
- const lane = gathererLane(0, 700, 1250, 5000)
121
+ const lane = gathererLane(0, 700, 1_250_000, 5000)
122
122
  expect(gatherEnergyCost(lane, 0, STRATUM, ITEM_MASS, RICHNESS)).toBe(0)
123
123
  expect(gatherEnergyCost(lane, 100, STRATUM, ITEM_MASS, RICHNESS)).toBeGreaterThan(0)
124
124
  })
125
125
 
126
126
  test('splitCost across two lanes is monotonic non-decreasing in quantity', () => {
127
- const reaching = [gathererLane(0, 700, 1250, 5000), gathererLane(1, 700, 1250, 5000)]
127
+ const reaching = [
128
+ gathererLane(0, 700, 1_250_000, 5000),
129
+ gathererLane(1, 700, 1_250_000, 5000),
130
+ ]
128
131
  const c100 = splitCost(reaching, 100, STRATUM, ITEM_MASS, RICHNESS)
129
132
  const c200 = splitCost(reaching, 200, STRATUM, ITEM_MASS, RICHNESS)
130
133
  expect(c200).toBeGreaterThanOrEqual(c100)
131
134
  })
132
135
 
133
136
  test('maxQtyForCharge returns hi when the whole batch fits the charge', () => {
134
- const reaching = [gathererLane(0, 700, 1250, 5000)]
137
+ const reaching = [gathererLane(0, 700, 1_250_000, 5000)]
135
138
  // capacity huge → the full 500 fits
136
139
  expect(maxQtyForCharge(reaching, 500, 1_000_000, STRATUM, ITEM_MASS, RICHNESS)).toBe(500)
137
140
  })
138
141
 
139
142
  test('maxQtyForCharge finds the boundary: result fits, result+1 does not', () => {
140
- const reaching = [gathererLane(0, 700, 1250, 5000)]
141
- const capacity = 40
143
+ const reaching = [gathererLane(0, 700, 1_250_000, 5000)]
144
+ const capacity = 40_000
142
145
  const q = maxQtyForCharge(reaching, 100_000, capacity, STRATUM, ITEM_MASS, RICHNESS)
143
146
  expect(splitCost(reaching, q, STRATUM, ITEM_MASS, RICHNESS)).toBeLessThanOrEqual(capacity)
144
147
  expect(splitCost(reaching, q + 1, STRATUM, ITEM_MASS, RICHNESS)).toBeGreaterThan(capacity)
@@ -157,9 +160,9 @@ describe('buildGatherPlan', () => {
157
160
 
158
161
  test('single cycle when the target fits one charge and energy is full', () => {
159
162
  const e = entity({
160
- gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
161
- generator: energyStats(1200, 2),
162
- energy: 1200,
163
+ gatherer_lanes: [gathererLane(0, 700, 1_250_000, 5000)],
164
+ generator: energyStats(1_200_000, 2000),
165
+ energy: 1_200_000,
163
166
  })
164
167
  const plan = buildGatherPlan(e, STRATUM, {quantity: 100}, OPTS)
165
168
  expect(plan.cycleCount).toBe(1)
@@ -172,9 +175,12 @@ describe('buildGatherPlan', () => {
172
175
 
173
176
  test('two limpets split one charge proportional to yield, finish within 1s of each other', () => {
174
177
  const e = entity({
175
- gatherer_lanes: [gathererLane(0, 200, 500, 5000), gathererLane(1, 400, 500, 5000)],
176
- generator: energyStats(60_000, 2),
177
- energy: 60_000,
178
+ gatherer_lanes: [
179
+ gathererLane(0, 200, 500_000, 5000),
180
+ gathererLane(1, 400, 500_000, 5000),
181
+ ],
182
+ generator: energyStats(60_000_000, 2000),
183
+ energy: 60_000_000,
178
184
  })
179
185
  const plan = buildGatherPlan(e, STRATUM, {quantity: 60}, OPTS)
180
186
  expect(plan.cycleCount).toBe(1)
@@ -185,9 +191,9 @@ describe('buildGatherPlan', () => {
185
191
  })
186
192
 
187
193
  test('multi-cycle: fills beyond one charge; totalOre equals fill target; each cycle fits the charge', () => {
188
- const CAP = 40
194
+ const CAP = 40_000
189
195
  const e = entity({
190
- gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
196
+ gatherer_lanes: [gathererLane(0, 700, 1_250_000, 5000)],
191
197
  generator: energyStats(CAP, 2),
192
198
  energy: CAP,
193
199
  })
@@ -214,9 +220,9 @@ describe('buildGatherPlan', () => {
214
220
 
215
221
  test('cap reasons: hold binds, reserve binds, requested binds', () => {
216
222
  const e = entity({
217
- gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
218
- generator: energyStats(60_000, 2),
219
- energy: 60_000,
223
+ gatherer_lanes: [gathererLane(0, 700, 1_250_000, 5000)],
224
+ generator: energyStats(60_000_000, 2000),
225
+ energy: 60_000_000,
220
226
  })
221
227
  expect(
222
228
  buildGatherPlan(e, STRATUM, 'max', {...OPTS, holdRoom: 300, reserveRemaining: 9999}).cap
@@ -228,9 +234,9 @@ describe('buildGatherPlan', () => {
228
234
  })
229
235
 
230
236
  test('total ETA sums per-cycle recharge + gather seconds', () => {
231
- const CAP = 40
237
+ const CAP = 40_000
232
238
  const e = entity({
233
- gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
239
+ gatherer_lanes: [gathererLane(0, 700, 1_250_000, 5000)],
234
240
  generator: energyStats(CAP, 2),
235
241
  energy: CAP,
236
242
  })
@@ -242,11 +248,11 @@ describe('buildGatherPlan', () => {
242
248
  test('excludes limpets that cannot reach the stratum and warns', () => {
243
249
  const e = entity({
244
250
  gatherer_lanes: [
245
- gathererLane(0, 700, 1250, 5000), // reaches 100
246
- gathererLane(1, 700, 1250, 50), // does not reach 100
251
+ gathererLane(0, 700, 1_250_000, 5000), // reaches 100
252
+ gathererLane(1, 700, 1_250_000, 50), // does not reach 100
247
253
  ],
248
- generator: energyStats(60_000, 2),
249
- energy: 60_000,
254
+ generator: energyStats(60_000_000, 2000),
255
+ energy: 60_000_000,
250
256
  })
251
257
  const plan = buildGatherPlan(e, STRATUM, {quantity: 100}, OPTS)
252
258
  expect(plan.cycles[0].limpets).toHaveLength(1)
@@ -256,9 +262,12 @@ describe('buildGatherPlan', () => {
256
262
 
257
263
  test('reports structured limpet reach counts (all reaching)', () => {
258
264
  const e = entity({
259
- gatherer_lanes: [gathererLane(0, 700, 1250, 5000), gathererLane(1, 700, 1250, 5000)],
260
- generator: energyStats(60_000, 2),
261
- energy: 60_000,
265
+ gatherer_lanes: [
266
+ gathererLane(0, 700, 1_250_000, 5000),
267
+ gathererLane(1, 700, 1_250_000, 5000),
268
+ ],
269
+ generator: energyStats(60_000_000, 2000),
270
+ energy: 60_000_000,
262
271
  })
263
272
  const plan = buildGatherPlan(e, STRATUM, {quantity: 50}, OPTS)
264
273
  expect(plan.totalLimpets).toBe(2)
@@ -268,11 +277,11 @@ describe('buildGatherPlan', () => {
268
277
  test('reachingCount excludes limpets that cannot reach the stratum', () => {
269
278
  const e = entity({
270
279
  gatherer_lanes: [
271
- gathererLane(0, 700, 1250, 5000), // reaches 100
272
- gathererLane(1, 700, 1250, 50), // does not reach 100
280
+ gathererLane(0, 700, 1_250_000, 5000), // reaches 100
281
+ gathererLane(1, 700, 1_250_000, 50), // does not reach 100
273
282
  ],
274
- generator: energyStats(60_000, 2),
275
- energy: 60_000,
283
+ generator: energyStats(60_000_000, 2000),
284
+ energy: 60_000_000,
276
285
  })
277
286
  const plan = buildGatherPlan(e, STRATUM, {quantity: 50}, OPTS)
278
287
  expect(plan.totalLimpets).toBe(2)
@@ -280,9 +289,9 @@ describe('buildGatherPlan', () => {
280
289
  })
281
290
 
282
291
  test('single-limpet ship still fills across cycles', () => {
283
- const CAP = 40
292
+ const CAP = 40_000
284
293
  const e = entity({
285
- gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
294
+ gatherer_lanes: [gathererLane(0, 700, 1_250_000, 5000)],
286
295
  generator: energyStats(CAP, 2),
287
296
  energy: CAP,
288
297
  })
@@ -293,18 +302,18 @@ describe('buildGatherPlan', () => {
293
302
 
294
303
  test('throws when no gatherer reaches the stratum', () => {
295
304
  const e = entity({
296
- gatherer_lanes: [gathererLane(0, 700, 1250, 50)],
297
- generator: energyStats(1200, 2),
298
- energy: 1200,
305
+ gatherer_lanes: [gathererLane(0, 700, 1_250_000, 50)],
306
+ generator: energyStats(1_200_000, 2000),
307
+ energy: 1_200_000,
299
308
  })
300
309
  expect(() => buildGatherPlan(e, STRATUM, 'max', OPTS)).toThrow('no gatherer reaches')
301
310
  })
302
311
 
303
312
  test('zero fill target yields an empty plan (no cycles)', () => {
304
313
  const e = entity({
305
- gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
306
- generator: energyStats(1200, 2),
307
- energy: 1200,
314
+ gatherer_lanes: [gathererLane(0, 700, 1_250_000, 5000)],
315
+ generator: energyStats(1_200_000, 2000),
316
+ energy: 1_200_000,
308
317
  })
309
318
  const plan = buildGatherPlan(e, STRATUM, 'max', {...OPTS, holdRoom: 0})
310
319
  expect(plan.cycleCount).toBe(0)
@@ -314,9 +323,9 @@ describe('buildGatherPlan', () => {
314
323
 
315
324
  test('first cycle skips recharge when current energy already covers the small batch', () => {
316
325
  const e = entity({
317
- gatherer_lanes: [gathererLane(0, 700, 1250, 5000)],
318
- generator: energyStats(60_000, 2),
319
- energy: 60_000, // full
326
+ gatherer_lanes: [gathererLane(0, 700, 1_250_000, 5000)],
327
+ generator: energyStats(60_000_000, 2000),
328
+ energy: 60_000_000, // full
320
329
  })
321
330
  const plan = buildGatherPlan(e, STRATUM, {quantity: 50}, OPTS)
322
331
  expect(plan.cycles[0].rechargeBefore).toBe(false)
@@ -42,7 +42,7 @@ test('resolveItem resolves the station hub with hullmass and zero cargo capacity
42
42
  const resolved = resolveItem(UInt16.from(ITEM_HUB_T1_PACKED), stats)
43
43
  const hull = resolved.attributes?.find((g) => g.capability === 'Hull')
44
44
  expect(hull?.attributes.find((a) => a.label === 'Mass')?.value).toBe(
45
- computeBaseHullmass({density: 100})
45
+ computeBaseHullmass(ITEM_HUB_T1_PACKED, {density: 100})
46
46
  )
47
47
  expect(hull?.attributes.find((a) => a.label === 'Capacity')?.value).toBe(0)
48
48
  })
@@ -2,6 +2,7 @@ import {UInt16, UInt64} from '@wharfkit/antelope'
2
2
  import type {UInt16Type, UInt64Type} from '@wharfkit/antelope'
3
3
  import type {ResourceCategory} from '../types'
4
4
  import {getItem, getModules} from '../data/catalog'
5
+ import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
5
6
  import {getEntityLayout} from '../data/recipes-runtime'
6
7
  import {entityMetadata, itemMetadata} from '../data/metadata'
7
8
  import {
@@ -31,6 +32,7 @@ import {
31
32
  computeStorageCapabilities,
32
33
  } from '../derivation/capabilities'
33
34
  import {applySlotMultiplierUint32} from '../entities/slot-multiplier'
35
+ import {toWholeEnergy} from '../nft/description'
34
36
  import {categoryColors, componentIcon, itemAbbreviations, moduleIcon} from '../data/colors'
35
37
  import type {ServerContract} from '../contracts'
36
38
 
@@ -171,8 +173,8 @@ function computeCapabilityGroup(
171
173
  return {
172
174
  capability: 'Generator',
173
175
  attributes: [
174
- {label: 'Capacity', value: caps.capacity},
175
- {label: 'Recharge', value: caps.recharge},
176
+ {label: 'Capacity', value: toWholeEnergy(caps.capacity)},
177
+ {label: 'Recharge', value: toWholeEnergy(caps.recharge)},
176
178
  ],
177
179
  }
178
180
  }
@@ -182,7 +184,7 @@ function computeCapabilityGroup(
182
184
  capability: 'Gatherer',
183
185
  attributes: [
184
186
  {label: 'Yield', value: caps.yield},
185
- {label: 'Drain', value: caps.drain},
187
+ {label: 'Drain', value: toWholeEnergy(caps.drain)},
186
188
  {label: 'Depth', value: caps.depth},
187
189
  ],
188
190
  }
@@ -204,7 +206,7 @@ function computeCapabilityGroup(
204
206
  capability: 'Crafting',
205
207
  attributes: [
206
208
  {label: 'Speed', value: caps.speed},
207
- {label: 'Drain', value: caps.drain},
209
+ {label: 'Drain', value: toWholeEnergy(caps.drain)},
208
210
  ],
209
211
  }
210
212
  }
@@ -215,12 +217,12 @@ function computeCapabilityGroup(
215
217
  attributes: [
216
218
  {label: 'Capacity', value: caps.capacity},
217
219
  {label: 'Efficiency', value: caps.efficiency},
218
- {label: 'Drain', value: caps.drain},
220
+ {label: 'Drain', value: toWholeEnergy(caps.drain)},
219
221
  ],
220
222
  }
221
223
  }
222
224
  case MODULE_STORAGE: {
223
- const caps = computeStorageCapabilities(stats)
225
+ const caps = computeStorageCapabilities(stats, tier)
224
226
  return {
225
227
  capability: 'Storage',
226
228
  attributes: [
@@ -228,6 +230,7 @@ function computeCapabilityGroup(
228
230
  label: 'Cargo Capacity',
229
231
  value: applySlotMultiplierUint32(caps.capacity, outputPct),
230
232
  },
233
+ {label: 'Drain', value: toWholeEnergy(caps.drain)},
231
234
  ],
232
235
  }
233
236
  }
@@ -238,7 +241,7 @@ function computeCapabilityGroup(
238
241
  attributes: [
239
242
  {
240
243
  label: 'Energy Capacity',
241
- value: applySlotMultiplierUint32(caps.capacity, outputPct),
244
+ value: toWholeEnergy(applySlotMultiplierUint32(caps.capacity, outputPct)),
242
245
  },
243
246
  ],
244
247
  }
@@ -282,14 +285,26 @@ function resolveModule(id: number, stats?: UInt64Type): ResolvedItem {
282
285
 
283
286
  function hullCapsForEntity(
284
287
  itemId: number,
285
- decoded: Record<string, number>
288
+ decoded: Record<string, number>,
289
+ packedStats: bigint
286
290
  ): {
287
291
  hullmass: number
288
292
  capacity: number
289
293
  } {
294
+ // A ship hull consumes all five positional packed channels. This is separate
295
+ // from the recipe-facing stat labels, which remain historical for Ship T1.
296
+ const hullStats = getPackedEntityType(itemId)?.equals(ENTITY_SHIP)
297
+ ? {
298
+ strength: decodeStat(packedStats, 0),
299
+ hardness: decodeStat(packedStats, 1),
300
+ plasticity: decodeStat(packedStats, 2),
301
+ volatility: decodeStat(packedStats, 3),
302
+ conductivity: decodeStat(packedStats, 4),
303
+ }
304
+ : decoded
290
305
  return {
291
- hullmass: computeBaseHullmass(decoded),
292
- capacity: computeBaseCapacity(itemId, decoded),
306
+ hullmass: computeBaseHullmass(itemId, hullStats),
307
+ capacity: computeBaseCapacity(itemId, hullStats),
293
308
  }
294
309
  }
295
310
 
@@ -309,7 +324,7 @@ function resolveEntity(
309
324
  if (decoded.strength === undefined) decoded.strength = decodeStat(bigStats, 0)
310
325
  if (decoded.density === undefined) decoded.density = decodeStat(bigStats, 1)
311
326
  if (decoded.hardness === undefined) decoded.hardness = decodeStat(bigStats, 2)
312
- const hullCaps = hullCapsForEntity(id, decoded)
327
+ const hullCaps = hullCapsForEntity(id, decoded, bigStats)
313
328
  attributes = [
314
329
  {
315
330
  capability: 'Hull',
@@ -23,14 +23,15 @@ export function taskCargoEffect(task: Task): CargoEffect {
23
23
  case TaskType.UNLOAD:
24
24
  return {added: [], removed: task.cargo}
25
25
  case TaskType.GATHER:
26
- return task.entitytarget ? {added: [], removed: []} : {added: task.cargo, removed: []}
26
+ return task.couplings.length > 0
27
+ ? {added: [], removed: []}
28
+ : {added: task.cargo, removed: []}
27
29
  case TaskType.CRAFT:
28
30
  if (task.cargo.length === 0) return {added: [], removed: []}
29
- return {added: [task.cargo[task.cargo.length - 1]], removed: task.cargo.slice(0, -1)}
30
- case TaskType.DEPLOY:
31
- return task.cargo.length > 0
32
- ? {added: [], removed: [task.cargo[0]]}
33
- : {added: [], removed: []}
31
+ return {
32
+ added: task.couplings.length === 0 ? [task.cargo[task.cargo.length - 1]] : [],
33
+ removed: task.cargo.slice(0, -1),
34
+ }
34
35
  default:
35
36
  return {added: [], removed: []}
36
37
  }