@shipload/sdk 1.0.0-next.64 → 1.0.0-next.66

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 (79) hide show
  1. package/lib/shipload.d.ts +754 -316
  2. package/lib/shipload.js +2650 -1336
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +2564 -1308
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +219 -145
  7. package/lib/testing.js +910 -557
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +910 -557
  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 +110 -5
  18. package/src/contracts/platform.ts +10 -1
  19. package/src/contracts/server.ts +498 -331
  20. package/src/data/charters.json +325 -127
  21. package/src/data/entities.json +24 -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/recipes.json +56 -95
  26. package/src/derivation/build-methods.test.ts +59 -1
  27. package/src/derivation/build-methods.ts +2 -0
  28. package/src/derivation/capabilities.ts +7 -7
  29. package/src/derivation/strata.ts +5 -3
  30. package/src/derivation/tiers.ts +5 -5
  31. package/src/entity/hydrate.test.ts +156 -0
  32. package/src/entity/hydrate.ts +86 -0
  33. package/src/errors.ts +9 -0
  34. package/src/format.ts +15 -13
  35. package/src/index-module.ts +33 -5
  36. package/src/influence/ballot.test.ts +103 -0
  37. package/src/influence/ballot.ts +98 -0
  38. package/src/influence/charter-metadata.test.ts +136 -0
  39. package/src/influence/charter-metadata.ts +236 -0
  40. package/src/influence/charters.test.ts +51 -65
  41. package/src/influence/charters.ts +70 -40
  42. package/src/influence/civic.test.ts +278 -0
  43. package/src/influence/civic.ts +269 -0
  44. package/src/influence/constants.ts +30 -9
  45. package/src/influence/decomp.ts +15 -15
  46. package/src/influence/duration.ts +42 -6
  47. package/src/influence/index.ts +49 -6
  48. package/src/influence/influence.test.ts +76 -9
  49. package/src/influence/valuation.ts +17 -17
  50. package/src/managers/actions.ts +65 -37
  51. package/src/managers/base.ts +4 -0
  52. package/src/managers/context.ts +3 -1
  53. package/src/managers/index.ts +5 -2
  54. package/src/managers/influence.test.ts +18 -20
  55. package/src/managers/influence.ts +162 -84
  56. package/src/managers/jobs.test.ts +2 -2
  57. package/src/managers/jobs.ts +4 -4
  58. package/src/nft/description.ts +10 -11
  59. package/src/planner/index.ts +54 -0
  60. package/src/planner/planner.test.ts +127 -2
  61. package/src/resolution/display-name.ts +1 -1
  62. package/src/scheduling/availability.test.ts +113 -0
  63. package/src/scheduling/availability.ts +48 -8
  64. package/src/scheduling/jobs.ts +1 -1
  65. package/src/scheduling/projection.ts +2 -16
  66. package/src/scheduling/task-cargo.test.ts +62 -1
  67. package/src/scheduling/task-cargo.ts +25 -10
  68. package/src/scheduling/task-effects.test.ts +35 -0
  69. package/src/scheduling/task-effects.ts +34 -0
  70. package/src/scheduling/unwrap.test.ts +3 -3
  71. package/src/scheduling/unwrap.ts +3 -3
  72. package/src/shipload.ts +23 -4
  73. package/src/travel/reach.test.ts +86 -0
  74. package/src/travel/reach.ts +23 -10
  75. package/src/travel/route-build.test.ts +63 -0
  76. package/src/travel/route-build.ts +60 -0
  77. package/src/travel/travel.ts +2 -1
  78. package/src/types.ts +6 -2
  79. package/src/utils/hash.ts +18 -2
@@ -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,44 +76,59 @@ 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
74
84
  }
75
85
 
86
+ export interface FoundedWorldRef {
87
+ x: number
88
+ y: number
89
+ mandate: number
90
+ }
91
+
76
92
  export interface VoteOption {
77
93
  nodeId: number
78
94
  cost: bigint
79
- eligible: boolean
80
- total: bigint | undefined
95
+ seat: number
96
+ weight: bigint
97
+ rank: number
81
98
  }
82
99
 
83
100
  export interface VoteStandings {
84
101
  locationId: bigint
102
+ ballotId: bigint
85
103
  epoch: number
86
- chosen: number
87
- chosenEpoch: number
88
- mandate: number
89
- ballotEpoch: number | undefined
90
- talliesSuppressed: boolean
104
+ settledEpoch: number
105
+ seats: number
106
+ settling: boolean
107
+ queue: number[]
91
108
  options: VoteOption[]
109
+ picks: number[]
92
110
  }
93
111
 
94
- export interface VoteCast {
112
+ export interface BallotVote {
95
113
  account: Name
96
- epoch: number
97
- nodeId: number
98
- weight: bigint
114
+ picks: number[]
99
115
  }
100
116
 
101
- export interface PendingBallot {
102
- locationId: bigint
103
- x: number
104
- y: number
105
- 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
106
132
  }
107
133
 
108
134
  function big(value: unknown): bigint {
@@ -119,13 +145,13 @@ export class InfluenceManager extends BaseManager {
119
145
 
120
146
  return {
121
147
  epoch: Number(result.epoch),
122
- playerLifetime: big(result.player_lifetime),
123
- playerActive: big(result.player_active),
124
148
  standingLifetime: big(result.standing_lifetime),
125
149
  standingActive: big(result.standing_active),
150
+ citizenshipLifetime: big(result.citizenship_lifetime),
151
+ citizenshipActive: big(result.citizenship_active),
126
152
  locationLifetime: big(result.location_lifetime),
127
153
  locationActive: big(result.location_active),
128
- watermark: big(result.watermark),
154
+ spent: big(result.spent),
129
155
  mandate: Number(result.mandate),
130
156
  founder: Name.from(result.founder),
131
157
  founded: Number(result.founded),
@@ -164,6 +190,17 @@ export class InfluenceManager extends BaseManager {
164
190
  })
165
191
  }
166
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
+
167
204
  async getCitizenryName(location: CoordinatesType): Promise<string | undefined> {
168
205
  const game = await this.getGame()
169
206
  return citizenryName(game.config.seed, location)
@@ -189,20 +226,20 @@ export class InfluenceManager extends BaseManager {
189
226
  )
190
227
 
191
228
  let totalAtomic = 0n
192
- let totalMassKg = 0
229
+ let totalMass = 0
193
230
  const rows = bundle.map((item) => {
194
231
  const valueAtomic = valueCargoItem(item, demand, pricing)
195
- const massKg = getItem(item.itemId).mass * item.quantity
232
+ const mass = getItem(item.itemId).mass * item.quantity
196
233
  totalAtomic += valueAtomic
197
- totalMassKg += massKg
198
- return {...item, valueAtomic, massKg}
234
+ totalMass += mass
235
+ return {...item, valueAtomic, mass}
199
236
  })
200
237
 
201
238
  return {
202
239
  rows,
203
240
  totalAtomic,
204
- totalMassKg,
205
- durationSeconds: contributeDuration(totalMassKg, opts.altitudeZ ?? 0),
241
+ totalMass,
242
+ durationSeconds: contributeDuration(totalMass, opts.altitudeZ ?? 0),
206
243
  demand,
207
244
  }
208
245
  }
@@ -216,8 +253,8 @@ export class InfluenceManager extends BaseManager {
216
253
  return {
217
254
  locationId: big(result.location_id),
218
255
  lifetime: big(result.lifetime),
219
- watermark: big(result.watermark),
220
- surplus: big(result.surplus),
256
+ spent: big(result.spent),
257
+ unassigned: big(result.unassigned),
221
258
  mandate: Number(result.mandate),
222
259
  nextCost: big(result.next_cost),
223
260
  prereqsMet: Boolean(result.prereqs_met),
@@ -225,8 +262,19 @@ export class InfluenceManager extends BaseManager {
225
262
  epoch: Number(result.epoch),
226
263
  built: result.built.map((b) => ({
227
264
  nodeId: Number(b.node_id),
228
- completedEpoch: Number(b.completed_epoch),
229
- 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),
230
278
  })),
231
279
  }
232
280
  }
@@ -247,73 +295,104 @@ export class InfluenceManager extends BaseManager {
247
295
  return (await this.server.readonly('getmintcfg')) as ServerContract.Types.mintcfg_result
248
296
  }
249
297
 
250
- async getVotes(location: CoordinatesType): Promise<VoteStandings> {
298
+ async getMintReady(): Promise<number> {
299
+ return Number(await this.server.readonly('getmintready'))
300
+ }
301
+
302
+ async getVoteReady(): Promise<number> {
303
+ return Number(await this.server.readonly('getvoteready'))
304
+ }
305
+
306
+ async getCharterReady(): Promise<FoundedWorldRef[]> {
307
+ const result = (await this.server.readonly(
308
+ 'getchrtready'
309
+ )) as ServerContract.Types.charterready_result
310
+ return result.worlds.map((w) => ({
311
+ x: Number(w.x),
312
+ y: Number(w.y),
313
+ mandate: Number(w.mandate),
314
+ }))
315
+ }
316
+
317
+ async getVotes(location: CoordinatesType, player?: NameType): Promise<VoteStandings> {
251
318
  const result = (await this.server.readonly('getvotes', {
252
319
  x: Int64.from(location.x),
253
320
  y: Int64.from(location.y),
321
+ player: Name.from(player ?? ''),
254
322
  })) as ServerContract.Types.vote_result
255
323
 
256
- const epoch = Number(result.epoch)
257
- const rawBallotEpoch = Number(result.ballot_epoch)
258
- const ballotEpoch = rawBallotEpoch === 0 ? undefined : rawBallotEpoch
259
- const talliesSuppressed = ballotEpoch !== undefined && ballotEpoch < epoch
260
-
261
324
  return {
262
325
  locationId: big(result.location_id),
263
- epoch,
264
- chosen: Number(result.chosen),
265
- chosenEpoch: Number(result.chosen_epoch),
266
- mandate: Number(result.effective),
267
- ballotEpoch,
268
- 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)),
269
332
  options: result.options.map((option) => ({
270
333
  nodeId: Number(option.node_id),
271
334
  cost: big(option.cost),
272
- eligible: Boolean(option.eligible),
273
- total: talliesSuppressed ? undefined : big(option.total),
335
+ seat: Number(option.seat),
336
+ weight: big(option.weight),
337
+ rank: Number(option.rank),
274
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,
275
367
  }
276
368
  }
277
369
 
278
- async getVoteCasts(location: CoordinatesType): Promise<VoteCast[]> {
370
+ async getBallotVotes(ballotId: bigint): Promise<BallotVote[]> {
279
371
  const rows = (await this.server
280
- .table('infvote', coordsToLocationId(location))
281
- .all()) as ServerContract.Types.infvote_row[]
372
+ .table('ballotpicks', UInt64.from(ballotId))
373
+ .all()) as ServerContract.Types.ballotpicks_row[]
282
374
  return rows.map((row) => ({
283
375
  account: Name.from(row.account),
284
- epoch: Number(row.epoch),
285
- nodeId: Number(row.node_id),
286
- weight: big(row.weight),
376
+ picks: row.picks.map((n) => Number(n)),
287
377
  }))
288
378
  }
289
379
 
290
- async getVoteTallies(location: CoordinatesType): Promise<{nodeId: number; total: bigint}[]> {
291
- const rows = (await this.server
292
- .table('inftally', coordsToLocationId(location))
293
- .all()) as ServerContract.Types.inftally_row[]
294
- return rows.map((row) => ({nodeId: Number(row.node_id), total: big(row.total)}))
295
- }
296
-
297
380
  async getBuiltCharters(location: CoordinatesType): Promise<BuiltCharter[]> {
298
381
  const rows = (await this.server
299
- .table('charters', coordsToLocationId(location))
300
- .all()) as ServerContract.Types.charters_row[]
301
- 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)}))
302
385
  }
303
386
 
304
- async getBallotQueue(): Promise<PendingBallot[]> {
387
+ async getBuildings(location: CoordinatesType): Promise<WorldBuilding[]> {
305
388
  const rows = (await this.server
306
- .table('infballot')
307
- .all()) as ServerContract.Types.infballot_row[]
308
- return rows.map((row) => ({
309
- locationId: big(row.location_id),
310
- ...locationIdToCoords(row.location_id),
311
- epoch: Number(row.epoch),
312
- }))
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)}))
313
392
  }
314
393
 
315
394
  async getFoundedWorlds(opts: {withMandate?: boolean} = {}): Promise<FoundedWorld[]> {
316
- 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[]
317
396
  const epoch = (await this.getState()).epoch
318
397
  const worlds = rows.map((row) => ({
319
398
  locationId: big(row.location_id),
@@ -321,11 +400,10 @@ export class InfluenceManager extends BaseManager {
321
400
  lifetime: big(row.lifetime),
322
401
  active: decayActive(
323
402
  big(row.active),
324
- Math.max(0, Number(epoch) - Number(row.last_update_epoch))
403
+ Math.max(0, Number(epoch) - Number(row.active_epoch))
325
404
  ),
326
- watermark: big(row.watermark),
327
- chosen: Number(row.chosen),
328
- chosenEpoch: Number(row.chosen_epoch),
405
+ spent: big(row.spent),
406
+ ballotId: big(row.ballot_id),
329
407
  mandate: undefined as number | undefined,
330
408
  founder: Name.from(row.founder),
331
409
  founded: Number(row.founded),
@@ -341,7 +419,7 @@ export class InfluenceManager extends BaseManager {
341
419
 
342
420
  async isFounded(location: CoordinatesType): Promise<boolean> {
343
421
  const id = coordsToLocationId(location)
344
- const row = await this.server.table('infloc').get(UInt64.from(id))
422
+ const row = await this.server.table('worlds').get(UInt64.from(id))
345
423
  return row !== undefined
346
424
  }
347
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