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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/lib/shipload.d.ts +398 -51
  2. package/lib/shipload.js +1481 -400
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +1442 -401
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +101 -20
  7. package/lib/testing.js +201 -57
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +201 -57
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/capabilities/crafting.ts +2 -3
  13. package/src/capabilities/gathering.test.ts +16 -0
  14. package/src/capabilities/gathering.ts +8 -11
  15. package/src/contracts/server.ts +147 -29
  16. package/src/coordinates/address.ts +88 -0
  17. package/src/coordinates/constants.test.ts +15 -0
  18. package/src/coordinates/constants.ts +23 -0
  19. package/src/coordinates/index.ts +15 -0
  20. package/src/coordinates/memo.test.ts +47 -0
  21. package/src/coordinates/memo.ts +20 -0
  22. package/src/coordinates/permutation.ts +77 -0
  23. package/src/coordinates/regions.ts +48 -0
  24. package/src/coordinates/sectors.ts +115 -0
  25. package/src/data/capability-formulas.ts +0 -1
  26. package/src/data/entities.json +4 -4
  27. package/src/data/items.json +5 -5
  28. package/src/data/recipes.json +39 -65
  29. package/src/derivation/capabilities.test.ts +133 -0
  30. package/src/derivation/capabilities.ts +66 -14
  31. package/src/derivation/rollups.test.ts +55 -0
  32. package/src/derivation/rollups.ts +56 -0
  33. package/src/derivation/wormhole.ts +115 -0
  34. package/src/entities/makers.ts +30 -3
  35. package/src/errors.ts +2 -0
  36. package/src/index-module.ts +38 -2
  37. package/src/managers/actions.ts +79 -5
  38. package/src/managers/construction.ts +6 -4
  39. package/src/managers/context.ts +9 -0
  40. package/src/managers/coordinates.ts +14 -0
  41. package/src/managers/plot.ts +2 -4
  42. package/src/nft/description.ts +25 -6
  43. package/src/planner/index.ts +127 -0
  44. package/src/planner/planner.test.ts +319 -0
  45. package/src/resolution/resolve-item.ts +4 -1
  46. package/src/scheduling/availability.ts +1 -1
  47. package/src/scheduling/cancel.test.ts +348 -0
  48. package/src/scheduling/cancel.ts +209 -0
  49. package/src/scheduling/lanes.test.ts +249 -0
  50. package/src/scheduling/lanes.ts +140 -2
  51. package/src/scheduling/projection.ts +75 -16
  52. package/src/scheduling/schedule.ts +3 -1
  53. package/src/shipload.ts +5 -0
  54. package/src/testing/projection-parity.ts +26 -2
  55. package/src/travel/travel.ts +116 -105
  56. package/src/types/capabilities.ts +23 -6
  57. package/src/types/entity.ts +3 -3
  58. package/src/types.ts +2 -1
  59. package/src/utils/system.ts +11 -0
@@ -0,0 +1,115 @@
1
+ import type {Checksum256Type} from '@wharfkit/antelope'
2
+ import {hash512} from '../utils/hash'
3
+
4
+ export const WH = {
5
+ RSIZE: 75,
6
+ ZONE: 16384,
7
+ THRESHOLD: 8192,
8
+ MIN_REACH: 50000,
9
+ TRANSIT_SPEED: 500,
10
+ } as const
11
+
12
+ const HALF = Math.round(Math.log2(WH.ZONE))
13
+ const MASK = WH.ZONE - 1
14
+
15
+ function roll16(seed: Checksum256Type, str: string): number {
16
+ const h = hash512(seed, str).array
17
+ return (h[0] << 8) | h[1]
18
+ }
19
+ function feistelF(seed: Checksum256Type, x: number, round: number, key: string): number {
20
+ return roll16(seed, `feistel-${key}-${round}-${x}`) & MASK
21
+ }
22
+ export function feistel(seed: Checksum256Type, idx: number, key: string): number {
23
+ let L = (idx >>> HALF) & MASK
24
+ let R = idx & MASK
25
+ for (let r = 0; r < 4; r++) {
26
+ const nR = L ^ feistelF(seed, R, r, key)
27
+ L = R
28
+ R = nR
29
+ }
30
+ return (L << HALF) | R
31
+ }
32
+ export function feistelInv(seed: Checksum256Type, idx: number, key: string): number {
33
+ let L = (idx >>> HALF) & MASK
34
+ let R = idx & MASK
35
+ for (let r = 3; r >= 0; r--) {
36
+ const nL = R ^ feistelF(seed, L, r, key)
37
+ R = L
38
+ L = nL
39
+ }
40
+ return (L << HALF) | R
41
+ }
42
+
43
+ type Region = {rx: number; ry: number}
44
+
45
+ export function regionOf(x: number, y: number): Region {
46
+ return {rx: Math.floor(x / WH.RSIZE), ry: Math.floor(y / WH.RSIZE)}
47
+ }
48
+ export function partnerRegion(seed: Checksum256Type, R: Region): Region {
49
+ const qx = Math.floor(R.rx / WH.ZONE)
50
+ const qy = Math.floor(R.ry / WH.ZONE)
51
+ const zx = qx * WH.ZONE
52
+ const zy = qy * WH.ZONE
53
+ const key = `${qx}:${qy}`
54
+ const idx = (R.ry - zy) * WH.ZONE + (R.rx - zx)
55
+ const p = feistelInv(seed, feistel(seed, idx, key) ^ 1, key)
56
+ return {rx: zx + (p % WH.ZONE), ry: zy + Math.floor(p / WH.ZONE)}
57
+ }
58
+ function regKey(R: Region): string {
59
+ return `${R.rx}:${R.ry}`
60
+ }
61
+ function pairKey(a: Region, b: Region): string {
62
+ const ka = regKey(a)
63
+ const kb = regKey(b)
64
+ return ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`
65
+ }
66
+ function endpointInRegion(seed: Checksum256Type, R: Region, key: string): {x: number; y: number} {
67
+ const h = hash512(seed, `wh-endpoint-${key}-${regKey(R)}`).array
68
+ const ox = ((h[0] << 24) | (h[1] << 16) | (h[2] << 8) | h[3]) >>> 0
69
+ const oy = ((h[4] << 24) | (h[5] << 16) | (h[6] << 8) | h[7]) >>> 0
70
+ return {x: R.rx * WH.RSIZE + (ox % WH.RSIZE), y: R.ry * WH.RSIZE + (oy % WH.RSIZE)}
71
+ }
72
+ function dist(a: {x: number; y: number}, b: {x: number; y: number}): number {
73
+ return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
74
+ }
75
+ function wormholeOfRegion(
76
+ seed: Checksum256Type,
77
+ R: Region
78
+ ): {A: {x: number; y: number}; B: {x: number; y: number}} | null {
79
+ const P = partnerRegion(seed, R)
80
+ if (P.rx === R.rx && P.ry === R.ry) return null
81
+ const key = pairKey(R, P)
82
+ if (roll16(seed, `wh-exists-${key}`) >= WH.THRESHOLD) return null
83
+ const A = endpointInRegion(seed, R, key)
84
+ const B = endpointInRegion(seed, P, key)
85
+ if (dist(A, B) < WH.MIN_REACH) return null
86
+ return {A, B}
87
+ }
88
+ export function wormholeAtRegionEndpoint(
89
+ seed: Checksum256Type,
90
+ rx: number,
91
+ ry: number
92
+ ): {from: {x: number; y: number}; to: {x: number; y: number}} | null {
93
+ const w = wormholeOfRegion(seed, {rx, ry})
94
+ if (!w) return null
95
+ return {from: w.A, to: w.B}
96
+ }
97
+ export function wormholeAt(
98
+ seed: Checksum256Type,
99
+ x: number,
100
+ y: number
101
+ ): {x: number; y: number} | null {
102
+ const w = wormholeOfRegion(seed, regionOf(x, y))
103
+ if (!w || w.A.x !== x || w.A.y !== y) return null
104
+ return w.B
105
+ }
106
+ export function isValidWormholePair(
107
+ seed: Checksum256Type,
108
+ ax: number,
109
+ ay: number,
110
+ bx: number,
111
+ by: number
112
+ ): boolean {
113
+ const to = wormholeAt(seed, ax, ay)
114
+ return to !== null && to.x === bx && to.y === by
115
+ }
@@ -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
  }
package/src/errors.ts CHANGED
@@ -56,6 +56,8 @@ export const GROUP_HAUL_CAPACITY_EXCEEDED =
56
56
  'Group travel requires sufficient hauler capacity for all non-self-propelled entities.'
57
57
  export const CANCEL_CONTAINS_GROUPED_TASK =
58
58
  'Cannot cancel range containing grouped task - cancel non-grouped tasks first.'
59
+ export const WOULD_STRAND = 'Cancelling this would leave a later task without the cargo it needs.'
60
+ export const WOULD_OVERFILL = 'Cancelling this would overfill the other entity with returned cargo.'
59
61
  export const WARP_NO_CAPABILITY = 'Entity does not have warp capability.'
60
62
  export const WARP_HAS_SCHEDULE = 'Entity must be idle to warp.'
61
63
  export const WARP_HAS_CARGO = 'Entity must have no cargo to warp.'
@@ -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
@@ -87,6 +85,7 @@ export type {EpochInfo} from './scheduling/epoch'
87
85
  export {
88
86
  getSystemName,
89
87
  hasSystem,
88
+ getLocationKind,
90
89
  getLocationType,
91
90
  getLocationTypeName,
92
91
  isGatherableLocation,
@@ -157,6 +156,7 @@ export {
157
156
  calc_flighttime,
158
157
  calc_loader_acceleration,
159
158
  calc_loader_flighttime,
159
+ calc_onesided_duration,
160
160
  calc_orbital_altitude,
161
161
  calc_rechargetime,
162
162
  calc_ship_acceleration,
@@ -164,6 +164,7 @@ export {
164
164
  calc_ship_mass,
165
165
  calc_ship_rechargetime,
166
166
  calc_transfer_duration,
167
+ calc_transit_duration,
167
168
  calculateFlightTime,
168
169
  calculateLoadTimeBreakdown,
169
170
  calculateRefuelingTime,
@@ -202,8 +203,17 @@ export {
202
203
  candidateLaneCompletesAt,
203
204
  laneKeyForModule,
204
205
  rawScheduleEnd,
206
+ resolveLaneGatherer,
207
+ resolveLaneCrafter,
208
+ resolveLaneLoader,
209
+ selectGatherLane,
205
210
  workerLaneKey,
206
211
  } from './scheduling/lanes'
212
+ export type {
213
+ ResolvedGathererLane,
214
+ ResolvedCrafterLane,
215
+ ResolvedLoaderLane,
216
+ } from './scheduling/lanes'
207
217
  export {ScheduleAccessor, createScheduleAccessor} from './scheduling/accessor'
208
218
  export {InventoryAccessor, createInventoryAccessor} from './entities/inventory-accessor'
209
219
  export type {HasCargo} from './entities/inventory-accessor'
@@ -232,6 +242,15 @@ export type {TaskCargoChange, TaskCargoDirection} from './scheduling/task-cargo'
232
242
  export {composeIdleResolve} from './scheduling/idle-resolve'
233
243
  export type {CounterpartLookup, IdleResolveTarget} from './scheduling/idle-resolve'
234
244
 
245
+ export {cancelEligibility, CancelBlockReason} from './scheduling/cancel'
246
+ export type {
247
+ CancelPlan,
248
+ CancelEffects,
249
+ CancelRefund,
250
+ CancelReleasedHold,
251
+ CancelEligibilityInput,
252
+ } from './scheduling/cancel'
253
+
235
254
  export {
236
255
  projectedCargoAvailableAt,
237
256
  availableForItem,
@@ -356,6 +375,19 @@ export {
356
375
  } from './derivation/capabilities'
357
376
  export type {GathererDepthParams, ComputedCapabilities} from './derivation/capabilities'
358
377
 
378
+ export {
379
+ WH,
380
+ feistel,
381
+ feistelInv,
382
+ regionOf,
383
+ partnerRegion,
384
+ wormholeAt,
385
+ wormholeAtRegionEndpoint,
386
+ isValidWormholePair,
387
+ } from './derivation/wormhole'
388
+
389
+ export {rollupGatherer, rollupCrafter, rollupLoaders} from './derivation/rollups'
390
+
359
391
  export {resolveItem} from './resolution/resolve-item'
360
392
  export type {
361
393
  ResolvedItem,
@@ -460,8 +492,12 @@ export {
460
492
  } from './data/tiers'
461
493
 
462
494
  export {formatMass, formatMassDelta, formatMassScaled, formatLocation} from './format'
495
+ export * from './coordinates'
463
496
 
464
497
  export {displayName, baseName, describeItem} from './resolution/display-name'
465
498
  export type {DescribeOptions} from './resolution/display-name'
466
499
 
467
500
  export * from './subscriptions'
501
+
502
+ export {planParallelGather, planParallelTransfer} from './planner'
503
+ export type {LanePlanEntry, PlanTarget, GatherPlanEntity} from './planner'
@@ -3,8 +3,10 @@ import {
3
3
  Checksum256,
4
4
  type Checksum256Type,
5
5
  Int64,
6
+ type Int64Type,
6
7
  Name,
7
8
  type NameType,
9
+ Transaction,
8
10
  UInt8,
9
11
  type UInt8Type,
10
12
  UInt16,
@@ -37,13 +39,17 @@ export class ActionsManager extends BaseManager {
37
39
  })
38
40
  }
39
41
 
40
- grouptravel(entities: EntityRefInput[], destination: CoordinatesType, recharge = true): Action {
41
- const entityRefs = entities.map((e) =>
42
+ private entityRefs(entities: EntityRefInput[]) {
43
+ return entities.map((e) =>
42
44
  ServerContract.Types.entity_ref.from({
43
45
  entity_type: e.entityType,
44
46
  entity_id: UInt64.from(e.entityId),
45
47
  })
46
48
  )
49
+ }
50
+
51
+ grouptravel(entities: EntityRefInput[], destination: CoordinatesType, recharge = true): Action {
52
+ const entityRefs = this.entityRefs(entities)
47
53
  const x = Int64.from(destination.x)
48
54
  const y = Int64.from(destination.y)
49
55
 
@@ -55,6 +61,44 @@ export class ActionsManager extends BaseManager {
55
61
  })
56
62
  }
57
63
 
64
+ transit(shipId: UInt64Type, entrance: CoordinatesType, exit: CoordinatesType): Action {
65
+ return this.server.action('transit', {
66
+ id: UInt64.from(shipId),
67
+ ax: Int64.from(entrance.x),
68
+ ay: Int64.from(entrance.y),
69
+ bx: Int64.from(exit.x),
70
+ by: Int64.from(exit.y),
71
+ })
72
+ }
73
+
74
+ grouptransit(
75
+ entities: EntityRefInput[],
76
+ entrance: CoordinatesType,
77
+ exit: CoordinatesType
78
+ ): Action {
79
+ const entityRefs = this.entityRefs(entities)
80
+ return this.server.action('grouptransit', {
81
+ entities: entityRefs,
82
+ ax: Int64.from(entrance.x),
83
+ ay: Int64.from(entrance.y),
84
+ bx: Int64.from(exit.x),
85
+ by: Int64.from(exit.y),
86
+ })
87
+ }
88
+
89
+ getwormhole(x: Int64Type, y: Int64Type): Action {
90
+ return this.server.action('getwormhole', {x: Int64.from(x), y: Int64.from(y)})
91
+ }
92
+
93
+ getdistance(origin: CoordinatesType, destination: CoordinatesType): Action {
94
+ return this.server.action('getdistance', {
95
+ ax: Int64.from(origin.x),
96
+ ay: Int64.from(origin.y),
97
+ bx: Int64.from(destination.x),
98
+ by: Int64.from(destination.y),
99
+ })
100
+ }
101
+
58
102
  resolve(entityId: UInt64Type, count?: UInt64Type): Action {
59
103
  const params: ServerContract.ActionParams.resolve = {
60
104
  id: UInt64.from(entityId),
@@ -139,13 +183,39 @@ export class ActionsManager extends BaseManager {
139
183
  sourceId: UInt64Type,
140
184
  destinationId: UInt64Type,
141
185
  stratum: UInt16Type,
142
- quantity: UInt32Type
186
+ quantity: UInt32Type,
187
+ slot?: UInt8Type
143
188
  ): Action {
144
- return this.server.action('gather', {
189
+ const params: ServerContract.ActionParams.gather = {
145
190
  source_id: UInt64.from(sourceId),
146
191
  destination_id: UInt64.from(destinationId),
147
192
  stratum: UInt16.from(stratum),
148
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,
149
219
  })
150
220
  }
151
221
 
@@ -165,7 +235,8 @@ export class ActionsManager extends BaseManager {
165
235
  recipeId: number,
166
236
  quantity: number,
167
237
  inputs: ServerContract.ActionParams.Type.cargo_item[],
168
- target?: UInt64Type
238
+ target?: UInt64Type,
239
+ slot?: UInt8Type
169
240
  ): Action {
170
241
  const params: ServerContract.ActionParams.craft = {
171
242
  id: UInt64.from(entityId),
@@ -176,6 +247,9 @@ export class ActionsManager extends BaseManager {
176
247
  if (target !== undefined) {
177
248
  params.target = UInt64.from(target)
178
249
  }
250
+ if (slot !== undefined) {
251
+ params.slot = UInt8.from(slot)
252
+ }
179
253
  return this.server.action('craft', params)
180
254
  }
181
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
+ }