@shipload/sdk 1.0.0-next.38 → 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.
@@ -131,6 +131,17 @@ export function computeHaulerCapabilities(stats: Record<string, number>): {
131
131
  }
132
132
  }
133
133
 
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
+
134
145
  export function computeStorageCapabilities(stats: Record<string, number>): {
135
146
  capacity: number
136
147
  } {
@@ -160,6 +171,8 @@ import {
160
171
  ITEM_CONTAINER_T2_PACKED,
161
172
  ITEM_EXTRACTOR_T1_PACKED,
162
173
  ITEM_FACTORY_T1_PACKED,
174
+ ITEM_MASS_CATCHER_T1_PACKED,
175
+ ITEM_MASS_DRIVER_T1_PACKED,
163
176
  ITEM_SHIP_T1_PACKED,
164
177
  ITEM_WAREHOUSE_T1_PACKED,
165
178
  } from '../data/item-ids'
@@ -174,9 +187,10 @@ import {
174
187
  MODULE_CRAFTER,
175
188
  MODULE_HAULER,
176
189
  MODULE_WARP,
190
+ MODULE_LAUNCHER,
177
191
  } from '../capabilities/modules'
178
192
  import {getItem} from '../data/catalog'
179
- import {decodeCraftedItemStats} from './crafting'
193
+ import {decodeCraftedItemStats, decodeStat} from './crafting'
180
194
  import {
181
195
  applySlotMultiplier,
182
196
  applySlotMultiplierUint32,
@@ -186,6 +200,7 @@ import {
186
200
  type InstalledModule,
187
201
  } from '../entities/slot-multiplier'
188
202
  import type {EntitySlot} from '../data/recipes-runtime'
203
+ import {computeTravelDrain} from '../nft/description'
189
204
 
190
205
  export function computeBaseCapacity(itemId: number, stats: Record<string, number>): number {
191
206
  switch (itemId) {
@@ -193,6 +208,8 @@ export function computeBaseCapacity(itemId: number, stats: Record<string, number
193
208
  return computeShipHullCapabilities(stats).capacity
194
209
  case ITEM_EXTRACTOR_T1_PACKED:
195
210
  case ITEM_FACTORY_T1_PACKED:
211
+ case ITEM_MASS_DRIVER_T1_PACKED:
212
+ case ITEM_MASS_CATCHER_T1_PACKED:
196
213
  case ITEM_CONTAINER_T1_PACKED:
197
214
  return computeContainerCapabilities(stats).capacity
198
215
  case ITEM_WAREHOUSE_T1_PACKED:
@@ -258,6 +275,7 @@ export interface ComputedCapabilities {
258
275
  crafterLanes?: CrafterLaneEntry[]
259
276
  hauler?: {capacity: number; efficiency: number; drain: number}
260
277
  warp?: {range: number}
278
+ launcher?: {chargeRate: number; velocity: number; drain: number}
261
279
  }
262
280
 
263
281
  export function computeEntityCapabilities(
@@ -267,7 +285,8 @@ export function computeEntityCapabilities(
267
285
  layout: EntitySlot[]
268
286
  ): ComputedCapabilities {
269
287
  let totalThrust = 0
270
- let totalEngineDrain = 0
288
+ let totalEngineThm = 0
289
+ let engineCount = 0
271
290
  let hasEngine = false
272
291
 
273
292
  let totalGenCapacity = 0
@@ -300,6 +319,11 @@ export function computeEntityCapabilities(
300
319
  let totalWarpRange = 0
301
320
  let hasWarp = false
302
321
 
322
+ let totalLauncherChargeRate = 0
323
+ let totalLauncherVelocity = 0
324
+ let totalLauncherDrain = 0
325
+ let hasLauncher = false
326
+
303
327
  let totalBatteryCapacity = 0
304
328
 
305
329
  const gathererLanes: GathererLaneEntry[] = []
@@ -317,7 +341,8 @@ export function computeEntityCapabilities(
317
341
  hasEngine = true
318
342
  const caps = computeEngineCapabilities(decodedStats)
319
343
  totalThrust += applySlotMultiplier(caps.thrust, amp)
320
- totalEngineDrain += caps.drain
344
+ totalEngineThm += decodedStats.thermal ?? 0
345
+ engineCount += 1
321
346
  } else if (modType === MODULE_GENERATOR) {
322
347
  hasGenerator = true
323
348
  const caps = computeGeneratorCapabilities(decodedStats)
@@ -376,6 +401,19 @@ export function computeEntityCapabilities(
376
401
  hasWarp = true
377
402
  const caps = computeWarpCapabilities(decodedStats)
378
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)
379
417
  } else if (modType === MODULE_BATTERY) {
380
418
  const caps = computeBatteryCapabilities(decodedStats)
381
419
  totalBatteryCapacity += applySlotMultiplierUint32(caps.capacity, amp)
@@ -392,7 +430,8 @@ export function computeEntityCapabilities(
392
430
  }
393
431
 
394
432
  if (hasEngine) {
395
- 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)}
396
435
  }
397
436
  if (hasGenerator) {
398
437
  result.generator = {
@@ -432,6 +471,13 @@ export function computeEntityCapabilities(
432
471
  if (hasWarp) {
433
472
  result.warp = {range: totalWarpRange}
434
473
  }
474
+ if (hasLauncher) {
475
+ result.launcher = {
476
+ chargeRate: totalLauncherChargeRate,
477
+ velocity: totalLauncherVelocity,
478
+ drain: totalLauncherDrain,
479
+ }
480
+ }
435
481
 
436
482
  return result
437
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({
@@ -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
 
@@ -383,6 +390,7 @@ export {
383
390
  computeContainerCapabilities,
384
391
  computeContainerT2Capabilities,
385
392
  computeWarpCapabilities,
393
+ computeLauncherCapabilities,
386
394
  computeBaseCapacity,
387
395
  computeEntityCapabilities,
388
396
  GATHERER_DEPTH_TABLE,
@@ -485,6 +493,10 @@ export {
485
493
  computeBaseCapacityWarehouse,
486
494
  computeEngineThrust,
487
495
  computeEngineDrain,
496
+ computeTravelDrain,
497
+ ENGINE_DRAIN_BASE,
498
+ ENGINE_DRAIN_REF_THRUST,
499
+ ENGINE_DRAIN_REF_THM,
488
500
  computeGeneratorCap,
489
501
  computeGeneratorRech,
490
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),
@@ -62,6 +62,16 @@ export function computeBaseCapacityWarehouse(stats: bigint): number {
62
62
 
63
63
  export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
64
64
  export const computeEngineDrain = (thm: number): number => 2 * Math.max(30, 50 - idiv(thm, 70))
65
+ export const ENGINE_DRAIN_BASE = 118
66
+ export const ENGINE_DRAIN_REF_THRUST = 775
67
+ export const ENGINE_DRAIN_REF_THM = 500
68
+
69
+ export const computeTravelDrain = (totalThrust: number, avgThm: number): number => {
70
+ 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)
74
+ }
65
75
  export const computeGeneratorCap = (com: number): number => 950 + idiv(com, 2)
66
76
  export const computeGeneratorRech = (fin: number): number => 2 * (1 + idiv(fin * 3, 1000))
67
77
  export const computeGathererYield = (str: number): number => 200 + str
@@ -0,0 +1,37 @@
1
+ import {expect, test} from 'bun:test'
2
+ import {UInt16, UInt64} from '@wharfkit/antelope'
3
+ import {resolveItem} from './resolve-item'
4
+ import {encodeStats} from '../derivation/crafting'
5
+ import {computeContainerCapabilities} from '../derivation/capabilities'
6
+ import {
7
+ ITEM_EXTRACTOR_T1_PACKED,
8
+ ITEM_FACTORY_T1_PACKED,
9
+ ITEM_MASS_DRIVER_T1_PACKED,
10
+ ITEM_MASS_CATCHER_T1_PACKED,
11
+ } from '../data/item-ids'
12
+
13
+ function hullStats(strength: number, density: number, hardness: number): UInt64 {
14
+ return UInt64.from(encodeStats([strength, density, hardness]).toString())
15
+ }
16
+
17
+ const CONTAINER_ENTITIES = [
18
+ ['factory', ITEM_FACTORY_T1_PACKED],
19
+ ['extractor', ITEM_EXTRACTOR_T1_PACKED],
20
+ ['mass driver', ITEM_MASS_DRIVER_T1_PACKED],
21
+ ['mass catcher', ITEM_MASS_CATCHER_T1_PACKED],
22
+ ] as const
23
+
24
+ for (const [label, itemId] of CONTAINER_ENTITIES) {
25
+ test(`resolveItem resolves ${label} hull capacity via container formula`, () => {
26
+ const stats = hullStats(300, 100, 400)
27
+ const resolved = resolveItem(UInt16.from(itemId), stats)
28
+ const hull = resolved.attributes?.find((g) => g.capability === 'Hull')
29
+ const capacity = hull?.attributes.find((a) => a.label === 'Capacity')?.value
30
+ const expected = computeContainerCapabilities({
31
+ strength: 300,
32
+ hardness: 400,
33
+ density: 100,
34
+ }).capacity
35
+ expect(capacity).toBe(expected)
36
+ })
37
+ }
@@ -39,6 +39,9 @@ import {
39
39
  ITEM_CONTAINER_T1_PACKED,
40
40
  ITEM_CONTAINER_T2_PACKED,
41
41
  ITEM_EXTRACTOR_T1_PACKED,
42
+ ITEM_FACTORY_T1_PACKED,
43
+ ITEM_MASS_CATCHER_T1_PACKED,
44
+ ITEM_MASS_DRIVER_T1_PACKED,
42
45
  ITEM_SHIP_T1_PACKED,
43
46
  ITEM_WAREHOUSE_T1_PACKED,
44
47
  } from '../data/item-ids'
@@ -287,7 +290,9 @@ function hullCapsForEntity(
287
290
  case ITEM_WAREHOUSE_T1_PACKED:
288
291
  return computeWarehouseHullCapabilities(decoded)
289
292
  case ITEM_EXTRACTOR_T1_PACKED:
290
- return computeShipHullCapabilities(decoded)
293
+ case ITEM_FACTORY_T1_PACKED:
294
+ case ITEM_MASS_DRIVER_T1_PACKED:
295
+ case ITEM_MASS_CATCHER_T1_PACKED:
291
296
  case ITEM_CONTAINER_T1_PACKED:
292
297
  return computeContainerCapabilities(decoded)
293
298
  case ITEM_CONTAINER_T2_PACKED:
@@ -41,6 +41,7 @@ export interface ProjectedEntity {
41
41
  loaderLanes: ServerContract.Types.loader_lane[]
42
42
  generator?: ServerContract.Types.energy_stats
43
43
  hauler?: ServerContract.Types.hauler_stats
44
+ launcher?: ServerContract.Types.launcher_stats
44
45
  readonly cargoMass: UInt64
45
46
  readonly totalMass: UInt64
46
47
  readonly gathererLanes: ServerContract.Types.gatherer_lane[]
@@ -49,6 +50,7 @@ export interface ProjectedEntity {
49
50
  hasMovement(): boolean
50
51
  hasStorage(): boolean
51
52
  hasLoaders(): boolean
53
+ hasLauncher(): boolean
52
54
 
53
55
  capabilities(): EntityCapabilities
54
56
  state(): EntityState
@@ -64,6 +66,7 @@ export interface Projectable extends ScheduleData {
64
66
  gatherer_lanes?: ServerContract.Types.gatherer_lane[]
65
67
  crafter_lanes?: ServerContract.Types.crafter_lane[]
66
68
  hauler?: ServerContract.Types.hauler_stats
69
+ launcher?: ServerContract.Types.launcher_stats
67
70
  capacity?: UInt32
68
71
  cargo: ServerContract.Types.cargo_item[]
69
72
  cargomass: UInt32
@@ -91,6 +94,7 @@ interface ProjectedCaps {
91
94
  gathererLanes: ServerContract.Types.gatherer_lane[]
92
95
  crafterLanes: ServerContract.Types.crafter_lane[]
93
96
  hauler?: ServerContract.Types.hauler_stats
97
+ launcher?: ServerContract.Types.launcher_stats
94
98
  }
95
99
 
96
100
  function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
@@ -164,6 +168,13 @@ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
164
168
  gathererLanes: (caps.gathererLanes ?? []).map(toGathererLane),
165
169
  crafterLanes: (caps.crafterLanes ?? []).map(toCrafterLane),
166
170
  hauler: caps.hauler ? ServerContract.Types.hauler_stats.from(caps.hauler) : undefined,
171
+ launcher: caps.launcher
172
+ ? ServerContract.Types.launcher_stats.from({
173
+ charge_rate: caps.launcher.chargeRate,
174
+ velocity: caps.launcher.velocity,
175
+ drain: caps.launcher.drain,
176
+ })
177
+ : undefined,
167
178
  }
168
179
  }
169
180
 
@@ -180,6 +191,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
180
191
  entity.engines === undefined ||
181
192
  entity.generator === undefined ||
182
193
  entity.hauler === undefined ||
194
+ entity.launcher === undefined ||
183
195
  entity.capacity === undefined
184
196
  const caps = needsRecompute ? recomputeCaps(entity) : undefined
185
197
 
@@ -190,6 +202,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
190
202
  const engines = entity.engines ?? caps?.engines
191
203
  const generator = entity.generator ?? caps?.generator
192
204
  const hauler = entity.hauler ?? caps?.hauler
205
+ const launcher = entity.launcher ?? caps?.launcher
193
206
  const capacity = entity.capacity ?? caps?.capacity
194
207
 
195
208
  const cargo: CargoStack[] = entity.cargo.map(cargoItemToStack)
@@ -203,6 +216,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
203
216
  engines,
204
217
  generator,
205
218
  hauler,
219
+ launcher,
206
220
  loaderLanes,
207
221
  gathererLanes,
208
222
  crafterLanes,
@@ -229,12 +243,17 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
229
243
  return this.loaderLanes.length > 0
230
244
  },
231
245
 
246
+ hasLauncher() {
247
+ return this.launcher !== undefined
248
+ },
249
+
232
250
  capabilities(): EntityCapabilities {
233
251
  return {
234
252
  hullmass: this.shipMass,
235
253
  capacity: this.capacity ? UInt32.from(this.capacity) : undefined,
236
254
  engines: this.engines,
237
255
  generator: this.generator,
256
+ launcher: this.launcher,
238
257
  }
239
258
  },
240
259
 
@@ -59,6 +59,7 @@ export interface EntityCapabilities {
59
59
  gatherer?: GathererStats
60
60
  crafter?: CrafterStats
61
61
  hauler?: ServerContract.Types.hauler_stats
62
+ launcher?: ServerContract.Types.launcher_stats
62
63
  }
63
64
 
64
65
  export interface EntityState {
@@ -92,3 +93,7 @@ export function capsHasMass(caps: EntityCapabilities): boolean {
92
93
  export function capsHasHauler(caps: EntityCapabilities): boolean {
93
94
  return caps.hauler !== undefined
94
95
  }
96
+
97
+ export function capsHasLauncher(caps: EntityCapabilities): boolean {
98
+ return caps.launcher !== undefined
99
+ }
package/src/types.ts CHANGED
@@ -129,6 +129,7 @@ export type ModuleType =
129
129
  | 'storage'
130
130
  | 'hauler'
131
131
  | 'battery'
132
+ | 'catcher'
132
133
 
133
134
  export const RESOURCE_TIER_ADJECTIVES: Record<number, string> = {
134
135
  1: 'Crude',