@shipload/sdk 1.0.0-next.7 → 1.0.0-next.70

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 (203) hide show
  1. package/lib/scan.d.ts +52 -0
  2. package/lib/scan.js +162 -0
  3. package/lib/scan.js.map +1 -0
  4. package/lib/scan.m.js +152 -0
  5. package/lib/scan.m.js.map +1 -0
  6. package/lib/shipload.d.ts +5696 -1503
  7. package/lib/shipload.js +20294 -5200
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +20371 -5683
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1648 -0
  12. package/lib/testing.js +6980 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +6974 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.test.ts +82 -0
  18. package/src/capabilities/craftable.ts +82 -0
  19. package/src/capabilities/crafting.test.ts +80 -0
  20. package/src/capabilities/crafting.ts +50 -8
  21. package/src/capabilities/gathering.test.ts +29 -0
  22. package/src/capabilities/gathering.ts +38 -20
  23. package/src/capabilities/hauling.ts +0 -5
  24. package/src/capabilities/index.ts +0 -1
  25. package/src/capabilities/modules.ts +27 -30
  26. package/src/capabilities/movement.ts +1 -1
  27. package/src/capabilities/storage.test.ts +26 -0
  28. package/src/capabilities/storage.ts +22 -7
  29. package/src/contracts/fund.ts +306 -0
  30. package/src/contracts/platform.ts +312 -3
  31. package/src/contracts/server.ts +2661 -466
  32. package/src/contracts/token.ts +170 -0
  33. package/src/contracts.ts +2 -0
  34. package/src/coordinates/address.ts +88 -0
  35. package/src/coordinates/constants.test.ts +15 -0
  36. package/src/coordinates/constants.ts +23 -0
  37. package/src/coordinates/index.ts +15 -0
  38. package/src/coordinates/memo.test.ts +47 -0
  39. package/src/coordinates/memo.ts +20 -0
  40. package/src/coordinates/permutation.ts +77 -0
  41. package/src/coordinates/regions.ts +48 -0
  42. package/src/coordinates/sectors.ts +115 -0
  43. package/src/data/capabilities.ts +49 -18
  44. package/src/data/capability-formulas.ts +26 -14
  45. package/src/data/catalog.test.ts +19 -0
  46. package/src/data/catalog.ts +13 -3
  47. package/src/data/charters.json +396 -0
  48. package/src/data/citizenry-adjectives.json +42 -0
  49. package/src/data/citizenry-nouns.json +50 -0
  50. package/src/data/colors.ts +14 -32
  51. package/src/data/entities.json +421 -15
  52. package/src/data/item-ids.ts +53 -13
  53. package/src/data/items.json +330 -77
  54. package/src/data/kind-registry.json +212 -0
  55. package/src/data/kind-registry.ts +192 -0
  56. package/src/data/metadata.ts +312 -51
  57. package/src/data/recipes-runtime.ts +6 -23
  58. package/src/data/recipes.json +2173 -173
  59. package/src/data/tiers.ts +9 -6
  60. package/src/derivation/build-methods.test.ts +71 -0
  61. package/src/derivation/build-methods.ts +50 -0
  62. package/src/derivation/capabilities.test.ts +348 -0
  63. package/src/derivation/capabilities.ts +707 -0
  64. package/src/derivation/capability-mappings.ts +60 -16
  65. package/src/derivation/crafting.test.ts +17 -0
  66. package/src/derivation/crafting.ts +57 -34
  67. package/src/derivation/index.ts +27 -2
  68. package/src/derivation/recipe-usage.test.ts +88 -0
  69. package/src/derivation/recipe-usage.ts +141 -0
  70. package/src/derivation/reserve-regen.ts +34 -0
  71. package/src/derivation/resources.ts +9 -1
  72. package/src/derivation/rollups.test.ts +55 -0
  73. package/src/derivation/rollups.ts +72 -0
  74. package/src/derivation/stars.test.ts +67 -0
  75. package/src/derivation/stars.ts +25 -0
  76. package/src/derivation/stat-scaling.ts +12 -0
  77. package/src/derivation/stats.ts +6 -6
  78. package/src/derivation/strata.ts +5 -3
  79. package/src/derivation/stratum.ts +26 -22
  80. package/src/derivation/tiers.ts +40 -7
  81. package/src/derivation/upgrades.ts +14 -0
  82. package/src/derivation/wormhole.ts +154 -0
  83. package/src/entities/entity.ts +102 -0
  84. package/src/entities/gamestate.ts +3 -28
  85. package/src/entities/makers.ts +144 -136
  86. package/src/entities/slot-multiplier.ts +43 -0
  87. package/src/entity/hydrate.test.ts +156 -0
  88. package/src/entity/hydrate.ts +86 -0
  89. package/src/errors.ts +21 -16
  90. package/src/format.ts +42 -9
  91. package/src/index-module.ts +411 -32
  92. package/src/influence/ballot.test.ts +103 -0
  93. package/src/influence/ballot.ts +98 -0
  94. package/src/influence/categories.ts +32 -0
  95. package/src/influence/charter-metadata.test.ts +136 -0
  96. package/src/influence/charter-metadata.ts +236 -0
  97. package/src/influence/charters.test.ts +112 -0
  98. package/src/influence/charters.ts +150 -0
  99. package/src/influence/citizenry.ts +38 -0
  100. package/src/influence/civic-shuttle.test.ts +202 -0
  101. package/src/influence/civic-shuttle.ts +142 -0
  102. package/src/influence/civic.test.ts +287 -0
  103. package/src/influence/civic.ts +269 -0
  104. package/src/influence/constants.ts +84 -0
  105. package/src/influence/decay.ts +20 -0
  106. package/src/influence/decomp.ts +95 -0
  107. package/src/influence/demand.ts +106 -0
  108. package/src/influence/duration.ts +101 -0
  109. package/src/influence/index.ts +118 -0
  110. package/src/influence/influence.test.ts +450 -0
  111. package/src/influence/quality.ts +25 -0
  112. package/src/influence/valuation.ts +154 -0
  113. package/src/managers/actions.ts +940 -93
  114. package/src/managers/balances.test.ts +161 -0
  115. package/src/managers/balances.ts +93 -0
  116. package/src/managers/base.ts +10 -2
  117. package/src/managers/cluster.test.ts +39 -0
  118. package/src/managers/cluster.ts +53 -0
  119. package/src/managers/construction-types.ts +80 -0
  120. package/src/managers/construction.ts +453 -0
  121. package/src/managers/context.ts +58 -1
  122. package/src/managers/coordinates.ts +14 -0
  123. package/src/managers/entities.ts +35 -63
  124. package/src/managers/epochs.ts +72 -1
  125. package/src/managers/index.ts +38 -1
  126. package/src/managers/influence.test.ts +56 -0
  127. package/src/managers/influence.ts +425 -0
  128. package/src/managers/jobs.test.ts +80 -0
  129. package/src/managers/jobs.ts +54 -0
  130. package/src/managers/locations.ts +25 -29
  131. package/src/managers/nft.test.ts +21 -0
  132. package/src/managers/nft.ts +110 -0
  133. package/src/managers/players.ts +25 -0
  134. package/src/managers/plot.ts +122 -0
  135. package/src/nft/atomicassets.abi.json +1342 -0
  136. package/src/nft/atomicassets.ts +237 -0
  137. package/src/nft/atomicdata.ts +130 -0
  138. package/src/nft/buildImmutableData.ts +388 -0
  139. package/src/nft/description.ts +222 -100
  140. package/src/nft/index.ts +3 -0
  141. package/src/planner/index.ts +296 -0
  142. package/src/planner/planner.test.ts +459 -0
  143. package/src/receipts.ts +22 -0
  144. package/src/resolution/describe-module.ts +43 -25
  145. package/src/resolution/display-name.ts +39 -11
  146. package/src/resolution/resolve-item.test.ts +48 -0
  147. package/src/resolution/resolve-item.ts +112 -58
  148. package/src/scan/index.ts +244 -0
  149. package/src/scan/scan-wasm.base64.ts +2 -0
  150. package/src/scheduling/accessor.ts +65 -23
  151. package/src/scheduling/availability.test.ts +320 -0
  152. package/src/scheduling/availability.ts +251 -0
  153. package/src/scheduling/cancel.test.ts +550 -0
  154. package/src/scheduling/cancel.ts +254 -0
  155. package/src/scheduling/cluster-stock.test.ts +31 -0
  156. package/src/scheduling/cluster-stock.ts +15 -0
  157. package/src/scheduling/energy.ts +47 -0
  158. package/src/scheduling/epoch.ts +1 -0
  159. package/src/scheduling/idle-resolve.ts +45 -0
  160. package/src/scheduling/jobs.test.ts +49 -0
  161. package/src/scheduling/jobs.ts +137 -0
  162. package/src/scheduling/lane-core.ts +128 -0
  163. package/src/scheduling/lanes.test.ts +250 -0
  164. package/src/scheduling/lanes.ts +227 -0
  165. package/src/scheduling/projection.ts +265 -149
  166. package/src/scheduling/schedule.test.ts +163 -0
  167. package/src/scheduling/schedule.ts +282 -116
  168. package/src/scheduling/task-cargo.test.ts +110 -0
  169. package/src/scheduling/task-cargo.ts +62 -0
  170. package/src/scheduling/task-effects.test.ts +35 -0
  171. package/src/scheduling/task-effects.ts +34 -0
  172. package/src/scheduling/unwrap.test.ts +86 -0
  173. package/src/scheduling/unwrap.ts +190 -0
  174. package/src/shipload.ts +63 -4
  175. package/src/subscriptions/manager.cluster.test.ts +51 -0
  176. package/src/subscriptions/manager.ts +245 -172
  177. package/src/subscriptions/mappers.ts +5 -8
  178. package/src/subscriptions/types.ts +28 -3
  179. package/src/testing/catalog-hash.ts +19 -0
  180. package/src/testing/index.ts +2 -0
  181. package/src/testing/projection-parity.ts +167 -0
  182. package/src/travel/reach.test.ts +86 -0
  183. package/src/travel/reach.ts +34 -0
  184. package/src/travel/route-build.test.ts +63 -0
  185. package/src/travel/route-build.ts +60 -0
  186. package/src/travel/route-planner.ts +288 -0
  187. package/src/travel/route-simulator.ts +170 -0
  188. package/src/travel/travel.ts +190 -123
  189. package/src/types/capabilities.ts +37 -8
  190. package/src/types/entity.ts +3 -3
  191. package/src/types/index.ts +0 -1
  192. package/src/types.ts +81 -23
  193. package/src/utils/cargo.test.ts +14 -0
  194. package/src/utils/cargo.ts +2 -0
  195. package/src/utils/display-name.ts +70 -0
  196. package/src/utils/hash.ts +22 -2
  197. package/src/utils/system.ts +24 -5
  198. package/src/capabilities/loading.ts +0 -8
  199. package/src/entities/container.ts +0 -123
  200. package/src/entities/ship-deploy.ts +0 -295
  201. package/src/entities/ship.ts +0 -221
  202. package/src/entities/warehouse.ts +0 -127
  203. package/src/types/entity-traits.ts +0 -69
@@ -0,0 +1,170 @@
1
+ import type {UInt64Type} from '@wharfkit/antelope'
2
+ import {PRECISION, TRAVEL_MAX_DURATION} from '../types'
3
+ import {
4
+ calc_energyusage,
5
+ calc_group_flighttime,
6
+ calc_rechargetime,
7
+ distanceBetweenPoints,
8
+ } from './travel'
9
+
10
+ export interface RouteMoverInput {
11
+ ref: {entityType: string; entityId: UInt64Type}
12
+ hasMovement: boolean
13
+ engines?: {thrust: number; drain: number}
14
+ generator?: {capacity: number; recharge: number}
15
+ hauler?: {capacity: number; efficiency: number}
16
+ mass: number
17
+ energy: number
18
+ priorMobilityEnd: number
19
+ narrowBarrierEnd: number
20
+ /** Max end (seconds-from-now) over all this entity's lanes incl. worker/craft; gates recharges like the contract's all_lanes_end. Default 0. */
21
+ allLanesEnd?: number
22
+ }
23
+
24
+ export interface RouteLegSim {
25
+ from: {x: number; y: number}
26
+ to: {x: number; y: number}
27
+ distanceCells: number
28
+ energyCostByMover: Record<string, number>
29
+ rechargeBefore: boolean
30
+ rechargeSeconds: number
31
+ flightSeconds: number
32
+ }
33
+
34
+ export interface RouteSim {
35
+ legs: RouteLegSim[]
36
+ totalSeconds: number
37
+ reachable: boolean
38
+ }
39
+
40
+ export function simulateRoute(
41
+ movers: RouteMoverInput[],
42
+ waypoints: {x: number; y: number}[],
43
+ origin: {x: number; y: number},
44
+ recharge: boolean
45
+ ): RouteSim {
46
+ const totalThrust = movers
47
+ .filter((m) => m.hasMovement && m.engines)
48
+ .reduce((sum, m) => sum + m.engines!.thrust, 0)
49
+
50
+ const totalMass = movers.reduce((sum, m) => sum + m.mass, 0)
51
+
52
+ const haulCount = movers.filter((m) => !m.hasMovement).length
53
+
54
+ const pooledHaulCap = movers
55
+ .filter((m) => m.hasMovement && m.hauler)
56
+ .reduce((sum, m) => sum + m.hauler!.capacity, 0)
57
+
58
+ const weightedHaulEffNum = movers
59
+ .filter((m) => m.hasMovement && m.hauler)
60
+ .reduce((sum, m) => sum + m.hauler!.efficiency * m.hauler!.capacity, 0)
61
+
62
+ const energyByMover: Map<string, number> = new Map(
63
+ movers.map((m) => [String(m.ref.entityId), m.energy])
64
+ )
65
+
66
+ let reachable = true
67
+ const legs: RouteLegSim[] = []
68
+
69
+ const mobilityBarrier = movers
70
+ .filter((m) => m.hasMovement)
71
+ .reduce((mx, m) => Math.max(mx, m.priorMobilityEnd, m.narrowBarrierEnd), 0)
72
+
73
+ const rechargeFloor = movers
74
+ .filter((m) => m.hasMovement && m.generator)
75
+ .reduce((mx, m) => Math.max(mx, m.allLanesEnd ?? 0), 0)
76
+
77
+ let clock = 0
78
+
79
+ let from = origin
80
+ for (const to of waypoints) {
81
+ const distance = distanceBetweenPoints(from.x, from.y, to.x, to.y)
82
+ const distanceNum = Number(distance)
83
+ const distanceCells = distanceNum / PRECISION
84
+
85
+ const energyCostByMover: Record<string, number> = {}
86
+
87
+ for (const m of movers) {
88
+ if (!m.hasMovement || !m.engines) continue
89
+ const key = String(m.ref.entityId)
90
+ energyCostByMover[key] = Number(calc_energyusage(distance, m.engines.drain))
91
+ }
92
+
93
+ let rechargeBefore = false
94
+ let rechargeSeconds = 0
95
+
96
+ if (recharge) {
97
+ let maxDur = 0
98
+ for (const m of movers) {
99
+ if (!m.hasMovement || !m.generator) continue
100
+ const key = String(m.ref.entityId)
101
+ const curEnergy = energyByMover.get(key) ?? 0
102
+ const dur = Number(
103
+ calc_rechargetime(m.generator.capacity, curEnergy, m.generator.recharge)
104
+ )
105
+ if (dur > maxDur) maxDur = dur
106
+ }
107
+ rechargeSeconds = maxDur
108
+ rechargeBefore = rechargeSeconds > 0
109
+
110
+ if (rechargeBefore) {
111
+ for (const m of movers) {
112
+ if (!m.hasMovement || !m.generator) continue
113
+ const key = String(m.ref.entityId)
114
+ energyByMover.set(key, m.generator.capacity)
115
+ }
116
+ clock = Math.max(clock, rechargeFloor) + rechargeSeconds
117
+ }
118
+ }
119
+
120
+ for (const m of movers) {
121
+ if (!m.hasMovement || !m.engines) continue
122
+ const key = String(m.ref.entityId)
123
+ const cost = energyCostByMover[key] ?? 0
124
+ const curEnergy = energyByMover.get(key) ?? 0
125
+
126
+ if (recharge && m.generator) {
127
+ if (cost > m.generator.capacity) reachable = false
128
+ } else {
129
+ if (cost > curEnergy) reachable = false
130
+ }
131
+ }
132
+
133
+ const flightSeconds = Number(
134
+ calc_group_flighttime(
135
+ totalThrust,
136
+ haulCount,
137
+ pooledHaulCap,
138
+ weightedHaulEffNum,
139
+ totalMass,
140
+ distance
141
+ )
142
+ )
143
+
144
+ if (flightSeconds > TRAVEL_MAX_DURATION) reachable = false
145
+
146
+ for (const m of movers) {
147
+ if (!m.hasMovement || !m.engines) continue
148
+ const key = String(m.ref.entityId)
149
+ const cost = energyCostByMover[key] ?? 0
150
+ const curEnergy = energyByMover.get(key) ?? 0
151
+ energyByMover.set(key, Math.max(0, curEnergy - cost))
152
+ }
153
+
154
+ clock = Math.max(clock, mobilityBarrier) + flightSeconds
155
+
156
+ legs.push({
157
+ from,
158
+ to,
159
+ distanceCells,
160
+ energyCostByMover,
161
+ rechargeBefore,
162
+ rechargeSeconds,
163
+ flightSeconds,
164
+ })
165
+
166
+ from = to
167
+ }
168
+
169
+ return {legs, totalSeconds: clock, reachable}
170
+ }
@@ -14,7 +14,6 @@ import {
14
14
  type Checksum256,
15
15
  Int64,
16
16
  type Int64Type,
17
- UInt16,
18
17
  UInt32,
19
18
  type UInt32Type,
20
19
  UInt64,
@@ -23,17 +22,30 @@ import {
23
22
 
24
23
  import type {ServerContract} from '../contracts'
25
24
  import {
25
+ BASE_HAUL_PENALTY_MILLI,
26
26
  BASE_ORBITAL_MASS,
27
27
  type CargoMassInfo,
28
28
  type Distance,
29
+ HAULER_EFFICIENCY_DENOM,
30
+ MASS_STAT_SCALE,
29
31
  MAX_ORBITAL_ALTITUDE,
30
32
  MIN_ORBITAL_ALTITUDE,
33
+ MIN_TRANSFER_DISTANCE_ORBITAL_VESSEL,
34
+ MIN_TRANSFER_DISTANCE_PLANETARY_STRUCTURE,
31
35
  PRECISION,
32
36
  type ShipLike,
33
37
  TaskType,
34
38
  } from '../types'
39
+ import {EntityClass} from '../data/kind-registry'
35
40
  import {getItem} from '../data/catalog'
36
41
  import {hasSystem} from '../utils/system'
42
+ import {WH} from '../derivation/wormhole'
43
+ import * as scheduleModel from '../scheduling/schedule'
44
+ import type {ScheduleData} from '../scheduling/schedule'
45
+
46
+ function isPositionalTask(task: ServerContract.Types.task): boolean {
47
+ return task.type.equals(TaskType.TRAVEL) || task.type.equals(TaskType.TRANSIT)
48
+ }
37
49
 
38
50
  export function calc_orbital_altitude(mass: number): number {
39
51
  if (mass <= BASE_ORBITAL_MASS) {
@@ -111,15 +123,16 @@ export function getInterpolatedPosition(
111
123
  taskIndex: number,
112
124
  taskProgress: number
113
125
  ): FloatPosition {
114
- if (!entity.schedule || entity.schedule.tasks.length === 0) {
126
+ const tasks = mobilityTasks(entity)
127
+ if (tasks.length === 0) {
115
128
  return {x: Number(entity.coordinates.x), y: Number(entity.coordinates.y)}
116
129
  }
117
130
  if (taskIndex < 0) {
118
- const settled = getFlightOrigin(entity, entity.schedule.tasks.length)
131
+ const settled = getFlightOrigin(entity, tasks.length)
119
132
  return {x: Number(settled.x), y: Number(settled.y)}
120
133
  }
121
- const task = entity.schedule.tasks[taskIndex]
122
- if (!task.type.equals(TaskType.TRAVEL) || !task.coordinates) {
134
+ const task = tasks[taskIndex]
135
+ if (!isPositionalTask(task) || !task.coordinates) {
123
136
  const origin = getFlightOrigin(entity, taskIndex)
124
137
  return {x: Number(origin.x), y: Number(origin.y)}
125
138
  }
@@ -176,14 +189,15 @@ export function calc_rechargetime(
176
189
  const cap = UInt32.from(capacity)
177
190
  const eng = UInt32.from(energy)
178
191
  if (eng.gte(cap)) return UInt32.zero
179
- return cap.subtracting(eng).dividing(recharge)
192
+ const ticks = cap.subtracting(eng).dividing(recharge)
193
+ return ticks.equals(UInt32.zero) ? UInt32.from(1) : ticks
180
194
  }
181
195
 
182
196
  export function calc_ship_rechargetime(ship: ShipLike): UInt32 {
183
197
  if (!ship.generator) return UInt32.from(0)
184
198
  return calc_rechargetime(
185
199
  ship.generator.capacity,
186
- ship.energy ?? UInt16.from(0),
200
+ ship.energy ?? UInt32.from(0),
187
201
  ship.generator.recharge
188
202
  )
189
203
  }
@@ -192,20 +206,46 @@ export function calc_flighttime(distance: UInt64Type, acceleration: number): UIn
192
206
  return UInt32.from(2 * Math.sqrt(Number(distance) / acceleration))
193
207
  }
194
208
 
209
+ export function calc_travel_flighttime(distance: UInt64Type, acceleration: number): UInt32 {
210
+ const cruiseTransitionDistance = 2 * PRECISION
211
+ if (Number(distance) <= cruiseTransitionDistance) {
212
+ return calc_flighttime(distance, acceleration)
213
+ }
214
+
215
+ const cruiseVelocity = Math.sqrt(acceleration * cruiseTransitionDistance)
216
+ return UInt32.from(Number(distance) / cruiseVelocity + cruiseVelocity / acceleration)
217
+ }
218
+
219
+ export function calc_transit_duration(ax: number, ay: number, bx: number, by: number): UInt32 {
220
+ const distance = distanceBetweenPoints(ax, ay, bx, by)
221
+ return UInt32.from(Math.floor(distance.toNumber() / (PRECISION * WH.TRANSIT_SPEED)))
222
+ }
223
+
224
+ // The active entity's chosen loader lane (lowest slot), mirroring cargo.cpp lane selection.
225
+ export function shipLoaderLane(ship: ShipLike): {thrust: number; mass: number} | undefined {
226
+ const lanes = ship.loader_lanes ?? []
227
+ if (lanes.length === 0) return undefined
228
+ let lowest = lanes[0]
229
+ for (const lane of lanes) {
230
+ if (Number(lane.slot_index) < Number(lowest.slot_index)) lowest = lane
231
+ }
232
+ return {thrust: Number(lowest.thrust), mass: Number(lowest.mass)}
233
+ }
234
+
195
235
  export function calc_loader_flighttime(ship: ShipLike, mass: UInt64, altitude?: number): UInt32 {
196
236
  const z = altitude ?? ship.coordinates.z?.toNumber() ?? calc_orbital_altitude(Number(mass))
197
237
  return calc_flighttime(z, calc_loader_acceleration(ship, mass))
198
238
  }
199
239
 
200
240
  export function calc_loader_acceleration(ship: ShipLike, mass: UInt64): number {
201
- const thrust = ship.loaders ? Number(ship.loaders.thrust) : 0
202
- const loaderMass = ship.loaders ? Number(ship.loaders.mass) : 0
203
- return calc_acceleration(thrust, Number(mass) + loaderMass)
241
+ const lane = shipLoaderLane(ship)
242
+ const thrust = lane ? lane.thrust : 0
243
+ return calc_acceleration(thrust, Number(mass))
204
244
  }
205
245
 
206
246
  export function calc_ship_flighttime(ship: ShipLike, mass: UInt64, distance: UInt64): UInt32 {
207
247
  const acceleration = calc_ship_acceleration(ship, mass)
208
- return calc_flighttime(distance, acceleration)
248
+ return calc_travel_flighttime(distance, acceleration)
209
249
  }
210
250
 
211
251
  export function calc_ship_acceleration(ship: ShipLike, mass: UInt64): number {
@@ -214,7 +254,7 @@ export function calc_ship_acceleration(ship: ShipLike, mass: UInt64): number {
214
254
  }
215
255
 
216
256
  export function calc_acceleration(thrust: number, mass: number): number {
217
- return (thrust / mass) * PRECISION
257
+ return (thrust / (mass * MASS_STAT_SCALE)) * PRECISION
218
258
  }
219
259
 
220
260
  export function calc_ship_mass(ship: ShipLike, cargos: CargoMassInfo[]): UInt64 {
@@ -222,8 +262,10 @@ export function calc_ship_mass(ship: ShipLike, cargos: CargoMassInfo[]): UInt64
222
262
 
223
263
  mass.add(ship.hullmass)
224
264
 
225
- if (ship.loaders && ship.loaders.quantity.gt(UInt32.zero)) {
226
- mass.add(ship.loaders.mass.multiplying(ship.loaders.quantity))
265
+ if (ship.loader_lanes && ship.loader_lanes.length > 0) {
266
+ for (const l of ship.loader_lanes) {
267
+ mass.add(UInt64.from(l.mass))
268
+ }
227
269
  }
228
270
 
229
271
  for (const cargo of cargos) {
@@ -234,8 +276,31 @@ export function calc_ship_mass(ship: ShipLike, cargos: CargoMassInfo[]): UInt64
234
276
  return mass
235
277
  }
236
278
 
279
+ export function calc_group_flighttime(
280
+ totalThrust: number,
281
+ haulCount: number,
282
+ pooledHaulCap: number,
283
+ weightedHaulEffNum: number,
284
+ totalMass: number,
285
+ distance: UInt64Type
286
+ ): UInt32 {
287
+ const avgHaulEff = pooledHaulCap > 0 ? Math.trunc(weightedHaulEffNum / pooledHaulCap) : 0
288
+ let effectiveThrust = totalThrust
289
+ if (haulCount > 0) {
290
+ const penaltyMilli =
291
+ 1000 +
292
+ Math.trunc(
293
+ (haulCount * BASE_HAUL_PENALTY_MILLI * (HAULER_EFFICIENCY_DENOM - avgHaulEff)) /
294
+ HAULER_EFFICIENCY_DENOM
295
+ )
296
+ effectiveThrust = Math.trunc((totalThrust * 1000) / penaltyMilli)
297
+ }
298
+ const acceleration = calc_acceleration(effectiveThrust, totalMass)
299
+ return calc_travel_flighttime(distance, acceleration)
300
+ }
301
+
237
302
  export function calc_energyusage(distance: UInt64Type, drain: UInt32Type): UInt32 {
238
- return UInt64.from(distance).dividing(PRECISION).multiplying(drain)
303
+ return UInt64.from(distance).multiplying(drain).dividing(PRECISION)
239
304
  }
240
305
 
241
306
  export function calculateTransferTime(
@@ -258,10 +323,10 @@ export function calculateTransferTime(
258
323
  return UInt32.from(0)
259
324
  }
260
325
 
261
- if (!ship.loaders) return UInt32.from(0)
262
- mass = UInt64.from(mass).adding(ship.loaders.mass)
263
- const transfer_time = calc_loader_flighttime(ship, mass)
264
- return transfer_time.dividing(ship.loaders.quantity)
326
+ const lane = shipLoaderLane(ship)
327
+ if (!lane) return UInt32.from(0)
328
+ mass = UInt64.from(mass).adding(UInt64.from(lane.mass))
329
+ return calc_loader_flighttime(ship, mass)
265
330
  }
266
331
 
267
332
  export function calculateRefuelingTime(ship: ShipLike): UInt32 {
@@ -316,25 +381,22 @@ export function calculateLoadTimeBreakdown(
316
381
  let unloadTime = 0
317
382
  let loadTime = 0
318
383
 
319
- if (mass_unload.gt(UInt64.zero) && ship.loaders) {
320
- const totalMass = UInt64.from(mass_unload).adding(ship.loaders.mass)
384
+ const lane = shipLoaderLane(ship)
385
+
386
+ if (mass_unload.gt(UInt64.zero) && lane) {
387
+ const totalMass = UInt64.from(mass_unload).adding(UInt64.from(lane.mass))
321
388
  unloadTime = Number(calc_loader_flighttime(ship, totalMass))
322
389
  }
323
390
 
324
- if (mass_load.gt(UInt64.zero) && ship.loaders) {
325
- const totalMass = UInt64.from(mass_load).adding(ship.loaders.mass)
391
+ if (mass_load.gt(UInt64.zero) && lane) {
392
+ const totalMass = UInt64.from(mass_load).adding(UInt64.from(lane.mass))
326
393
  loadTime = Number(calc_loader_flighttime(ship, totalMass))
327
394
  }
328
395
 
329
- const numLoaders = ship.loaders ? Number(ship.loaders.quantity) : 0
330
- const totalTime = numLoaders > 0 ? (unloadTime + loadTime) / numLoaders : 0
331
- const unloadTimePerLoader = numLoaders > 0 ? unloadTime / numLoaders : 0
332
- const loadTimePerLoader = numLoaders > 0 ? loadTime / numLoaders : 0
333
-
334
396
  return {
335
- unloadTime: unloadTimePerLoader,
336
- loadTime: loadTimePerLoader,
337
- totalTime,
397
+ unloadTime,
398
+ loadTime,
399
+ totalTime: unloadTime + loadTime,
338
400
  unloadMass: Number(mass_unload),
339
401
  loadMass: Number(mass_load),
340
402
  }
@@ -368,24 +430,16 @@ export function estimateTravelTime(
368
430
  let loadTime = UInt32.zero
369
431
  let unloadTime = UInt32.zero
370
432
 
371
- if (
372
- loadMass &&
373
- UInt32.from(loadMass).gt(UInt32.zero) &&
374
- ship.loaders &&
375
- ship.loaders.quantity.gt(UInt32.zero)
376
- ) {
377
- const totalMass = UInt64.from(loadMass).adding(ship.loaders.mass)
378
- loadTime = calc_loader_flighttime(ship, totalMass).dividing(ship.loaders.quantity)
433
+ const lane = shipLoaderLane(ship)
434
+
435
+ if (loadMass && UInt32.from(loadMass).gt(UInt32.zero) && lane) {
436
+ const totalMass = UInt64.from(loadMass).adding(UInt64.from(lane.mass))
437
+ loadTime = calc_loader_flighttime(ship, totalMass)
379
438
  }
380
439
 
381
- if (
382
- unloadMass &&
383
- UInt32.from(unloadMass).gt(UInt32.zero) &&
384
- ship.loaders &&
385
- ship.loaders.quantity.gt(UInt32.zero)
386
- ) {
387
- const totalMass = UInt64.from(unloadMass).adding(ship.loaders.mass)
388
- unloadTime = calc_loader_flighttime(ship, totalMass).dividing(ship.loaders.quantity)
440
+ if (unloadMass && UInt32.from(unloadMass).gt(UInt32.zero) && lane) {
441
+ const totalMass = UInt64.from(unloadMass).adding(UInt64.from(lane.mass))
442
+ unloadTime = calc_loader_flighttime(ship, totalMass)
389
443
  }
390
444
 
391
445
  return {
@@ -417,30 +471,51 @@ export function hasEnergyForDistance(ship: ShipLike, distance: UInt64Type): bool
417
471
  return UInt64.from(ship.energy ?? 0).gte(energyNeeded)
418
472
  }
419
473
 
474
+ export interface TransferLoaderLane {
475
+ slot_index?: {toNumber(): number} | number
476
+ thrust: {toNumber(): number} | number
477
+ mass: {toNumber(): number} | number
478
+ }
479
+
420
480
  export interface TransferEntity {
421
481
  location: {z?: {toNumber(): number} | number}
422
- loaders?: {
423
- thrust: {toNumber(): number} | number
424
- mass: {toNumber(): number} | number
425
- quantity: {toNumber(): number} | number
482
+ entityClass: EntityClass
483
+ loaderLanes?: TransferLoaderLane[]
484
+ }
485
+
486
+ function toNum(v: {toNumber(): number} | number | undefined): number {
487
+ if (v === undefined) return 0
488
+ return typeof v === 'number' ? v : v.toNumber()
489
+ }
490
+
491
+ // Mirrors cargo.cpp worker_lane_key_or_mobility: lowest-slot loader lane (display has no busy context).
492
+ function chosenLoaderLane(entity: TransferEntity): TransferLoaderLane | undefined {
493
+ const lanes = entity.loaderLanes ?? []
494
+ if (lanes.length === 0) return undefined
495
+ let lowest = lanes[0]
496
+ for (const lane of lanes) {
497
+ if (toNum(lane.slot_index) < toNum(lowest.slot_index)) lowest = lane
426
498
  }
499
+ return lowest
427
500
  }
428
501
 
429
- export interface HasScheduleAndLocation {
502
+ export interface HasScheduleAndLocation extends ScheduleData {
430
503
  coordinates: ServerContract.ActionParams.Type.coordinates
431
- schedule?: ServerContract.Types.schedule
504
+ }
505
+
506
+ function mobilityTasks(entity: HasScheduleAndLocation): ServerContract.Types.task[] {
507
+ return scheduleModel.mobilityLane(entity)?.schedule.tasks ?? []
432
508
  }
433
509
 
434
510
  export function getFlightOrigin(
435
511
  entity: HasScheduleAndLocation,
436
512
  flightTaskIndex: number
437
513
  ): ServerContract.ActionParams.Type.coordinates {
438
- if (!entity.schedule) return entity.coordinates
439
-
514
+ const tasks = mobilityTasks(entity)
440
515
  let origin = entity.coordinates
441
- for (let i = 0; i < flightTaskIndex && i < entity.schedule.tasks.length; i++) {
442
- const task = entity.schedule.tasks[i]
443
- if (task.type.equals(TaskType.TRAVEL) && task.coordinates) {
516
+ for (let i = 0; i < flightTaskIndex && i < tasks.length; i++) {
517
+ const task = tasks[i]
518
+ if (isPositionalTask(task) && task.coordinates) {
444
519
  origin = task.coordinates
445
520
  }
446
521
  }
@@ -450,11 +525,10 @@ export function getFlightOrigin(
450
525
  export function getDestinationLocation(
451
526
  entity: HasScheduleAndLocation
452
527
  ): ServerContract.ActionParams.Type.coordinates | undefined {
453
- if (!entity.schedule) return undefined
454
-
455
- for (let i = entity.schedule.tasks.length - 1; i >= 0; i--) {
456
- const task = entity.schedule.tasks[i]
457
- if (task.type.equals(TaskType.TRAVEL) && task.coordinates) {
528
+ const tasks = mobilityTasks(entity)
529
+ for (let i = tasks.length - 1; i >= 0; i--) {
530
+ const task = tasks[i]
531
+ if (isPositionalTask(task) && task.coordinates) {
458
532
  return task.coordinates
459
533
  }
460
534
  }
@@ -467,16 +541,17 @@ export function getPositionAt(
467
541
  taskIndex: number,
468
542
  taskProgress: number
469
543
  ): ServerContract.ActionParams.Type.coordinates {
470
- if (!entity.schedule || entity.schedule.tasks.length === 0) {
544
+ const tasks = mobilityTasks(entity)
545
+ if (tasks.length === 0) {
471
546
  return entity.coordinates
472
547
  }
473
548
  if (taskIndex < 0) {
474
- return getFlightOrigin(entity, entity.schedule.tasks.length)
549
+ return getFlightOrigin(entity, tasks.length)
475
550
  }
476
551
 
477
- const task = entity.schedule.tasks[taskIndex]
552
+ const task = tasks[taskIndex]
478
553
 
479
- if (!task.type.equals(TaskType.TRAVEL) || !task.coordinates) {
554
+ if (!isPositionalTask(task) || !task.coordinates) {
480
555
  return getFlightOrigin(entity, taskIndex)
481
556
  }
482
557
 
@@ -490,68 +565,60 @@ export function getPositionAt(
490
565
  }
491
566
  }
492
567
 
493
- export function calc_transfer_duration(
494
- source: TransferEntity,
495
- dest: TransferEntity,
568
+ export function minTransferDistance(entityClass: EntityClass): number {
569
+ return entityClass === EntityClass.OrbitalVessel
570
+ ? MIN_TRANSFER_DISTANCE_ORBITAL_VESSEL
571
+ : MIN_TRANSFER_DISTANCE_PLANETARY_STRUCTURE
572
+ }
573
+
574
+ // Mirrors cargo.cpp calc_onesided_duration: single active loader's thrust + mass, no ÷quantity.
575
+ export function calc_onesided_duration(
576
+ loaderThrust: number,
577
+ loaderMass: number,
578
+ activeZ: number,
579
+ counterpartZ: number,
580
+ activeEntityClass: EntityClass,
581
+ counterpartEntityClass: EntityClass,
496
582
  cargoMass: number
497
583
  ): number {
498
- if (cargoMass === 0) {
584
+ if (cargoMass === 0 || loaderThrust === 0) {
499
585
  return 0
500
586
  }
587
+ const rawDistance = Math.abs(activeZ - counterpartZ)
588
+ const minDistance = Math.max(
589
+ minTransferDistance(activeEntityClass),
590
+ minTransferDistance(counterpartEntityClass)
591
+ )
592
+ const distance = rawDistance < minDistance ? minDistance : rawDistance
593
+ const totalMass = cargoMass + loaderMass
594
+ const acceleration = calc_acceleration(loaderThrust, totalMass)
595
+ const flightTime = Math.floor(2 * Math.sqrt(distance / acceleration))
596
+ return flightTime === 0 ? 1 : flightTime
597
+ }
501
598
 
502
- let totalThrust = 0
503
- let totalLoaderMass = 0
504
- let totalQuantity = 0
505
-
506
- if (source.loaders) {
507
- const thrust =
508
- typeof source.loaders.thrust === 'number'
509
- ? source.loaders.thrust
510
- : source.loaders.thrust.toNumber()
511
- const mass =
512
- typeof source.loaders.mass === 'number'
513
- ? source.loaders.mass
514
- : source.loaders.mass.toNumber()
515
- const qty =
516
- typeof source.loaders.quantity === 'number'
517
- ? source.loaders.quantity
518
- : source.loaders.quantity.toNumber()
519
- totalThrust += thrust * qty
520
- totalLoaderMass += mass * qty
521
- totalQuantity += qty
522
- }
523
-
524
- if (dest.loaders) {
525
- const thrust =
526
- typeof dest.loaders.thrust === 'number'
527
- ? dest.loaders.thrust
528
- : dest.loaders.thrust.toNumber()
529
- const mass =
530
- typeof dest.loaders.mass === 'number' ? dest.loaders.mass : dest.loaders.mass.toNumber()
531
- const qty =
532
- typeof dest.loaders.quantity === 'number'
533
- ? dest.loaders.quantity
534
- : dest.loaders.quantity.toNumber()
535
- totalThrust += thrust * qty
536
- totalLoaderMass += mass * qty
537
- totalQuantity += qty
538
- }
539
-
540
- if (totalThrust === 0 || totalQuantity === 0) {
599
+ // Mirrors cargo.cpp: the active (loader-bearing) entity's chosen loader lane drives the duration.
600
+ export function calc_transfer_duration(
601
+ source: TransferEntity,
602
+ dest: TransferEntity,
603
+ cargoMass: number
604
+ ): number {
605
+ const active = chosenLoaderLane(source) ? source : dest
606
+ const counterpart = active === source ? dest : source
607
+ const lane = chosenLoaderLane(active)
608
+ if (!lane) {
541
609
  return 0
542
610
  }
543
611
 
544
- const sourceZ =
545
- typeof source.location.z === 'number'
546
- ? source.location.z
547
- : (source.location.z?.toNumber() ?? 0)
548
- const destZ =
549
- typeof dest.location.z === 'number' ? dest.location.z : (dest.location.z?.toNumber() ?? 0)
550
- const distance = Math.abs(sourceZ - destZ)
551
-
552
- const totalMass = cargoMass + totalLoaderMass
553
- const acceleration = calc_acceleration(totalThrust, totalMass)
554
- const flightTime = 2 * Math.sqrt(distance / acceleration)
555
-
556
- return Math.floor(flightTime / totalQuantity)
612
+ const activeZ = toNum(active.location.z)
613
+ const counterpartZ = toNum(counterpart.location.z)
614
+
615
+ return calc_onesided_duration(
616
+ toNum(lane.thrust),
617
+ toNum(lane.mass),
618
+ activeZ,
619
+ counterpartZ,
620
+ active.entityClass,
621
+ counterpart.entityClass,
622
+ cargoMass
623
+ )
557
624
  }