@shipload/sdk 1.0.0-next.4 → 1.0.0-next.40

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 (127) hide show
  1. package/lib/shipload.d.ts +2473 -973
  2. package/lib/shipload.js +11529 -5211
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +11338 -5162
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +970 -0
  7. package/lib/testing.js +4013 -0
  8. package/lib/testing.js.map +1 -0
  9. package/lib/testing.m.js +4007 -0
  10. package/lib/testing.m.js.map +1 -0
  11. package/package.json +15 -2
  12. package/src/capabilities/craftable.ts +51 -0
  13. package/src/capabilities/crafting.test.ts +7 -0
  14. package/src/capabilities/crafting.ts +5 -6
  15. package/src/capabilities/gathering.test.ts +16 -0
  16. package/src/capabilities/gathering.ts +35 -18
  17. package/src/capabilities/index.ts +0 -1
  18. package/src/capabilities/modules.ts +9 -0
  19. package/src/capabilities/storage.ts +16 -1
  20. package/src/contracts/platform.ts +231 -3
  21. package/src/contracts/server.ts +1021 -481
  22. package/src/coordinates/address.ts +88 -0
  23. package/src/coordinates/constants.test.ts +15 -0
  24. package/src/coordinates/constants.ts +23 -0
  25. package/src/coordinates/index.ts +15 -0
  26. package/src/coordinates/memo.test.ts +47 -0
  27. package/src/coordinates/memo.ts +20 -0
  28. package/src/coordinates/permutation.ts +77 -0
  29. package/src/coordinates/regions.ts +48 -0
  30. package/src/coordinates/sectors.ts +115 -0
  31. package/src/data/capabilities.ts +12 -5
  32. package/src/data/capability-formulas.ts +14 -7
  33. package/src/data/catalog.ts +0 -5
  34. package/src/data/colors.ts +14 -47
  35. package/src/data/entities.json +76 -10
  36. package/src/data/item-ids.ts +18 -12
  37. package/src/data/items.json +321 -38
  38. package/src/data/kind-registry.json +109 -0
  39. package/src/data/kind-registry.ts +165 -0
  40. package/src/data/metadata.ts +119 -33
  41. package/src/data/recipes-runtime.ts +3 -23
  42. package/src/data/recipes.json +238 -117
  43. package/src/derivation/build-methods.ts +45 -0
  44. package/src/derivation/capabilities.test.ts +151 -0
  45. package/src/derivation/capabilities.ts +512 -0
  46. package/src/derivation/capability-mappings.ts +9 -12
  47. package/src/derivation/crafting.ts +23 -24
  48. package/src/derivation/index.ts +25 -2
  49. package/src/derivation/recipe-usage.test.ts +78 -0
  50. package/src/derivation/recipe-usage.ts +141 -0
  51. package/src/derivation/reserve-regen.ts +34 -0
  52. package/src/derivation/resources.ts +125 -38
  53. package/src/derivation/rollups.test.ts +55 -0
  54. package/src/derivation/rollups.ts +56 -0
  55. package/src/derivation/stars.test.ts +51 -0
  56. package/src/derivation/stars.ts +15 -0
  57. package/src/derivation/stats.ts +6 -6
  58. package/src/derivation/stratum.ts +17 -20
  59. package/src/derivation/tiers.ts +40 -7
  60. package/src/derivation/wormhole.ts +136 -0
  61. package/src/entities/entity.ts +98 -0
  62. package/src/entities/gamestate.ts +3 -28
  63. package/src/entities/makers.ts +124 -134
  64. package/src/entities/slot-multiplier.ts +43 -0
  65. package/src/errors.ts +12 -16
  66. package/src/format.ts +26 -4
  67. package/src/index-module.ts +267 -47
  68. package/src/managers/actions.ts +528 -95
  69. package/src/managers/base.ts +6 -2
  70. package/src/managers/construction-types.ts +80 -0
  71. package/src/managers/construction.ts +412 -0
  72. package/src/managers/context.ts +20 -1
  73. package/src/managers/coordinates.ts +14 -0
  74. package/src/managers/entities.ts +18 -66
  75. package/src/managers/epochs.ts +40 -0
  76. package/src/managers/index.ts +17 -1
  77. package/src/managers/locations.ts +25 -29
  78. package/src/managers/nft.test.ts +14 -0
  79. package/src/managers/nft.ts +70 -0
  80. package/src/managers/plot.ts +122 -0
  81. package/src/nft/atomicassets.abi.json +1342 -0
  82. package/src/nft/atomicassets.ts +237 -0
  83. package/src/nft/atomicdata.ts +130 -0
  84. package/src/nft/buildImmutableData.ts +338 -0
  85. package/src/nft/description.ts +98 -24
  86. package/src/nft/index.ts +3 -0
  87. package/src/planner/index.ts +127 -0
  88. package/src/planner/planner.test.ts +319 -0
  89. package/src/resolution/describe-module.ts +18 -13
  90. package/src/resolution/display-name.ts +38 -10
  91. package/src/resolution/resolve-item.test.ts +37 -0
  92. package/src/resolution/resolve-item.ts +55 -24
  93. package/src/scheduling/accessor.ts +68 -22
  94. package/src/scheduling/availability.ts +108 -0
  95. package/src/scheduling/cancel.test.ts +348 -0
  96. package/src/scheduling/cancel.ts +209 -0
  97. package/src/scheduling/energy.ts +47 -0
  98. package/src/scheduling/idle-resolve.ts +45 -0
  99. package/src/scheduling/lane-core.ts +128 -0
  100. package/src/scheduling/lanes.test.ts +249 -0
  101. package/src/scheduling/lanes.ts +198 -0
  102. package/src/scheduling/projection.ts +209 -105
  103. package/src/scheduling/schedule.ts +241 -104
  104. package/src/scheduling/task-cargo.ts +46 -0
  105. package/src/shipload.ts +21 -1
  106. package/src/subscriptions/manager.ts +229 -142
  107. package/src/subscriptions/mappers.ts +5 -8
  108. package/src/subscriptions/types.ts +11 -3
  109. package/src/testing/catalog-hash.ts +19 -0
  110. package/src/testing/index.ts +2 -0
  111. package/src/testing/projection-parity.ts +167 -0
  112. package/src/travel/reach.ts +23 -0
  113. package/src/travel/route-planner.ts +196 -0
  114. package/src/travel/travel.ts +200 -112
  115. package/src/types/capabilities.ts +29 -6
  116. package/src/types/entity.ts +3 -3
  117. package/src/types/index.ts +0 -1
  118. package/src/types.ts +28 -13
  119. package/src/utils/cargo.ts +27 -0
  120. package/src/utils/display-name.ts +70 -0
  121. package/src/utils/system.ts +36 -24
  122. package/src/capabilities/loading.ts +0 -8
  123. package/src/entities/container.ts +0 -108
  124. package/src/entities/ship-deploy.ts +0 -259
  125. package/src/entities/ship.ts +0 -204
  126. package/src/entities/warehouse.ts +0 -119
  127. package/src/types/entity-traits.ts +0 -69
@@ -4,6 +4,8 @@ import {
4
4
  MODULE_ENGINE,
5
5
  MODULE_GATHERER,
6
6
  MODULE_GENERATOR,
7
+ MODULE_BATTERY,
8
+ MODULE_HAULER,
7
9
  MODULE_LOADER,
8
10
  MODULE_STORAGE,
9
11
  MODULE_WARP,
@@ -13,8 +15,12 @@ import {
13
15
  ITEM_CONTAINER_T2_PACKED,
14
16
  ITEM_CRAFTER_T1,
15
17
  ITEM_ENGINE_T1,
18
+ ITEM_EXTRACTOR_T1_PACKED,
19
+ ITEM_FACTORY_T1_PACKED,
16
20
  ITEM_GATHERER_T1,
17
21
  ITEM_GENERATOR_T1,
22
+ ITEM_HAULER_T1,
23
+ ITEM_BATTERY_T1,
18
24
  ITEM_LOADER_T1,
19
25
  ITEM_SHIP_T1_PACKED,
20
26
  ITEM_STORAGE_T1,
@@ -22,6 +28,8 @@ import {
22
28
  ITEM_WARP_T1,
23
29
  } from '../data/item-ids'
24
30
  import {decodeStat} from '../derivation/crafting'
31
+ import {gathererDepthForTier} from '../derivation/capabilities'
32
+ import {getItem} from '../data/catalog'
25
33
 
26
34
  function idiv(a: number, b: number): number {
27
35
  return Math.floor(a / b)
@@ -29,33 +37,68 @@ function idiv(a: number, b: number): number {
29
37
 
30
38
  export function computeBaseHullmass(stats: bigint): number {
31
39
  const density = decodeStat(stats, 1)
32
- return 25000 + 75 * density
40
+ return 100000 - 75 * density
33
41
  }
34
42
 
35
43
  export function computeBaseCapacityShip(stats: bigint): number {
36
- const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
37
- return Math.floor(1_000_000 * 10 ** (s / 2997))
44
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
45
+ return Math.floor(5_000_000 * 6 ** (s / 1998))
46
+ }
47
+
48
+ export function computeBaseCapacityContainer(stats: bigint): number {
49
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
50
+ return Math.floor(22_000_000 * 6 ** (s / 1998))
51
+ }
52
+
53
+ export function computeBaseCapacityContainerT2(stats: bigint): number {
54
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
55
+ return Math.floor(24_000_000 * 6 ** (s / 2947))
38
56
  }
39
57
 
40
58
  export function computeBaseCapacityWarehouse(stats: bigint): number {
41
- const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
42
- return Math.floor(20_000_000 * 10 ** (s / 2997))
59
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
60
+ return Math.floor(100_000_000 * 6 ** (s / 1998))
43
61
  }
44
62
 
45
63
  export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
46
- export const computeEngineDrain = (thm: number): number => Math.max(30, 50 - idiv(thm, 70))
47
- export const computeGeneratorCap = (com: number): number => 300 + idiv(com, 6)
48
- export const computeGeneratorRech = (fin: number): number => 1 + idiv(fin * 3, 1000)
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
+ }
75
+ export const computeGeneratorCap = (com: number): number => 950 + idiv(com, 2)
76
+ export const computeGeneratorRech = (fin: number): number => 2 * (1 + idiv(fin * 3, 1000))
49
77
  export const computeGathererYield = (str: number): number => 200 + str
50
78
  export const computeGathererDrain = (con: number): number =>
51
- Math.max(250, 1250 - idiv(con * 25, 20))
52
- export const computeGathererDepth = (tol: number): number => 200 + idiv(tol * 3, 2)
53
- export const computeGathererSpeed = (ref: number): number => 100 + idiv(ref * 4, 5)
79
+ 2 * Math.max(250, 1250 - idiv(con * 25, 20))
80
+ export const computeGathererDepth = (tol: number, tier: number): number =>
81
+ gathererDepthForTier(tol, tier)
54
82
  export const computeLoaderMass = (ins: number): number => Math.max(200, 2000 - ins * 2)
55
- export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla, 500)
83
+ export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla * pla, 10000)
56
84
  export const computeCrafterSpeed = (rea: number): number => 100 + idiv(rea * 4, 5)
57
85
  export const computeCrafterDrain = (fin: number): number => Math.max(5, 30 - idiv(fin, 33))
86
+ export const computeHaulerCapacity = (fin: number): number => Math.max(1, 1 + idiv(fin, 400))
87
+ export const computeHaulerEfficiency = (con: number): number => 2000 + con * 6
88
+ export const computeHaulerDrain = (com: number): number => Math.max(3, 15 - idiv(com, 80))
58
89
  export const computeWarpRange = (stat: number): number => 100 + stat * 3
90
+ export const computeCargoBayCapacity = (
91
+ strength: number,
92
+ density: number,
93
+ hardness: number,
94
+ cohesion: number
95
+ ): number => 10_000_000 + idiv((strength + density + hardness + cohesion) * 50_000_000, 3996)
96
+ export const computeBatteryBankCapacity = (
97
+ volatility: number,
98
+ thermal: number,
99
+ plasticity: number,
100
+ insulation: number
101
+ ): number => 2_500 + idiv((volatility + thermal + plasticity + insulation) * 7_500, 3996)
59
102
 
60
103
  export function entityDisplayName(itemId: number): string {
61
104
  switch (itemId) {
@@ -63,6 +106,10 @@ export function entityDisplayName(itemId: number): string {
63
106
  return 'Ship'
64
107
  case ITEM_WAREHOUSE_T1_PACKED:
65
108
  return 'Warehouse'
109
+ case ITEM_EXTRACTOR_T1_PACKED:
110
+ return 'Extractor'
111
+ case ITEM_FACTORY_T1_PACKED:
112
+ return 'Factory'
66
113
  case ITEM_CONTAINER_T1_PACKED:
67
114
  return 'Container'
68
115
  case ITEM_CONTAINER_T2_PACKED:
@@ -85,9 +132,13 @@ export function moduleDisplayName(itemId: number): string {
85
132
  case ITEM_CRAFTER_T1:
86
133
  return 'Crafter'
87
134
  case ITEM_STORAGE_T1:
88
- return 'Storage'
135
+ return 'Cargo Bay'
136
+ case ITEM_HAULER_T1:
137
+ return 'Hauler'
89
138
  case ITEM_WARP_T1:
90
139
  return 'Warp'
140
+ case ITEM_BATTERY_T1:
141
+ return 'Battery Bank'
91
142
  default:
92
143
  return 'Module'
93
144
  }
@@ -119,11 +170,12 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
119
170
  case MODULE_GATHERER: {
120
171
  const str = decodeStat(stats, 0)
121
172
  const tol = decodeStat(stats, 1)
122
- const con = decodeStat(stats, 3)
123
- const ref = decodeStat(stats, 4)
173
+ const con = decodeStat(stats, 2)
174
+ const tier = getItem(itemId).tier
124
175
  out += ` Yield ${computeGathererYield(str)} Depth ${computeGathererDepth(
125
- tol
126
- )} Speed ${computeGathererSpeed(ref)} Drain ${computeGathererDrain(con)}`
176
+ tol,
177
+ tier
178
+ )} Drain ${computeGathererDrain(con)}`
127
179
  break
128
180
  }
129
181
  case MODULE_LOADER: {
@@ -134,17 +186,23 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
134
186
  }
135
187
  case MODULE_CRAFTER: {
136
188
  const rea = decodeStat(stats, 0)
137
- const com = decodeStat(stats, 1)
138
- out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(com)}`
189
+ const con = decodeStat(stats, 1)
190
+ out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(con)}`
139
191
  break
140
192
  }
141
193
  case MODULE_STORAGE: {
142
194
  const str = decodeStat(stats, 0)
143
- const fin = decodeStat(stats, 2)
144
- const sat = decodeStat(stats, 3)
145
- const sum = str + fin + sat
146
- const pct = 10 + idiv(sum * 10, 2997)
147
- out += ` +${pct}% capacity`
195
+ const den = decodeStat(stats, 1)
196
+ const hrd = decodeStat(stats, 2)
197
+ const com = decodeStat(stats, 3)
198
+ out += ` Cargo Capacity ${computeCargoBayCapacity(str, den, hrd, com)}`
199
+ break
200
+ }
201
+ case MODULE_HAULER: {
202
+ const res = decodeStat(stats, 0)
203
+ const pla = decodeStat(stats, 1)
204
+ const ref = decodeStat(stats, 2)
205
+ out += ` Capacity ${computeHaulerCapacity(res)} Efficiency ${computeHaulerEfficiency(pla)} Drain ${computeHaulerDrain(ref)}`
148
206
  break
149
207
  }
150
208
  case MODULE_WARP: {
@@ -152,6 +210,14 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
152
210
  out += ` Range ${computeWarpRange(stat)}`
153
211
  break
154
212
  }
213
+ case MODULE_BATTERY: {
214
+ const vol = decodeStat(stats, 0)
215
+ const thm = decodeStat(stats, 1)
216
+ const pla = decodeStat(stats, 2)
217
+ const ins = decodeStat(stats, 3)
218
+ out += ` Energy Capacity ${computeBatteryBankCapacity(vol, thm, pla, ins)}`
219
+ break
220
+ }
155
221
  }
156
222
  return out
157
223
  }
@@ -168,6 +234,14 @@ export function buildEntityDescription(
168
234
  baseCapacity = computeBaseCapacityShip(hullStats)
169
235
  } else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
170
236
  baseCapacity = computeBaseCapacityWarehouse(hullStats)
237
+ } else if (
238
+ itemId === ITEM_EXTRACTOR_T1_PACKED ||
239
+ itemId === ITEM_FACTORY_T1_PACKED ||
240
+ itemId === ITEM_CONTAINER_T1_PACKED
241
+ ) {
242
+ baseCapacity = computeBaseCapacityContainer(hullStats)
243
+ } else if (itemId === ITEM_CONTAINER_T2_PACKED) {
244
+ baseCapacity = computeBaseCapacityContainerT2(hullStats)
171
245
  }
172
246
 
173
247
  let out = entityDisplayName(itemId)
package/src/nft/index.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export * from './deserializers'
2
2
  export * from './description'
3
+ export * from './atomicdata'
4
+ export * from './atomicassets'
5
+ export * from './buildImmutableData'
@@ -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
+ }
@@ -0,0 +1,319 @@
1
+ import {UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
2
+ import {expect, test, describe} from 'bun:test'
3
+ import type {GathererStats} from '../types/capabilities'
4
+ import {ServerContract} from '../contracts'
5
+ import {calc_gather_duration} from '../capabilities/gathering'
6
+ import {planParallelGather, planParallelTransfer, type GatherPlanEntity} from './index'
7
+
8
+ function gathererLane(
9
+ slotIndex: number,
10
+ yieldVal: number,
11
+ drain: number,
12
+ depth: number
13
+ ): ServerContract.Types.gatherer_lane {
14
+ return ServerContract.Types.gatherer_lane.from({
15
+ slot_index: UInt8.from(slotIndex),
16
+ yield: UInt16.from(yieldVal),
17
+ drain: UInt32.from(drain),
18
+ depth: UInt16.from(depth),
19
+ output_pct: UInt16.from(100),
20
+ })
21
+ }
22
+
23
+ function loaderLane(
24
+ slotIndex: number,
25
+ mass: number,
26
+ thrust: number
27
+ ): ServerContract.Types.loader_lane {
28
+ return ServerContract.Types.loader_lane.from({
29
+ slot_index: UInt8.from(slotIndex),
30
+ mass: UInt32.from(mass),
31
+ thrust: UInt16.from(thrust),
32
+ output_pct: UInt16.from(100),
33
+ })
34
+ }
35
+
36
+ function energyStats(capacity: number, recharge: number): ServerContract.Types.energy_stats {
37
+ return ServerContract.Types.energy_stats.from({
38
+ capacity: UInt32.from(capacity),
39
+ recharge: UInt32.from(recharge),
40
+ })
41
+ }
42
+
43
+ interface EntityOverrides {
44
+ gatherer_lanes?: ServerContract.Types.gatherer_lane[]
45
+ loader_lanes?: ServerContract.Types.loader_lane[]
46
+ generator?: ServerContract.Types.energy_stats
47
+ energy?: number
48
+ lanes?: ServerContract.Types.lane[]
49
+ }
50
+
51
+ function entity(overrides: EntityOverrides = {}): GatherPlanEntity {
52
+ return {
53
+ gatherer_lanes: overrides.gatherer_lanes ?? [],
54
+ loader_lanes: overrides.loader_lanes ?? [],
55
+ generator: overrides.generator,
56
+ energy: overrides.energy !== undefined ? UInt16.from(overrides.energy) : undefined,
57
+ lanes: overrides.lanes ?? [],
58
+ coordinates: ServerContract.Types.coordinates.from({x: 0, y: 0}),
59
+ cargo: [],
60
+ cargomass: UInt32.from(0),
61
+ }
62
+ }
63
+
64
+ const NOW = new Date('2026-06-21T00:00:00.000Z')
65
+
66
+ describe('planParallelGather', () => {
67
+ test('sanity: single-gatherer qty 20 = ~35s matches calc_gather_duration', () => {
68
+ const gatherer: GathererStats = {
69
+ yield: UInt16.from(57),
70
+ drain: UInt32.from(500),
71
+ depth: UInt16.from(5000),
72
+ }
73
+ const dur = calc_gather_duration(gatherer, 228, 20, 0, 1000)
74
+ expect(Number(dur)).toBeCloseTo(35, 0)
75
+ })
76
+
77
+ test('two gatherers: quantities proportional to yield, durations within 1s', () => {
78
+ const YIELD1 = 200
79
+ const YIELD2 = 400
80
+ const DEPTH = 5000
81
+ const DRAIN = 500
82
+ const QUANTITY = 60
83
+ const STRATUM = 0
84
+
85
+ const e = entity({
86
+ gatherer_lanes: [
87
+ gathererLane(0, YIELD1, DRAIN, DEPTH),
88
+ gathererLane(1, YIELD2, DRAIN, DEPTH),
89
+ ],
90
+ generator: energyStats(10000, 100),
91
+ energy: 10000,
92
+ })
93
+
94
+ const plan = planParallelGather(e, {quantity: QUANTITY}, STRATUM, NOW)
95
+
96
+ expect(plan).toHaveLength(2)
97
+ expect(plan.reduce((s, p) => s + p.quantity, 0)).toBe(QUANTITY)
98
+
99
+ const q1 = plan.find((p) => p.slot === 0)!.quantity
100
+ const q2 = plan.find((p) => p.slot === 1)!.quantity
101
+ expect(q1 + q2).toBe(QUANTITY)
102
+ expect(q2 / q1).toBeCloseTo(YIELD2 / YIELD1, 0)
103
+
104
+ const ITEM_MASS = 228
105
+ const RICHNESS = 1000
106
+ const g1: GathererStats = {
107
+ yield: UInt16.from(YIELD1),
108
+ drain: UInt32.from(DRAIN),
109
+ depth: UInt16.from(DEPTH),
110
+ }
111
+ const g2: GathererStats = {
112
+ yield: UInt16.from(YIELD2),
113
+ drain: UInt32.from(DRAIN),
114
+ depth: UInt16.from(DEPTH),
115
+ }
116
+ const dur1 = Number(calc_gather_duration(g1, ITEM_MASS, q1, STRATUM, RICHNESS))
117
+ const dur2 = Number(calc_gather_duration(g2, ITEM_MASS, q2, STRATUM, RICHNESS))
118
+ expect(Math.abs(dur1 - dur2)).toBeLessThan(1)
119
+ })
120
+
121
+ test("'max' target: uses all reaching lanes, each slot gets >= 1 unit", () => {
122
+ const e = entity({
123
+ gatherer_lanes: [gathererLane(0, 200, 500, 5000), gathererLane(1, 300, 500, 5000)],
124
+ generator: energyStats(10000, 100),
125
+ energy: 10000,
126
+ })
127
+
128
+ const plan = planParallelGather(e, 'max', 0, NOW)
129
+
130
+ expect(plan.length).toBeGreaterThan(0)
131
+ for (const entry of plan) {
132
+ expect(entry.quantity).toBeGreaterThanOrEqual(1)
133
+ }
134
+ })
135
+
136
+ test('energy-starved: drops the lowest-yield lane(s) until the pool sustains the plan', () => {
137
+ // Full-Q energy: 3 lanes=>144, 2 lanes(drop yield=50)=>120; pool 130 fits 2 but not 3.
138
+ const e = entity({
139
+ gatherer_lanes: [
140
+ gathererLane(0, 50, 10000, 5000),
141
+ gathererLane(1, 100, 10000, 5000),
142
+ gathererLane(2, 100, 10000, 5000),
143
+ ],
144
+ generator: energyStats(10000, 1),
145
+ energy: 130,
146
+ })
147
+
148
+ const plan = planParallelGather(e, {quantity: 120}, 0, NOW)
149
+
150
+ // Lowest-yield lane (slot 0) dropped; the two yield-100 lanes survive.
151
+ expect(plan).toHaveLength(2)
152
+ expect(plan.find((p) => p.slot === 0)).toBeUndefined()
153
+ expect(plan.reduce((s, p) => s + p.quantity, 0)).toBe(120)
154
+ })
155
+
156
+ test('energy-starved: single lane caps quantity to the sustainable max', () => {
157
+ // energyPerUnit=1, full Q=120 costs 120 > pool 50, so quantity caps at 50.
158
+ const e = entity({
159
+ gatherer_lanes: [gathererLane(0, 100, 10000, 5000)],
160
+ generator: energyStats(10000, 1),
161
+ energy: 50,
162
+ })
163
+
164
+ const plan = planParallelGather(e, {quantity: 120}, 0, NOW)
165
+
166
+ expect(plan).toHaveLength(1)
167
+ expect(plan[0].slot).toBe(0)
168
+ expect(plan[0].quantity).toBe(50)
169
+ })
170
+
171
+ test('energy-starved: projected energy nets out a queued gather task', () => {
172
+ // A queued task costing 9970 leaves 30 projected energy => quantity caps at 30.
173
+ const queued = ServerContract.Types.lane.from({
174
+ lane_key: UInt8.from(0),
175
+ schedule: {
176
+ started: NOW.toISOString().slice(0, -1),
177
+ tasks: [
178
+ ServerContract.Types.task.from({
179
+ type: UInt8.from(5),
180
+ duration: UInt32.from(100),
181
+ cancelable: 0,
182
+ cargo: [],
183
+ entitytarget: {entity_type: 'ship', entity_id: UInt64.from(1)},
184
+ energy_cost: UInt32.from(9970),
185
+ }),
186
+ ],
187
+ },
188
+ })
189
+
190
+ const e = entity({
191
+ gatherer_lanes: [gathererLane(0, 100, 10000, 5000)],
192
+ generator: energyStats(10000, 1),
193
+ energy: 10000,
194
+ lanes: [queued],
195
+ })
196
+
197
+ const plan = planParallelGather(e, {quantity: 120}, 0, NOW)
198
+
199
+ expect(plan).toHaveLength(1)
200
+ expect(plan[0].quantity).toBe(30)
201
+ })
202
+
203
+ test('stratum filter: shallow lane excluded, only deep lane used', () => {
204
+ const e = entity({
205
+ gatherer_lanes: [gathererLane(0, 200, 500, 500), gathererLane(1, 300, 500, 5000)],
206
+ generator: energyStats(10000, 100),
207
+ energy: 10000,
208
+ })
209
+
210
+ const plan = planParallelGather(e, {quantity: 10}, 2000, NOW)
211
+
212
+ expect(plan).toHaveLength(1)
213
+ expect(plan[0].slot).toBe(1)
214
+ })
215
+
216
+ test('no reaching gatherers throws', () => {
217
+ const e = entity({
218
+ gatherer_lanes: [gathererLane(0, 200, 500, 100)],
219
+ generator: energyStats(10000, 100),
220
+ energy: 10000,
221
+ })
222
+
223
+ expect(() => planParallelGather(e, {quantity: 10}, 2000, NOW)).toThrow(
224
+ 'no gatherer reaches this stratum'
225
+ )
226
+ })
227
+
228
+ test('two identical gatherers: per-lane quantity halved, durations equal and within 1s', () => {
229
+ const YIELD = 200
230
+ const DEPTH = 5000
231
+ const DRAIN = 500
232
+ const QUANTITY = 60
233
+ const STRATUM = 0
234
+
235
+ const eSingle = entity({
236
+ gatherer_lanes: [gathererLane(0, YIELD, DRAIN, DEPTH)],
237
+ generator: energyStats(10000, 100),
238
+ energy: 10000,
239
+ })
240
+ const eDouble = entity({
241
+ gatherer_lanes: [
242
+ gathererLane(0, YIELD, DRAIN, DEPTH),
243
+ gathererLane(1, YIELD, DRAIN, DEPTH),
244
+ ],
245
+ generator: energyStats(10000, 100),
246
+ energy: 10000,
247
+ })
248
+
249
+ const planSingle = planParallelGather(eSingle, {quantity: QUANTITY}, STRATUM, NOW)
250
+ const planDouble = planParallelGather(eDouble, {quantity: QUANTITY}, STRATUM, NOW)
251
+
252
+ expect(planSingle).toHaveLength(1)
253
+ expect(planDouble).toHaveLength(2)
254
+
255
+ const singleQ = planSingle[0].quantity
256
+ const doubleQ1 = planDouble[0].quantity
257
+ const doubleQ2 = planDouble[1].quantity
258
+ expect(doubleQ1 + doubleQ2).toBe(QUANTITY)
259
+
260
+ const ITEM_MASS = 228
261
+ const RICHNESS = 1000
262
+ const g: GathererStats = {
263
+ yield: UInt16.from(YIELD),
264
+ drain: UInt32.from(DRAIN),
265
+ depth: UInt16.from(DEPTH),
266
+ }
267
+ const durSingle = Number(calc_gather_duration(g, ITEM_MASS, singleQ, STRATUM, RICHNESS))
268
+ const durDouble1 = Number(calc_gather_duration(g, ITEM_MASS, doubleQ1, STRATUM, RICHNESS))
269
+ const durDouble2 = Number(calc_gather_duration(g, ITEM_MASS, doubleQ2, STRATUM, RICHNESS))
270
+
271
+ expect(durDouble1).toBeCloseTo(durSingle / 2, 0)
272
+ expect(durDouble2).toBeCloseTo(durSingle / 2, 0)
273
+ expect(Math.abs(durDouble1 - durDouble2)).toBeLessThan(1)
274
+ })
275
+ })
276
+
277
+ describe('planParallelTransfer', () => {
278
+ test('two loader lanes: quantities proportional to thrust, sums to target', () => {
279
+ const THRUST1 = 100
280
+ const THRUST2 = 200
281
+ const QUANTITY = 90
282
+
283
+ const e = entity({
284
+ loader_lanes: [loaderLane(0, 500, THRUST1), loaderLane(1, 500, THRUST2)],
285
+ })
286
+
287
+ const plan = planParallelTransfer(e, {quantity: QUANTITY})
288
+
289
+ expect(plan).toHaveLength(2)
290
+ expect(plan.reduce((s, p) => s + p.quantity, 0)).toBe(QUANTITY)
291
+
292
+ const q1 = plan.find((p) => p.slot === 0)!.quantity
293
+ const q2 = plan.find((p) => p.slot === 1)!.quantity
294
+ expect(q2 / q1).toBeCloseTo(THRUST2 / THRUST1, 0)
295
+ })
296
+
297
+ test('no loader lanes: returns empty plan', () => {
298
+ const plan = planParallelTransfer(entity({loader_lanes: []}), {quantity: 10})
299
+ expect(plan).toHaveLength(0)
300
+ })
301
+
302
+ test('thrust=0 loader lane (no-loader/mobility case): returns empty plan', () => {
303
+ const plan = planParallelTransfer(entity({loader_lanes: [loaderLane(0, 500, 0)]}), {
304
+ quantity: 10,
305
+ })
306
+ expect(plan).toHaveLength(0)
307
+ })
308
+
309
+ test("'max' target: each loader lane gets >= 1 unit", () => {
310
+ const e = entity({
311
+ loader_lanes: [loaderLane(0, 500, 100), loaderLane(1, 500, 200)],
312
+ })
313
+ const plan = planParallelTransfer(e, 'max')
314
+ expect(plan.length).toBeGreaterThan(0)
315
+ for (const entry of plan) {
316
+ expect(entry.quantity).toBeGreaterThanOrEqual(1)
317
+ }
318
+ })
319
+ })