@shipload/sdk 1.0.0-next.65 → 1.0.0-next.67

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 (85) hide show
  1. package/lib/shipload.d.ts +786 -321
  2. package/lib/shipload.js +2731 -1305
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +2644 -1286
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +223 -146
  7. package/lib/testing.js +907 -563
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +907 -563
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/capabilities/crafting.test.ts +51 -6
  13. package/src/capabilities/crafting.ts +19 -4
  14. package/src/capabilities/gathering.ts +2 -2
  15. package/src/capabilities/storage.test.ts +26 -0
  16. package/src/capabilities/storage.ts +6 -6
  17. package/src/contracts/fund.ts +79 -2
  18. package/src/contracts/platform.ts +10 -1
  19. package/src/contracts/server.ts +521 -330
  20. package/src/data/charters.json +325 -127
  21. package/src/data/entities.json +29 -24
  22. package/src/data/items.json +114 -114
  23. package/src/data/kind-registry.json +3 -3
  24. package/src/data/kind-registry.ts +5 -0
  25. package/src/data/metadata.ts +9 -2
  26. package/src/data/recipes.json +56 -95
  27. package/src/derivation/build-methods.test.ts +59 -1
  28. package/src/derivation/build-methods.ts +2 -0
  29. package/src/derivation/capabilities.ts +7 -7
  30. package/src/derivation/strata.ts +5 -3
  31. package/src/derivation/tiers.ts +5 -5
  32. package/src/entity/hydrate.test.ts +156 -0
  33. package/src/entity/hydrate.ts +86 -0
  34. package/src/errors.ts +9 -0
  35. package/src/format.ts +15 -13
  36. package/src/index-module.ts +32 -5
  37. package/src/influence/ballot.test.ts +103 -0
  38. package/src/influence/ballot.ts +98 -0
  39. package/src/influence/charter-metadata.test.ts +136 -0
  40. package/src/influence/charter-metadata.ts +236 -0
  41. package/src/influence/charters.test.ts +51 -65
  42. package/src/influence/charters.ts +70 -40
  43. package/src/influence/civic-shuttle.test.ts +159 -0
  44. package/src/influence/civic-shuttle.ts +115 -0
  45. package/src/influence/civic.test.ts +287 -0
  46. package/src/influence/civic.ts +269 -0
  47. package/src/influence/constants.ts +30 -9
  48. package/src/influence/decomp.ts +15 -15
  49. package/src/influence/duration.ts +42 -6
  50. package/src/influence/index.ts +59 -6
  51. package/src/influence/influence.test.ts +76 -9
  52. package/src/influence/valuation.ts +17 -17
  53. package/src/managers/actions.ts +99 -37
  54. package/src/managers/base.ts +4 -0
  55. package/src/managers/context.ts +3 -1
  56. package/src/managers/epochs.ts +32 -1
  57. package/src/managers/index.ts +4 -2
  58. package/src/managers/influence.test.ts +18 -20
  59. package/src/managers/influence.ts +137 -84
  60. package/src/managers/jobs.test.ts +2 -2
  61. package/src/managers/jobs.ts +4 -4
  62. package/src/nft/description.ts +10 -11
  63. package/src/planner/index.ts +54 -0
  64. package/src/planner/planner.test.ts +127 -2
  65. package/src/resolution/display-name.ts +1 -1
  66. package/src/scheduling/availability.test.ts +113 -0
  67. package/src/scheduling/availability.ts +48 -8
  68. package/src/scheduling/epoch.ts +1 -0
  69. package/src/scheduling/jobs.ts +1 -1
  70. package/src/scheduling/projection.ts +2 -16
  71. package/src/scheduling/task-cargo.test.ts +62 -1
  72. package/src/scheduling/task-cargo.ts +25 -10
  73. package/src/scheduling/task-effects.test.ts +35 -0
  74. package/src/scheduling/task-effects.ts +34 -0
  75. package/src/scheduling/unwrap.test.ts +3 -3
  76. package/src/scheduling/unwrap.ts +3 -3
  77. package/src/shipload.ts +22 -3
  78. package/src/travel/reach.test.ts +86 -0
  79. package/src/travel/reach.ts +23 -10
  80. package/src/travel/route-build.test.ts +63 -0
  81. package/src/travel/route-build.ts +60 -0
  82. package/src/travel/travel.ts +2 -1
  83. package/src/types.ts +6 -2
  84. package/src/utils/hash.ts +18 -2
  85. package/src/utils/system.ts +6 -1
@@ -4,6 +4,7 @@ import type {ServerContract} from '../contracts'
4
4
  import {coordsToLocationId, locationIdToCoords, type CoordinatesType} from '../types'
5
5
  import {
6
6
  type BuiltCharter,
7
+ type WorldBuilding,
7
8
  citizenryName,
8
9
  contributeDuration,
9
10
  decayActive,
@@ -12,19 +13,20 @@ import {
12
13
  valueCargoItem,
13
14
  type DemandTriple,
14
15
  type DemandView,
16
+ isCivicEntity,
15
17
  type ValuedItem,
16
18
  } from '../influence'
17
19
  import {getItem} from '../data/catalog'
18
20
 
19
21
  export interface InfluenceStanding {
20
22
  epoch: number
21
- playerLifetime: bigint
22
- playerActive: bigint
23
23
  standingLifetime: bigint
24
24
  standingActive: bigint
25
+ citizenshipLifetime: bigint
26
+ citizenshipActive: bigint
25
27
  locationLifetime: bigint
26
28
  locationActive: bigint
27
- watermark: bigint
29
+ spent: bigint
28
30
  mandate: number
29
31
  founder: Name
30
32
  founded: number
@@ -33,14 +35,23 @@ export interface InfluenceStanding {
33
35
  export interface CharterProgress {
34
36
  locationId: bigint
35
37
  lifetime: bigint
36
- watermark: bigint
37
- surplus: bigint
38
+ spent: bigint
39
+ unassigned: bigint
38
40
  mandate: number
39
41
  nextCost: bigint
40
42
  prereqsMet: boolean
41
43
  buildable: boolean
42
44
  epoch: number
43
- built: {nodeId: number; completedEpoch: number; entityId: bigint}[]
45
+ built: BuiltCharter[]
46
+ ballotId: bigint
47
+ settling: boolean
48
+ queue: QueuedSeat[]
49
+ progress: NodeProgress[]
50
+ }
51
+
52
+ export interface NodeProgress {
53
+ nodeId: number
54
+ amount: bigint
44
55
  }
45
56
 
46
57
  export interface ContributePreviewRow {
@@ -48,13 +59,13 @@ export interface ContributePreviewRow {
48
59
  quantity: number
49
60
  stats: bigint
50
61
  valueAtomic: bigint
51
- massKg: number
62
+ mass: number
52
63
  }
53
64
 
54
65
  export interface ContributePreview {
55
66
  rows: ContributePreviewRow[]
56
67
  totalAtomic: bigint
57
- totalMassKg: number
68
+ totalMass: number
58
69
  durationSeconds: number
59
70
  demand: DemandView
60
71
  }
@@ -65,9 +76,8 @@ export interface FoundedWorld {
65
76
  y: number
66
77
  lifetime: bigint
67
78
  active: bigint
68
- watermark: bigint
69
- chosen: number
70
- chosenEpoch: number
79
+ spent: bigint
80
+ ballotId: bigint
71
81
  mandate: number | undefined
72
82
  founder: Name
73
83
  founded: number
@@ -82,33 +92,43 @@ export interface FoundedWorldRef {
82
92
  export interface VoteOption {
83
93
  nodeId: number
84
94
  cost: bigint
85
- eligible: boolean
86
- total: bigint | undefined
95
+ seat: number
96
+ weight: bigint
97
+ rank: number
87
98
  }
88
99
 
89
100
  export interface VoteStandings {
90
101
  locationId: bigint
102
+ ballotId: bigint
91
103
  epoch: number
92
- chosen: number
93
- chosenEpoch: number
94
- mandate: number
95
- ballotEpoch: number | undefined
96
- talliesSuppressed: boolean
104
+ settledEpoch: number
105
+ seats: number
106
+ settling: boolean
107
+ queue: number[]
97
108
  options: VoteOption[]
109
+ picks: number[]
98
110
  }
99
111
 
100
- export interface VoteCast {
112
+ export interface BallotVote {
101
113
  account: Name
102
- epoch: number
103
- nodeId: number
104
- weight: bigint
114
+ picks: number[]
105
115
  }
106
116
 
107
- export interface PendingBallot {
108
- locationId: bigint
109
- x: number
110
- y: number
111
- epoch: number
117
+ export interface Ballot {
118
+ ballotId: bigint
119
+ kind: number
120
+ subject: bigint
121
+ seats: number
122
+ settledEpoch: number
123
+ queue: number[]
124
+ settling: boolean
125
+ }
126
+
127
+ export interface QueuedSeat {
128
+ nodeId: number
129
+ cost: bigint
130
+ progress: bigint
131
+ gap: bigint
112
132
  }
113
133
 
114
134
  function big(value: unknown): bigint {
@@ -125,13 +145,13 @@ export class InfluenceManager extends BaseManager {
125
145
 
126
146
  return {
127
147
  epoch: Number(result.epoch),
128
- playerLifetime: big(result.player_lifetime),
129
- playerActive: big(result.player_active),
130
148
  standingLifetime: big(result.standing_lifetime),
131
149
  standingActive: big(result.standing_active),
150
+ citizenshipLifetime: big(result.citizenship_lifetime),
151
+ citizenshipActive: big(result.citizenship_active),
132
152
  locationLifetime: big(result.location_lifetime),
133
153
  locationActive: big(result.location_active),
134
- watermark: big(result.watermark),
154
+ spent: big(result.spent),
135
155
  mandate: Number(result.mandate),
136
156
  founder: Name.from(result.founder),
137
157
  founded: Number(result.founded),
@@ -170,6 +190,17 @@ export class InfluenceManager extends BaseManager {
170
190
  })
171
191
  }
172
192
 
193
+ async getCivicOwner(): Promise<Name> {
194
+ const row = (await this.server.table('civicconfig').get()) as
195
+ | ServerContract.Types.civicconfig_row
196
+ | undefined
197
+ return row ? Name.from(row.civic_owner) : Name.from(this.server.account)
198
+ }
199
+
200
+ async isCivic(entity: {owner: NameType}): Promise<boolean> {
201
+ return isCivicEntity(entity, await this.getCivicOwner())
202
+ }
203
+
173
204
  async getCitizenryName(location: CoordinatesType): Promise<string | undefined> {
174
205
  const game = await this.getGame()
175
206
  return citizenryName(game.config.seed, location)
@@ -195,20 +226,20 @@ export class InfluenceManager extends BaseManager {
195
226
  )
196
227
 
197
228
  let totalAtomic = 0n
198
- let totalMassKg = 0
229
+ let totalMass = 0
199
230
  const rows = bundle.map((item) => {
200
231
  const valueAtomic = valueCargoItem(item, demand, pricing)
201
- const massKg = getItem(item.itemId).mass * item.quantity
232
+ const mass = getItem(item.itemId).mass * item.quantity
202
233
  totalAtomic += valueAtomic
203
- totalMassKg += massKg
204
- return {...item, valueAtomic, massKg}
234
+ totalMass += mass
235
+ return {...item, valueAtomic, mass}
205
236
  })
206
237
 
207
238
  return {
208
239
  rows,
209
240
  totalAtomic,
210
- totalMassKg,
211
- durationSeconds: contributeDuration(totalMassKg, opts.altitudeZ ?? 0),
241
+ totalMass,
242
+ durationSeconds: contributeDuration(totalMass, opts.altitudeZ ?? 0),
212
243
  demand,
213
244
  }
214
245
  }
@@ -222,8 +253,8 @@ export class InfluenceManager extends BaseManager {
222
253
  return {
223
254
  locationId: big(result.location_id),
224
255
  lifetime: big(result.lifetime),
225
- watermark: big(result.watermark),
226
- surplus: big(result.surplus),
256
+ spent: big(result.spent),
257
+ unassigned: big(result.unassigned),
227
258
  mandate: Number(result.mandate),
228
259
  nextCost: big(result.next_cost),
229
260
  prereqsMet: Boolean(result.prereqs_met),
@@ -231,8 +262,19 @@ export class InfluenceManager extends BaseManager {
231
262
  epoch: Number(result.epoch),
232
263
  built: result.built.map((b) => ({
233
264
  nodeId: Number(b.node_id),
234
- completedEpoch: Number(b.completed_epoch),
235
- entityId: big(b.entity_id),
265
+ repeats: Number(b.repeats),
266
+ })),
267
+ ballotId: big(result.ballot_id),
268
+ settling: Boolean(result.settling),
269
+ queue: result.queue.map((seat) => ({
270
+ nodeId: Number(seat.node_id),
271
+ cost: big(seat.cost),
272
+ progress: big(seat.progress),
273
+ gap: big(seat.gap),
274
+ })),
275
+ progress: result.progress.map((p) => ({
276
+ nodeId: Number(p.node_id),
277
+ amount: big(p.amount),
236
278
  })),
237
279
  }
238
280
  }
@@ -272,73 +314,85 @@ export class InfluenceManager extends BaseManager {
272
314
  }))
273
315
  }
274
316
 
275
- async getVotes(location: CoordinatesType): Promise<VoteStandings> {
317
+ async getVotes(location: CoordinatesType, player?: NameType): Promise<VoteStandings> {
276
318
  const result = (await this.server.readonly('getvotes', {
277
319
  x: Int64.from(location.x),
278
320
  y: Int64.from(location.y),
321
+ player: Name.from(player ?? ''),
279
322
  })) as ServerContract.Types.vote_result
280
323
 
281
- const epoch = Number(result.epoch)
282
- const rawBallotEpoch = Number(result.ballot_epoch)
283
- const ballotEpoch = rawBallotEpoch === 0 ? undefined : rawBallotEpoch
284
- const talliesSuppressed = ballotEpoch !== undefined && ballotEpoch < epoch
285
-
286
324
  return {
287
325
  locationId: big(result.location_id),
288
- epoch,
289
- chosen: Number(result.chosen),
290
- chosenEpoch: Number(result.chosen_epoch),
291
- mandate: Number(result.effective),
292
- ballotEpoch,
293
- talliesSuppressed,
326
+ ballotId: big(result.ballot_id),
327
+ epoch: Number(result.epoch),
328
+ settledEpoch: Number(result.settled_epoch),
329
+ seats: Number(result.seats),
330
+ settling: Boolean(result.settling),
331
+ queue: result.queue.map((n) => Number(n)),
294
332
  options: result.options.map((option) => ({
295
333
  nodeId: Number(option.node_id),
296
334
  cost: big(option.cost),
297
- eligible: Boolean(option.eligible),
298
- total: talliesSuppressed ? undefined : big(option.total),
335
+ seat: Number(option.seat),
336
+ weight: big(option.weight),
337
+ rank: Number(option.rank),
299
338
  })),
339
+ picks: result.picks.map((n) => Number(n)),
340
+ }
341
+ }
342
+
343
+ async getBallotId(location: CoordinatesType): Promise<bigint | undefined> {
344
+ const row = (await this.server
345
+ .table('worlds')
346
+ .get(UInt64.from(coordsToLocationId(location)))) as
347
+ | ServerContract.Types.worlds_row
348
+ | undefined
349
+ return row ? big(row.ballot_id) : undefined
350
+ }
351
+
352
+ async getBallot(ballotId: bigint): Promise<Ballot | undefined> {
353
+ const row = (await this.server.table('ballot').get(UInt64.from(ballotId))) as
354
+ | ServerContract.Types.ballot_row
355
+ | undefined
356
+ if (!row) return undefined
357
+ const epoch = Number((await this.getState()).epoch)
358
+ const settledEpoch = Number(row.settled_epoch)
359
+ return {
360
+ ballotId: big(row.ballot_id),
361
+ kind: Number(row.kind),
362
+ subject: big(row.subject),
363
+ seats: Number(row.seats),
364
+ settledEpoch,
365
+ queue: row.queue.map((n) => Number(n)),
366
+ settling: settledEpoch < epoch,
300
367
  }
301
368
  }
302
369
 
303
- async getVoteCasts(location: CoordinatesType): Promise<VoteCast[]> {
370
+ async getBallotVotes(ballotId: bigint): Promise<BallotVote[]> {
304
371
  const rows = (await this.server
305
- .table('infvote', coordsToLocationId(location))
306
- .all()) as ServerContract.Types.infvote_row[]
372
+ .table('ballotpicks', UInt64.from(ballotId))
373
+ .all()) as ServerContract.Types.ballotpicks_row[]
307
374
  return rows.map((row) => ({
308
375
  account: Name.from(row.account),
309
- epoch: Number(row.epoch),
310
- nodeId: Number(row.node_id),
311
- weight: big(row.weight),
376
+ picks: row.picks.map((n) => Number(n)),
312
377
  }))
313
378
  }
314
379
 
315
- async getVoteTallies(location: CoordinatesType): Promise<{nodeId: number; total: bigint}[]> {
316
- const rows = (await this.server
317
- .table('inftally', coordsToLocationId(location))
318
- .all()) as ServerContract.Types.inftally_row[]
319
- return rows.map((row) => ({nodeId: Number(row.node_id), total: big(row.total)}))
320
- }
321
-
322
380
  async getBuiltCharters(location: CoordinatesType): Promise<BuiltCharter[]> {
323
381
  const rows = (await this.server
324
- .table('charters', coordsToLocationId(location))
325
- .all()) as ServerContract.Types.charters_row[]
326
- return rows.map((row) => ({nodeId: Number(row.node_id), entityId: big(row.entity_id)}))
382
+ .table('mandates', coordsToLocationId(location))
383
+ .all()) as ServerContract.Types.mandates_row[]
384
+ return rows.map((row) => ({nodeId: Number(row.node_id), repeats: Number(row.repeats)}))
327
385
  }
328
386
 
329
- async getBallotQueue(): Promise<PendingBallot[]> {
387
+ async getBuildings(location: CoordinatesType): Promise<WorldBuilding[]> {
330
388
  const rows = (await this.server
331
- .table('infballot')
332
- .all()) as ServerContract.Types.infballot_row[]
333
- return rows.map((row) => ({
334
- locationId: big(row.location_id),
335
- ...locationIdToCoords(row.location_id),
336
- epoch: Number(row.epoch),
337
- }))
389
+ .table('buildings', coordsToLocationId(location))
390
+ .all()) as ServerContract.Types.buildings_row[]
391
+ return rows.map((row) => ({entityId: big(row.entity_id), building: Number(row.building)}))
338
392
  }
339
393
 
340
394
  async getFoundedWorlds(opts: {withMandate?: boolean} = {}): Promise<FoundedWorld[]> {
341
- const rows = (await this.server.table('infloc').all()) as ServerContract.Types.infloc_row[]
395
+ const rows = (await this.server.table('worlds').all()) as ServerContract.Types.worlds_row[]
342
396
  const epoch = (await this.getState()).epoch
343
397
  const worlds = rows.map((row) => ({
344
398
  locationId: big(row.location_id),
@@ -346,11 +400,10 @@ export class InfluenceManager extends BaseManager {
346
400
  lifetime: big(row.lifetime),
347
401
  active: decayActive(
348
402
  big(row.active),
349
- Math.max(0, Number(epoch) - Number(row.last_update_epoch))
403
+ Math.max(0, Number(epoch) - Number(row.active_epoch))
350
404
  ),
351
- watermark: big(row.watermark),
352
- chosen: Number(row.chosen),
353
- chosenEpoch: Number(row.chosen_epoch),
405
+ spent: big(row.spent),
406
+ ballotId: big(row.ballot_id),
354
407
  mandate: undefined as number | undefined,
355
408
  founder: Name.from(row.founder),
356
409
  founded: Number(row.founded),
@@ -366,7 +419,7 @@ export class InfluenceManager extends BaseManager {
366
419
 
367
420
  async isFounded(location: CoordinatesType): Promise<boolean> {
368
421
  const id = coordsToLocationId(location)
369
- const row = await this.server.table('infloc').get(UInt64.from(id))
422
+ const row = await this.server.table('worlds').get(UInt64.from(id))
370
423
  return row !== undefined
371
424
  }
372
425
  }
@@ -5,7 +5,7 @@ import {JobsManager} from './jobs'
5
5
  const OWNER = 'eggmaple.gm'
6
6
  const row = (over: Record<string, unknown> = {}) => ({
7
7
  id: {toNumber: () => 7},
8
- workshop: {toNumber: () => 42},
8
+ building: {toNumber: () => 42},
9
9
  socket: {toNumber: () => 0},
10
10
  owner: Name.from(OWNER),
11
11
  ship_id: {toNumber: () => 100},
@@ -33,7 +33,7 @@ describe('JobsManager.getOwnedJobs', () => {
33
33
  )
34
34
  const jobs = await m.getOwnedJobs(OWNER, {now: new Date('2026-07-26T11:30:00Z')})
35
35
  expect(jobs).toHaveLength(1)
36
- expect(jobs[0]).toMatchObject({id: 7, workshop: 42, quantity: 5, status: 'ready'})
36
+ expect(jobs[0]).toMatchObject({id: 7, building: 42, quantity: 5, status: 'ready'})
37
37
  expect(jobs[0].coords).toEqual({x: 12, y: 34})
38
38
  expect(jobs[0].output).toEqual({item: 'out'} as never)
39
39
  expect(jobs[0].inputs).toEqual([{item: 'in'}] as never)
@@ -3,7 +3,7 @@ import {BaseManager} from './base'
3
3
  import {jobStatus, splitJobCargo, type OwnedJob} from '../scheduling/jobs'
4
4
  import type {ServerContract} from '../contracts'
5
5
 
6
- type JobRow = ServerContract.Types.job_row
6
+ type JobRow = ServerContract.Types.craftjob_row
7
7
 
8
8
  export class JobsManager extends BaseManager {
9
9
  async getOwnedJobs(owner: NameType, opts?: {now?: Date}): Promise<OwnedJob[]> {
@@ -14,7 +14,7 @@ export class JobsManager extends BaseManager {
14
14
  try {
15
15
  // index_position 'tertiary' = nodeos slot 3 = owner's secondary index; no ABI metadata for it.
16
16
  rows = (await this.server
17
- .table('jobs')
17
+ .table('craftjobs')
18
18
  .query({
19
19
  index_position: 'tertiary',
20
20
  key_type: 'i64',
@@ -23,7 +23,7 @@ export class JobsManager extends BaseManager {
23
23
  })
24
24
  .all()) as JobRow[]
25
25
  } catch {
26
- rows = (await this.server.table('jobs').all()) as JobRow[]
26
+ rows = (await this.server.table('craftjobs').all()) as JobRow[]
27
27
  }
28
28
 
29
29
  return rows.filter((r) => ownerName.equals(r.owner)).map((r) => this.parseOwnedJob(r, now))
@@ -35,7 +35,7 @@ export class JobsManager extends BaseManager {
35
35
  const {output, inputs} = splitJobCargo(r.cargo)
36
36
  return {
37
37
  id: r.id.toNumber(),
38
- workshop: r.workshop.toNumber(),
38
+ building: r.building.toNumber(),
39
39
  socket: r.socket.toNumber(),
40
40
  shipId: r.ship_id.toNumber(),
41
41
  coords: {x: r.coords.x.toNumber(), y: r.coords.y.toNumber()},
@@ -28,7 +28,7 @@ import {
28
28
  BATTERY_CAPACITY_TIER_PCT,
29
29
  } from '../derivation/capabilities'
30
30
  import {getItem, tryGetItem} from '../data/catalog'
31
- import type {ModuleType} from '../types'
31
+ import {MASS_UNITS_PER_TONNE, type ModuleType} from '../types'
32
32
  import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
33
33
  import {getBaseHullmassFor} from '../derivation/capabilities'
34
34
  import {computeEffectiveModuleStat} from '../derivation/stat-scaling'
@@ -41,9 +41,8 @@ export function toWholeEnergy(milli: number): number {
41
41
  return idiv(milli + 500, 1000)
42
42
  }
43
43
 
44
- export function formatMassTonnes(kg: number): string {
45
- const tenths = idiv(kg + 50, 100)
46
- return `${idiv(tenths, 10)}.${tenths % 10} t`
44
+ export function formatMassTonnes(mass: number): string {
45
+ return `${idiv(mass, MASS_UNITS_PER_TONNE)}.${mass % MASS_UNITS_PER_TONNE} t`
47
46
  }
48
47
 
49
48
  function isShipHull(itemId: number): boolean {
@@ -67,27 +66,27 @@ export function computeBaseHullmass(itemId: number, stats: bigint): number {
67
66
  }
68
67
 
69
68
  export function computeBaseCapacityShip(stats: bigint): number {
70
- return Math.floor(5_000_000 * 6 ** (sumPackedShipChannels(stats) / 4995))
69
+ return Math.floor(50_000 * 6 ** (sumPackedShipChannels(stats) / 4995))
71
70
  }
72
71
 
73
72
  export function computeBaseCapacityContainer(stats: bigint): number {
74
73
  const s = decodeStat(stats, 0) + decodeStat(stats, 2)
75
- return Math.floor(22_000_000 * 6 ** (s / 1998))
74
+ return Math.floor(220_000 * 6 ** (s / 1998))
76
75
  }
77
76
 
78
77
  export function computeBaseCapacityWarehouse(stats: bigint): number {
79
78
  const s = decodeStat(stats, 0) + decodeStat(stats, 2)
80
- return Math.floor(100_000_000 * 6 ** (s / 1998))
79
+ return Math.floor(1_000_000 * 6 ** (s / 1998))
81
80
  }
82
81
 
83
82
  export function computeBaseCapacityWorkshop(stats: bigint): number {
84
83
  const s = decodeStat(stats, 0) + decodeStat(stats, 2)
85
- return Math.floor(5_000_000 * 6 ** (s / 1998))
84
+ return Math.floor(50_000 * 6 ** (s / 1998))
86
85
  }
87
86
 
88
87
  export function computeBaseCapacityDepot(stats: bigint): number {
89
88
  const s = decodeStat(stats, 0) + decodeStat(stats, 2)
90
- return Math.floor(50_000_000 * 6 ** (s / 1998))
89
+ return Math.floor(500_000 * 6 ** (s / 1998))
91
90
  }
92
91
 
93
92
  const CAPACITY_FN_BY_KIND: Record<string, (stats: bigint) => number> = {
@@ -131,7 +130,7 @@ export const computeGathererDrain = (con: number): number =>
131
130
  2 * Math.max(250_000, 1_250_000 - con * 1250)
132
131
  export const computeGathererDepth = (tol: number, tier: number): number =>
133
132
  gathererDepthForTier(tol, tier)
134
- export const computeLoaderMass = (ins: number): number => Math.max(200, 2000 - ins * 2)
133
+ export const computeLoaderMass = (ins: number): number => Math.max(2, 20 - idiv(ins, 50))
135
134
  export const computeLoaderThrust = (pla: number, tier: number): number =>
136
135
  idiv((1 + idiv(pla * pla, 10000)) * moduleTierPct(LOADER_THRUST_TIER_PCT, tier), 100)
137
136
  export const computeCrafterSpeed = (rea: number, tier: number): number =>
@@ -164,7 +163,7 @@ export const computeCargoBayCapacity = (
164
163
  tier: number
165
164
  ): number =>
166
165
  idiv(
167
- (10_000_000 + idiv((strength + density + hardness) * 50_000_000, 2997)) *
166
+ (100_000 + idiv((strength + density + hardness) * 500_000, 2997)) *
168
167
  moduleTierPct(CARGO_BAY_CAPACITY_TIER_PCT, tier),
169
168
  100
170
169
  )
@@ -3,6 +3,9 @@ import type {ServerContract} from '../contracts'
3
3
  import {calc_gather_duration, calc_gather_energy} from '../capabilities/gathering'
4
4
  import {calc_rechargetime} from '../travel/travel'
5
5
  import {projectRemainingAt, type Projectable} from '../scheduling/projection'
6
+ import {selectGatherLane} from '../scheduling/lanes'
7
+
8
+ export type GatherLane = ServerContract.Types.gatherer_lane
6
9
 
7
10
  export interface LanePlanEntry {
8
11
  slot: number
@@ -224,6 +227,57 @@ export function buildGatherPlan(
224
227
  }
225
228
  }
226
229
 
230
+ export interface SingleGatherPlan {
231
+ laneKey: number
232
+ quantity: number
233
+ energyCost: number
234
+ needsRecharge: boolean
235
+ }
236
+
237
+ // None of these are PRECISION-scaled: stratum/itemMass/richness/energy fields are plain units, same as maxQtyForCharge/splitCost.
238
+ export interface SingleGatherInput {
239
+ modules: ServerContract.Types.module_entry[]
240
+ entityItemId: number
241
+ lanes: ServerContract.Types.lane[]
242
+ gathererLanes: GatherLane[]
243
+ stratum: number
244
+ desiredQuantity: number
245
+ itemMass: number
246
+ richness: number
247
+ generatorCapacity: number
248
+ currentEnergy: number
249
+ }
250
+
251
+ export function planSingleGather(opts: SingleGatherInput): SingleGatherPlan {
252
+ const {
253
+ modules,
254
+ entityItemId,
255
+ lanes,
256
+ gathererLanes,
257
+ stratum,
258
+ desiredQuantity,
259
+ itemMass,
260
+ richness,
261
+ generatorCapacity,
262
+ currentEnergy,
263
+ } = opts
264
+
265
+ const laneKey = selectGatherLane(modules, entityItemId, lanes, stratum)
266
+ const lane = gathererLanes.find((l) => l.slot_index.toNumber() === laneKey - 1)
267
+ if (!lane) throw new Error('gatherer lane not found for selected slot')
268
+
269
+ const quantity = maxQtyForCharge(
270
+ [lane],
271
+ desiredQuantity,
272
+ generatorCapacity,
273
+ stratum,
274
+ itemMass,
275
+ richness
276
+ )
277
+ const energyCost = splitCost([lane], quantity, stratum, itemMass, richness)
278
+ return {laneKey, quantity, energyCost, needsRecharge: currentEnergy < energyCost}
279
+ }
280
+
227
281
  export function planParallelTransfer(
228
282
  entity: GatherPlanEntity,
229
283
  target: PlanTarget