@shipload/sdk 1.0.0-next.34 → 1.0.0-next.36

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 (59) hide show
  1. package/lib/shipload.d.ts +398 -51
  2. package/lib/shipload.js +1481 -400
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +1442 -401
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +101 -20
  7. package/lib/testing.js +201 -57
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +201 -57
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/capabilities/crafting.ts +2 -3
  13. package/src/capabilities/gathering.test.ts +16 -0
  14. package/src/capabilities/gathering.ts +8 -11
  15. package/src/contracts/server.ts +147 -29
  16. package/src/coordinates/address.ts +88 -0
  17. package/src/coordinates/constants.test.ts +15 -0
  18. package/src/coordinates/constants.ts +23 -0
  19. package/src/coordinates/index.ts +15 -0
  20. package/src/coordinates/memo.test.ts +47 -0
  21. package/src/coordinates/memo.ts +20 -0
  22. package/src/coordinates/permutation.ts +77 -0
  23. package/src/coordinates/regions.ts +48 -0
  24. package/src/coordinates/sectors.ts +115 -0
  25. package/src/data/capability-formulas.ts +0 -1
  26. package/src/data/entities.json +4 -4
  27. package/src/data/items.json +5 -5
  28. package/src/data/recipes.json +39 -65
  29. package/src/derivation/capabilities.test.ts +133 -0
  30. package/src/derivation/capabilities.ts +66 -14
  31. package/src/derivation/rollups.test.ts +55 -0
  32. package/src/derivation/rollups.ts +56 -0
  33. package/src/derivation/wormhole.ts +115 -0
  34. package/src/entities/makers.ts +30 -3
  35. package/src/errors.ts +2 -0
  36. package/src/index-module.ts +38 -2
  37. package/src/managers/actions.ts +79 -5
  38. package/src/managers/construction.ts +6 -4
  39. package/src/managers/context.ts +9 -0
  40. package/src/managers/coordinates.ts +14 -0
  41. package/src/managers/plot.ts +2 -4
  42. package/src/nft/description.ts +25 -6
  43. package/src/planner/index.ts +127 -0
  44. package/src/planner/planner.test.ts +319 -0
  45. package/src/resolution/resolve-item.ts +4 -1
  46. package/src/scheduling/availability.ts +1 -1
  47. package/src/scheduling/cancel.test.ts +348 -0
  48. package/src/scheduling/cancel.ts +209 -0
  49. package/src/scheduling/lanes.test.ts +249 -0
  50. package/src/scheduling/lanes.ts +140 -2
  51. package/src/scheduling/projection.ts +75 -16
  52. package/src/scheduling/schedule.ts +3 -1
  53. package/src/shipload.ts +5 -0
  54. package/src/testing/projection-parity.ts +26 -2
  55. package/src/travel/travel.ts +116 -105
  56. package/src/types/capabilities.ts +23 -6
  57. package/src/types/entity.ts +3 -3
  58. package/src/types.ts +2 -1
  59. package/src/utils/system.ts +11 -0
@@ -2,7 +2,6 @@ import {Name, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
2
2
  import {ServerContract} from '../contracts'
3
3
  import {Coordinates, TaskType} from '../types'
4
4
  import {
5
- capsHasLoaders,
6
5
  capsHasMovement,
7
6
  capsHasStorage,
8
7
  type EntityCapabilities,
@@ -18,7 +17,7 @@ import {
18
17
  } from '../errors'
19
18
  import {getEntityLayout, getRecipe, type RecipeInput} from '../data/recipes-runtime'
20
19
  import {computeEntityCapabilities} from '../derivation/capabilities'
21
- import {decodeCraftedItemStats} from '../derivation/crafting'
20
+ import {decodeCraftedItemStats, decodeStat} from '../derivation/crafting'
22
21
  import {packedModulesToInstalled, type InstalledModule} from '../entities/slot-multiplier'
23
22
  import {lerp} from '../travel/travel'
24
23
  import {
@@ -39,11 +38,13 @@ export interface ProjectedEntity {
39
38
  shipMass: UInt32
40
39
  capacity?: UInt64
41
40
  engines?: ServerContract.Types.movement_stats
42
- loaders?: ServerContract.Types.loader_stats
41
+ loaderLanes: ServerContract.Types.loader_lane[]
43
42
  generator?: ServerContract.Types.energy_stats
44
43
  hauler?: ServerContract.Types.hauler_stats
45
44
  readonly cargoMass: UInt64
46
45
  readonly totalMass: UInt64
46
+ readonly gathererLanes: ServerContract.Types.gatherer_lane[]
47
+ readonly crafterLanes: ServerContract.Types.crafter_lane[]
47
48
 
48
49
  hasMovement(): boolean
49
50
  hasStorage(): boolean
@@ -59,7 +60,9 @@ export interface Projectable extends ScheduleData {
59
60
  hullmass?: UInt32
60
61
  generator?: ServerContract.Types.energy_stats
61
62
  engines?: ServerContract.Types.movement_stats
62
- loaders?: ServerContract.Types.loader_stats
63
+ loader_lanes?: ServerContract.Types.loader_lane[]
64
+ gatherer_lanes?: ServerContract.Types.gatherer_lane[]
65
+ crafter_lanes?: ServerContract.Types.crafter_lane[]
63
66
  hauler?: ServerContract.Types.hauler_stats
64
67
  capacity?: UInt32
65
68
  cargo: ServerContract.Types.cargo_item[]
@@ -84,7 +87,9 @@ interface ProjectedCaps {
84
87
  capacity?: UInt32
85
88
  engines?: ServerContract.Types.movement_stats
86
89
  generator?: ServerContract.Types.energy_stats
87
- loaders?: ServerContract.Types.loader_stats
90
+ loaderLanes: ServerContract.Types.loader_lane[]
91
+ gathererLanes: ServerContract.Types.gatherer_lane[]
92
+ crafterLanes: ServerContract.Types.crafter_lane[]
88
93
  hauler?: ServerContract.Types.hauler_stats
89
94
  }
90
95
 
@@ -101,10 +106,53 @@ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
101
106
  typeof entity.item_id === 'number' ? entity.item_id : entity.item_id.value
102
107
  )
103
108
  const hullStats = decodeCraftedItemStats(itemId, entity.stats)
109
+ if (hullStats.strength === undefined) hullStats.strength = decodeStat(entity.stats, 0)
110
+ if (hullStats.hardness === undefined) hullStats.hardness = decodeStat(entity.stats, 2)
104
111
  const layout = getEntityLayout(itemId)?.slots ?? []
105
112
  const installed = toInstalledModules(entity.modules)
106
113
  const caps = computeEntityCapabilities(hullStats, itemId, installed, layout)
107
114
 
115
+ const toLoaderLane = (l: {
116
+ slotIndex: number
117
+ mass: number
118
+ thrust: number
119
+ outputPct: number
120
+ }): ServerContract.Types.loader_lane =>
121
+ ServerContract.Types.loader_lane.from({
122
+ slot_index: l.slotIndex,
123
+ mass: l.mass,
124
+ thrust: l.thrust,
125
+ output_pct: l.outputPct,
126
+ })
127
+
128
+ const toGathererLane = (l: {
129
+ slotIndex: number
130
+ yield: number
131
+ drain: number
132
+ depth: number
133
+ outputPct: number
134
+ }): ServerContract.Types.gatherer_lane =>
135
+ ServerContract.Types.gatherer_lane.from({
136
+ slot_index: l.slotIndex,
137
+ yield: l.yield,
138
+ drain: l.drain,
139
+ depth: l.depth,
140
+ output_pct: l.outputPct,
141
+ })
142
+
143
+ const toCrafterLane = (l: {
144
+ slotIndex: number
145
+ speed: number
146
+ drain: number
147
+ outputPct: number
148
+ }): ServerContract.Types.crafter_lane =>
149
+ ServerContract.Types.crafter_lane.from({
150
+ slot_index: l.slotIndex,
151
+ speed: l.speed,
152
+ drain: l.drain,
153
+ output_pct: l.outputPct,
154
+ })
155
+
108
156
  return {
109
157
  hullmass: UInt32.from(caps.hullmass),
110
158
  capacity: UInt32.from(caps.capacity),
@@ -112,15 +160,23 @@ function recomputeCaps(entity: Projectable): ProjectedCaps | undefined {
112
160
  generator: caps.generator
113
161
  ? ServerContract.Types.energy_stats.from(caps.generator)
114
162
  : undefined,
115
- loaders: caps.loaders ? ServerContract.Types.loader_stats.from(caps.loaders) : undefined,
163
+ loaderLanes: (caps.loaderLanes ?? []).map(toLoaderLane),
164
+ gathererLanes: (caps.gathererLanes ?? []).map(toGathererLane),
165
+ crafterLanes: (caps.crafterLanes ?? []).map(toCrafterLane),
116
166
  hauler: caps.hauler ? ServerContract.Types.hauler_stats.from(caps.hauler) : undefined,
117
167
  }
118
168
  }
119
169
 
170
+ function loaderLanesTotalMass(lanes: ServerContract.Types.loader_lane[]): UInt64 {
171
+ let total = 0
172
+ for (const l of lanes) total += Number(l.mass)
173
+ return UInt64.from(total)
174
+ }
175
+
120
176
  export function createProjectedEntity(entity: Projectable): ProjectedEntity {
121
177
  const needsRecompute =
122
178
  entity.hullmass === undefined ||
123
- entity.loaders === undefined ||
179
+ entity.loader_lanes === undefined ||
124
180
  entity.engines === undefined ||
125
181
  entity.generator === undefined ||
126
182
  entity.hauler === undefined ||
@@ -128,7 +184,9 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
128
184
  const caps = needsRecompute ? recomputeCaps(entity) : undefined
129
185
 
130
186
  const shipMass = UInt32.from(entity.hullmass ?? caps?.hullmass ?? 0)
131
- const loaders = entity.loaders ?? caps?.loaders
187
+ const loaderLanes = entity.loader_lanes ?? caps?.loaderLanes ?? []
188
+ const gathererLanes = entity.gatherer_lanes ?? caps?.gathererLanes ?? []
189
+ const crafterLanes = entity.crafter_lanes ?? caps?.crafterLanes ?? []
132
190
  const engines = entity.engines ?? caps?.engines
133
191
  const generator = entity.generator ?? caps?.generator
134
192
  const hauler = entity.hauler ?? caps?.hauler
@@ -145,18 +203,18 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
145
203
  engines,
146
204
  generator,
147
205
  hauler,
148
- loaders,
206
+ loaderLanes,
207
+ gathererLanes,
208
+ crafterLanes,
149
209
 
150
210
  get cargoMass() {
151
211
  return calcStacksMass(this.cargo)
152
212
  },
153
213
 
154
214
  get totalMass() {
155
- let mass = UInt64.from(this.shipMass).adding(this.cargoMass)
156
- if (this.loaders) {
157
- mass = mass.adding(this.loaders.mass.multiplying(this.loaders.quantity))
158
- }
159
- return mass
215
+ return UInt64.from(this.shipMass)
216
+ .adding(this.cargoMass)
217
+ .adding(loaderLanesTotalMass(this.loaderLanes))
160
218
  },
161
219
 
162
220
  hasMovement() {
@@ -168,7 +226,7 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
168
226
  },
169
227
 
170
228
  hasLoaders() {
171
- return capsHasLoaders(this.capabilities())
229
+ return this.loaderLanes.length > 0
172
230
  },
173
231
 
174
232
  capabilities(): EntityCapabilities {
@@ -177,7 +235,6 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
177
235
  capacity: this.capacity ? UInt32.from(this.capacity) : undefined,
178
236
  engines: this.engines,
179
237
  generator: this.generator,
180
- loaders: this.loaders,
181
238
  }
182
239
  },
183
240
 
@@ -303,6 +360,7 @@ function applyTask(projected: ProjectedEntity, task: ServerContract.Types.task):
303
360
  break
304
361
  case TaskType.TRAVEL:
305
362
  case TaskType.WARP:
363
+ case TaskType.TRANSIT:
306
364
  applyFlightTask(projected, task, {complete: true})
307
365
  break
308
366
  case TaskType.LOAD:
@@ -461,6 +519,7 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
461
519
  break
462
520
  case TaskType.TRAVEL:
463
521
  case TaskType.WARP:
522
+ case TaskType.TRANSIT:
464
523
  applyFlightTask(projected, task, {complete: taskComplete, progress})
465
524
  break
466
525
  case TaskType.LOAD:
@@ -294,7 +294,9 @@ function entityDoesTaskType(entity: ScheduleData, taskType: TaskType, now: Date)
294
294
 
295
295
  export function isInFlight(entity: ScheduleData, now: Date): boolean {
296
296
  const lane = mobilityLane(entity)
297
- return lane ? core.currentTaskType(lane.schedule, now) === TaskType.TRAVEL : false
297
+ if (!lane) return false
298
+ const t = core.currentTaskType(lane.schedule, now)
299
+ return t === TaskType.TRAVEL || t === TaskType.TRANSIT
298
300
  }
299
301
 
300
302
  export function isRecharging(entity: ScheduleData, now: Date): boolean {
package/src/shipload.ts CHANGED
@@ -7,6 +7,7 @@ import {GameContext} from './managers/context'
7
7
  import type {EntitiesManager} from './managers/entities'
8
8
  import type {PlayersManager} from './managers/players'
9
9
  import type {LocationsManager} from './managers/locations'
10
+ import type {CoordinatesManager} from './managers/coordinates'
10
11
  import type {EpochsManager} from './managers/epochs'
11
12
  import type {ActionsManager} from './managers/actions'
12
13
  import type {NftManager} from './managers/nft'
@@ -110,6 +111,10 @@ export class Shipload {
110
111
  return this._context.locations
111
112
  }
112
113
 
114
+ get coordinates(): CoordinatesManager {
115
+ return this._context.coordinates
116
+ }
117
+
113
118
  get epochs(): EpochsManager {
114
119
  return this._context.epochs
115
120
  }
@@ -12,9 +12,11 @@ export interface ContractProjectedState {
12
12
  hullmass?: UInt32
13
13
  capacity?: UInt32
14
14
  engines?: ServerContract.Types.movement_stats
15
- loaders?: ServerContract.Types.loader_stats
16
15
  generator?: ServerContract.Types.energy_stats
17
16
  hauler?: ServerContract.Types.hauler_stats
17
+ gatherer_lanes: ServerContract.Types.gatherer_lane[]
18
+ crafter_lanes: ServerContract.Types.crafter_lane[]
19
+ loader_lanes: ServerContract.Types.loader_lane[]
18
20
  }
19
21
 
20
22
  export interface ProjectionComparisonOptions {
@@ -55,10 +57,32 @@ export function assertProjectionEquals(
55
57
  record('capacity', toNum(contract.capacity), sdk.capacity ? Number(sdk.capacity) : undefined)
56
58
 
57
59
  recordStatBlock('engines', contract.engines, sdk.engines)
58
- recordStatBlock('loaders', contract.loaders, sdk.loaders)
59
60
  recordStatBlock('generator', contract.generator, sdk.generator)
60
61
  recordStatBlock('hauler', contract.hauler, sdk.hauler)
61
62
 
63
+ const normLane = (l: {slot_index?: unknown; [k: string]: unknown}) =>
64
+ Object.fromEntries(
65
+ Object.entries(l)
66
+ .filter(([k]) => k !== 'slot_index')
67
+ .map(([k, v]) => [k, toNum(v) ?? 0])
68
+ )
69
+
70
+ const compareLanes = (name: string, cLanes: unknown[], sLanes: unknown[]) => {
71
+ if (cLanes.length !== sLanes.length) {
72
+ mismatches.push(` ${name}.length: contract=${cLanes.length} sdk=${sLanes.length}`)
73
+ return
74
+ }
75
+ for (let i = 0; i < cLanes.length; i++) {
76
+ const cn = JSON.stringify(normLane(cLanes[i] as Record<string, unknown>))
77
+ const sn = JSON.stringify(normLane(sLanes[i] as Record<string, unknown>))
78
+ if (cn !== sn) mismatches.push(` ${name}[${i}]: contract=${cn} sdk=${sn}`)
79
+ }
80
+ }
81
+
82
+ compareLanes('gatherer_lanes', contract.gatherer_lanes as unknown[], sdk.gathererLanes ?? [])
83
+ compareLanes('crafter_lanes', contract.crafter_lanes as unknown[], sdk.crafterLanes ?? [])
84
+ compareLanes('loader_lanes', contract.loader_lanes as unknown[], sdk.loaderLanes ?? [])
85
+
62
86
  if (contract.cargo.length > 0 || sdk.cargo.length > 0) {
63
87
  const contractCargo = normaliseCargo(mergeContractCargo(contract.cargo))
64
88
  const sdkCargo = normaliseCargo(sdk.cargo)
@@ -37,9 +37,14 @@ import {
37
37
  import {EntityClass} from '../data/kind-registry'
38
38
  import {getItem} from '../data/catalog'
39
39
  import {hasSystem} from '../utils/system'
40
+ import {WH} from '../derivation/wormhole'
40
41
  import * as scheduleModel from '../scheduling/schedule'
41
42
  import type {ScheduleData} from '../scheduling/schedule'
42
43
 
44
+ function isPositionalTask(task: ServerContract.Types.task): boolean {
45
+ return task.type.equals(TaskType.TRAVEL) || task.type.equals(TaskType.TRANSIT)
46
+ }
47
+
43
48
  export function calc_orbital_altitude(mass: number): number {
44
49
  if (mass <= BASE_ORBITAL_MASS) {
45
50
  return MIN_ORBITAL_ALTITUDE
@@ -125,7 +130,7 @@ export function getInterpolatedPosition(
125
130
  return {x: Number(settled.x), y: Number(settled.y)}
126
131
  }
127
132
  const task = tasks[taskIndex]
128
- if (!task.type.equals(TaskType.TRAVEL) || !task.coordinates) {
133
+ if (!isPositionalTask(task) || !task.coordinates) {
129
134
  const origin = getFlightOrigin(entity, taskIndex)
130
135
  return {x: Number(origin.x), y: Number(origin.y)}
131
136
  }
@@ -198,15 +203,31 @@ export function calc_flighttime(distance: UInt64Type, acceleration: number): UIn
198
203
  return UInt32.from(2 * Math.sqrt(Number(distance) / acceleration))
199
204
  }
200
205
 
206
+ export function calc_transit_duration(ax: number, ay: number, bx: number, by: number): UInt32 {
207
+ const distance = distanceBetweenPoints(ax, ay, bx, by)
208
+ return UInt32.from(Math.floor(distance.toNumber() / (PRECISION * WH.TRANSIT_SPEED)))
209
+ }
210
+
211
+ // The active entity's chosen loader lane (lowest slot), mirroring cargo.cpp lane selection.
212
+ export function shipLoaderLane(ship: ShipLike): {thrust: number; mass: number} | undefined {
213
+ const lanes = ship.loader_lanes ?? []
214
+ if (lanes.length === 0) return undefined
215
+ let lowest = lanes[0]
216
+ for (const lane of lanes) {
217
+ if (Number(lane.slot_index) < Number(lowest.slot_index)) lowest = lane
218
+ }
219
+ return {thrust: Number(lowest.thrust), mass: Number(lowest.mass)}
220
+ }
221
+
201
222
  export function calc_loader_flighttime(ship: ShipLike, mass: UInt64, altitude?: number): UInt32 {
202
223
  const z = altitude ?? ship.coordinates.z?.toNumber() ?? calc_orbital_altitude(Number(mass))
203
224
  return calc_flighttime(z, calc_loader_acceleration(ship, mass))
204
225
  }
205
226
 
206
227
  export function calc_loader_acceleration(ship: ShipLike, mass: UInt64): number {
207
- const thrust = ship.loaders ? Number(ship.loaders.thrust) : 0
208
- const loaderMass = ship.loaders ? Number(ship.loaders.mass) : 0
209
- return calc_acceleration(thrust, Number(mass) + loaderMass)
228
+ const lane = shipLoaderLane(ship)
229
+ const thrust = lane ? lane.thrust : 0
230
+ return calc_acceleration(thrust, Number(mass))
210
231
  }
211
232
 
212
233
  export function calc_ship_flighttime(ship: ShipLike, mass: UInt64, distance: UInt64): UInt32 {
@@ -228,8 +249,10 @@ export function calc_ship_mass(ship: ShipLike, cargos: CargoMassInfo[]): UInt64
228
249
 
229
250
  mass.add(ship.hullmass)
230
251
 
231
- if (ship.loaders && ship.loaders.quantity.gt(UInt32.zero)) {
232
- mass.add(ship.loaders.mass.multiplying(ship.loaders.quantity))
252
+ if (ship.loader_lanes && ship.loader_lanes.length > 0) {
253
+ for (const l of ship.loader_lanes) {
254
+ mass.add(UInt64.from(l.mass))
255
+ }
233
256
  }
234
257
 
235
258
  for (const cargo of cargos) {
@@ -264,10 +287,10 @@ export function calculateTransferTime(
264
287
  return UInt32.from(0)
265
288
  }
266
289
 
267
- if (!ship.loaders) return UInt32.from(0)
268
- mass = UInt64.from(mass).adding(ship.loaders.mass)
269
- const transfer_time = calc_loader_flighttime(ship, mass)
270
- return transfer_time.dividing(ship.loaders.quantity)
290
+ const lane = shipLoaderLane(ship)
291
+ if (!lane) return UInt32.from(0)
292
+ mass = UInt64.from(mass).adding(UInt64.from(lane.mass))
293
+ return calc_loader_flighttime(ship, mass)
271
294
  }
272
295
 
273
296
  export function calculateRefuelingTime(ship: ShipLike): UInt32 {
@@ -322,25 +345,22 @@ export function calculateLoadTimeBreakdown(
322
345
  let unloadTime = 0
323
346
  let loadTime = 0
324
347
 
325
- if (mass_unload.gt(UInt64.zero) && ship.loaders) {
326
- const totalMass = UInt64.from(mass_unload).adding(ship.loaders.mass)
348
+ const lane = shipLoaderLane(ship)
349
+
350
+ if (mass_unload.gt(UInt64.zero) && lane) {
351
+ const totalMass = UInt64.from(mass_unload).adding(UInt64.from(lane.mass))
327
352
  unloadTime = Number(calc_loader_flighttime(ship, totalMass))
328
353
  }
329
354
 
330
- if (mass_load.gt(UInt64.zero) && ship.loaders) {
331
- const totalMass = UInt64.from(mass_load).adding(ship.loaders.mass)
355
+ if (mass_load.gt(UInt64.zero) && lane) {
356
+ const totalMass = UInt64.from(mass_load).adding(UInt64.from(lane.mass))
332
357
  loadTime = Number(calc_loader_flighttime(ship, totalMass))
333
358
  }
334
359
 
335
- const numLoaders = ship.loaders ? Number(ship.loaders.quantity) : 0
336
- const totalTime = numLoaders > 0 ? (unloadTime + loadTime) / numLoaders : 0
337
- const unloadTimePerLoader = numLoaders > 0 ? unloadTime / numLoaders : 0
338
- const loadTimePerLoader = numLoaders > 0 ? loadTime / numLoaders : 0
339
-
340
360
  return {
341
- unloadTime: unloadTimePerLoader,
342
- loadTime: loadTimePerLoader,
343
- totalTime,
361
+ unloadTime,
362
+ loadTime,
363
+ totalTime: unloadTime + loadTime,
344
364
  unloadMass: Number(mass_unload),
345
365
  loadMass: Number(mass_load),
346
366
  }
@@ -374,24 +394,16 @@ export function estimateTravelTime(
374
394
  let loadTime = UInt32.zero
375
395
  let unloadTime = UInt32.zero
376
396
 
377
- if (
378
- loadMass &&
379
- UInt32.from(loadMass).gt(UInt32.zero) &&
380
- ship.loaders &&
381
- ship.loaders.quantity.gt(UInt32.zero)
382
- ) {
383
- const totalMass = UInt64.from(loadMass).adding(ship.loaders.mass)
384
- loadTime = calc_loader_flighttime(ship, totalMass).dividing(ship.loaders.quantity)
397
+ const lane = shipLoaderLane(ship)
398
+
399
+ if (loadMass && UInt32.from(loadMass).gt(UInt32.zero) && lane) {
400
+ const totalMass = UInt64.from(loadMass).adding(UInt64.from(lane.mass))
401
+ loadTime = calc_loader_flighttime(ship, totalMass)
385
402
  }
386
403
 
387
- if (
388
- unloadMass &&
389
- UInt32.from(unloadMass).gt(UInt32.zero) &&
390
- ship.loaders &&
391
- ship.loaders.quantity.gt(UInt32.zero)
392
- ) {
393
- const totalMass = UInt64.from(unloadMass).adding(ship.loaders.mass)
394
- unloadTime = calc_loader_flighttime(ship, totalMass).dividing(ship.loaders.quantity)
404
+ if (unloadMass && UInt32.from(unloadMass).gt(UInt32.zero) && lane) {
405
+ const totalMass = UInt64.from(unloadMass).adding(UInt64.from(lane.mass))
406
+ unloadTime = calc_loader_flighttime(ship, totalMass)
395
407
  }
396
408
 
397
409
  return {
@@ -423,14 +435,32 @@ export function hasEnergyForDistance(ship: ShipLike, distance: UInt64Type): bool
423
435
  return UInt64.from(ship.energy ?? 0).gte(energyNeeded)
424
436
  }
425
437
 
438
+ export interface TransferLoaderLane {
439
+ slot_index?: {toNumber(): number} | number
440
+ thrust: {toNumber(): number} | number
441
+ mass: {toNumber(): number} | number
442
+ }
443
+
426
444
  export interface TransferEntity {
427
445
  location: {z?: {toNumber(): number} | number}
428
446
  entityClass: EntityClass
429
- loaders?: {
430
- thrust: {toNumber(): number} | number
431
- mass: {toNumber(): number} | number
432
- quantity: {toNumber(): number} | number
447
+ loaderLanes?: TransferLoaderLane[]
448
+ }
449
+
450
+ function toNum(v: {toNumber(): number} | number | undefined): number {
451
+ if (v === undefined) return 0
452
+ return typeof v === 'number' ? v : v.toNumber()
453
+ }
454
+
455
+ // Mirrors cargo.cpp worker_lane_key_or_mobility: lowest-slot loader lane (display has no busy context).
456
+ function chosenLoaderLane(entity: TransferEntity): TransferLoaderLane | undefined {
457
+ const lanes = entity.loaderLanes ?? []
458
+ if (lanes.length === 0) return undefined
459
+ let lowest = lanes[0]
460
+ for (const lane of lanes) {
461
+ if (toNum(lane.slot_index) < toNum(lowest.slot_index)) lowest = lane
433
462
  }
463
+ return lowest
434
464
  }
435
465
 
436
466
  export interface HasScheduleAndLocation extends ScheduleData {
@@ -449,7 +479,7 @@ export function getFlightOrigin(
449
479
  let origin = entity.coordinates
450
480
  for (let i = 0; i < flightTaskIndex && i < tasks.length; i++) {
451
481
  const task = tasks[i]
452
- if (task.type.equals(TaskType.TRAVEL) && task.coordinates) {
482
+ if (isPositionalTask(task) && task.coordinates) {
453
483
  origin = task.coordinates
454
484
  }
455
485
  }
@@ -462,7 +492,7 @@ export function getDestinationLocation(
462
492
  const tasks = mobilityTasks(entity)
463
493
  for (let i = tasks.length - 1; i >= 0; i--) {
464
494
  const task = tasks[i]
465
- if (task.type.equals(TaskType.TRAVEL) && task.coordinates) {
495
+ if (isPositionalTask(task) && task.coordinates) {
466
496
  return task.coordinates
467
497
  }
468
498
  }
@@ -485,7 +515,7 @@ export function getPositionAt(
485
515
 
486
516
  const task = tasks[taskIndex]
487
517
 
488
- if (!task.type.equals(TaskType.TRAVEL) || !task.coordinates) {
518
+ if (!isPositionalTask(task) || !task.coordinates) {
489
519
  return getFlightOrigin(entity, taskIndex)
490
520
  }
491
521
 
@@ -505,73 +535,54 @@ export function minTransferDistance(entityClass: EntityClass): number {
505
535
  : MIN_TRANSFER_DISTANCE_PLANETARY_STRUCTURE
506
536
  }
507
537
 
508
- export function calc_transfer_duration(
509
- source: TransferEntity,
510
- dest: TransferEntity,
538
+ // Mirrors cargo.cpp calc_onesided_duration: single active loader's thrust + mass, no ÷quantity.
539
+ export function calc_onesided_duration(
540
+ loaderThrust: number,
541
+ loaderMass: number,
542
+ activeZ: number,
543
+ counterpartZ: number,
544
+ activeEntityClass: EntityClass,
545
+ counterpartEntityClass: EntityClass,
511
546
  cargoMass: number
512
547
  ): number {
513
- if (cargoMass === 0) {
514
- return 0
515
- }
516
-
517
- let totalThrust = 0
518
- let totalLoaderMass = 0
519
- let totalQuantity = 0
520
-
521
- if (source.loaders) {
522
- const thrust =
523
- typeof source.loaders.thrust === 'number'
524
- ? source.loaders.thrust
525
- : source.loaders.thrust.toNumber()
526
- const mass =
527
- typeof source.loaders.mass === 'number'
528
- ? source.loaders.mass
529
- : source.loaders.mass.toNumber()
530
- const qty =
531
- typeof source.loaders.quantity === 'number'
532
- ? source.loaders.quantity
533
- : source.loaders.quantity.toNumber()
534
- totalThrust += thrust * qty
535
- totalLoaderMass += mass * qty
536
- totalQuantity += qty
537
- }
538
-
539
- if (dest.loaders) {
540
- const thrust =
541
- typeof dest.loaders.thrust === 'number'
542
- ? dest.loaders.thrust
543
- : dest.loaders.thrust.toNumber()
544
- const mass =
545
- typeof dest.loaders.mass === 'number' ? dest.loaders.mass : dest.loaders.mass.toNumber()
546
- const qty =
547
- typeof dest.loaders.quantity === 'number'
548
- ? dest.loaders.quantity
549
- : dest.loaders.quantity.toNumber()
550
- totalThrust += thrust * qty
551
- totalLoaderMass += mass * qty
552
- totalQuantity += qty
553
- }
554
-
555
- if (totalThrust === 0 || totalQuantity === 0) {
548
+ if (cargoMass === 0 || loaderThrust === 0) {
556
549
  return 0
557
550
  }
558
-
559
- const sourceZ =
560
- typeof source.location.z === 'number'
561
- ? source.location.z
562
- : (source.location.z?.toNumber() ?? 0)
563
- const destZ =
564
- typeof dest.location.z === 'number' ? dest.location.z : (dest.location.z?.toNumber() ?? 0)
565
- const rawDistance = Math.abs(sourceZ - destZ)
551
+ const rawDistance = Math.abs(activeZ - counterpartZ)
566
552
  const minDistance = Math.max(
567
- minTransferDistance(source.entityClass),
568
- minTransferDistance(dest.entityClass)
553
+ minTransferDistance(activeEntityClass),
554
+ minTransferDistance(counterpartEntityClass)
569
555
  )
570
556
  const distance = rawDistance < minDistance ? minDistance : rawDistance
557
+ const totalMass = cargoMass + loaderMass
558
+ const acceleration = calc_acceleration(loaderThrust, totalMass)
559
+ const flightTime = Math.floor(2 * Math.sqrt(distance / acceleration))
560
+ return flightTime === 0 ? 1 : flightTime
561
+ }
571
562
 
572
- const totalMass = cargoMass + totalLoaderMass
573
- const acceleration = calc_acceleration(totalThrust, totalMass)
574
- const flightTime = 2 * Math.sqrt(distance / acceleration)
563
+ // Mirrors cargo.cpp: the active (loader-bearing) entity's chosen loader lane drives the duration.
564
+ export function calc_transfer_duration(
565
+ source: TransferEntity,
566
+ dest: TransferEntity,
567
+ cargoMass: number
568
+ ): number {
569
+ const active = chosenLoaderLane(source) ? source : dest
570
+ const counterpart = active === source ? dest : source
571
+ const lane = chosenLoaderLane(active)
572
+ if (!lane) {
573
+ return 0
574
+ }
575
575
 
576
- return Math.floor(flightTime / totalQuantity)
576
+ const activeZ = toNum(active.location.z)
577
+ const counterpartZ = toNum(counterpart.location.z)
578
+
579
+ return calc_onesided_duration(
580
+ toNum(lane.thrust),
581
+ toNum(lane.mass),
582
+ activeZ,
583
+ counterpartZ,
584
+ active.entityClass,
585
+ counterpart.entityClass,
586
+ cargoMass
587
+ )
577
588
  }
@@ -1,6 +1,23 @@
1
- import type {Name, UInt16, UInt32} from '@wharfkit/antelope'
1
+ import type {Name, UInt16, UInt32, UInt8} from '@wharfkit/antelope'
2
2
  import type {ServerContract} from '../contracts'
3
3
 
4
+ export interface LoaderStats {
5
+ mass: {toNumber(): number; multiplying(v: unknown): {toNumber(): number}}
6
+ thrust: {toNumber(): number}
7
+ quantity: {toNumber(): number; gt(v: unknown): boolean}
8
+ }
9
+
10
+ export interface GathererStats {
11
+ yield: {toNumber(): number}
12
+ drain: {toNumber(): number}
13
+ depth: {toNumber(): number; toString(): string}
14
+ }
15
+
16
+ export interface CrafterStats {
17
+ speed: {toNumber(): number}
18
+ drain: {toNumber(): number}
19
+ }
20
+
4
21
  export interface MovementCapability {
5
22
  engines: ServerContract.Types.movement_stats
6
23
  generator: ServerContract.Types.energy_stats
@@ -17,11 +34,11 @@ export interface StorageCapability {
17
34
  }
18
35
 
19
36
  export interface LoaderCapability {
20
- loaders: ServerContract.Types.loader_stats
37
+ loaders: LoaderStats
21
38
  }
22
39
 
23
40
  export interface GathererCapability {
24
- gatherer: ServerContract.Types.gatherer_stats
41
+ gatherer: GathererStats
25
42
  }
26
43
 
27
44
  export interface MassCapability {
@@ -38,9 +55,9 @@ export interface EntityCapabilities {
38
55
  capacity?: UInt32
39
56
  engines?: ServerContract.Types.movement_stats
40
57
  generator?: ServerContract.Types.energy_stats
41
- loaders?: ServerContract.Types.loader_stats
42
- gatherer?: ServerContract.Types.gatherer_stats
43
- crafter?: ServerContract.Types.crafter_stats
58
+ loaders?: LoaderStats
59
+ gatherer?: GathererStats
60
+ crafter?: CrafterStats
44
61
  hauler?: ServerContract.Types.hauler_stats
45
62
  }
46
63
 
@@ -3,6 +3,7 @@ import type {ServerContract} from '../contracts'
3
3
  import type {Coordinates} from '../types'
4
4
  import type {
5
5
  EnergyCapability,
6
+ GathererCapability,
6
7
  LoaderCapability,
7
8
  MassCapability,
8
9
  MovementCapability,
@@ -24,9 +25,8 @@ export type ShipEntity = Entity &
24
25
  StorageCapability &
25
26
  Partial<LoaderCapability> &
26
27
  MassCapability &
27
- ScheduleCapability & {
28
- gatherer?: ServerContract.Types.gatherer_stats
29
- }
28
+ ScheduleCapability &
29
+ Partial<GathererCapability>
30
30
 
31
31
  export type WarehouseEntity = Entity &
32
32
  StorageCapability &