@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
@@ -2,7 +2,7 @@ import {getEntityLayout} from '../data/recipes-runtime'
2
2
  import {ENTITY_SHIP, getPackedEntityType} from '../data/kind-registry'
3
3
  import {computeEffectiveModuleStat} from './stat-scaling'
4
4
 
5
- export const DEFAULT_BASE_HULLMASS = 100_000
5
+ export const DEFAULT_BASE_HULLMASS = 1_000
6
6
 
7
7
  export function getBaseHullmassFor(itemId: number): number {
8
8
  return getEntityLayout(itemId)?.baseHullmass ?? DEFAULT_BASE_HULLMASS
@@ -35,7 +35,7 @@ export function computeShipHullCapabilities(
35
35
  const exponent = statSum / 4995.0
36
36
  return {
37
37
  hullmass: computeBaseHullmass(itemId, stats),
38
- capacity: Math.floor(5000000 * 6 ** exponent),
38
+ capacity: Math.floor(50000 * 6 ** exponent),
39
39
  }
40
40
  }
41
41
 
@@ -126,7 +126,7 @@ export function computeLoaderCapabilities(
126
126
  const insulation = stats.insulation
127
127
  const plasticity = stats.plasticity
128
128
  return {
129
- mass: Math.max(200, 2000 - Math.floor(insulation * 2)),
129
+ mass: Math.max(2, 20 - Math.floor(insulation / 50)),
130
130
  thrust: Math.floor(
131
131
  ((1 + Math.floor((plasticity * plasticity) / 10000)) *
132
132
  moduleTierPct(LOADER_THRUST_TIER_PCT, tier)) /
@@ -359,7 +359,7 @@ export function computeWarehouseHullCapabilities(stats: Record<string, number>):
359
359
  const exponent = statSum / 1998.0
360
360
  return {
361
361
  hullmass: computeBaseHullmass(ITEM_WAREHOUSE_T1_PACKED, stats),
362
- capacity: Math.floor(100000000 * 6 ** exponent),
362
+ capacity: Math.floor(1000000 * 6 ** exponent),
363
363
  }
364
364
  }
365
365
 
@@ -371,7 +371,7 @@ export function computeDepotHullCapabilities(stats: Record<string, number>): {
371
371
  const exponent = statSum / 1998.0
372
372
  return {
373
373
  hullmass: computeBaseHullmass(ITEM_DEPOT_T1_PACKED, stats),
374
- capacity: Math.floor(50000000 * 6 ** exponent),
374
+ capacity: Math.floor(500000 * 6 ** exponent),
375
375
  }
376
376
  }
377
377
 
@@ -383,7 +383,7 @@ export function computeWorkshopHullCapabilities(stats: Record<string, number>):
383
383
  const exponent = statSum / 1998.0
384
384
  return {
385
385
  hullmass: computeBaseHullmass(ITEM_WORKSHOP_T1_PACKED, stats),
386
- capacity: Math.floor(5000000 * 6 ** exponent),
386
+ capacity: Math.floor(50000 * 6 ** exponent),
387
387
  }
388
388
  }
389
389
 
@@ -702,6 +702,6 @@ export function computeContainerCapabilities(stats: Record<string, number>): {
702
702
  const exponent = statSum / 1998.0
703
703
  return {
704
704
  hullmass: computeBaseHullmass(ITEM_CONTAINER_T1_PACKED, stats),
705
- capacity: Math.floor(22000000 * 6 ** exponent),
705
+ capacity: Math.floor(220000 * 6 ** exponent),
706
706
  }
707
707
  }
@@ -1,5 +1,5 @@
1
- import type {Checksum256Type} from '@wharfkit/antelope'
2
- import type {CoordinatesType} from '../types'
1
+ import {Checksum256, type Checksum256Type} from '@wharfkit/antelope'
2
+ import {Coordinates, type CoordinatesType} from '../types'
3
3
  import {deriveLocationStatic} from '../utils/system'
4
4
  import {deriveLocationSize} from './location-size'
5
5
  import {deriveResourceStats, deriveStratum, type ResourceStats} from './stratum'
@@ -26,9 +26,11 @@ export function deriveStrata(
26
26
  if (size === 0) return []
27
27
 
28
28
  const subtype = Number(loc.subtype)
29
+ const seed = Checksum256.from(epochSeed)
30
+ const at = Coordinates.from(coords)
29
31
  const out: DerivedStratum[] = []
30
32
  for (let i = 0; i < size; i++) {
31
- const s = deriveStratum(epochSeed, coords, i, locType, subtype, size)
33
+ const s = deriveStratum(seed, at, i, locType, subtype, size)
32
34
  if (s.reserve === 0) continue
33
35
  out.push({
34
36
  index: i,
@@ -8,11 +8,11 @@ export interface TierRange {
8
8
  }
9
9
 
10
10
  export const RESERVE_TIERS: Record<ReserveTier, TierRange> = {
11
- small: {min: 3_600_000, max: 14_400_000},
12
- medium: {min: 24_000_000, max: 48_000_000},
13
- large: {min: 96_000_000, max: 168_000_000},
14
- massive: {min: 240_000_000, max: 600_000_000},
15
- motherlode: {min: 960_000_000, max: 2_400_000_000},
11
+ small: {min: 36_000, max: 144_000},
12
+ medium: {min: 240_000, max: 480_000},
13
+ large: {min: 960_000, max: 1_680_000},
14
+ massive: {min: 2_400_000, max: 6_000_000},
15
+ motherlode: {min: 9_600_000, max: 24_000_000},
16
16
  }
17
17
 
18
18
  const SHALLOW_THRESHOLDS = {
@@ -0,0 +1,156 @@
1
+ import {UInt64} from '@wharfkit/antelope'
2
+ import {describe, expect, test} from 'bun:test'
3
+ import {entityToRouteMover, hydrateEntityLanes} from './hydrate'
4
+
5
+ describe('hydrateEntityLanes', () => {
6
+ test('maps camelCase gathererLanes to gatherer_lane with snake_case fields', () => {
7
+ const lanes = hydrateEntityLanes({
8
+ gathererLanes: [
9
+ {slotIndex: 2, yield: 700, drain: 1_250_000, depth: 5000, outputPct: 100},
10
+ ],
11
+ })
12
+ expect(lanes).toHaveLength(1)
13
+ expect(lanes[0].slot_index.toNumber()).toBe(2)
14
+ expect(lanes[0].yield.toNumber()).toBe(700)
15
+ expect(lanes[0].drain.toNumber()).toBe(1_250_000)
16
+ expect(lanes[0].depth.toNumber()).toBe(5000)
17
+ expect(lanes[0].output_pct.toNumber()).toBe(100)
18
+ })
19
+
20
+ test('falls back to a single synthetic lane from caps.gatherer when gathererLanes is absent', () => {
21
+ const lanes = hydrateEntityLanes({gatherer: {yield: 300, drain: 50, depth: 1000}})
22
+ expect(lanes).toHaveLength(1)
23
+ expect(lanes[0].slot_index.toNumber()).toBe(0)
24
+ expect(lanes[0].yield.toNumber()).toBe(300)
25
+ expect(lanes[0].drain.toNumber()).toBe(50)
26
+ expect(lanes[0].depth.toNumber()).toBe(1000)
27
+ expect(lanes[0].output_pct.toNumber()).toBe(100)
28
+ })
29
+
30
+ test('prefers gathererLanes over the caps.gatherer fallback when both are present', () => {
31
+ const lanes = hydrateEntityLanes({
32
+ gathererLanes: [{slotIndex: 1, yield: 10, drain: 20, depth: 30, outputPct: 40}],
33
+ gatherer: {yield: 999, drain: 999, depth: 999},
34
+ })
35
+ expect(lanes).toHaveLength(1)
36
+ expect(lanes[0].slot_index.toNumber()).toBe(1)
37
+ })
38
+
39
+ test('returns an empty array when there is no gatherer capability at all', () => {
40
+ expect(hydrateEntityLanes({})).toEqual([])
41
+ })
42
+
43
+ test('clamps out-of-range and non-integer fields to the contract field bounds', () => {
44
+ const lanes = hydrateEntityLanes({
45
+ gathererLanes: [
46
+ {
47
+ slotIndex: -5,
48
+ yield: 999_999,
49
+ drain: -1,
50
+ depth: 70_000.7,
51
+ outputPct: Number.NaN,
52
+ },
53
+ ],
54
+ })
55
+ expect(lanes[0].slot_index.toNumber()).toBe(0)
56
+ expect(lanes[0].yield.toNumber()).toBe(65_535)
57
+ expect(lanes[0].drain.toNumber()).toBe(0)
58
+ expect(lanes[0].depth.toNumber()).toBe(65_535)
59
+ expect(lanes[0].output_pct.toNumber()).toBe(0)
60
+ })
61
+
62
+ test('clamps a slot index above the uint8 ceiling', () => {
63
+ const lanes = hydrateEntityLanes({
64
+ gathererLanes: [{slotIndex: 300, yield: 0, drain: 0, depth: 0, outputPct: 0}],
65
+ })
66
+ expect(lanes[0].slot_index.toNumber()).toBe(255)
67
+ })
68
+
69
+ test('clamps drain above the uint32 ceiling', () => {
70
+ const lanes = hydrateEntityLanes({
71
+ gathererLanes: [{slotIndex: 0, yield: 0, drain: 5_000_000_000, depth: 0, outputPct: 0}],
72
+ })
73
+ expect(lanes[0].drain.toNumber()).toBe(4_294_967_295)
74
+ })
75
+ })
76
+
77
+ describe('entityToRouteMover', () => {
78
+ test('sums hullmass, cargomass, and loader lane mass into total mass', () => {
79
+ const mover = entityToRouteMover(
80
+ {id: UInt64.from(1), energy: 500, cargomass: 200},
81
+ {
82
+ hullmass: 1000,
83
+ loaderLanes: [
84
+ {slotIndex: 0, mass: 50, thrust: 10, outputPct: 100},
85
+ {slotIndex: 1, mass: 25, thrust: 10, outputPct: 100},
86
+ ],
87
+ }
88
+ )
89
+ expect(mover.mass).toBe(1000 + 200 + 50 + 25)
90
+ expect(mover.energy).toBe(500)
91
+ expect(mover.ref).toEqual({entityType: 'ship', entityId: UInt64.from(1)})
92
+ })
93
+
94
+ test('derives hasMovement from the presence of both engines and a generator', () => {
95
+ const withMovement = entityToRouteMover(
96
+ {id: UInt64.from(1)},
97
+ {
98
+ hullmass: 0,
99
+ engines: {thrust: 100, drain: 10},
100
+ generator: {capacity: 1000, recharge: 10},
101
+ }
102
+ )
103
+ expect(withMovement.hasMovement).toBe(true)
104
+
105
+ const withoutMovement = entityToRouteMover({id: UInt64.from(1)}, {hullmass: 0})
106
+ expect(withoutMovement.hasMovement).toBe(false)
107
+ })
108
+
109
+ test('an explicit hasMovement opt overrides the derived value', () => {
110
+ const mover = entityToRouteMover(
111
+ {id: UInt64.from(1)},
112
+ {
113
+ hullmass: 0,
114
+ engines: {thrust: 100, drain: 10},
115
+ generator: {capacity: 1000, recharge: 10},
116
+ },
117
+ {hasMovement: false}
118
+ )
119
+ expect(mover.hasMovement).toBe(false)
120
+ })
121
+
122
+ test('narrows hauler down to capacity and efficiency, dropping drain and capacityByTier', () => {
123
+ const mover = entityToRouteMover(
124
+ {id: UInt64.from(1)},
125
+ {
126
+ hullmass: 0,
127
+ hauler: {
128
+ capacity: 500,
129
+ efficiency: 80,
130
+ drain: 10,
131
+ capacityByTier: [{tier: 1, capacity: 500}],
132
+ },
133
+ }
134
+ )
135
+ expect(mover.hauler).toEqual({capacity: 500, efficiency: 80})
136
+ })
137
+
138
+ test('defaults energy, priorMobilityEnd, and narrowBarrierEnd to zero when unset', () => {
139
+ const mover = entityToRouteMover({id: UInt64.from(1)}, {hullmass: 0})
140
+ expect(mover.energy).toBe(0)
141
+ expect(mover.priorMobilityEnd).toBe(0)
142
+ expect(mover.narrowBarrierEnd).toBe(0)
143
+ })
144
+
145
+ test('opts override entityType and scheduling fields', () => {
146
+ const mover = entityToRouteMover(
147
+ {id: UInt64.from(7)},
148
+ {hullmass: 0},
149
+ {entityType: 'starbase', priorMobilityEnd: 30, narrowBarrierEnd: 15, allLanesEnd: 45}
150
+ )
151
+ expect(mover.ref).toEqual({entityType: 'starbase', entityId: UInt64.from(7)})
152
+ expect(mover.priorMobilityEnd).toBe(30)
153
+ expect(mover.narrowBarrierEnd).toBe(15)
154
+ expect(mover.allLanesEnd).toBe(45)
155
+ })
156
+ })
@@ -0,0 +1,86 @@
1
+ import type {UInt32Type, UInt64Type} from '@wharfkit/antelope'
2
+ import {ServerContract} from '../contracts'
3
+ import type {ComputedCapabilities} from '../derivation/capabilities'
4
+ import type {RouteMoverInput} from '../travel/route-simulator'
5
+ import type {GatherLane} from '../planner'
6
+
7
+ const U8_MAX = 255
8
+ const U16_MAX = 65_535
9
+ const U32_MAX = 4_294_967_295
10
+
11
+ function clampField(value: number, max: number): number {
12
+ return Math.min(max, Math.max(0, Math.floor(Number(value) || 0)))
13
+ }
14
+
15
+ export interface GathererLaneSource {
16
+ gathererLanes?: ComputedCapabilities['gathererLanes']
17
+ gatherer?: ComputedCapabilities['gatherer']
18
+ }
19
+
20
+ export function hydrateEntityLanes(caps: GathererLaneSource): GatherLane[] {
21
+ const lanes = caps.gathererLanes?.length
22
+ ? caps.gathererLanes
23
+ : caps.gatherer
24
+ ? [
25
+ {
26
+ slotIndex: 0,
27
+ yield: caps.gatherer.yield,
28
+ drain: caps.gatherer.drain,
29
+ depth: caps.gatherer.depth,
30
+ outputPct: 100,
31
+ },
32
+ ]
33
+ : []
34
+ return lanes.map((lane) =>
35
+ ServerContract.Types.gatherer_lane.from({
36
+ slot_index: clampField(lane.slotIndex, U8_MAX),
37
+ yield: clampField(lane.yield, U16_MAX),
38
+ drain: clampField(lane.drain, U32_MAX),
39
+ depth: clampField(lane.depth, U16_MAX),
40
+ output_pct: clampField(lane.outputPct, U16_MAX),
41
+ })
42
+ )
43
+ }
44
+
45
+ export interface RouteMoverRow {
46
+ id: UInt64Type
47
+ energy?: UInt32Type | number
48
+ cargomass?: UInt32Type | number
49
+ }
50
+
51
+ export type RouteMoverCaps = Pick<
52
+ ComputedCapabilities,
53
+ 'hullmass' | 'engines' | 'generator' | 'hauler' | 'loaderLanes'
54
+ >
55
+
56
+ export interface RouteMoverOpts {
57
+ entityType?: string
58
+ hasMovement?: boolean
59
+ priorMobilityEnd?: number
60
+ narrowBarrierEnd?: number
61
+ allLanesEnd?: number
62
+ }
63
+
64
+ export function entityToRouteMover(
65
+ row: RouteMoverRow,
66
+ caps: RouteMoverCaps,
67
+ opts: RouteMoverOpts = {}
68
+ ): RouteMoverInput {
69
+ const loaderMass = (caps.loaderLanes ?? []).reduce((sum, lane) => sum + Number(lane.mass), 0)
70
+ const mass = Number(row.cargomass ?? 0) + Number(caps.hullmass ?? 0) + loaderMass
71
+ return {
72
+ ref: {entityType: opts.entityType ?? 'ship', entityId: row.id},
73
+ hasMovement:
74
+ opts.hasMovement ?? (caps.engines !== undefined && caps.generator !== undefined),
75
+ engines: caps.engines,
76
+ generator: caps.generator,
77
+ hauler: caps.hauler
78
+ ? {capacity: caps.hauler.capacity, efficiency: caps.hauler.efficiency}
79
+ : undefined,
80
+ mass,
81
+ energy: Number(row.energy ?? 0),
82
+ priorMobilityEnd: opts.priorMobilityEnd ?? 0,
83
+ narrowBarrierEnd: opts.narrowBarrierEnd ?? 0,
84
+ allLanesEnd: opts.allLanesEnd,
85
+ }
86
+ }
package/src/errors.ts CHANGED
@@ -94,3 +94,12 @@ export const ITEM_NOT_AVAILABLE_AT_LOCATION = 'Item is not tradeable at ship loc
94
94
  export const INSUFFICIENT_BALANCE = 'Insufficient balance.'
95
95
  export const INSUFFICIENT_ITEM_QUANTITY = 'Insufficient quantity in cargo.'
96
96
  export const INSUFFICIENT_ITEM_SUPPLY = 'Insufficient supply of item at location.'
97
+
98
+ export const VOTE_NOT_CITIZEN = 'player is not a citizen of this world'
99
+ export const BALLOT_SETTLING = 'world is settling the last epoch'
100
+ export const BALLOT_NOT_DUE = 'ballot is not due'
101
+ export const BALLOT_NONE_DUE = 'no ballots are due'
102
+
103
+ export function isSettling(error: unknown): boolean {
104
+ return error instanceof Error && error.message.includes(BALLOT_SETTLING)
105
+ }
package/src/format.ts CHANGED
@@ -1,7 +1,9 @@
1
- export function formatMass(kg: number): string {
2
- if (kg === 0) return '0 t'
3
- const sign = kg < 0 ? '-' : ''
4
- const centitonnes = Math.round(Math.abs(kg) / 10)
1
+ import {MASS_UNITS_PER_TONNE} from './types'
2
+
3
+ export function formatMass(mass: number): string {
4
+ if (mass === 0) return '0 t'
5
+ const sign = mass < 0 ? '-' : ''
6
+ const centitonnes = Math.abs(mass) * 10
5
7
  const t = Math.floor(centitonnes / 100)
6
8
  const frac = centitonnes % 100
7
9
  if (frac === 0) return `${sign}${t} t`
@@ -9,10 +11,10 @@ export function formatMass(kg: number): string {
9
11
  return `${sign}${t}.${fracStr} t`
10
12
  }
11
13
 
12
- export function formatMassDelta(kg: number): string {
13
- if (kg === 0) return '0 t'
14
- const sign = kg > 0 ? '+' : '-'
15
- return `${sign}${formatMass(Math.abs(kg))}`
14
+ export function formatMassDelta(mass: number): string {
15
+ if (mass === 0) return '0 t'
16
+ const sign = mass > 0 ? '+' : '-'
17
+ return `${sign}${formatMass(Math.abs(mass))}`
16
18
  }
17
19
 
18
20
  export function formatLocation(loc: {x: number; y: number}): string {
@@ -23,14 +25,14 @@ function trim(n: number, digits = 1): string {
23
25
  return n.toFixed(digits).replace(/\.?0+$/, '')
24
26
  }
25
27
 
26
- export function formatMassScaled(kg: number): string {
27
- if (kg === 0) return '0 t'
28
- const sign = kg < 0 ? '-' : ''
29
- const tonnes = Math.abs(kg) / 1000
28
+ export function formatMassScaled(mass: number): string {
29
+ if (mass === 0) return '0 t'
30
+ const sign = mass < 0 ? '-' : ''
31
+ const tonnes = Math.abs(mass) / MASS_UNITS_PER_TONNE
30
32
  if (tonnes >= 1_000_000_000) return `${sign}${trim(tonnes / 1_000_000_000)}b t`
31
33
  if (tonnes >= 1_000_000) return `${sign}${trim(tonnes / 1_000_000)}m t`
32
34
  if (tonnes >= 1_000) return `${sign}${trim(tonnes / 1_000)}k t`
33
- return formatMass(kg)
35
+ return formatMass(mass)
34
36
  }
35
37
 
36
38
  export function formatInfluence(atomic: bigint | number | string): string {
@@ -8,6 +8,7 @@ export * from './data/recipes-runtime'
8
8
  // Preserves both value and type sides through rollup-plugin-dts bundling.
9
9
  export {Types as ServerTypes} from './contracts/server'
10
10
  export {Types as PlatformTypes} from './contracts/platform'
11
+ export {Types as FundTypes} from './contracts/fund'
11
12
 
12
13
  import type {ServerContract} from './contracts'
13
14
  import type {Entity as EntityType} from './entities/entity'
@@ -63,8 +64,10 @@ export type {
63
64
  FoundedWorld,
64
65
  FoundedWorldRef,
65
66
  InfluenceStanding,
66
- PendingBallot,
67
- VoteCast,
67
+ Ballot,
68
+ BallotVote,
69
+ NodeProgress,
70
+ QueuedSeat,
68
71
  VoteOption,
69
72
  VoteStandings,
70
73
  PlayerRosterEntry,
@@ -99,6 +102,7 @@ export {resolveLockedAmount, wrapCostKey} from './managers/nft'
99
102
  export {
100
103
  getItem,
101
104
  getItems,
105
+ tryGetItem,
102
106
  itemIds,
103
107
  getResources,
104
108
  getComponents,
@@ -258,6 +262,12 @@ export type {ReachStats} from './travel/reach'
258
262
  export {simulateRoute} from './travel/route-simulator'
259
263
  export type {RouteMoverInput, RouteLegSim, RouteSim} from './travel/route-simulator'
260
264
 
265
+ export {
266
+ planRouteWithRetry,
267
+ contractRouteLegCost,
268
+ contractRouteHeuristic,
269
+ } from './travel/route-build'
270
+
261
271
  export * as schedule from './scheduling/schedule'
262
272
  export {LANE_MOBILITY, LANE_BARRIER} from './scheduling/schedule'
263
273
  export type {
@@ -304,8 +314,8 @@ export type {
304
314
  ProjectionOptions,
305
315
  } from './scheduling/projection'
306
316
 
307
- export {taskCargoChanges} from './scheduling/task-cargo'
308
- export type {TaskCargoChange, TaskCargoDirection} from './scheduling/task-cargo'
317
+ export {taskCargoChanges, taskCargoChangesChecked} from './scheduling/task-cargo'
318
+ export type {TaskCargoChange, TaskCargoDirection, TaskCargoResult} from './scheduling/task-cargo'
309
319
 
310
320
  export {
311
321
  jobsToLanes,
@@ -346,12 +356,13 @@ export {
346
356
  cargoReadyAt,
347
357
  taskCargoEffect,
348
358
  calcCounterpartDelivery,
359
+ incomingSources,
349
360
  cargoKey,
350
361
  cargoInputKey,
351
362
  hasSourceCoupling,
352
363
  hasIncomingCoupling,
353
364
  } from './scheduling/availability'
354
- export type {CargoInput, IncomingSource} from './scheduling/availability'
365
+ export type {CargoInput, IncomingHoldEntity, IncomingSource} from './scheduling/availability'
355
366
 
356
367
  export {clusterStockAvailable} from './scheduling/cluster-stock'
357
368
 
@@ -385,6 +396,10 @@ export {
385
396
  CAP_UNDEPLOY,
386
397
  CAP_DEMOLISH,
387
398
  CAP_MODULES,
399
+ CAP_CATCH,
400
+ CAP_STORE_CARGO,
401
+ canCatch,
402
+ canStoreCargo,
388
403
  isShip,
389
404
  isWarehouse,
390
405
  isExtractor,
@@ -656,17 +671,29 @@ export {
656
671
  gatherEnergyCost,
657
672
  maxQtyForCharge,
658
673
  planParallelTransfer,
674
+ planSingleGather,
659
675
  splitCost,
660
676
  } from './planner'
661
677
  export type {
662
678
  BuildGatherPlanOpts,
663
679
  FillCap,
664
680
  GatherCycle,
681
+ GatherLane,
665
682
  GatherLimpet,
666
683
  GatherPlan,
667
684
  GatherPlanEntity,
668
685
  LanePlanEntry,
669
686
  PlanTarget,
687
+ SingleGatherInput,
688
+ SingleGatherPlan,
670
689
  } from './planner'
671
690
 
691
+ export {entityToRouteMover, hydrateEntityLanes} from './entity/hydrate'
692
+ export type {
693
+ GathererLaneSource,
694
+ RouteMoverCaps,
695
+ RouteMoverOpts,
696
+ RouteMoverRow,
697
+ } from './entity/hydrate'
698
+
672
699
  export * from './influence'
@@ -0,0 +1,103 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+ import {
3
+ charterEligible,
4
+ charterEligibleChained,
5
+ CHARTER_REGISTRY,
6
+ type CharterWorld,
7
+ } from './charters'
8
+ import {projectBallot} from './ballot'
9
+
10
+ const NEXUS = 20001
11
+ const DOCK = 30100001
12
+ const WORKSHOP_TUNEUP = 10100201
13
+ const DOCK_TUNEUP = 30100301
14
+ const DEPOT = 40100001
15
+
16
+ const world: CharterWorld = {built: [{nodeId: 10001, repeats: 0}]}
17
+
18
+ describe('charterEligibleChained', () => {
19
+ test('with nothing seated it equals charterEligible', () => {
20
+ for (const node of CHARTER_REGISTRY) {
21
+ expect(charterEligibleChained(world, node, [])).toBe(charterEligible(world, node))
22
+ }
23
+ })
24
+
25
+ test('a seated prerequisite opens a dependent, a seated node closes itself', () => {
26
+ const tuneup = CHARTER_REGISTRY.find((n) => n.nodeId === DOCK_TUNEUP)!
27
+ expect(charterEligibleChained(world, tuneup, [])).toBe(false)
28
+ expect(charterEligibleChained(world, tuneup, [DOCK])).toBe(true)
29
+ const dock = CHARTER_REGISTRY.find((n) => n.nodeId === DOCK)!
30
+ expect(charterEligibleChained(world, dock, [DOCK])).toBe(false)
31
+ })
32
+ })
33
+
34
+ describe('projectBallot', () => {
35
+ const voters = [
36
+ {account: 'carol', weight: 9_000n, picks: [DOCK, DOCK_TUNEUP]},
37
+ {account: 'dave', weight: 5_000n, picks: [NEXUS, DOCK_TUNEUP]},
38
+ {account: 'erin', weight: 0n, picks: [NEXUS]},
39
+ ]
40
+
41
+ test('one seat: the heavier pick wins and the frontier carries round weights', () => {
42
+ const result = projectBallot({world, seats: 1, voters})
43
+ expect(result.seats).toEqual([{nodeId: DOCK, weight: 9_000n}])
44
+ const nexus = result.options.find((o) => o.nodeId === NEXUS)!
45
+ expect(nexus.seat).toBe(0)
46
+ expect(nexus.weight).toBe(5_000n)
47
+ const tuneup = result.options.find((o) => o.nodeId === DOCK_TUNEUP)!
48
+ expect(tuneup.seat).toBe(0)
49
+ expect(tuneup.weight).toBe(9_000n)
50
+ expect(result.options.find((o) => o.nodeId === 10001)).toBeUndefined()
51
+ })
52
+
53
+ test('two seats: the dock tune-up chains behind the dock', () => {
54
+ const result = projectBallot({world, seats: 2, voters})
55
+ expect(result.seats.map((s) => s.nodeId)).toEqual([DOCK, DOCK_TUNEUP])
56
+ expect(result.seats[1].weight).toBe(9_000n)
57
+ })
58
+
59
+ test('a tie goes to the lower option id', () => {
60
+ const tied = [
61
+ {account: 'a', weight: 100n, picks: [DOCK]},
62
+ {account: 'b', weight: 100n, picks: [NEXUS]},
63
+ ]
64
+ expect(projectBallot({world, seats: 1, voters: tied}).seats[0].nodeId).toBe(NEXUS)
65
+ })
66
+
67
+ test('a zero round ends early and an empty queue stays empty, even with one node left', () => {
68
+ expect(projectBallot({world, seats: 4, voters}).seats.length).toBe(3)
69
+ expect(projectBallot({world, seats: 1, voters: []}).seats).toEqual([])
70
+ const almostDone: CharterWorld = {
71
+ built: CHARTER_REGISTRY.filter((n) => n.nodeId !== 40100404).map((n) => ({
72
+ nodeId: n.nodeId,
73
+ repeats: 0,
74
+ })),
75
+ }
76
+ expect(projectBallot({world: almostDone, seats: 1, voters: []}).seats).toEqual([])
77
+ expect(
78
+ projectBallot({
79
+ world: almostDone,
80
+ seats: 1,
81
+ voters: [{account: 'a', weight: 5n, picks: [40100404]}],
82
+ }).seats
83
+ ).toEqual([{nodeId: 40100404, weight: 5n}])
84
+ })
85
+
86
+ test('the caller picks are ranked on the options', () => {
87
+ const result = projectBallot({world, seats: 1, voters, picks: [NEXUS, DOCK]})
88
+ expect(result.options.find((o) => o.nodeId === NEXUS)!.rank).toBe(1)
89
+ expect(result.options.find((o) => o.nodeId === DOCK)!.rank).toBe(2)
90
+ expect(result.options.find((o) => o.nodeId === WORKSHOP_TUNEUP)!.rank).toBe(0)
91
+ })
92
+
93
+ test('a pick slot is consumed by the first eligible option regardless of the voter weight behind it', () => {
94
+ const scenario = [
95
+ {account: 'grace', weight: 50n, picks: [NEXUS, DEPOT]},
96
+ {account: 'frank', weight: 45n, picks: [DEPOT]},
97
+ {account: 'erin', weight: 0n, picks: [NEXUS, DEPOT]},
98
+ ]
99
+ expect(projectBallot({world, seats: 1, voters: scenario}).seats).toEqual([
100
+ {nodeId: NEXUS, weight: 50n},
101
+ ])
102
+ })
103
+ })