@shipload/sdk 1.0.0-next.37 → 1.0.0-next.39

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.
@@ -197,8 +197,8 @@ export const itemMetadata: Record<number, ItemMetadata> = {
197
197
  color: '#B877FF',
198
198
  },
199
199
  10105: {
200
- name: 'Storage',
201
- description: 'Expands cargo capacity based on hull material quality.',
200
+ name: 'Cargo Bay',
201
+ description: 'Expanded cargo storage with reinforced internal holds.',
202
202
  color: '#8B7355',
203
203
  },
204
204
  10106: {
@@ -214,11 +214,15 @@ export const itemMetadata: Record<number, ItemMetadata> = {
214
214
  color: '#9be4ff',
215
215
  },
216
216
  10108: {
217
- name: 'Battery',
218
- description:
219
- 'Extends energy capacity. Stores additional charge produced by generators, letting builds chain more high-drain actions between recharges.',
217
+ name: 'Battery Bank',
218
+ description: 'Stores additional charge produced by generators.',
220
219
  color: '#4ADBFF',
221
220
  },
221
+ 10109: {
222
+ name: 'Drive Coil',
223
+ description: 'Accelerates and launches cargo payloads toward a remote mass catcher.',
224
+ color: '#E86344',
225
+ },
222
226
 
223
227
  // === Entities (packed, T1) ===
224
228
  10200: {
@@ -247,6 +251,17 @@ export const itemMetadata: Record<number, ItemMetadata> = {
247
251
  description: 'Planetary fabrication facility with generator and crafter module slots.',
248
252
  color: '#7BA7D4',
249
253
  },
254
+ 10205: {
255
+ name: 'Mass Driver',
256
+ description: 'Planetary launch platform with generator and drive coil module slots.',
257
+ color: '#E86344',
258
+ },
259
+ 10206: {
260
+ name: 'Mass Catcher',
261
+ description:
262
+ 'Planetary receiving platform with storage module slots; catches launched payloads.',
263
+ color: '#4AE898',
264
+ },
250
265
 
251
266
  // === Components (T2) ===
252
267
  20001: {
@@ -274,6 +289,8 @@ export const entityMetadata: Record<number, EntityMetadata> = {
274
289
  10202: {moduleSlotLabels: ['Loader', 'Storage', 'Storage', 'Storage', 'Storage']},
275
290
  10203: {moduleSlotLabels: ['Generator', 'Gatherer']},
276
291
  10204: {moduleSlotLabels: ['Generator', 'Crafter']},
292
+ 10205: {moduleSlotLabels: ['Generator', 'Drive Coil']},
293
+ 10206: {moduleSlotLabels: ['Storage', 'Storage', 'Storage']},
277
294
  }
278
295
 
279
296
  for (const item of items as Array<{id: number}>) {
@@ -450,11 +450,11 @@
450
450
  "outputMass": 960000,
451
451
  "inputs": [
452
452
  {
453
- "itemId": 10008,
453
+ "itemId": 10009,
454
454
  "quantity": 300
455
455
  },
456
456
  {
457
- "itemId": 10009,
457
+ "itemId": 10006,
458
458
  "quantity": 300
459
459
  }
460
460
  ],
@@ -462,7 +462,7 @@
462
462
  {
463
463
  "sources": [
464
464
  {
465
- "inputIndex": 1,
465
+ "inputIndex": 0,
466
466
  "statIndex": 0
467
467
  }
468
468
  ]
@@ -470,8 +470,8 @@
470
470
  {
471
471
  "sources": [
472
472
  {
473
- "inputIndex": 0,
474
- "statIndex": 1
473
+ "inputIndex": 1,
474
+ "statIndex": 0
475
475
  }
476
476
  ]
477
477
  }
@@ -4,11 +4,16 @@ import {
4
4
  computeGathererCapabilities,
5
5
  computeCrafterCapabilities,
6
6
  computeLoaderCapabilities,
7
+ computeBaseCapacity,
8
+ computeContainerCapabilities,
7
9
  } from './capabilities'
8
10
  import {applySlotMultiplier, U16_MAX} from '../entities/slot-multiplier'
9
11
  import {encodeStats} from './crafting'
10
12
  import {
11
13
  ITEM_EXTRACTOR_T1_PACKED,
14
+ ITEM_FACTORY_T1_PACKED,
15
+ ITEM_MASS_DRIVER_T1_PACKED,
16
+ ITEM_MASS_CATCHER_T1_PACKED,
12
17
  ITEM_GATHERER_T1,
13
18
  ITEM_CRAFTER_T1,
14
19
  ITEM_LOADER_T1,
@@ -20,14 +25,27 @@ function makeGathererStats(strength: number, tolerance: number, conductivity: nu
20
25
  return encodeStats([strength, tolerance, conductivity, 0])
21
26
  }
22
27
 
23
- function makeCrafterStats(reactivity: number, fineness: number): bigint {
24
- return encodeStats([reactivity, fineness])
28
+ function makeCrafterStats(reactivity: number, conductivity: number): bigint {
29
+ return encodeStats([reactivity, conductivity])
25
30
  }
26
31
 
27
32
  function makeLoaderStats(insulation: number, plasticity: number): bigint {
28
33
  return encodeStats([insulation, plasticity])
29
34
  }
30
35
 
36
+ test('computeBaseCapacity uses container formula for all container-class entities', () => {
37
+ const stats = {strength: 300, hardness: 400, density: 100}
38
+ const expected = computeContainerCapabilities(stats).capacity
39
+ for (const itemId of [
40
+ ITEM_EXTRACTOR_T1_PACKED,
41
+ ITEM_FACTORY_T1_PACKED,
42
+ ITEM_MASS_DRIVER_T1_PACKED,
43
+ ITEM_MASS_CATCHER_T1_PACKED,
44
+ ]) {
45
+ expect(computeBaseCapacity(itemId, stats)).toBe(expected)
46
+ }
47
+ })
48
+
31
49
  test('computeEntityCapabilities emits gathererLanes alongside legacy gatherer sum', () => {
32
50
  // Two gatherers with distinct stats in separate slots, amp=100 for both
33
51
  const gathStats1 = makeGathererStats(300, 200, 400)
@@ -92,7 +110,7 @@ test('computeEntityCapabilities emits crafterLanes alongside legacy crafter sum'
92
110
  expect(result.crafterLanes!.length).toBe(1)
93
111
  expect(result.crafterLanes![0].slotIndex).toBe(0)
94
112
 
95
- const caps = computeCrafterCapabilities({reactivity: 400, fineness: 300})
113
+ const caps = computeCrafterCapabilities({reactivity: 400, conductivity: 300})
96
114
  const expectedSpeed = applySlotMultiplier(caps.speed, 120)
97
115
  expect(result.crafterLanes![0].speed).toBe(expectedSpeed)
98
116
  expect(result.crafterLanes![0].drain).toBe(caps.drain)
@@ -107,11 +107,11 @@ export function computeCrafterCapabilities(stats: Record<string, number>): {
107
107
  drain: number
108
108
  } {
109
109
  const rea = stats.reactivity
110
- const fin = stats.fineness
110
+ const con = stats.conductivity
111
111
 
112
112
  return {
113
113
  speed: 100 + Math.floor((rea * 4) / 5),
114
- drain: Math.max(5, 30 - Math.floor(fin / 33)),
114
+ drain: Math.max(5, 30 - Math.floor(con / 33)),
115
115
  }
116
116
  }
117
117
 
@@ -131,23 +131,39 @@ export function computeHaulerCapabilities(stats: Record<string, number>): {
131
131
  }
132
132
  }
133
133
 
134
- export function computeStorageCapabilities(
135
- stats: Record<string, number>,
136
- baseCapacity: number
137
- ): {
138
- capacityBonus: number
134
+ export function computeLauncherCapabilities(
135
+ stats: {charge_rate: number; velocity: number; drain: number},
136
+ amp = 100
137
+ ): {chargeRate: number; velocity: number; drain: number} {
138
+ return {
139
+ chargeRate: Math.floor((stats.charge_rate * amp) / 100),
140
+ velocity: Math.floor((stats.velocity * amp) / 100),
141
+ drain: stats.drain,
142
+ }
143
+ }
144
+
145
+ export function computeStorageCapabilities(stats: Record<string, number>): {
146
+ capacity: number
139
147
  } {
140
- const strength = stats.strength
141
- const density = stats.density
142
- const hardness = stats.hardness
143
- const cohesion = stats.cohesion
148
+ const strength = stats.strength ?? 0
149
+ const density = stats.density ?? 0
150
+ const hardness = stats.hardness ?? 0
151
+ const cohesion = stats.cohesion ?? 0
144
152
 
145
153
  const statSum = strength + density + hardness + cohesion
146
- const capacityBonus = Math.floor(
147
- (baseCapacity * (10 + Math.floor((statSum * 10) / 2997))) / 100
148
- )
154
+ return {capacity: 10_000_000 + Math.floor((statSum * 50_000_000) / 3996)}
155
+ }
149
156
 
150
- return {capacityBonus}
157
+ export function computeBatteryCapabilities(stats: Record<string, number>): {
158
+ capacity: number
159
+ } {
160
+ const volatility = stats.volatility ?? 0
161
+ const thermal = stats.thermal ?? 0
162
+ const plasticity = stats.plasticity ?? 0
163
+ const insulation = stats.insulation ?? 0
164
+
165
+ const statSum = volatility + thermal + plasticity + insulation
166
+ return {capacity: 2_500 + Math.floor((statSum * 7_500) / 3996)}
151
167
  }
152
168
 
153
169
  import {
@@ -155,6 +171,8 @@ import {
155
171
  ITEM_CONTAINER_T2_PACKED,
156
172
  ITEM_EXTRACTOR_T1_PACKED,
157
173
  ITEM_FACTORY_T1_PACKED,
174
+ ITEM_MASS_CATCHER_T1_PACKED,
175
+ ITEM_MASS_DRIVER_T1_PACKED,
158
176
  ITEM_SHIP_T1_PACKED,
159
177
  ITEM_WAREHOUSE_T1_PACKED,
160
178
  } from '../data/item-ids'
@@ -169,17 +187,20 @@ import {
169
187
  MODULE_CRAFTER,
170
188
  MODULE_HAULER,
171
189
  MODULE_WARP,
190
+ MODULE_LAUNCHER,
172
191
  } from '../capabilities/modules'
173
192
  import {getItem} from '../data/catalog'
174
- import {decodeCraftedItemStats} from './crafting'
193
+ import {decodeCraftedItemStats, decodeStat} from './crafting'
175
194
  import {
176
195
  applySlotMultiplier,
196
+ applySlotMultiplierUint32,
177
197
  clampUint16,
178
198
  clampUint32,
179
199
  getSlotAmp,
180
200
  type InstalledModule,
181
201
  } from '../entities/slot-multiplier'
182
202
  import type {EntitySlot} from '../data/recipes-runtime'
203
+ import {computeTravelDrain} from '../nft/description'
183
204
 
184
205
  export function computeBaseCapacity(itemId: number, stats: Record<string, number>): number {
185
206
  switch (itemId) {
@@ -187,6 +208,8 @@ export function computeBaseCapacity(itemId: number, stats: Record<string, number
187
208
  return computeShipHullCapabilities(stats).capacity
188
209
  case ITEM_EXTRACTOR_T1_PACKED:
189
210
  case ITEM_FACTORY_T1_PACKED:
211
+ case ITEM_MASS_DRIVER_T1_PACKED:
212
+ case ITEM_MASS_CATCHER_T1_PACKED:
190
213
  case ITEM_CONTAINER_T1_PACKED:
191
214
  return computeContainerCapabilities(stats).capacity
192
215
  case ITEM_WAREHOUSE_T1_PACKED:
@@ -252,6 +275,7 @@ export interface ComputedCapabilities {
252
275
  crafterLanes?: CrafterLaneEntry[]
253
276
  hauler?: {capacity: number; efficiency: number; drain: number}
254
277
  warp?: {range: number}
278
+ launcher?: {chargeRate: number; velocity: number; drain: number}
255
279
  }
256
280
 
257
281
  export function computeEntityCapabilities(
@@ -261,7 +285,8 @@ export function computeEntityCapabilities(
261
285
  layout: EntitySlot[]
262
286
  ): ComputedCapabilities {
263
287
  let totalThrust = 0
264
- let totalEngineDrain = 0
288
+ let totalEngineThm = 0
289
+ let engineCount = 0
265
290
  let hasEngine = false
266
291
 
267
292
  let totalGenCapacity = 0
@@ -278,7 +303,7 @@ export function computeEntityCapabilities(
278
303
  let maxGathDepth = 0
279
304
  let hasGatherer = false
280
305
 
281
- let totalStorageBonus = 0
306
+ let totalStorageCapacity = 0
282
307
  const baseCapacity = computeBaseCapacity(itemId, stats)
283
308
  let installedModuleMass = 0
284
309
 
@@ -294,8 +319,12 @@ export function computeEntityCapabilities(
294
319
  let totalWarpRange = 0
295
320
  let hasWarp = false
296
321
 
297
- let totalBatteryStatSum = 0
298
- let batteryCount = 0
322
+ let totalLauncherChargeRate = 0
323
+ let totalLauncherVelocity = 0
324
+ let totalLauncherDrain = 0
325
+ let hasLauncher = false
326
+
327
+ let totalBatteryCapacity = 0
299
328
 
300
329
  const gathererLanes: GathererLaneEntry[] = []
301
330
  const crafterLanes: CrafterLaneEntry[] = []
@@ -312,7 +341,8 @@ export function computeEntityCapabilities(
312
341
  hasEngine = true
313
342
  const caps = computeEngineCapabilities(decodedStats)
314
343
  totalThrust += applySlotMultiplier(caps.thrust, amp)
315
- totalEngineDrain += caps.drain
344
+ totalEngineThm += decodedStats.thermal ?? 0
345
+ engineCount += 1
316
346
  } else if (modType === MODULE_GENERATOR) {
317
347
  hasGenerator = true
318
348
  const caps = computeGeneratorCapabilities(decodedStats)
@@ -346,8 +376,8 @@ export function computeEntityCapabilities(
346
376
  outputPct: amp,
347
377
  })
348
378
  } else if (modType === MODULE_STORAGE) {
349
- const caps = computeStorageCapabilities(decodedStats, baseCapacity)
350
- totalStorageBonus += caps.capacityBonus
379
+ const caps = computeStorageCapabilities(decodedStats)
380
+ totalStorageCapacity += applySlotMultiplierUint32(caps.capacity, amp)
351
381
  } else if (modType === MODULE_CRAFTER) {
352
382
  hasCrafter = true
353
383
  const caps = computeCrafterCapabilities(decodedStats)
@@ -371,29 +401,37 @@ export function computeEntityCapabilities(
371
401
  hasWarp = true
372
402
  const caps = computeWarpCapabilities(decodedStats)
373
403
  totalWarpRange += applySlotMultiplier(caps.range, amp)
404
+ } else if (modType === MODULE_LAUNCHER) {
405
+ hasLauncher = true
406
+ const caps = computeLauncherCapabilities(
407
+ {
408
+ charge_rate: decodedStats.charge_rate ?? decodeStat(mod.stats, 0),
409
+ velocity: decodedStats.velocity ?? decodeStat(mod.stats, 1),
410
+ drain: decodedStats.drain ?? decodeStat(mod.stats, 2),
411
+ },
412
+ amp
413
+ )
414
+ totalLauncherChargeRate = clampUint16(totalLauncherChargeRate + caps.chargeRate)
415
+ totalLauncherVelocity = clampUint16(totalLauncherVelocity + caps.velocity)
416
+ totalLauncherDrain = clampUint16(totalLauncherDrain + caps.drain)
374
417
  } else if (modType === MODULE_BATTERY) {
375
- batteryCount++
376
- const vol = decodedStats.volatility ?? 0
377
- const thm = decodedStats.thermal ?? 0
378
- const pla = decodedStats.plasticity ?? 0
379
- const ins = decodedStats.insulation ?? 0
380
- totalBatteryStatSum += vol + thm + pla + ins
418
+ const caps = computeBatteryCapabilities(decodedStats)
419
+ totalBatteryCapacity += applySlotMultiplierUint32(caps.capacity, amp)
381
420
  }
382
421
  }
383
422
 
384
- if (hasGenerator && batteryCount > 0) {
385
- const genCapBase = totalGenCapacity
386
- const bonusPctNum = 10 * batteryCount + Math.floor((totalBatteryStatSum * 10) / 2997)
387
- totalGenCapacity += Math.floor((genCapBase * bonusPctNum) / 100)
423
+ if (hasGenerator && totalBatteryCapacity > 0) {
424
+ totalGenCapacity += totalBatteryCapacity
388
425
  }
389
426
 
390
427
  const result: ComputedCapabilities = {
391
428
  hullmass: computeBaseHullmass(stats) + installedModuleMass,
392
- capacity: baseCapacity + totalStorageBonus,
429
+ capacity: clampUint32(baseCapacity + totalStorageCapacity),
393
430
  }
394
431
 
395
432
  if (hasEngine) {
396
- result.engines = {thrust: totalThrust, drain: totalEngineDrain}
433
+ const avgThm = engineCount > 0 ? Math.trunc(totalEngineThm / engineCount) : 0
434
+ result.engines = {thrust: totalThrust, drain: computeTravelDrain(totalThrust, avgThm)}
397
435
  }
398
436
  if (hasGenerator) {
399
437
  result.generator = {
@@ -433,6 +471,13 @@ export function computeEntityCapabilities(
433
471
  if (hasWarp) {
434
472
  result.warp = {range: totalWarpRange}
435
473
  }
474
+ if (hasLauncher) {
475
+ result.launcher = {
476
+ chargeRate: totalLauncherChargeRate,
477
+ velocity: totalLauncherVelocity,
478
+ drain: totalLauncherDrain,
479
+ }
480
+ }
436
481
 
437
482
  return result
438
483
  }
@@ -142,6 +142,13 @@ export function makeEntity(packedItemId: number, state: EntityStateInput): Entit
142
142
  if (caps.generator) info.generator = caps.generator
143
143
  if (caps.hauler) info.hauler = caps.hauler
144
144
  if (caps.warp) info.warp = caps.warp
145
+ if (caps.launcher) {
146
+ info.launcher = ServerContract.Types.launcher_stats.from({
147
+ charge_rate: caps.launcher.chargeRate,
148
+ velocity: caps.launcher.velocity,
149
+ drain: caps.launcher.drain,
150
+ })
151
+ }
145
152
 
146
153
  info.gatherer_lanes = (caps.gathererLanes ?? []).map((l) =>
147
154
  ServerContract.Types.gatherer_lane.from({
@@ -34,6 +34,10 @@ export function applySlotMultiplier(value: number, outputPct: number): number {
34
34
  return clampUint16(Math.floor((value * outputPct) / 100))
35
35
  }
36
36
 
37
+ export function applySlotMultiplierUint32(value: number, outputPct: number): number {
38
+ return clampUint32(Math.floor((value * outputPct) / 100))
39
+ }
40
+
37
41
  export function getSlotAmp(layout: EntitySlot[], slotIndex: number): number {
38
42
  return layout[slotIndex]?.outputPct ?? 100
39
43
  }
@@ -62,7 +62,14 @@ export type {
62
62
  ScheduledBuild,
63
63
  Reservation,
64
64
  } from './managers'
65
- export type {EntityRefInput} from './managers/actions'
65
+ export type {
66
+ EntityRefInput,
67
+ LaunchNumericInput,
68
+ LaunchQuote,
69
+ LaunchQuoteCatcher,
70
+ LaunchQuoteLauncher,
71
+ LaunchStatsInput,
72
+ } from './managers/actions'
66
73
  export type {WrapDeposit} from './managers/nft'
67
74
  export {resolveLockedAmount} from './managers/nft'
68
75
 
@@ -191,7 +198,7 @@ export type {
191
198
  HasScheduleAndLocation,
192
199
  } from './travel/travel'
193
200
 
194
- export {planRoute, sdkSystemGraph} from './travel/route-planner'
201
+ export {planRoute, sdkSystemGraph, MAX_LEGS} from './travel/route-planner'
195
202
  export type {
196
203
  Coord,
197
204
  Neighbor,
@@ -379,9 +386,11 @@ export {
379
386
  computeCrafterCapabilities,
380
387
  computeWarehouseHullCapabilities,
381
388
  computeStorageCapabilities,
389
+ computeBatteryCapabilities,
382
390
  computeContainerCapabilities,
383
391
  computeContainerT2Capabilities,
384
392
  computeWarpCapabilities,
393
+ computeLauncherCapabilities,
385
394
  computeBaseCapacity,
386
395
  computeEntityCapabilities,
387
396
  GATHERER_DEPTH_TABLE,
@@ -484,6 +493,10 @@ export {
484
493
  computeBaseCapacityWarehouse,
485
494
  computeEngineThrust,
486
495
  computeEngineDrain,
496
+ computeTravelDrain,
497
+ ENGINE_DRAIN_BASE,
498
+ ENGINE_DRAIN_REF_THRUST,
499
+ ENGINE_DRAIN_REF_THM,
487
500
  computeGeneratorCap,
488
501
  computeGeneratorRech,
489
502
  computeGathererYield,
@@ -17,9 +17,144 @@ import {
17
17
  type UInt64Type,
18
18
  } from '@wharfkit/antelope'
19
19
  import {BaseManager} from './base'
20
- import type {CoordinatesType} from '../types'
20
+ import {Coordinates, PRECISION, type CoordinatesType} from '../types'
21
21
  import {ServerContract} from '../contracts'
22
22
  import {ATOMICASSETS_ABI, SHIPLOAD_COLLECTION} from '../nft/atomicassets'
23
+ import {getItem} from '../data/catalog'
24
+
25
+ const CHARGE_K = 1n
26
+ const ENERGY_DIVISOR = 1_000_000n
27
+ const UINT32_MAX = 4_294_967_295
28
+ const UINT32_MAX_BIGINT = 4_294_967_295n
29
+ const UINT32_MOD = 4_294_967_296n
30
+ const UINT64_MAX = 18_446_744_073_709_551_615n
31
+ const PRECISION_BIGINT = BigInt(PRECISION)
32
+
33
+ export type LaunchNumericInput =
34
+ | number
35
+ | bigint
36
+ | string
37
+ | {toNumber(): number}
38
+ | {toString(): string}
39
+
40
+ export interface LaunchStatsInput {
41
+ charge_rate?: LaunchNumericInput
42
+ chargeRate?: LaunchNumericInput
43
+ velocity: LaunchNumericInput
44
+ drain: LaunchNumericInput
45
+ }
46
+
47
+ export interface LaunchQuoteLauncher {
48
+ coordinates: CoordinatesType
49
+ launcher: LaunchStatsInput
50
+ generator?: {capacity: LaunchNumericInput}
51
+ }
52
+
53
+ export interface LaunchQuoteCatcher {
54
+ coordinates: CoordinatesType
55
+ }
56
+
57
+ export interface LaunchQuote {
58
+ chargeTime: number
59
+ flightTime: number
60
+ arrival: Date
61
+ energyCost: number
62
+ maxReach: bigint
63
+ }
64
+
65
+ function toNumber(value: LaunchNumericInput): number {
66
+ if (typeof value === 'number') return Math.trunc(value)
67
+ if (typeof value === 'bigint') return Number(value)
68
+ if (typeof value === 'string') return Number(value)
69
+ if ('toNumber' in value && typeof value.toNumber === 'function') return value.toNumber()
70
+ return Number(value.toString())
71
+ }
72
+
73
+ function requiredNumber(value: LaunchNumericInput | undefined, label: string): number {
74
+ if (value === undefined) throw new Error(`launch quote requires ${label}`)
75
+ return toNumber(value)
76
+ }
77
+
78
+ function toBigInt(value: LaunchNumericInput | undefined): bigint {
79
+ if (value === undefined) return 0n
80
+ if (typeof value === 'bigint') return value
81
+ if (typeof value === 'number') return BigInt(Math.trunc(value))
82
+ if (typeof value === 'string') return BigInt(value)
83
+ return BigInt(value.toString())
84
+ }
85
+
86
+ function saturatingMul(lhs: bigint, rhs: bigint): bigint {
87
+ if (lhs !== 0n && rhs > UINT64_MAX / lhs) {
88
+ return UINT64_MAX
89
+ }
90
+ return lhs * rhs
91
+ }
92
+
93
+ function clampLaunchResult(value: bigint): number {
94
+ if (value < 1n) return 1
95
+ if (value > UINT32_MAX_BIGINT) return UINT32_MAX
96
+ return Number(value)
97
+ }
98
+
99
+ function toUint32(value: bigint): bigint {
100
+ return value % UINT32_MOD
101
+ }
102
+
103
+ function calcDistance(origin: CoordinatesType, destination: CoordinatesType): bigint {
104
+ const a = Coordinates.from(origin)
105
+ const b = Coordinates.from(destination)
106
+ const dx = toNumber(a.x) - toNumber(b.x)
107
+ const dy = toNumber(a.y) - toNumber(b.y)
108
+ return BigInt(Math.trunc(Math.sqrt(dx * dx + dy * dy) * PRECISION))
109
+ }
110
+
111
+ function calcCargoItemMassUint32(item: ServerContract.ActionParams.Type.cargo_item): bigint {
112
+ let mass = toUint32(BigInt(getItem(item.item_id).mass) * toUint32(toBigInt(item.quantity)))
113
+
114
+ for (const mod of item.modules) {
115
+ if (mod.installed) {
116
+ mass = toUint32(mass + BigInt(getItem(mod.installed.item_id).mass))
117
+ }
118
+ }
119
+
120
+ return mass
121
+ }
122
+
123
+ function calcPayloadMass(items: ServerContract.ActionParams.Type.cargo_item[]): bigint {
124
+ let mass = 0n
125
+ for (const item of items) {
126
+ mass = toUint32(mass + calcCargoItemMassUint32(item))
127
+ }
128
+ return mass
129
+ }
130
+
131
+ function calcChargeTime(chargeRate: number, mass: bigint): number {
132
+ const rate = BigInt(chargeRate || 1)
133
+ return clampLaunchResult((mass * CHARGE_K) / rate)
134
+ }
135
+
136
+ function calcFlightTime(velocity: number, distance: bigint): number {
137
+ const v = BigInt(velocity || 1)
138
+ return clampLaunchResult(distance / (v * PRECISION_BIGINT))
139
+ }
140
+
141
+ function calcLaunchEnergy(drain: number, mass: bigint, distance: bigint): number {
142
+ const e =
143
+ saturatingMul(saturatingMul(mass, distance / PRECISION_BIGINT), BigInt(drain)) /
144
+ ENERGY_DIVISOR
145
+ return clampLaunchResult(e)
146
+ }
147
+
148
+ function calcMaxReach(energyBudget: bigint, mass: bigint, drain: number): bigint {
149
+ if (energyBudget < 1n) return 0n
150
+ if (energyBudget >= UINT32_MAX_BIGINT || mass === 0n || drain === 0) return UINT64_MAX
151
+
152
+ const numerator = (energyBudget + 1n) * ENERGY_DIVISOR - 1n
153
+ const denominator = mass * BigInt(drain)
154
+ const distanceUnits = numerator / denominator
155
+ const maxDistance = distanceUnits * PRECISION_BIGINT + (PRECISION_BIGINT - 1n)
156
+ return maxDistance > UINT64_MAX ? UINT64_MAX : maxDistance
157
+ }
23
158
 
24
159
  export type EntityRefInput = {
25
160
  entityType: NameType
@@ -166,6 +301,46 @@ export class ActionsManager extends BaseManager {
166
301
  })
167
302
  }
168
303
 
304
+ launch(
305
+ launcherId: UInt64Type,
306
+ catcherId: UInt64Type,
307
+ items: ServerContract.ActionParams.Type.cargo_item[]
308
+ ): Action {
309
+ return this.server.action('launch', {
310
+ launcher_id: UInt64.from(launcherId),
311
+ catcher_id: UInt64.from(catcherId),
312
+ items,
313
+ })
314
+ }
315
+
316
+ getLaunchQuote(
317
+ launcher: LaunchQuoteLauncher,
318
+ catcher: LaunchQuoteCatcher,
319
+ items: ServerContract.ActionParams.Type.cargo_item[],
320
+ start = new Date()
321
+ ): LaunchQuote {
322
+ const chargeRate = requiredNumber(
323
+ launcher.launcher.charge_rate ?? launcher.launcher.chargeRate,
324
+ 'launcher charge rate'
325
+ )
326
+ const velocity = requiredNumber(launcher.launcher.velocity, 'launcher velocity')
327
+ const drain = requiredNumber(launcher.launcher.drain, 'launcher drain')
328
+ const mass = calcPayloadMass(items)
329
+ const distance = calcDistance(launcher.coordinates, catcher.coordinates)
330
+ const chargeTime = calcChargeTime(chargeRate, mass)
331
+ const flightTime = calcFlightTime(velocity, distance)
332
+ const energyCost = calcLaunchEnergy(drain, mass, distance)
333
+ const maxReach = calcMaxReach(toBigInt(launcher.generator?.capacity), mass, drain)
334
+
335
+ return {
336
+ chargeTime,
337
+ flightTime,
338
+ arrival: new Date(start.getTime() + (chargeTime + flightTime) * 1000),
339
+ energyCost,
340
+ maxReach,
341
+ }
342
+ }
343
+
169
344
  foundCompany(account: NameType, name: string): Action {
170
345
  return this.platform.action('foundcompany', {
171
346
  account: Name.from(account),