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

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 (52) hide show
  1. package/lib/shipload.d.ts +289 -80
  2. package/lib/shipload.js +3099 -2600
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +3076 -2601
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +66 -20
  7. package/lib/testing.js +95 -57
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +95 -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 +45 -29
  16. package/src/coordinates/address.ts +9 -5
  17. package/src/coordinates/constants.test.ts +15 -0
  18. package/src/coordinates/constants.ts +5 -3
  19. package/src/coordinates/index.ts +11 -0
  20. package/src/coordinates/memo.test.ts +47 -0
  21. package/src/coordinates/memo.ts +20 -0
  22. package/src/data/capability-formulas.ts +0 -1
  23. package/src/data/entities.json +4 -4
  24. package/src/data/items.json +5 -5
  25. package/src/data/recipes.json +39 -65
  26. package/src/derivation/capabilities.test.ts +133 -0
  27. package/src/derivation/capabilities.ts +66 -14
  28. package/src/derivation/rollups.test.ts +55 -0
  29. package/src/derivation/rollups.ts +56 -0
  30. package/src/entities/makers.ts +30 -3
  31. package/src/index-module.ts +30 -2
  32. package/src/managers/actions.ts +34 -3
  33. package/src/managers/construction.ts +6 -4
  34. package/src/managers/context.ts +9 -0
  35. package/src/managers/coordinates.ts +14 -0
  36. package/src/managers/plot.ts +2 -4
  37. package/src/nft/description.ts +25 -6
  38. package/src/planner/index.ts +127 -0
  39. package/src/planner/planner.test.ts +319 -0
  40. package/src/resolution/resolve-item.ts +4 -1
  41. package/src/scheduling/cancel.test.ts +21 -0
  42. package/src/scheduling/lanes.test.ts +249 -0
  43. package/src/scheduling/lanes.ts +140 -2
  44. package/src/scheduling/projection.ts +73 -16
  45. package/src/shipload.ts +5 -0
  46. package/src/testing/projection-parity.ts +26 -2
  47. package/src/travel/reach.ts +23 -0
  48. package/src/travel/route-planner.ts +157 -0
  49. package/src/travel/travel.ts +102 -101
  50. package/src/types/capabilities.ts +23 -6
  51. package/src/types/entity.ts +3 -3
  52. package/src/types.ts +1 -1
@@ -0,0 +1,56 @@
1
+ import {UInt8, UInt16, UInt32} from '@wharfkit/antelope'
2
+ import type {ServerContract} from '../contracts'
3
+
4
+ export function rollupGatherer(
5
+ lanes: ServerContract.Types.gatherer_lane[]
6
+ ): {yield: UInt16; drain: UInt32; depth: UInt16} | undefined {
7
+ if (lanes.length === 0) return undefined
8
+ let totalYield = 0
9
+ let totalDrain = 0
10
+ let maxDepth = 0
11
+ for (const l of lanes) {
12
+ totalYield += Number(l.yield)
13
+ totalDrain += Number(l.drain)
14
+ const d = Number(l.depth)
15
+ if (d > maxDepth) maxDepth = d
16
+ }
17
+ return {
18
+ yield: UInt16.from(Math.min(totalYield, 65535)),
19
+ drain: UInt32.from(totalDrain),
20
+ depth: UInt16.from(maxDepth),
21
+ }
22
+ }
23
+
24
+ export function rollupCrafter(
25
+ lanes: ServerContract.Types.crafter_lane[]
26
+ ): {speed: UInt16; drain: UInt32} | undefined {
27
+ if (lanes.length === 0) return undefined
28
+ let totalSpeed = 0
29
+ let totalDrain = 0
30
+ for (const l of lanes) {
31
+ totalSpeed += Number(l.speed)
32
+ totalDrain += Number(l.drain)
33
+ }
34
+ return {
35
+ speed: UInt16.from(Math.min(totalSpeed, 65535)),
36
+ drain: UInt32.from(totalDrain),
37
+ }
38
+ }
39
+
40
+ export function rollupLoaders(
41
+ lanes: ServerContract.Types.loader_lane[]
42
+ ): {mass: UInt32; thrust: UInt16; quantity: UInt8} | undefined {
43
+ if (lanes.length === 0) return undefined
44
+ const count = lanes.length
45
+ let totalMass = 0
46
+ let totalThrust = 0
47
+ for (const l of lanes) {
48
+ totalMass += Number(l.mass)
49
+ totalThrust += Number(l.thrust)
50
+ }
51
+ return {
52
+ mass: UInt32.from(Math.floor(totalMass / count)),
53
+ thrust: UInt16.from(Math.min(totalThrust, 65535)),
54
+ quantity: UInt8.from(count),
55
+ }
56
+ }
@@ -140,13 +140,40 @@ export function makeEntity(packedItemId: number, state: EntityStateInput): Entit
140
140
 
141
141
  if (caps.engines) info.engines = caps.engines
142
142
  if (caps.generator) info.generator = caps.generator
143
- if (caps.gatherer) info.gatherer = caps.gatherer
144
- if (caps.loaders) info.loaders = caps.loaders
145
- if (caps.crafter) info.crafter = caps.crafter
146
143
  if (caps.hauler) info.hauler = caps.hauler
147
144
  if (caps.warp) info.warp = caps.warp
145
+
146
+ info.gatherer_lanes = (caps.gathererLanes ?? []).map((l) =>
147
+ ServerContract.Types.gatherer_lane.from({
148
+ slot_index: l.slotIndex,
149
+ yield: l.yield,
150
+ drain: l.drain,
151
+ depth: l.depth,
152
+ output_pct: l.outputPct,
153
+ })
154
+ )
155
+ info.crafter_lanes = (caps.crafterLanes ?? []).map((l) =>
156
+ ServerContract.Types.crafter_lane.from({
157
+ slot_index: l.slotIndex,
158
+ speed: l.speed,
159
+ drain: l.drain,
160
+ output_pct: l.outputPct,
161
+ })
162
+ )
163
+ info.loader_lanes = (caps.loaderLanes ?? []).map((l) =>
164
+ ServerContract.Types.loader_lane.from({
165
+ slot_index: l.slotIndex,
166
+ mass: l.mass,
167
+ thrust: l.thrust,
168
+ output_pct: l.outputPct,
169
+ })
170
+ )
148
171
  }
149
172
 
173
+ if (!info.gatherer_lanes) info.gatherer_lanes = []
174
+ if (!info.crafter_lanes) info.crafter_lanes = []
175
+ if (!info.loader_lanes) info.loader_lanes = []
176
+
150
177
  const entityInfo = ServerContract.Types.entity_info.from(info)
151
178
  return new Entity(entityInfo)
152
179
  }
@@ -26,13 +26,11 @@ export type {InstalledModule} from './entities/slot-multiplier'
26
26
 
27
27
  export type movement_stats = ServerContract.Types.movement_stats
28
28
  export type energy_stats = ServerContract.Types.energy_stats
29
- export type loader_stats = ServerContract.Types.loader_stats
30
29
  export type schedule = ServerContract.Types.schedule
31
30
  export type lane = ServerContract.Types.lane
32
31
  export type task = ServerContract.Types.task
33
32
  export type cargo_item = ServerContract.Types.cargo_item
34
33
  export type entity_row = ServerContract.Types.entity_row
35
- export type gatherer_stats = ServerContract.Types.gatherer_stats
36
34
 
37
35
  export type location_static = ServerContract.Types.location_static
38
36
  export type location_derived = ServerContract.Types.location_derived
@@ -158,6 +156,7 @@ export {
158
156
  calc_flighttime,
159
157
  calc_loader_acceleration,
160
158
  calc_loader_flighttime,
159
+ calc_onesided_duration,
161
160
  calc_orbital_altitude,
162
161
  calc_rechargetime,
163
162
  calc_ship_acceleration,
@@ -192,6 +191,21 @@ export type {
192
191
  HasScheduleAndLocation,
193
192
  } from './travel/travel'
194
193
 
194
+ export {planRoute, sdkSystemGraph} from './travel/route-planner'
195
+ export type {
196
+ Coord,
197
+ Neighbor,
198
+ SystemGraph,
199
+ RoutePlan,
200
+ RouteFailure,
201
+ RouteResult,
202
+ RouteFailureReason,
203
+ PlanRouteParams,
204
+ } from './travel/route-planner'
205
+
206
+ export {computePerLegReach, computeGroupPerLegReach} from './travel/reach'
207
+ export type {ReachStats} from './travel/reach'
208
+
195
209
  export * as schedule from './scheduling/schedule'
196
210
  export {LANE_MOBILITY, LANE_BARRIER} from './scheduling/schedule'
197
211
  export type {
@@ -204,8 +218,17 @@ export {
204
218
  candidateLaneCompletesAt,
205
219
  laneKeyForModule,
206
220
  rawScheduleEnd,
221
+ resolveLaneGatherer,
222
+ resolveLaneCrafter,
223
+ resolveLaneLoader,
224
+ selectGatherLane,
207
225
  workerLaneKey,
208
226
  } from './scheduling/lanes'
227
+ export type {
228
+ ResolvedGathererLane,
229
+ ResolvedCrafterLane,
230
+ ResolvedLoaderLane,
231
+ } from './scheduling/lanes'
209
232
  export {ScheduleAccessor, createScheduleAccessor} from './scheduling/accessor'
210
233
  export {InventoryAccessor, createInventoryAccessor} from './entities/inventory-accessor'
211
234
  export type {HasCargo} from './entities/inventory-accessor'
@@ -378,6 +401,8 @@ export {
378
401
  isValidWormholePair,
379
402
  } from './derivation/wormhole'
380
403
 
404
+ export {rollupGatherer, rollupCrafter, rollupLoaders} from './derivation/rollups'
405
+
381
406
  export {resolveItem} from './resolution/resolve-item'
382
407
  export type {
383
408
  ResolvedItem,
@@ -488,3 +513,6 @@ export {displayName, baseName, describeItem} from './resolution/display-name'
488
513
  export type {DescribeOptions} from './resolution/display-name'
489
514
 
490
515
  export * from './subscriptions'
516
+
517
+ export {planParallelGather, planParallelTransfer} from './planner'
518
+ export type {LanePlanEntry, PlanTarget, GatherPlanEntity} from './planner'
@@ -6,6 +6,7 @@ import {
6
6
  type Int64Type,
7
7
  Name,
8
8
  type NameType,
9
+ Transaction,
9
10
  UInt8,
10
11
  type UInt8Type,
11
12
  UInt16,
@@ -182,13 +183,39 @@ export class ActionsManager extends BaseManager {
182
183
  sourceId: UInt64Type,
183
184
  destinationId: UInt64Type,
184
185
  stratum: UInt16Type,
185
- quantity: UInt32Type
186
+ quantity: UInt32Type,
187
+ slot?: UInt8Type
186
188
  ): Action {
187
- return this.server.action('gather', {
189
+ const params: ServerContract.ActionParams.gather = {
188
190
  source_id: UInt64.from(sourceId),
189
191
  destination_id: UInt64.from(destinationId),
190
192
  stratum: UInt16.from(stratum),
191
193
  quantity: UInt32.from(quantity),
194
+ }
195
+ if (slot !== undefined) {
196
+ params.slot = UInt8.from(slot)
197
+ }
198
+ return this.server.action('gather', params)
199
+ }
200
+
201
+ // Packs N gather actions into one Transaction; the wallet/session fills in TAPoS at sign time.
202
+ bundleGather(
203
+ gathers: {
204
+ sourceId: UInt64Type
205
+ destinationId: UInt64Type
206
+ stratum: UInt16Type
207
+ quantity: UInt32Type
208
+ slot?: UInt8Type
209
+ }[]
210
+ ): Transaction {
211
+ const actions = gathers.map(({sourceId, destinationId, stratum, quantity, slot}) =>
212
+ this.gather(sourceId, destinationId, stratum, quantity, slot)
213
+ )
214
+ return Transaction.from({
215
+ expiration: 0,
216
+ ref_block_num: 0,
217
+ ref_block_prefix: 0,
218
+ actions,
192
219
  })
193
220
  }
194
221
 
@@ -208,7 +235,8 @@ export class ActionsManager extends BaseManager {
208
235
  recipeId: number,
209
236
  quantity: number,
210
237
  inputs: ServerContract.ActionParams.Type.cargo_item[],
211
- target?: UInt64Type
238
+ target?: UInt64Type,
239
+ slot?: UInt8Type
212
240
  ): Action {
213
241
  const params: ServerContract.ActionParams.craft = {
214
242
  id: UInt64.from(entityId),
@@ -219,6 +247,9 @@ export class ActionsManager extends BaseManager {
219
247
  if (target !== undefined) {
220
248
  params.target = UInt64.from(target)
221
249
  }
250
+ if (slot !== undefined) {
251
+ params.slot = UInt8.from(slot)
252
+ }
222
253
  return this.server.action('craft', params)
223
254
  }
224
255
 
@@ -67,8 +67,9 @@ export class ConstructionManager extends BaseManager {
67
67
  if (!entity.owner.equals(target.ownerName)) continue
68
68
  if (entity.id.equals(target.entityId)) continue
69
69
  if (!coordsEqual(entity.coordinates, target.coordinates)) continue
70
- const speed = entity.crafter?.speed.toNumber()
71
- if (speed === undefined) continue
70
+ const crafterLanes = entity.crafter_lanes ?? []
71
+ if (crafterLanes.length === 0) continue
72
+ const speed = crafterLanes.reduce((s, l) => s + Number(l.speed), 0)
72
73
  out.push({
73
74
  entityId: entity.id,
74
75
  entityType: entity.type,
@@ -345,8 +346,9 @@ function partitionSources(
345
346
  const reserved = reservedByItemFor(entity)
346
347
  const relevant = matchRelevantCargo(entity, target, cargo, reserved)
347
348
  if (relevant.length === 0) continue
348
- const loaderCount = entity.loaders?.quantity.toNumber() ?? 0
349
- const loaderTotalMass = entity.loaders?.mass.toNumber() ?? 0
349
+ const loaderLanes = entity.loader_lanes ?? []
350
+ const loaderCount = loaderLanes.length
351
+ const loaderTotalMass = loaderLanes.reduce((s, l) => s + Number(l.mass), 0)
350
352
  const ref: SourceEntityRef = {
351
353
  entityId: entity.id,
352
354
  name: entity.id.toString(),
@@ -6,6 +6,7 @@ import {GameState} from '../entities/gamestate'
6
6
  import {EntitiesManager} from './entities'
7
7
  import {PlayersManager} from './players'
8
8
  import {LocationsManager} from './locations'
9
+ import {CoordinatesManager} from './coordinates'
9
10
  import {EpochsManager} from './epochs'
10
11
  import {ActionsManager} from './actions'
11
12
  import {NftManager} from './nft'
@@ -15,6 +16,7 @@ export class GameContext {
15
16
  private _entities?: EntitiesManager
16
17
  private _players?: PlayersManager
17
18
  private _locations?: LocationsManager
19
+ private _coordinates?: CoordinatesManager
18
20
  private _epochs?: EpochsManager
19
21
  private _actions?: ActionsManager
20
22
  private _nft?: NftManager
@@ -52,6 +54,13 @@ export class GameContext {
52
54
  return this._locations
53
55
  }
54
56
 
57
+ get coordinates(): CoordinatesManager {
58
+ if (!this._coordinates) {
59
+ this._coordinates = new CoordinatesManager(this)
60
+ }
61
+ return this._coordinates
62
+ }
63
+
55
64
  get epochs(): EpochsManager {
56
65
  if (!this._epochs) {
57
66
  this._epochs = new EpochsManager(this)
@@ -0,0 +1,14 @@
1
+ import {BaseManager} from './base'
2
+ import {type CoordinateAddress, decodeAddress, encodeAddressMemo} from '../coordinates'
3
+
4
+ export class CoordinatesManager extends BaseManager {
5
+ async encode(x: number, y: number): Promise<CoordinateAddress> {
6
+ const game = await this.getGame()
7
+ return encodeAddressMemo(game.config.seed, x, y)
8
+ }
9
+
10
+ async decode(addr: CoordinateAddress): Promise<{x: number; y: number}> {
11
+ const game = await this.getGame()
12
+ return decodeAddress(game.config.seed, addr)
13
+ }
14
+ }
@@ -5,6 +5,7 @@ import {computeInputMass} from '../derivation/crafting'
5
5
  import {calc_craft_duration} from '../capabilities/crafting'
6
6
  import {TaskType} from '../types'
7
7
  import {BaseManager} from './base'
8
+ import type {CrafterStats} from '../types/capabilities'
8
9
  import type {ServerContract} from '../contracts'
9
10
  import type {BuildableTarget, ScheduledBuild} from './construction-types'
10
11
 
@@ -112,10 +113,7 @@ export class PlotManager extends BaseManager {
112
113
  return this.progress(plot, cargo).isComplete
113
114
  }
114
115
 
115
- timeToComplete(
116
- plot: ServerContract.Types.entity_info,
117
- crafter: ServerContract.Types.crafter_stats
118
- ): number {
116
+ timeToComplete(plot: ServerContract.Types.entity_info, crafter: CrafterStats): number {
119
117
  const capacity = Number(plot.capacity?.toString() ?? '0')
120
118
  const speed = Number(crafter.speed.toString())
121
119
  if (speed === 0) return 0
@@ -15,6 +15,7 @@ import {
15
15
  ITEM_CRAFTER_T1,
16
16
  ITEM_ENGINE_T1,
17
17
  ITEM_EXTRACTOR_T1_PACKED,
18
+ ITEM_FACTORY_T1_PACKED,
18
19
  ITEM_GATHERER_T1,
19
20
  ITEM_GENERATOR_T1,
20
21
  ITEM_HAULER_T1,
@@ -38,13 +39,23 @@ export function computeBaseHullmass(stats: bigint): number {
38
39
  }
39
40
 
40
41
  export function computeBaseCapacityShip(stats: bigint): number {
41
- const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
42
- return Math.floor(5_000_000 * 6 ** (s / 2997))
42
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
43
+ return Math.floor(5_000_000 * 6 ** (s / 1998))
44
+ }
45
+
46
+ export function computeBaseCapacityContainer(stats: bigint): number {
47
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
48
+ return Math.floor(22_000_000 * 6 ** (s / 1998))
49
+ }
50
+
51
+ export function computeBaseCapacityContainerT2(stats: bigint): number {
52
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
53
+ return Math.floor(24_000_000 * 6 ** (s / 2947))
43
54
  }
44
55
 
45
56
  export function computeBaseCapacityWarehouse(stats: bigint): number {
46
- const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
47
- return Math.floor(100_000_000 * 6 ** (s / 2997))
57
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
58
+ return Math.floor(100_000_000 * 6 ** (s / 1998))
48
59
  }
49
60
 
50
61
  export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
@@ -73,6 +84,8 @@ export function entityDisplayName(itemId: number): string {
73
84
  return 'Warehouse'
74
85
  case ITEM_EXTRACTOR_T1_PACKED:
75
86
  return 'Extractor'
87
+ case ITEM_FACTORY_T1_PACKED:
88
+ return 'Factory'
76
89
  case ITEM_CONTAINER_T1_PACKED:
77
90
  return 'Container'
78
91
  case ITEM_CONTAINER_T2_PACKED:
@@ -188,8 +201,14 @@ export function buildEntityDescription(
188
201
  baseCapacity = computeBaseCapacityShip(hullStats)
189
202
  } else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
190
203
  baseCapacity = computeBaseCapacityWarehouse(hullStats)
191
- } else if (itemId === ITEM_EXTRACTOR_T1_PACKED) {
192
- baseCapacity = computeBaseCapacityShip(hullStats)
204
+ } else if (
205
+ itemId === ITEM_EXTRACTOR_T1_PACKED ||
206
+ itemId === ITEM_FACTORY_T1_PACKED ||
207
+ itemId === ITEM_CONTAINER_T1_PACKED
208
+ ) {
209
+ baseCapacity = computeBaseCapacityContainer(hullStats)
210
+ } else if (itemId === ITEM_CONTAINER_T2_PACKED) {
211
+ baseCapacity = computeBaseCapacityContainerT2(hullStats)
193
212
  }
194
213
 
195
214
  let out = entityDisplayName(itemId)
@@ -0,0 +1,127 @@
1
+ import type {GathererStats} from '../types/capabilities'
2
+ import type {ServerContract} from '../contracts'
3
+ import {
4
+ calc_gather_duration,
5
+ calc_gather_energy,
6
+ GATHER_MASS_DIVISOR,
7
+ } from '../capabilities/gathering'
8
+ import {projectRemainingAt, type Projectable} from '../scheduling/projection'
9
+
10
+ export interface LanePlanEntry {
11
+ slot: number
12
+ quantity: number
13
+ }
14
+
15
+ export type PlanTarget = {quantity: number} | 'max'
16
+
17
+ export interface GatherPlanEntity extends Projectable {
18
+ gatherer_lanes: ServerContract.Types.gatherer_lane[]
19
+ loader_lanes: ServerContract.Types.loader_lane[]
20
+ }
21
+
22
+ // massFactor=1 so plan-time cost is conservative; on-chain recomputes with real mass/richness.
23
+ const PLAN_ITEM_MASS = GATHER_MASS_DIVISOR
24
+ const PLAN_RICHNESS = 1000
25
+
26
+ // 'max' ceiling; Phase 3 passes a real reserve/capacity cap instead.
27
+ const MAX_PLAN_QTY = 10000
28
+
29
+ function gatherEnergyCost(
30
+ lane: ServerContract.Types.gatherer_lane,
31
+ quantity: number,
32
+ stratum: number
33
+ ): number {
34
+ const stats = lane as unknown as GathererStats
35
+ const dur = Number(
36
+ calc_gather_duration(stats, PLAN_ITEM_MASS, quantity, stratum, PLAN_RICHNESS)
37
+ )
38
+ return Number(calc_gather_energy(stats, dur))
39
+ }
40
+
41
+ function allocateProportional(
42
+ lanes: {slot: number; weight: number}[],
43
+ total: number
44
+ ): LanePlanEntry[] {
45
+ if (lanes.length === 0) return []
46
+ const weightSum = lanes.reduce((s, l) => s + l.weight, 0)
47
+ if (weightSum === 0) return []
48
+
49
+ const entries: LanePlanEntry[] = lanes.map((l) => ({
50
+ slot: l.slot,
51
+ quantity: Math.floor((total * l.weight) / weightSum),
52
+ }))
53
+
54
+ let remainder = total - entries.reduce((s, e) => s + e.quantity, 0)
55
+ for (let i = 0; remainder > 0; i = (i + 1) % entries.length) {
56
+ entries[i].quantity++
57
+ remainder--
58
+ }
59
+
60
+ return entries
61
+ }
62
+
63
+ export function planParallelGather(
64
+ entity: GatherPlanEntity,
65
+ target: PlanTarget,
66
+ stratum: number,
67
+ now: Date
68
+ ): LanePlanEntry[] {
69
+ const reaching = entity.gatherer_lanes.filter((l) => l.depth.toNumber() >= stratum)
70
+ if (reaching.length === 0) throw new Error('no gatherer reaches this stratum')
71
+
72
+ // Projected energy nets out already-queued/in-flight task costs (contract projected_energy()).
73
+ const energy = entity.generator ? Number(projectRemainingAt(entity, now).energy) : Infinity
74
+
75
+ const requestedQty = target === 'max' ? MAX_PLAN_QTY : (target as {quantity: number}).quantity
76
+
77
+ // Ascending by yield so slice(1) sheds the lowest-yield lane first when energy-starved.
78
+ let activeLanes = reaching.slice().sort((a, b) => a.yield.toNumber() - b.yield.toNumber())
79
+
80
+ while (activeLanes.length > 0) {
81
+ const laneWeights = activeLanes.map((l) => ({
82
+ slot: l.slot_index.toNumber(),
83
+ weight: l.yield.toNumber(),
84
+ }))
85
+
86
+ const proposed = allocateProportional(laneWeights, requestedQty)
87
+
88
+ const totalEnergyCost = proposed.reduce((sum, entry) => {
89
+ const lane = activeLanes.find((l) => l.slot_index.toNumber() === entry.slot)!
90
+ return sum + gatherEnergyCost(lane, entry.quantity, stratum)
91
+ }, 0)
92
+
93
+ if (totalEnergyCost <= energy) {
94
+ return proposed.filter((e) => e.quantity > 0)
95
+ }
96
+
97
+ if (activeLanes.length === 1) {
98
+ const lane = activeLanes[0]
99
+ const energyPerUnit = gatherEnergyCost(lane, 1, stratum)
100
+ if (energyPerUnit === 0) return proposed.filter((e) => e.quantity > 0)
101
+ const maxQty = Math.min(requestedQty, Math.floor(energy / energyPerUnit))
102
+ if (maxQty <= 0) return []
103
+ return [{slot: lane.slot_index.toNumber(), quantity: maxQty}]
104
+ }
105
+
106
+ activeLanes = activeLanes.slice(1)
107
+ }
108
+
109
+ return []
110
+ }
111
+
112
+ export function planParallelTransfer(
113
+ entity: GatherPlanEntity,
114
+ target: PlanTarget
115
+ ): LanePlanEntry[] {
116
+ const lanes = entity.loader_lanes.filter((l) => l.thrust.toNumber() > 0)
117
+ if (lanes.length === 0) return []
118
+
119
+ const requestedQty = target === 'max' ? MAX_PLAN_QTY : (target as {quantity: number}).quantity
120
+
121
+ const laneWeights = lanes.map((l) => ({
122
+ slot: l.slot_index.toNumber(),
123
+ weight: l.thrust.toNumber(),
124
+ }))
125
+
126
+ return allocateProportional(laneWeights, requestedQty).filter((e) => e.quantity > 0)
127
+ }