@shipload/sdk 1.0.0-next.5 → 1.0.0-next.51

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 (145) 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 +3167 -1353
  7. package/lib/shipload.js +13319 -5640
  8. package/lib/shipload.js.map +1 -1
  9. package/lib/shipload.m.js +13091 -5589
  10. package/lib/shipload.m.js.map +1 -1
  11. package/lib/testing.d.ts +1037 -0
  12. package/lib/testing.js +4363 -0
  13. package/lib/testing.js.map +1 -0
  14. package/lib/testing.m.js +4357 -0
  15. package/lib/testing.m.js.map +1 -0
  16. package/package.json +20 -2
  17. package/src/capabilities/craftable.ts +51 -0
  18. package/src/capabilities/crafting.test.ts +7 -0
  19. package/src/capabilities/crafting.ts +6 -7
  20. package/src/capabilities/gathering.test.ts +29 -0
  21. package/src/capabilities/gathering.ts +37 -19
  22. package/src/capabilities/index.ts +0 -1
  23. package/src/capabilities/modules.ts +22 -0
  24. package/src/capabilities/storage.ts +16 -1
  25. package/src/contracts/platform.ts +231 -3
  26. package/src/contracts/server.ts +1178 -511
  27. package/src/coordinates/address.ts +88 -0
  28. package/src/coordinates/constants.test.ts +15 -0
  29. package/src/coordinates/constants.ts +23 -0
  30. package/src/coordinates/index.ts +15 -0
  31. package/src/coordinates/memo.test.ts +47 -0
  32. package/src/coordinates/memo.ts +20 -0
  33. package/src/coordinates/permutation.ts +77 -0
  34. package/src/coordinates/regions.ts +48 -0
  35. package/src/coordinates/sectors.ts +115 -0
  36. package/src/data/capabilities.ts +31 -16
  37. package/src/data/capability-formulas.ts +21 -14
  38. package/src/data/catalog.ts +0 -5
  39. package/src/data/colors.ts +13 -47
  40. package/src/data/entities.json +154 -10
  41. package/src/data/item-ids.ts +31 -12
  42. package/src/data/items.json +192 -74
  43. package/src/data/kind-registry.json +131 -0
  44. package/src/data/kind-registry.ts +172 -0
  45. package/src/data/metadata.ts +166 -48
  46. package/src/data/recipes-runtime.ts +5 -23
  47. package/src/data/recipes.json +1003 -163
  48. package/src/derivation/build-methods.test.ts +13 -0
  49. package/src/derivation/build-methods.ts +48 -0
  50. package/src/derivation/capabilities.test.ts +151 -0
  51. package/src/derivation/capabilities.ts +527 -0
  52. package/src/derivation/capability-mappings.ts +58 -16
  53. package/src/derivation/crafting.ts +36 -33
  54. package/src/derivation/index.ts +28 -2
  55. package/src/derivation/recipe-usage.test.ts +81 -0
  56. package/src/derivation/recipe-usage.ts +141 -0
  57. package/src/derivation/reserve-regen.ts +34 -0
  58. package/src/derivation/resources.ts +98 -19
  59. package/src/derivation/rollups.test.ts +55 -0
  60. package/src/derivation/rollups.ts +56 -0
  61. package/src/derivation/stars.test.ts +67 -0
  62. package/src/derivation/stars.ts +25 -0
  63. package/src/derivation/stats.ts +6 -6
  64. package/src/derivation/stratum.ts +26 -22
  65. package/src/derivation/tiers.ts +40 -7
  66. package/src/derivation/upgrades.ts +14 -0
  67. package/src/derivation/wormhole.ts +141 -0
  68. package/src/entities/entity.ts +98 -0
  69. package/src/entities/gamestate.ts +3 -28
  70. package/src/entities/makers.ts +131 -134
  71. package/src/entities/slot-multiplier.ts +43 -0
  72. package/src/errors.ts +12 -16
  73. package/src/format.ts +26 -4
  74. package/src/index-module.ts +294 -34
  75. package/src/managers/actions.ts +616 -105
  76. package/src/managers/base.ts +6 -2
  77. package/src/managers/cluster.test.ts +39 -0
  78. package/src/managers/cluster.ts +53 -0
  79. package/src/managers/construction-types.ts +80 -0
  80. package/src/managers/construction.ts +412 -0
  81. package/src/managers/context.ts +29 -1
  82. package/src/managers/coordinates.ts +14 -0
  83. package/src/managers/entities.ts +27 -66
  84. package/src/managers/epochs.ts +40 -0
  85. package/src/managers/flatten-gather-plan.test.ts +80 -0
  86. package/src/managers/index.ts +20 -1
  87. package/src/managers/locations.ts +25 -29
  88. package/src/managers/nft.test.ts +14 -0
  89. package/src/managers/nft.ts +70 -0
  90. package/src/managers/players.ts +25 -0
  91. package/src/managers/plot.ts +122 -0
  92. package/src/nft/atomicassets.abi.json +1342 -0
  93. package/src/nft/atomicassets.ts +237 -0
  94. package/src/nft/atomicdata.ts +130 -0
  95. package/src/nft/buildImmutableData.ts +336 -0
  96. package/src/nft/description.ts +104 -36
  97. package/src/nft/index.ts +3 -0
  98. package/src/planner/index.ts +242 -0
  99. package/src/planner/planner.test.ts +325 -0
  100. package/src/resolution/describe-module.ts +43 -25
  101. package/src/resolution/display-name.ts +38 -10
  102. package/src/resolution/resolve-item.test.ts +48 -0
  103. package/src/resolution/resolve-item.ts +74 -47
  104. package/src/scan/index.ts +244 -0
  105. package/src/scan/scan-wasm.base64.ts +2 -0
  106. package/src/scheduling/accessor.ts +65 -23
  107. package/src/scheduling/availability.ts +108 -0
  108. package/src/scheduling/cancel.test.ts +348 -0
  109. package/src/scheduling/cancel.ts +209 -0
  110. package/src/scheduling/energy.ts +47 -0
  111. package/src/scheduling/idle-resolve.ts +45 -0
  112. package/src/scheduling/lane-core.ts +128 -0
  113. package/src/scheduling/lanes.test.ts +249 -0
  114. package/src/scheduling/lanes.ts +198 -0
  115. package/src/scheduling/projection.ts +210 -107
  116. package/src/scheduling/schedule.ts +240 -117
  117. package/src/scheduling/task-cargo.ts +46 -0
  118. package/src/scheduling/unwrap.test.ts +86 -0
  119. package/src/scheduling/unwrap.ts +190 -0
  120. package/src/shipload.ts +26 -1
  121. package/src/subscriptions/manager.cluster.test.ts +51 -0
  122. package/src/subscriptions/manager.ts +245 -172
  123. package/src/subscriptions/mappers.ts +5 -8
  124. package/src/subscriptions/types.ts +28 -3
  125. package/src/testing/catalog-hash.ts +19 -0
  126. package/src/testing/index.ts +2 -0
  127. package/src/testing/projection-parity.ts +167 -0
  128. package/src/travel/reach.ts +23 -0
  129. package/src/travel/route-planner.ts +277 -0
  130. package/src/travel/route-simulator.ts +174 -0
  131. package/src/travel/travel.ts +175 -118
  132. package/src/types/capabilities.ts +29 -6
  133. package/src/types/entity.ts +3 -3
  134. package/src/types/index.ts +0 -1
  135. package/src/types.ts +39 -14
  136. package/src/utils/cargo.test.ts +14 -0
  137. package/src/utils/cargo.ts +29 -0
  138. package/src/utils/display-name.ts +70 -0
  139. package/src/utils/system.ts +36 -24
  140. package/src/capabilities/loading.ts +0 -8
  141. package/src/entities/container.ts +0 -123
  142. package/src/entities/ship-deploy.ts +0 -295
  143. package/src/entities/ship.ts +0 -221
  144. package/src/entities/warehouse.ts +0 -127
  145. package/src/types/entity-traits.ts +0 -69
@@ -4,6 +4,7 @@ import {
4
4
  MODULE_ENGINE,
5
5
  MODULE_GATHERER,
6
6
  MODULE_GENERATOR,
7
+ MODULE_BATTERY,
7
8
  MODULE_HAULER,
8
9
  MODULE_LOADER,
9
10
  MODULE_STORAGE,
@@ -14,17 +15,25 @@ import {
14
15
  ITEM_CONTAINER_T2_PACKED,
15
16
  ITEM_CRAFTER_T1,
16
17
  ITEM_ENGINE_T1,
18
+ ITEM_EXTRACTOR_T1_PACKED,
19
+ ITEM_FACTORY_T1_PACKED,
17
20
  ITEM_GATHERER_T1,
21
+ ITEM_GATHERER_T2,
18
22
  ITEM_GENERATOR_T1,
23
+ ITEM_HAULER_SHIP_T2_PACKED,
19
24
  ITEM_HAULER_T1,
25
+ ITEM_HAULER_T2,
26
+ ITEM_BATTERY_T1,
27
+ ITEM_LAUNCHER_T1,
20
28
  ITEM_LOADER_T1,
29
+ ITEM_PROSPECTOR_T2_PACKED,
21
30
  ITEM_SHIP_T1_PACKED,
22
31
  ITEM_STORAGE_T1,
23
32
  ITEM_WAREHOUSE_T1_PACKED,
24
33
  ITEM_WARP_T1,
25
34
  } from '../data/item-ids'
26
35
  import {decodeStat} from '../derivation/crafting'
27
- import {gathererDepthForTier} from '../entities/ship-deploy'
36
+ import {gathererDepthForTier} from '../derivation/capabilities'
28
37
  import {getItem} from '../data/catalog'
29
38
 
30
39
  function idiv(a: number, b: number): number {
@@ -33,48 +42,83 @@ function idiv(a: number, b: number): number {
33
42
 
34
43
  export function computeBaseHullmass(stats: bigint): number {
35
44
  const density = decodeStat(stats, 1)
36
- return 25000 + 75 * density
45
+ return 100000 - 75 * density
37
46
  }
38
47
 
39
48
  export function computeBaseCapacityShip(stats: bigint): number {
40
- const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
41
- return Math.floor(1_000_000 * 10 ** (s / 2997))
49
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
50
+ return Math.floor(5_000_000 * 6 ** (s / 1998))
51
+ }
52
+
53
+ export function computeBaseCapacityContainer(stats: bigint): number {
54
+ const s = decodeStat(stats, 0) + decodeStat(stats, 2)
55
+ return Math.floor(22_000_000 * 6 ** (s / 1998))
42
56
  }
43
57
 
44
58
  export function computeBaseCapacityWarehouse(stats: bigint): number {
45
- const s = decodeStat(stats, 0) + decodeStat(stats, 2) + decodeStat(stats, 3)
46
- 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))
47
61
  }
48
62
 
49
63
  export const computeEngineThrust = (vol: number): number => 400 + idiv(vol * 3, 4)
50
- export const computeEngineDrain = (thm: number): number => Math.max(30, 50 - idiv(thm, 70))
51
- export const computeGeneratorCap = (com: number): number => 300 + idiv(com, 6)
52
- 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 => 1300 + idiv(com, 2)
76
+ export const computeGeneratorRech = (fin: number): number => 2 * (1 + idiv(fin * 3, 1000))
53
77
  export const computeGathererYield = (str: number): number => 200 + str
54
78
  export const computeGathererDrain = (con: number): number =>
55
- Math.max(250, 1250 - idiv(con * 25, 20))
79
+ 2 * Math.max(250, 1250 - idiv(con * 25, 20))
56
80
  export const computeGathererDepth = (tol: number, tier: number): number =>
57
81
  gathererDepthForTier(tol, tier)
58
- export const computeGathererSpeed = (ref: number): number => 100 + idiv(ref * 4, 5)
59
82
  export const computeLoaderMass = (ins: number): number => Math.max(200, 2000 - ins * 2)
60
- export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla, 500)
83
+ export const computeLoaderThrust = (pla: number): number => 1 + idiv(pla * pla, 10000)
61
84
  export const computeCrafterSpeed = (rea: number): number => 100 + idiv(rea * 4, 5)
62
85
  export const computeCrafterDrain = (fin: number): number => Math.max(5, 30 - idiv(fin, 33))
63
- export const computeHaulerCapacity = (fin: number): number => Math.max(1, 1 + idiv(fin, 400))
86
+ export const computeHaulerCapacity = (fin: number, tier: number): number =>
87
+ Math.max(tier, tier + idiv(fin, 400))
64
88
  export const computeHaulerEfficiency = (con: number): number => 2000 + con * 6
65
89
  export const computeHaulerDrain = (com: number): number => Math.max(3, 15 - idiv(com, 80))
66
90
  export const computeWarpRange = (stat: number): number => 100 + stat * 3
91
+ export const computeCargoBayCapacity = (
92
+ strength: number,
93
+ density: number,
94
+ hardness: number,
95
+ cohesion: number
96
+ ): number => 10_000_000 + idiv((strength + density + hardness + cohesion) * 50_000_000, 3996)
97
+ export const computeBatteryBankCapacity = (
98
+ volatility: number,
99
+ thermal: number,
100
+ plasticity: number,
101
+ insulation: number
102
+ ): number => 2_500 + idiv((volatility + thermal + plasticity + insulation) * 7_500, 3996)
67
103
 
68
104
  export function entityDisplayName(itemId: number): string {
69
105
  switch (itemId) {
70
106
  case ITEM_SHIP_T1_PACKED:
71
- return 'Ship'
107
+ return 'Roustabout'
72
108
  case ITEM_WAREHOUSE_T1_PACKED:
73
109
  return 'Warehouse'
110
+ case ITEM_EXTRACTOR_T1_PACKED:
111
+ return 'Mining Rig'
112
+ case ITEM_FACTORY_T1_PACKED:
113
+ return 'Factory'
74
114
  case ITEM_CONTAINER_T1_PACKED:
75
115
  return 'Container'
76
116
  case ITEM_CONTAINER_T2_PACKED:
77
117
  return 'Container'
118
+ case ITEM_PROSPECTOR_T2_PACKED:
119
+ return 'Prospector'
120
+ case ITEM_HAULER_SHIP_T2_PACKED:
121
+ return 'Hauler'
78
122
  default:
79
123
  return 'Entity'
80
124
  }
@@ -85,19 +129,25 @@ export function moduleDisplayName(itemId: number): string {
85
129
  case ITEM_ENGINE_T1:
86
130
  return 'Engine'
87
131
  case ITEM_GENERATOR_T1:
88
- return 'Generator'
132
+ return 'Power Core'
89
133
  case ITEM_GATHERER_T1:
90
- return 'Gatherer'
134
+ case ITEM_GATHERER_T2:
135
+ return 'Limpet Bay'
91
136
  case ITEM_LOADER_T1:
92
- return 'Loader'
137
+ return 'Shuttle Bay'
93
138
  case ITEM_CRAFTER_T1:
94
- return 'Crafter'
139
+ return 'Fabricator'
95
140
  case ITEM_STORAGE_T1:
96
- return 'Storage'
141
+ return 'Cargo Hold'
97
142
  case ITEM_HAULER_T1:
98
- return 'Hauler'
143
+ case ITEM_HAULER_T2:
144
+ return 'Tractor Beam'
99
145
  case ITEM_WARP_T1:
100
- return 'Warp'
146
+ return 'Warp Drive'
147
+ case ITEM_BATTERY_T1:
148
+ return 'Battery Bank'
149
+ case ITEM_LAUNCHER_T1:
150
+ return 'Drive Coil'
101
151
  default:
102
152
  return 'Module'
103
153
  }
@@ -129,13 +179,12 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
129
179
  case MODULE_GATHERER: {
130
180
  const str = decodeStat(stats, 0)
131
181
  const tol = decodeStat(stats, 1)
132
- const con = decodeStat(stats, 3)
133
- const ref = decodeStat(stats, 4)
182
+ const con = decodeStat(stats, 2)
134
183
  const tier = getItem(itemId).tier
135
184
  out += ` Yield ${computeGathererYield(str)} Depth ${computeGathererDepth(
136
185
  tol,
137
186
  tier
138
- )} Speed ${computeGathererSpeed(ref)} Drain ${computeGathererDrain(con)}`
187
+ )} Drain ${computeGathererDrain(con)}`
139
188
  break
140
189
  }
141
190
  case MODULE_LOADER: {
@@ -146,24 +195,24 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
146
195
  }
147
196
  case MODULE_CRAFTER: {
148
197
  const rea = decodeStat(stats, 0)
149
- const com = decodeStat(stats, 1)
150
- out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(com)}`
198
+ const con = decodeStat(stats, 1)
199
+ out += ` Speed ${computeCrafterSpeed(rea)} Drain ${computeCrafterDrain(con)}`
151
200
  break
152
201
  }
153
202
  case MODULE_STORAGE: {
154
203
  const str = decodeStat(stats, 0)
155
- const fin = decodeStat(stats, 2)
156
- const sat = decodeStat(stats, 3)
157
- const sum = str + fin + sat
158
- const pct = 10 + idiv(sum * 10, 2997)
159
- out += ` +${pct}% capacity`
204
+ const den = decodeStat(stats, 1)
205
+ const hrd = decodeStat(stats, 2)
206
+ const com = decodeStat(stats, 3)
207
+ out += ` Cargo Capacity ${computeCargoBayCapacity(str, den, hrd, com)}`
160
208
  break
161
209
  }
162
210
  case MODULE_HAULER: {
163
- const fin = decodeStat(stats, 0)
164
- const con = decodeStat(stats, 1)
165
- const com = decodeStat(stats, 2)
166
- out += ` Capacity ${computeHaulerCapacity(fin)} Efficiency ${computeHaulerEfficiency(con)} Drain ${computeHaulerDrain(com)}`
211
+ const res = decodeStat(stats, 0)
212
+ const pla = decodeStat(stats, 1)
213
+ const ref = decodeStat(stats, 2)
214
+ const tier = getItem(itemId).tier
215
+ out += ` Capacity ${computeHaulerCapacity(res, tier)} Efficiency ${computeHaulerEfficiency(pla)} Drain ${computeHaulerDrain(ref)}`
167
216
  break
168
217
  }
169
218
  case MODULE_WARP: {
@@ -171,6 +220,14 @@ export function formatModuleLine(slot: number, itemId: number, stats: bigint): s
171
220
  out += ` Range ${computeWarpRange(stat)}`
172
221
  break
173
222
  }
223
+ case MODULE_BATTERY: {
224
+ const vol = decodeStat(stats, 0)
225
+ const thm = decodeStat(stats, 1)
226
+ const pla = decodeStat(stats, 2)
227
+ const ins = decodeStat(stats, 3)
228
+ out += ` Energy Capacity ${computeBatteryBankCapacity(vol, thm, pla, ins)}`
229
+ break
230
+ }
174
231
  }
175
232
  return out
176
233
  }
@@ -183,10 +240,21 @@ export function buildEntityDescription(
183
240
  ): string {
184
241
  const hullMass = computeBaseHullmass(hullStats)
185
242
  let baseCapacity = 0
186
- if (itemId === ITEM_SHIP_T1_PACKED) {
243
+ if (
244
+ itemId === ITEM_SHIP_T1_PACKED ||
245
+ itemId === ITEM_PROSPECTOR_T2_PACKED ||
246
+ itemId === ITEM_HAULER_SHIP_T2_PACKED
247
+ ) {
187
248
  baseCapacity = computeBaseCapacityShip(hullStats)
188
249
  } else if (itemId === ITEM_WAREHOUSE_T1_PACKED) {
189
250
  baseCapacity = computeBaseCapacityWarehouse(hullStats)
251
+ } else if (
252
+ itemId === ITEM_EXTRACTOR_T1_PACKED ||
253
+ itemId === ITEM_FACTORY_T1_PACKED ||
254
+ itemId === ITEM_CONTAINER_T1_PACKED ||
255
+ itemId === ITEM_CONTAINER_T2_PACKED
256
+ ) {
257
+ baseCapacity = computeBaseCapacityContainer(hullStats)
190
258
  }
191
259
 
192
260
  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,242 @@
1
+ import type {GathererStats} from '../types/capabilities'
2
+ import type {ServerContract} from '../contracts'
3
+ import {calc_gather_duration, calc_gather_energy} from '../capabilities/gathering'
4
+ import {calc_rechargetime} from '../travel/travel'
5
+ import {projectRemainingAt, type Projectable} from '../scheduling/projection'
6
+
7
+ export interface LanePlanEntry {
8
+ slot: number
9
+ quantity: number
10
+ }
11
+
12
+ export type PlanTarget = {quantity: number} | 'max'
13
+
14
+ export interface GatherPlanEntity extends Projectable {
15
+ gatherer_lanes: ServerContract.Types.gatherer_lane[]
16
+ loader_lanes: ServerContract.Types.loader_lane[]
17
+ }
18
+
19
+ export interface GatherLimpet {
20
+ slot: number
21
+ quantity: number
22
+ durationSeconds: number
23
+ }
24
+
25
+ export interface GatherCycle {
26
+ rechargeBefore: boolean
27
+ rechargeSeconds: number
28
+ limpets: GatherLimpet[]
29
+ gatherSeconds: number
30
+ batchOre: number
31
+ }
32
+
33
+ export type FillCap = 'reserve' | 'hold' | 'requested'
34
+
35
+ export interface GatherPlan {
36
+ cycles: GatherCycle[]
37
+ cycleCount: number
38
+ totalOre: number
39
+ totalSeconds: number
40
+ cap: FillCap
41
+ reachingCount: number
42
+ totalLimpets: number
43
+ warnings: string[]
44
+ }
45
+
46
+ export interface BuildGatherPlanOpts {
47
+ richness: number
48
+ itemMass: number
49
+ holdRoom: number
50
+ reserveRemaining: number
51
+ now: Date
52
+ }
53
+
54
+ const MAX_CYCLES = 10_000
55
+
56
+ const MAX_TRANSFER_QTY = 10000
57
+
58
+ export function allocateProportional(
59
+ lanes: {slot: number; weight: number}[],
60
+ total: number
61
+ ): LanePlanEntry[] {
62
+ if (lanes.length === 0) return []
63
+ const weightSum = lanes.reduce((s, l) => s + l.weight, 0)
64
+ if (weightSum === 0) return []
65
+
66
+ const entries: LanePlanEntry[] = lanes.map((l) => ({
67
+ slot: l.slot,
68
+ quantity: Math.floor((total * l.weight) / weightSum),
69
+ }))
70
+
71
+ let remainder = total - entries.reduce((s, e) => s + e.quantity, 0)
72
+ for (let i = 0; remainder > 0; i = (i + 1) % entries.length) {
73
+ entries[i].quantity++
74
+ remainder--
75
+ }
76
+
77
+ return entries
78
+ }
79
+
80
+ function laneStats(lane: ServerContract.Types.gatherer_lane): GathererStats {
81
+ return lane as unknown as GathererStats
82
+ }
83
+
84
+ export function gatherEnergyCost(
85
+ lane: ServerContract.Types.gatherer_lane,
86
+ quantity: number,
87
+ stratum: number,
88
+ itemMass: number,
89
+ richness: number
90
+ ): number {
91
+ if (quantity <= 0) return 0
92
+ const dur = Number(calc_gather_duration(laneStats(lane), itemMass, quantity, stratum, richness))
93
+ return Number(calc_gather_energy(laneStats(lane), dur))
94
+ }
95
+
96
+ export function splitCost(
97
+ reaching: ServerContract.Types.gatherer_lane[],
98
+ quantity: number,
99
+ stratum: number,
100
+ itemMass: number,
101
+ richness: number
102
+ ): number {
103
+ if (quantity <= 0) return 0
104
+ const weights = reaching.map((l) => ({
105
+ slot: l.slot_index.toNumber(),
106
+ weight: l.yield.toNumber(),
107
+ }))
108
+ const split = allocateProportional(weights, quantity)
109
+ return split.reduce((sum, e) => {
110
+ const lane = reaching.find((l) => l.slot_index.toNumber() === e.slot)!
111
+ return sum + gatherEnergyCost(lane, e.quantity, stratum, itemMass, richness)
112
+ }, 0)
113
+ }
114
+
115
+ // Binary search is exact because splitCost is monotonic non-decreasing in Q.
116
+ export function maxQtyForCharge(
117
+ reaching: ServerContract.Types.gatherer_lane[],
118
+ hi: number,
119
+ capacity: number,
120
+ stratum: number,
121
+ itemMass: number,
122
+ richness: number
123
+ ): number {
124
+ if (hi <= 0) return 0
125
+ if (splitCost(reaching, hi, stratum, itemMass, richness) <= capacity) return hi
126
+ let lo = 0
127
+ let high = hi
128
+ while (lo < high) {
129
+ const mid = Math.ceil((lo + high) / 2)
130
+ if (splitCost(reaching, mid, stratum, itemMass, richness) <= capacity) lo = mid
131
+ else high = mid - 1
132
+ }
133
+ return lo
134
+ }
135
+
136
+ export function buildGatherPlan(
137
+ entity: GatherPlanEntity,
138
+ stratum: number,
139
+ target: PlanTarget,
140
+ opts: BuildGatherPlanOpts
141
+ ): GatherPlan {
142
+ const {richness, itemMass, holdRoom, reserveRemaining, now} = opts
143
+ const warnings: string[] = []
144
+
145
+ const reaching = entity.gatherer_lanes.filter((l) => l.depth.toNumber() >= stratum)
146
+ if (reaching.length === 0) throw new Error('no gatherer reaches this stratum')
147
+ if (!entity.generator) throw new Error('entity has no generator')
148
+
149
+ const blocked = entity.gatherer_lanes.length - reaching.length
150
+ if (blocked > 0) {
151
+ warnings.push(
152
+ `${blocked} of ${entity.gatherer_lanes.length} limpets can't reach this depth`
153
+ )
154
+ }
155
+
156
+ const capacity = entity.generator.capacity.toNumber()
157
+ const rechargeRate = entity.generator.recharge.toNumber()
158
+ let energy = Number(projectRemainingAt(entity, now).energy)
159
+
160
+ const requested = target === 'max' ? Infinity : target.quantity
161
+ let fillTarget = requested
162
+ let cap: FillCap = 'requested'
163
+ if (holdRoom < fillTarget) {
164
+ fillTarget = holdRoom
165
+ cap = 'hold'
166
+ }
167
+ if (reserveRemaining < fillTarget) {
168
+ fillTarget = reserveRemaining
169
+ cap = 'reserve'
170
+ }
171
+ fillTarget = Math.max(0, Math.floor(fillTarget))
172
+
173
+ const cycles: GatherCycle[] = []
174
+ let remaining = fillTarget
175
+ let guard = 0
176
+ while (remaining > 0 && guard++ < MAX_CYCLES) {
177
+ const batch = maxQtyForCharge(reaching, remaining, capacity, stratum, itemMass, richness)
178
+ if (batch <= 0) {
179
+ warnings.push('a single gather cannot fit within one full charge')
180
+ break
181
+ }
182
+
183
+ const costFull = splitCost(reaching, batch, stratum, itemMass, richness)
184
+ const rechargeBefore = energy < costFull
185
+ const rechargeSeconds = rechargeBefore
186
+ ? Number(calc_rechargetime(capacity, energy, rechargeRate))
187
+ : 0
188
+ if (rechargeBefore) energy = capacity
189
+
190
+ const weights = reaching.map((l) => ({
191
+ slot: l.slot_index.toNumber(),
192
+ weight: l.yield.toNumber(),
193
+ }))
194
+ const split = allocateProportional(weights, batch).filter((e) => e.quantity > 0)
195
+ const limpets: GatherLimpet[] = split.map((e) => {
196
+ const lane = reaching.find((l) => l.slot_index.toNumber() === e.slot)!
197
+ const dur = Number(
198
+ calc_gather_duration(laneStats(lane), itemMass, e.quantity, stratum, richness)
199
+ )
200
+ return {slot: e.slot, quantity: e.quantity, durationSeconds: dur}
201
+ })
202
+ const actualCost = limpets.reduce((sum, l) => {
203
+ const lane = reaching.find((r) => r.slot_index.toNumber() === l.slot)!
204
+ return sum + Number(calc_gather_energy(laneStats(lane), l.durationSeconds))
205
+ }, 0)
206
+ energy = Math.max(0, energy - actualCost)
207
+
208
+ const gatherSeconds = limpets.reduce((m, l) => Math.max(m, l.durationSeconds), 0)
209
+ cycles.push({rechargeBefore, rechargeSeconds, limpets, gatherSeconds, batchOre: batch})
210
+ remaining -= batch
211
+ }
212
+
213
+ const totalOre = cycles.reduce((s, c) => s + c.batchOre, 0)
214
+ const totalSeconds = cycles.reduce((s, c) => s + c.rechargeSeconds + c.gatherSeconds, 0)
215
+ return {
216
+ cycles,
217
+ cycleCount: cycles.length,
218
+ totalOre,
219
+ totalSeconds,
220
+ cap,
221
+ reachingCount: reaching.length,
222
+ totalLimpets: entity.gatherer_lanes.length,
223
+ warnings,
224
+ }
225
+ }
226
+
227
+ export function planParallelTransfer(
228
+ entity: GatherPlanEntity,
229
+ target: PlanTarget
230
+ ): LanePlanEntry[] {
231
+ const lanes = entity.loader_lanes.filter((l) => l.thrust.toNumber() > 0)
232
+ if (lanes.length === 0) return []
233
+
234
+ const requestedQty = target === 'max' ? MAX_TRANSFER_QTY : target.quantity
235
+
236
+ const laneWeights = lanes.map((l) => ({
237
+ slot: l.slot_index.toNumber(),
238
+ weight: l.thrust.toNumber(),
239
+ }))
240
+
241
+ return allocateProportional(laneWeights, requestedQty).filter((e) => e.quantity > 0)
242
+ }